ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

hindsight:被动采集本地数字痕迹,用SQLite生成每周时间回看报告

hindsight:被动采集本地数字痕迹,用SQLite生成每周时间回看报告 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/HistoryWindows 下 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(ffile:{tmp}?modero, uriTrue)只读打开避免任何写操作。第二个坑是时间戳。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 --prettyformat:%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 生成“人性化”评语试了一周就撤了原因有三个它会生成听起来很顺、但实际是编造的结论。比如把“频繁查文档”概括成“说明你对新技术很渴求”——这话听着舒服但属于纯推测数据并不支撑。结果不可复现。同一份数据跑两次评语可能不同工具失去了我一贯依赖的确定性。每次调用都有成本而且隐私上多了一道不必要的边界。于是回归条件模板写成纯函数深水时间占比低于 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 这个名字提醒我的正是这件事真正有价值的回看是不干预过程的回看。
返回列表