☰
hindsight:被动采集本地数字痕迹,用SQLite生成每周时间回看报告
2026/9/29 6:25:45 网站建设 项目流程

Hindsight 这个名字来自那句老话:hindsight is 20/20,事后回看,一切无比清晰。但对我而言,现实恰恰相反——我对自己刚过去一周干了什么的“回看”,经常一团模糊。

典型场景是每周五的站会前五分钟。我需要总结这一周做了什么,于是坐在键盘前试图从记忆里捞证据,可捞出来的通常只有最近两三天做过的事,而且顺序全乱。git log 告诉我某个功能分支周二之后就再没碰过;浏览器历史里躺着四十多个标签页;终端里有一堆我自己都看不懂的命令。我说“这周主要在做 X”,但证据分明在说“你这周大部分时间在查资料、读文档、回消息,还有一部分时间根本说不清去哪了”。

这种“记忆版本”和“证据版本”之间的落差,就是 hindsight 这个项目想解决的问题。我花了两个周末,写了一个完全本地运行的小工具:它被动采集电脑上的数字痕迹——浏览器历史、git 提交日志、shell 历史、日历会议——统一清洗后落进一个 SQLite 数据库,每周自动生成一份“回看报告”,回答三个问题:这一周的时间到底花在哪了?我原本计划做什么?计划和现实之间差在哪?

如果你也经常觉得“一周忙得要死但说不出做了什么”,或者你想给自己搭一套不依赖云端、不需要手动打卡的个人数据分析管线,这篇内容可以当作一份踩坑记录和可复现笔记来看。我会把设计取舍、核心实现、以及我拿自己当小白鼠实测三个月的结果都摊开讲。

1. 一次让我脸红的周报,和“事后回看”该有的样子

1.1 周报前五分钟,是我认知失调的高发期

最开始我反思时,以为问题出在自己记性差。后来发现不对——我并不是没有记录,电脑、浏览器、终端、git 仓库每分每秒都在记录,我只是从来不去读这些记录。真正让我决定动手的,是一次特别典型的翻车:周五下午我自信满满地说“本周写了计费模块的接口”,晚上随手打开 git log 一查,那个模块最后一次提交其实是上周五,本周唯一一次提交是在改 README。

那一刻我的第一反应不是“我记错了”,而是“这不可能”。这种防御机制出现本身就说明:靠记忆复盘这件事,在数据面前根本不成立。人会把“最近记得的事”误当成“本周发生的事”,会把“查了几个小时资料”脑补成“推进了项目”,这些都是事后回看时最典型的认知偏差。

1.2 手动计时、自动跟踪、日历复盘,我为什么都没坚持下来

在写 hindsight 之前,我试过三类常见的解决方案,全部中途放弃,放弃的原因值得记录一下。

手动计时类,比如 Toggl、Clockify。核心逻辑是先按“开始”,事后再按“停止”。问题在于:真正进入心流的时候,停下来切换计时器是最伤的事情。我忘记开计时的次数远大于记住的次数;而一旦忘记,你会下意识地“补数据”而不是“记数据”。补出来的时间分布比不记还糟,因为它混合了想象和事实。

自动跟踪类,比如 RescueTime。不用手动操作,但通常需要系统级监控权限,数据默认走云端;分类算法是黑盒,只告诉我一个“工作效率 72%”的分数,却说不清这 72% 是怎么算的、哪段时间值得改。我试了两周就卸了,既不喜欢隐私流向云端,也无法从黑盒结论里提取可执行的判断。

日历复盘是另一种思路:周末打开日历看开了几个会、约了几件事。但它只反映“你计划了哪些安排”,不反映实际发生;如果一周的日历本来就稀稀拉拉,复盘就是无米之炊。而且日历复盘对“我花了四小时解决一个编译问题”这种事毫无感知。

结论很简单:不是我不自律,而是这些工具都把“记录”变成了额外负担。真正可靠的记录应该来自我本来就在产生的数据,而不是需要我再单独产出的数据。

1.3 hindsight 的三条非目标

