summarize CLI 缓存设计全解析:SQLite 存储、键构造、配置与驱逐策略
【免费下载链接】summarizePoint at any URL/YouTube/Podcast or file. Get the gist. CLI and Chrome Extension.项目地址: https://gitcode.com/GitHub_Trending/summarize/summarize
summarize 项目是一款面向 URL / YouTube / 播客 / 本地文件的"抓重点"工具,同时提供 CLI 与 Chrome 扩展。本文聚焦其 CLI 侧的缓存子系统(design 文档见 docs/cache.md),系统讲解缓存的设计目标、SQLite 存储结构、各类缓存键的 SHA-256 构造方式、配置文件参数、CLI 开关以及 TTL 与容量双重驱逐策略,并结合仓库源码逐一印证实现细节。读完本文,你将能准确判断一次运行是否会命中缓存、如何排查缓存失效、如何调优
cache配置,以及如何安全地清理缓存。
一、缓存设计目标与整体架构
summarize 的缓存是仅存在于 CLI 侧的轻量级 SQLite 缓存,整个缓存就是一个单文件数据库(另有 WAL/SHM 侧车文件)。它的设计目标在 docs/cache.md 中明确为四点:
- 避免重复的转录 / 提取 / 摘要:同一 URL 多次运行不必重复抓取与调用 LLM;
- 无文件蔓延、磁盘占用有界:通过容量上限(
maxMb)与 TTL 双重约束; - 安全的默认值、易于关闭:默认开启,但可通过 CLI 标志一键绕过;
- 仅使用原生 SQLite:Node 24(
node:sqlite)与 Bun(bun:sqlite)运行时自带能力,不引入任何第三方 SQLite 依赖。
代码职责划分非常清晰(见 src/cache.ts):
src/cache.ts:对外暴露缓存配置类型、路径解析、统计读取与清理工具;src/cache-store.ts:真正持有 SQLite 行数据,负责读写、驱逐(eviction)、幻灯片文件清理以及转录缓存适配器;src/cache-database.ts:把 Node / Bun 两种 SQLite 绑定隔离在同一个接口后面;src/cache-keys.ts:集中实现全部版本化(versioned)缓存键构造逻辑。
此外还有一个独立于 SQLite 的媒体下载缓存(见下文第五节),两者并不共享存储。
二、存储:单文件 SQLite 与 pragma 设置
默认路径与覆盖方式
- 默认数据库路径:
~/.summarize/cache.sqlite; - 覆盖方式:配置项
cache.path。
路径解析的完整逻辑位于 src/cache.ts 的resolveCachePath:
- 支持
~与~/前缀展开(借助环境变量HOME或USERPROFILE); - 绝对路径直接使用;
- 相对路径则基于 home 目录拼接;
- 当配置未提供且无法解析 home 时返回
null(即不启用数据库缓存)。
// resolveCachePath 关键行为(src/cache.ts) if (raw === "~" || raw.startsWith("~/")) { const expanded = raw === "~" ? home : join(home, raw.slice(2)); return resolvePath(expanded); } return isAbsolute(raw) ? raw : home ? resolvePath(join(home, raw)) : null;SQLite pragma 与表结构
打开数据库时会执行以下 pragma(见 src/cache-store.ts):
| pragma | 值 | 作用 |
|---|---|---|
busy_timeout | 5000 | 写锁竞争时最多等待 5 秒,避免并发 CLI 进程直接报错 |
journal_mode | WAL | 允许读写并发,配合-wal/-shm侧车文件 |
synchronous | NORMAL | 平衡持久性与写入性能(WAL 下常规选择) |
auto_vacuum | INCREMENTAL | 删除数据后通过PRAGMA incremental_vacuum逐步回收空间 |
缓存表cache_entries的结构(src/cache-store.ts):
CREATE TABLE IF NOT EXISTS cache_entries ( kind TEXT NOT NULL, -- 缓存类别:extract / summary / transcript / chat / slides key TEXT NOT NULL, -- SHA-256 缓存键 value TEXT NOT NULL, -- 序列化后的缓存负载(文本或 JSON) size_bytes INTEGER NOT NULL, -- 条目大小,用于容量驱逐 created_at INTEGER NOT NULL, last_accessed_at INTEGER NOT NULL, -- LRU 排序依据 expires_at INTEGER, -- TTL 过期时间戳,NULL 表示永不过期 PRIMARY KEY (kind, key) ); CREATE INDEX idx_cache_accessed ON cache_entries(last_accessed_at); CREATE INDEX idx_cache_expires ON cache_entries(expires_at);kind的合法取值由 core 包定义(packages/core/src/runtime/cache-store.ts):"extract" | "summary" | "transcript" | "chat" | "slides"。同一个 key 可以分属不同 kind,互不冲突(联合主键)。
写入采用 UPSERT(ON CONFLICT(kind, key) DO UPDATE),重复生成同键内容会覆盖旧条目并刷新last_accessed_at。值得注意的细节:close()时会先执行PRAGMA wal_checkpoint(TRUNCATE),把 WAL 合并回主库并截断侧车文件(src/cache-store.ts),这解释了为何--clear-cache需要同时删除-wal与-shm。
Node / Bun 双运行时绑定
src/cache-database.ts 通过检测全局Bun对象来决定加载哪个绑定:
if (isBun) { const mod = await import("bun:sqlite"); return new mod.Database(path); } const mod = await import("node:sqlite"); // Node 24 内置 DatabaseSync return new mod.DatabaseSync(path);另外,在 Node 上运行时它会过滤掉ExperimentalWarning中与 sqlite 相关的警告(installSqliteWarningFilter),避免噪声刷屏。这是"原生 SQLite only"目标的直接实现证据。
三、缓存键:版本化 SHA-256 构造
所有缓存键都是 SHA-256 十六进制摘要,由 Nodecrypto/ Buncrypto提供(src/cache-keys.ts)。键的构造逻辑集中在src/cache-keys.ts,核心思想是把决定缓存有效性的所有因素打包进 JSON 后整体哈希。
各类缓存键定义(docs/cache.md 原文)
| 缓存类别 | 键 | 说明 |
|---|---|---|
| Transcripts | sha256({url, namespace, fileMtime?, formatVersion}) | 本地文件路径会带上fileMtime以便在文件变更后自动失效 |
| Extracted content(URL → text/markdown) | sha256({url, extractSettings, formatVersion}) | 提取设置变化(如抓取选项)会使键改变 |
| Summaries | sha256({contentHash, promptHash, model, length, language, formatVersion}) | 即使 URL 不同,只要内容哈希相同也能命中(内容哈希优先) |
| Slides(清单 + 输出目录中的磁盘图片) | sha256({url, slideSettings, formatVersion}) | 幻灯片设置变化会使键改变 |
源码级键构造
对照 src/cache-keys.ts 可以还原每个键的具体字段:
Transcript 键(buildTranscriptCacheKeyValue,L155-L172):
hashJson({ url, namespace, fileMtime: fileMtime ?? null, formatVersion });namespace是转录来源命名空间,目前包含 YouTube 模式(例如yt:auto、yt:web),并由createCacheStore的transcriptNamespace参数注入(见 src/cache-store.ts)。本地媒体文件的fileMtime参与键计算,因此文件被重新生成后旧缓存自动失效。
Summary 键(buildSummaryCacheKeyValue,L100-L123):
hashJson({ contentHash, promptHash, model, lengthKey, languageKey, formatVersion });其中的contentHash与promptHash有专门构造规则:
promptHash(buildPromptHash,L35-L48):从 prompt 中抽取<instructions>与<context>两个标签块,拼合后哈希;若 prompt 中不存在任何标签则回退为对整个 prompt 做哈希;contentHash(buildPromptContentHash,L50-L60):取自实际发送给模型的<content>块,因此幻灯片时间轴、转录补充信息等会影响该键;若 prompt 内容为空,则回退为附件的二进制字节哈希(buildAttachmentContentHash,L62-L76,其中记录附件的 kind、mediaType、byteLength 与字节哈希)。
长度(buildLengthKey,L78-L82)区分预设模式与字符数模式:preset:short或chars:1200这类形式;语言(buildLanguageKey,L84-L86)在自动模式下记为auto,否则用语言 tag。
Slides 键(buildSlidesCacheKeyValue,L125-L153)包含url与一组幻灯片设置:ocr、outputDir、sceneThreshold、autoTuneThreshold、maxSlides、minDurationSeconds。
formatVersion:格式级失效开关
formatVersion是 core 包中的硬编码常量(packages/core/src/runtime/cache-store.ts 中CACHE_FORMAT_VERSION = 2)。当提示词格式或缓存负载结构发生不兼容变更时,维护者会提升该常量,使所有旧缓存一次性全部失效——这是比逐个改键更省事的全局失效手段。
摘要缓存"内容哈希优先"语义
摘要键不包含 URL,只包含contentHash。这意味着:同一篇文章以不同 URL 形式出现(例如带?utm_source参数、或镜像站),只要提取出的<content>块一致,摘要缓存就能命中,避免了重复调用 LLM。这是原文档强调的"cache hit even if URL differs (content hash wins)"。
四、配置:cache 与 cache.media
完整配置示例(docs/cache.md 原文):
{ "cache": { "enabled": true, "maxMb": 512, "ttlDays": 30, "path": "~/.summarize/cache.sqlite", "media": { "enabled": true, "maxMb": 2048, "ttlDays": 7, "path": "~/.summarize/cache/media", "verify": "size" } } }默认值(与源码 packages/core/src/runtime/cache-store.ts 一致):
cache.enabled = truecache.maxMb = 512cache.ttlDays = 30cache.path未设置(回退~/.summarize/cache.sqlite)cache.media.maxMb = 2048、cache.media.ttlDays = 7、cache.media.verify = "size"(src/media-cache.ts)
配置解析位于 src/config/sections.ts:
parseCacheConfig(L150-L175):读取cache对象,enabled必须是布尔值,maxMb/ttlDays必须是大于 0 的数字,path必须是非空字符串;任一字段非法都会抛出带路径与字段名的明确错误;parseMediaCacheConfig(L110-L148):cache.media必须是对象,verify只接受"none"、"size"、"hash"三者之一,否则报错。
类型定义见 src/config/types.ts。所有字段均可省略,省略即采用默认值。
五、媒体缓存(下载文件):独立于 SQLite 的文件缓存
需要特别区分:媒体缓存是用于已下载媒体文件(yt-dlp 或直接媒体 URL)的独立文件缓存,不是 SQLite 数据库(docs/cache.md 原文明确标注 "This isnotthe SQLite DB")。
- 默认路径:
~/.summarize/cache/media; - TTL:7 天;
- 容量上限:2048 MB;
- CLI:
--no-media-cache仅禁用媒体缓存; - 注意:
--no-cache不会禁用媒体缓存——二者互相独立。
实现位于 src/media-cache.ts,其机制与 SQLite 缓存有显著差异:
- 目录内一个
index.json充当索引(version: 1,按 URL 的 SHA-256 映射到文件名、大小、可选 sha256、媒体类型、时间戳与过期时间); - 通过
.lock文件实现跨进程索引锁:带 PID 心跳(每 30 秒刷新 mtime),锁文件超过 5 分钟判定为陈旧并接管(L134-L173); - 文件命名
{sha256(url)}{ext},扩展名根据原始文件名或 mediaType 推断(如.mp3、.m4a、.mp4、.m3u8,推断规则见 L81-L100); verify完整性校验三档:size(默认,比对记录大小与磁盘大小)、hash(重算文件 SHA-256)、none(不校验);- 写入先落到
.incoming-*暂存文件,再原子 rename 进缓存目录,避免半成品污染缓存; put时若单文件大小超过maxBytes则直接不缓存。
媒体缓存与 SQLite 缓存的对照:
| 维度 | SQLite 缓存 | 媒体缓存 |
|---|---|---|
| 存储形态 | 单文件cache.sqlite+ WAL/SHM | 目录 +index.json |
| 默认上限 | 512 MB | 2048 MB |
| 默认 TTL | 30 天 | 7 天 |
| 索引 | cache_entries表 | index.json |
| 并发控制 | SQLitebusy_timeout+ WAL | .lock文件 + 心跳 |
六、CLI 标志与运维命令
三个缓存相关 CLI 标志(定义见 src/run/help.ts):
| 标志 | 作用 |
|---|---|
--no-cache | 绕过摘要缓存的读写(LLM 输出)。提取/转录缓存仍然生效 |
--cache-stats | 打印缓存统计信息后退出 |
--clear-cache | 删除缓存数据库(连同 WAL/SHM),必须单独使用 |
校验逻辑位于 src/run/runner-setup.ts:--clear-cache与--cache-stats都要求"must be used alone",一旦与其他参数混用会直接抛错(错误文案见 src/locale.ts,支持多语言本地化)。--no-cache只影响摘要缓存,而幻灯片模式还有独立的--no-cache语义("Bypass slide cache (force re-extract)",见 src/run/help.ts),说明缓存开关是按缓存类别精确作用的。
统计与清理的实现
readCacheStats(src/cache.ts)以只读模式打开数据库(PRAGMA query_only = ON),按kind分组统计条目数并汇总size_bytes;磁盘占用统计(getSqliteFileSizeBytes,L121-L130)会把主库、-wal、-shm三个文件大小相加,且容忍侧车文件在 checkpoint 期间短暂消失。
clearCacheFiles(src/cache.ts)使用rmSync(force: true)依次删除主库、-wal、-shm,即使文件不存在也不会报错。
七、驱逐策略:TTL + LRU 双保险
SQLite 缓存的驱逐(src/cache-store.ts)
- TTL 清理(读/写时触发):
sweepExpired用expires_at <= now一次性查出全部过期条目并删除;读取时也会惰性检查单条是否过期(readEntry),过期即删并返回 miss; - 容量清理(写时触发):
enforceSize先求SUM(size_bytes),若超过maxBytes,则按last_accessed_at升序每次批量取 50 条最久未访问的条目删除(LRU 语义),直到总大小回落到上限以内,最后执行PRAGMA incremental_vacuum回收空间。
此外幻灯片缓存比较特殊:deleteEntry对kind === "slides"的条目会调用cleanupSlidesPayload,同时通过buildReferencedSlideArtifacts收集仍被其他 slides 条目引用的磁盘图片路径并予以保留(src/cache-store.ts),避免误删共享资源;clear()全量清理时也会先对 slides 条目做负载清理再清表。相关实现见 src/cache-slides-cleanup.ts。
媒体缓存的驱逐(src/media-cache.ts)
- TTL:
pruneExpired删除expiresAtMs已到期的条目(含磁盘文件与索引项); - 容量:
enforceMaxBytes先补齐缺失的sizeBytes,若总量超限则按lastAccessAtMs升序(LRU)逐条删除直到达标。
两套缓存都是"读时惰性校验 + 写时主动清扫"的组合,整体设计保证磁盘占用始终有界。
八、与其他组件的边界:扩展面板缓存、转录来源与共享格式
浏览器扩展使用独立面板缓存
CLI 的 SQLite 缓存与 Chrome 扩展的面板缓存完全隔离:扩展使用chrome.storage.local,URL 维度键控条目采用30 天 TTL 与 8 MB 容量上限;它与 core 共享可移植的行格式化(buildPortableCacheRow/parseCacheJson,见 packages/core/src/runtime/cache-store.ts),而 daemon 的提取与摘要缓存仍然使用 SQLite。
转录来源库存与命中诊断
core 持有转录来源库存(TRANSCRIPT_SOURCES),同时服务提取与 SQLite 读取两条路径,涵盖内嵌字幕与 YouTube 原生媒体;缓存命中时会保留条目的原始来源诊断信息(source、service、resourceKey、namespace、formatVersion一并写入负载,见 src/cache-store.ts),方便排查某条转录究竟来自哪个通道。
无第三方依赖
整个缓存体系只依赖 Node 24 / Bun 原生 SQLite 与 Node 内置crypto/fs模块,没有引入任何第三方 SQLite 或哈希库——这在 src/cache.ts 与 src/cache-database.ts 的 import 清单中可以完整验证。
九、实践建议与排查速查
基于上述设计与实现,给出以下可直接落地的操作建议:
- 判断是否命中摘要缓存:摘要键 =
sha256({contentHash, promptHash, model, lengthKey, languageKey, formatVersion})。若你修改了自定义 prompt、切换了模型、改动了输出长度或语言,键必然变化,属正常失效;若只是 URL 不同而正文相同,仍可命中。 - 本地文件转录不失效:请确认文件
mtime是否变化——转录键包含fileMtime,编辑文件后会自然产生新键。 - 限制磁盘占用:调小
cache.maxMb/cache.ttlDays,或把cache.path指向专用磁盘。 - 精确绕过缓存:只想强制重新总结用
--no-cache(提取/转录缓存仍生效);想完全关掉媒体下载缓存用--no-media-cache;两者互不影响。 - 查看与清理:
--cache-stats查看各 kind 条目数与总大小;--clear-cache一次性删除整个数据库(需单独使用,并会自动连带 WAL/SHM)。 - 怀疑缓存负载不兼容:关注
CACHE_FORMAT_VERSION(当前为 2),升级后旧缓存整体作废属预期行为。 - 媒体文件损坏:把
cache.media.verify从默认的size提升为hash,可在每次读取时重算 SHA-256 校验;追求极致性能可设为none。
延伸阅读:CLI 配置整体说明见 docs/config.md,命令行用法见 docs/commands/index.md 与 docs/cli.md;缓存相关测试覆盖可参考 tests/cache.store.test.ts、tests/cache.keys.test.ts、tests/cache-stats.test.ts、tests/cache.path.test.ts、tests/cache-state.refresh.test.ts 与 tests/media-cache.test.ts。
【免费下载链接】summarizePoint at any URL/YouTube/Podcast or file. Get the gist. CLI and Chrome Extension.项目地址: https://gitcode.com/GitHub_Trending/summarize/summarize
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考