动手之前,我先把“不做什么”想清楚了,这三条非目标决定了后续每个设计决策:

  • 不做实时提醒。hindsight 是事后分析工具,应该在一天、一周结束后说话,而不是在正在做事时打断你。
  • 不做云端同步。所有原始数据和计算结果只存在本地机器里,报告由本地脚本生成。我不需要为看自己的时间账单额外付一次 SaaS 订阅费。
  • 不做行为审判。报告的目标是理解,不是打分。我后来多次想加一个“本周效率得分”,每次都会因为这个非目标而放弃——单一得分很容易变成自我惩罚的工具,而不是理解自己的工具。

把非目标写下来之后,很多纠结都消失了。比如“要不要做实时仪表盘”“要不要存到云端”,答案都是明确的“不”。

2. 总体设计与选型:被动采集、本地优先

2.1 三条设计原则

hindsight 的整体设计只围绕三个原则展开。

第一,被动采集。浏览页面的时候,浏览器历史已经被写进数据库;提交代码的时候,git 记录已经存在仓库里;敲命令的时候,shell history 文件已经更新。hindsight 要做的只是定期去“读取”这些已经存在的记录,而不是要求用户主动打点。被动采集是这套系统能长期运转的地基。

第二,全本地。所有原始数据和计算结果只存在我的机器里,隐私方面省心;也更符合“回看”这种私人活动的性质。看到自己真实的时间流向,这件事本身非常暴露,我不希望它出现在别人的服务器上。

第三,低频输出。不做仪表盘、不做实时统计,每周五傍晚自动生成一份 Markdown 报告,控制在 A4 纸一页以内。频率太低容易遗忘,月度报告基本没人会认真看;频率太高容易焦虑,每天看只会反复自我谴责。一周一次刚好踩在“能引起注意但不会引起恐慌”的点上。

2.2 为什么是 Python + SQLite

先明确任务:读取五类本地文件,做时间换算和去重,按天和周做聚合,最后生成报告。这本质上是数据处理脚本,不是实时大数据平台。

选型上我没有纠结太久。Python 的标准库里有 sqlite3、pathlib、argparse,不用装任何第三方包就能把收集、清洗、报表全部跑起来。个人工具最怕依赖膨胀,标准库能解决就不引第三方。

SQLite 作为唯一存储,是这套架构里最顺手的决定:单文件、零配置、支持 WAL 模式,3.25 之后还内置了窗口函数。对 hindsight 来说,它既是写入库也是分析库,备份只需要复制一个文件。我认真考虑过几个替代品,对比之后还是回到 SQLite。

方案优势我放弃它的理由
Postgres成熟、功能全面个人工具要养一个常驻服务进程,备份迁移成本高
InfluxDB / TimescaleDB时序数据表征强数据量远没到能发挥优势的量级
DuckDB分析查询很爽增量写入不顺手,生命周期偏“一次性分析”
纯 CSV零依赖JOIN 和窗口函数全要手写,维护成本高
SQLite单文件、零配置、支持窗口函数最终选择

2.3 收集器与清洗层的边界:谁负责“读”,谁负责“懂”

架构上我把整个工具切得很薄,因为个人项目最怕“写的时候很爽,改的时候想删库”。分层的逻辑很简单:

  • 收集器(collect_*):每个数据源一个脚本,只负责把原始文件里的记录读取出来,统一转成 raw_events 表(source_ts、source_type、payload JSON),然后结束。它不关心分类、不关心去重、不关心报告。
  • 清洗层(clean.py):读取 raw_events,做时间换算、去重、分类、空闲判断,生成 events 表;再从 events 聚合出 episodes 表。
  • 分析层(report.py):只读 episodes 表和每日计划文件,输出报告。

这样划分带来的直接好处是:加一个新数据源,只需要新写一个收集器,清洗和分析完全不用动。我后来加日历事件收集器时,总共花了一个小时,就是因为边界已经画好了。这个经验在个人项目里尤其重要——没有产品经理帮你划分需求,自己不划清边界,代码很快就会变成一坨。

3. 核心实现:把碎片数据变成“可回看的事件”

3.1 浏览器历史:加锁文件、诡异时间戳和增量游标

浏览器历史是 hindsight 最核心的数据源,毕竟一天中大部分时间流向都体现在这里。以 Chromium 系浏览器为例,Linux 下的路径是~/.config/google-chrome/Default/History,Windows 下 Edge 在%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\History。这个文件本身就是一个 SQLite 数据库,核心表有两张:urls(id, url, title, visit_count, last_visit_time)和visits(id, url, visit_time, transition)。

这里有三坑。第一个是文件锁:Chrome 运行时会锁住 History 文件,直接连接会报 database is locked。标准做法是先复制一份再读。我用 Python 的shutil.copy2把文件复制到临时目录,然后用sqlite3.connect(f"file:{tmp}?mode=ro", uri=True)只读打开,避免任何写操作。

第二个坑是时间戳。Chromium 的 visit_time 是“1601 年 1 月 1 日以来的微秒数”(Windows 时间原点),转 Unix 秒的公式是:unix_ts = visit_time / 1_000_000 - 11_644_473_600。那个 11,644,473,600 就是 1601 年到 1970 年之间的总秒数。我第一次写完就忘了减,结果所有记录都被算到了 1975 年左右,差点以为浏览器历史是上个世纪的。

第三个坑是增量读取。全量扫历史文件会越来越慢,所以我在 state 表里记录每个数据源上次同步的位置,浏览器历史就存max(visit_time),下次只取比它大的记录。对应的查询大致是:

SELECT v.url, v.visit_time, u.title FROM visits v JOIN urls u ON v.url = u.url WHERE v.visit_time > ? ORDER BY v.visit_time ASC;

3.2 git 与 shell 历史:两类被忽略但信息量巨大的来源

浏览器历史只能说明“在看什么”,判断不了“在做什么”。git 和 shell 历史恰好补上这一块。

git 部分很简单:遍历配置文件里登记的仓库目录,对每个仓库跑一条命令拿增量提交:

git -C <repo> log --pretty=format:'%ct|%s' --since=<上次同步时间>

一条 commit 就是一条原始事件。我不关心分支、作者这些元信息,只关心“这个项目在什么时间点发生过一次代码变更”。

shell 历史是另一个宝藏。zsh 的历史格式是: 1717100000:0;command,冒号后是 Unix 秒,分号后是命令本身。解析后我用简单规则做分类:make/npm run/test/pytest这类归为“构建/测试”,vim/nvim/idea归为“写代码”,ssh/curl归为“环境排查”。

这里有一个典型盲区能说明这两类数据源的价值:很多时候你花了两小时在终端里试各种命令,最后没产生任何 commit。浏览器历史只会显示你查了十几次 Stack Overflow,看不出你其实在调试一个非常具体的问题。只有 git 历史加 shell 历史合起来,才能还原“尝试-失败-再尝试”的过程。

3.3 时区、去重、空闲判断:三个最容易翻车的地方

时区:Chrome 时间戳本身就是 UTC,浏览器历史没有时区概念,但分组统计一定要按“当地时间的一天”。我在 SQLite 里统一用date(start_ts, 'localtime')做按天聚合,避免在 Python 层反复做时区换算。这看起来是小问题,一旦错了,整周的日分布会全部偏移一天。

去重:浏览器“后退”会再产生一条 visit;同一页面两秒内被反复激活也会产生多条。我的规则是:同一 url、标题相同、间隔小于两秒的访问合并成一条,保留第一条的时间。宁可合并过头,也不保留噪音,因为报告粒度本来就是分钟级。

空闲判断:两件事之间隔多久算“断了”?我最初用固定 8 分钟——gap 大于 8 分钟就切出新的活动块。实测发现,读文档的场景经常出现 10 到 15 分钟的不连续阅读,被硬切成两段,导致“读文档”这种活动在报告里变得极其零碎。后来改成:前后分类相关时放宽到 20 分钟,分类变化时 8 分钟就切。阈值不是真理,是需要按工作习惯慢慢调的参数。

提示:这类数据里每小时都有一些记录,但加起来时间往往对不上,这类误差不用追求 100% 精确。hindsight 的目标是趋势判断,不是考勤打卡。

3.4 episodes:活动块的聚合模型

原始事件粒度太碎:一次五分钟的文档阅读、一次两分钟的提交、一条 curl 命令,谁也没法直接读。所以我引入了一个聚合单位叫 episode(活动块)。

做法是:先把所有事件按时间排序,然后从头扫描。相邻事件间隔小于阈值、且分类相同或相邻(比如“查文档→写代码”),就归到同一个 episode。每个 episode 记录开始时间、结束时间、主要分类和内部事件序列。举个例子:

14:02 查阅 sqlite 文档 (research) 14:05 打开 stack overflow (research) 14:07 修改 schema.py (coding) 14:21 运行 pytest (coding) 14:30 回到浏览器查另一篇文档 (research)

在 8 分钟阈值下,这会被聚合成一个从 14:02 到 14:30、主分类为 coding、带 research 穿插的 episode。报告里我会把它等价描述成“14:02-14:30 写 schema.py 及相关调研”,而不是五六个碎片。

聚合代码的核心逻辑不到一百行,用 Python 循环就能写完。真正值钱的是阈值和分类规则,那些要从一周真实数据里慢慢调出来。episodes 这个抽象是整份报告的基石,没有它,后面所有的指标都无法成立。

4. 报告生成:让“回看”不只是柱状图

4.1 指标设计:深水时间、切换成本、兔子洞指数

总时长是最没用的指标。一个人可以坐在电脑前 60 小时,但真正连续投入的时间可能只有 5 小时。我在反复尝试之后,最终沉淀了三个核心指标。

第一个是深水时间(deep time):时长不小于 30 分钟、且主分类为 coding 或 writing 的活动块累加时长。它比“总编码时长”更有意义,因为只有足够连续的时间块才真正推进过事情。第二个是切换成本(switch cost):每小时分类切换次数。算法是统计 episode 序列里前后主分类不同的次数,再除以活跃时长换算成“次/小时”。数字高,说明一天被拆成了碎渣。第三个是兔子洞指数(rabbit hole index):在工作时段内,浏览非工作域名的分钟数占该时段总浏览分钟数的比例。这个指标专门捕捉“本打算查个报错,结果看了一小时评测视频”的场景。

为什么不直接用“各分类占比”?因为占比只回答“时间去哪了”,回答不了“这些时间有没有形成块”。真正困扰人的不是总时长,而是不断被打断、每次只能维持十分钟的注意断裂。指标要能反映这种结构性问题,才有资格写进周报。

4.2 计划与实际对照:一篇 Markdown 计划文件就够了

hindsight 的“20/20”感,很大程度来自计划与实际的对照。我没有引入复杂的目标管理工具,就是每个工作日早上写一个 Markdown 计划文件:

# 2025-06-02 - [ ] 完成 schema 重构代码 #coding 2h - [ ] 写周会纪要 #writing 30m - [ ] 回复上下游邮件 #communication 30m

解析规则也很简单:正则提取#分类和时长,按分类累加得到“计划分钟数”,再和当天实际 episodes 的分类时间做减法,输出一张差异表。没有写计划的日期就不生成对照,只保留行为分析。

第一周跑出来的对照让我印象极深:计划里“coding 2h”实际只有 12 分钟;没计划的“research”实际有 4 小时。那一刻我才真正理解,所谓“忙”,很多时候是“在解决计划之外冒出来的问题”,而这些问题往往串成了兔子洞。

4.3 规则叙事模板,和为什么不用 LLM 写评语

报告里我不只放表格,还放几行文字总结。最初我也试过用 LLM 生成“人性化”评语,试了一周就撤了,原因有三个:

  1. 它会生成听起来很顺、但实际是编造的结论。比如把“频繁查文档”概括成“说明你对新技术很渴求”——这话听着舒服,但属于纯推测,数据并不支撑。
  2. 结果不可复现。同一份数据跑两次,评语可能不同;工具失去了我一贯依赖的确定性。
  3. 每次调用都有成本,而且隐私上多了一道不必要的边界。

于是回归条件模板,写成纯函数:

  • 深水时间占比低于 30%:输出“本周深水时间偏低,主要碎片来自 {top_switch_target} 方向的切换。”
  • 兔子洞指数大于 0.25:输出“本周探索型浏览占比偏高,集中在前 3 个非工作域名:{list}。”
  • 计划与实际差异大于 50%:输出“计划与执行偏差较大,超出计划最多的分类是 {category}(计划 {x},实际 {y})。”

模板规则的好处是确定、可测试、没有 API 成本。它不像真人写的那么生动,但足够精确,而这正是回看工具该有的性格。

5. 三个月实测:有用、翻车和算法迭代

5.1 第一份真实报告,和它逼我做的三件事

第一份完整周报是在项目第二周跑出来的,数字大致是:coding 19%、research 27%、communication 13%、leisure/media 34%、admin 7%。看到 leisure/media 排第一,我的第一反应是“分类器坏了吧”。点进明细才发现,YouTube 的“技术视频”和“娱乐视频”没有被区分,只要在 youtube.com 就统一算成 media,而这一周我确实看了大量技术演讲。

把误分类修正之后,报告依然指向一个让我不太舒服的事实:本周有效深水时间加起来不到 8 小时,而我在周一早上原本以为自己会有 15 小时。这份报告逼我做了三件事:把每周一上午设成两小时起步的免打扰深水块;把“先开标签页、以后慢慢看”的习惯改成“放进稍后读清单”;给常用工作域名建了一份白名单,避免“查文档”和“看视频”混在同一分类里。

这三件事没有一件是我靠回忆能自己总结出来的。回看工具的真正价值就在这里:它不告诉你该做什么,但会把你骗自己“我很努力”的叙事拆掉。

5.2 误报、盲区与数据缺口

三个月的使用里,最大的问题不是算法不够聪明,而是数据源本身有盲区。

无痕/访客模式完全不写历史。我调试 hindsight 时经常开无痕窗口,结果“调试工具”这个我实际投入了大量时间的事情,在报告里成了黑洞。线上会议也不写历史。Zoom、腾讯会议、飞书会议的时长只字不留在浏览器里,除非我去导出日历 ICS。后来我加了 collect_calendar.py,把 ICS 里的会议当成独立事件导入,报告才不再凭空多出一段“未知时间”。

域名的语义分裂是另一个麻烦:同一个 youtube.com,看技术演讲和看生活视频是完全不同的活动。domain 分类解决不了这个问题,我加了一层标题关键词规则,比如出现 talk、conference 归为学习,出现 vlog、review 归为娱乐。

多设备也是一个绕不过去的边界。公司笔记本、家里台式机、手机,各看各的。hindsight 只在主力开发机上运行,所以它回答的是“这台电脑上的时间去哪了”,而不是“我全部的时间去哪了”。接受这个局限之后,报告反而更有针对性了。

这些盲区教会我一件事:个人分析工具的质量上限,不取决于分析算法,而取决于你愿意接受多少数据缺口。与其强行补全所有设备,不如明确边界,在边界内做深。

5.3 测量会改变行为,指标会被自己污染

最好玩也最反直觉的经验是:指标一旦存在,行为就会偏移,而偏移后的行为会让指标变得不准确。我把兔子洞指数加入报告之后,自然开始少打开那几个非工作域名,这是好事。但两周后发现,“摸鱼”悄悄转移到了无痕窗口,报告里 leisure/media 反而降了,而“未知时间”涨了。

这让我明白两件事:指标的最佳作用不是“监控自己”,而是“提醒自己”。知道自己会被看见,这本身就是行为修正。同时,任何指标都必须和它的失效方式一起迭代。为了让报告继续有用,我在 state 表里专门记录了几条已知的逃避路径,报告里也加了一行“本周存在 {n} 条未计入分析的窗口打开记录”。

诚实地说,这已经有点猫鼠游戏的味道。我最終的定论是:把它当一面镜子,而不是裁判。镜子只反映事实,裁判才会让人想方设法规避。

5.4 防止工具变成自我惩罚:亮点事件与措辞

连续几周只看到“深水时间不足”“兔子洞偏高”之后,我发现自己会在周五晚上焦虑,而不是反思。这不是我想要的效果。于是我在 report.py 里加了一个“亮点事件”小节,专门挑出本周最长的深水块、连续深水天数、完成的提交数量等具体事实。

为了让亮点小节不至于变成鼓励性套话,我要求它只能陈述数据能支撑的事实,比如:

  • 本周最长深水块出现在周四上午,时长 1 小时 47 分。
  • 周一、周二连续两天深水时间超过 3 小时。

同时我统一了措辞:避免使用“浪费”“摸鱼”“虚度”这类词,改用“探索型浏览”“非计划时间”。措辞不是小事,因为每个周五你都会读到这份报告,语言会塑造你对过去一周的感受。这套改动做完,报告的可读性和可持续性都上了一个台阶——hindsight 被我保留到今天,和这一点有很大关系。

6. 部署与复刻:从零搭一套需要多少工作量

6.1 最小可用版本的五个文件

如果你也想复刻一套,最小可用版本只需要五个文件:三个收集器脚本、一个清洗脚本、一个报表脚本,外加自动生成的 state.db 和一份配置文件。核心数据库只用两张表,另外一张表存同步状态:

CREATE TABLE raw_events ( id INTEGER PRIMARY KEY, source TEXT NOT NULL, -- 'browser' / 'git' / 'shell' / 'calendar' source_ts INTEGER NOT NULL, -- Unix 秒 payload TEXT NOT NULL -- JSON 原文 ); CREATE TABLE episodes ( id INTEGER PRIMARY KEY, start_ts INTEGER NOT NULL, end_ts INTEGER NOT NULL, category TEXT NOT NULL, chain TEXT NOT NULL -- 内部事件序列描述 ); CREATE TABLE state ( key TEXT PRIMARY KEY, value TEXT NOT NULL );

每个收集器的接口就是三件事:读取源、清洗成 raw_events、更新 state。报表层只需要跑一句python -m hindsight.report --week 2025-W23,输出 Markdown 到~/.config/hindsight/reports/。

6.2 定时调度与数据备份

收集器用 cron 定时跑:浏览器历史每 5 分钟一次,git 和 shell 每 10 分钟一次。它们都很快,因为增量游标只读新增部分。周报固定在每周五 18:00 触发。我的 crontab 长这样:

*/5 * * * * cd ~/src/hindsight && python -m hindsight.collect --source browser */10 * * * * cd ~/src/hindsight && python -m hindsight.collect --source git --source shell 0 18 * * 5 cd ~/src/hindsight && python -m hindsight.report --week

数据库备份直接用 SQLite 的.backup命令做在线备份,每天凌晨一点自动复制到~/backups/hindsight/。还有一个容易被忽略的点:计划文件也要备份。它们和数据库一样重要,因为报告里的“计划与实际对照”依赖它们。

6.3 扩展方向与最后一公里的提示

hindsight 可以扩展的方向不少:把每周报告自动插进 Obsidian 周记正文;做一个浏览器扩展,手动把某次浏览标记成“高价值调研”;甚至把 episodes 抽象成个人事件总线,供其他小工具消费。但我的经验是:个人工具的死亡方式往往不是没人用,而是功能膨胀到维护成本超过使用收益。所以我的扩展节奏是“一个季度最多加一个功能,且必须能解决真实痛点”。

我到现在依然保留着每周五 18:00 自动生成报告的习惯。它之所以能被保留下来,恰恰因为不依赖我的自觉——不需要我点按钮、不需要我回忆、不需要我整理。它只是每周准时出现在那里,告诉我上周发生了什么。

最后分享一个我踩过几次坑之后摸出来的小技巧:报告生成时间最好固定在你不会工作的时刻,比如周五傍晚。如果你把生成时间设在周五下午 3 点,你会下意识地为了“即将被看见”而改变当天后半段的行为,报告数据就被污染了。hindsight 这个名字提醒我的正是这件事:真正有价值的回看,是不干预过程的回看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询