Releases: xscanzm/recall
Release list
0.5.12
0.5.11
v0.5.11 chore(release): 版本提升至 0.5.11 并更新发布说明
0.5.10
0.5.9
v0.5.9 — 安全加固 + Electron 43 升级 + 渲染层健壮性
本次版本以安全加固为主线:收紧更新安装、IPC 与更新下载链路的安全边界,为 Worker 默认模型代理加入按 IP 限流;同时升级 Electron 至 43.2.0 并启用 ASAR 完整性校验,补齐隐私数据一致性、数据库查询护栏与渲染层健壮性,并恢复测试覆盖率真实口径。
修复
1. 更新与 IPC 安全加固
- 安装包路径可信化:
update:installAndQuit移除渲染层可控的安装包路径参数,改为主进程内部已验证路径,配合 updates 目录守卫与契约严格校验,杜绝渲染层注入任意可执行路径 - IPC 全链路来源校验:所有 handler 增加 senderFrame 校验(fail-closed:null frame / 子 frame / 未注册窗口一律拒绝)
- 更新下载 URL 主机白名单:仅允许
recall-update.ppclaw.online,其余主机一律拒绝
2. Worker 默认模型代理按 IP 限流
- 基于 D1 原子计数实现按客户端 IP 每日限流,状态轮询请求豁免,超限返回
429+Retry-After
改进
1. 隐私与数据一致性
- 调试载荷自动清理:
model_jobs调试载荷默认保留 30 天自动删除,保留时长可用环境变量调整 - 级联删除事务化:
memory:deleteObject级联删除纳入事务,任一环节失败整体回滚 - 文档与代码事实对齐:README / doc 07 / worker README 的隐私声明与代码事实对齐(默认模型服务、遥测边界、R2 临时保留)
2. 数据库
- 分页护栏:observations / facts 时间范围查询增加 LIMIT 分页(默认 200)
- 动态 SQL 标识符校验:运行时动态 SQL 标识符增加允许集合校验(fail-closed)
- 复合索引:新增
timeline_blocks复合索引(date_key, start_at)(迁移 030)
3. 渲染层健壮性
- 乱序响应守卫:搜索 / 项目详情页快速连续操作不再被旧响应覆盖
- IPC 调用错误处理补全 + 定时器清理
- 对话框焦点陷阱 / Tab 循环 / Escape 关闭、tab 键盘导航、原生 alert/confirm 替换
- 列表组件 React.memo 化、删除死组件 ReportEditor、内联样式迁移
4. 工程与升级
- Electron 32 → 43.2.0(含 better-sqlite3 11→13、electron-builder 26),启用 ASAR 完整性 fuses(
EnableEmbeddedAsarIntegrityValidation+OnlyLoadAppFromAsar) - 测试覆盖率真实口径:vitest 恢复
all: true全量口径,基线 31.9 / 68.66 / 55.36 / 31.9,只升不降 ratchet 门禁 - UpdateService 核心路径单测:函数覆盖 92.3%
- CI 门禁增强:macOS 覆盖率、发布依赖测试 CI(
workflow_call跨文件 gate)、e2e 产物上传、test:maintenance接入 maintain-recall-data跨平台默认路径LinkerSceneJudgeWorker按职责拆分,ID 生成 / 日期 / 时区工具统一,handlers.ts通道模块化迁移(234 行)
已知限制
- 窗口拖拽在缩放显示器上存在约 4.5× 灵敏度偏差(Chromium 144 screenX 反馈所致,无崩溃影响,CI 正常屏幕不受影响)
验证
- TypeScript 主进程与渲染进程类型检查通过
- UpdateService 核心路径单测通过(函数覆盖 92.3%)
- 本地构建与 NSIS 安装包打包通过
v0.5.7 — macOS 客户端完整支持 + 免证书解隔离助手 + GitHub Actions 云端打包
本次版本正式发布 macOS 客户端支持,提供全平台原生 Worker 编译支持、macOS 屏幕录制/辅助功能权限引导、DMG 免证书解隔离安装助手及 GitHub Actions 自动云端打包流程。
新增与改进
1. macOS 免证书解隔离安装助手
- DMG 一键修复脚本:资源中加入可执行 Bash 脚本
双击修复安装.command,解决非商业签名应用在 macOS 上被提示“应用已损坏,无法打开”的问题,自动解除com.apple.quarantine隔离属性。 - 文档与官网支持:README 与官网新增 macOS 下载入口及 Mac 初次安装权限解锁说明。
2. macOS 权限与隐私系统适配
MacPermissionsService:支持检测 macOS 屏幕录制 (Screen Capture) 和辅助功能 (Accessibility) 权限状态,并提供一键跳转系统隐私设置面板接口。
3. Worker 跨平台运行与进程清理
- 适配 macOS 上无
.exe扩展名的二进制 Worker 可执行文件(rapidocr-worker)查找与运行机制; - 增强 POSIX 进程树清理 (
process.kill(-pid)),确保应用退出或引擎终止时 Mac 平台常驻 Worker 无残留; - 新增
scripts/build-rapidocr-worker.shmacOS 平台 Worker 打包脚本。
4. GitHub Actions 自动云端打包 (CI/CD)
- 新增
.github/workflows/mac-ci.ymlCI 检查,确保 Mac 环境下的打包构建与单测契约持续通过; - 更新
.github/workflows/release.yml发布流程,当发布 Tag 时自动在macos-latest机器上编译产出 Apple Silicon (arm64) 与 Intel (x64) 的 macOS.dmg与.zip镜像并关联提交到 GitHub Release。
v0.5.6 — 更新下载分片 + 断点续传(解决国内 R2 大文件下载不稳定)
本次版本聚焦解决用户反馈的"更新失败"问题:国内访问 Cloudflare R2 下载 180MB+ 安装包时连接不稳定,旧的流式下载一旦中断就必须从头开始,导致更新成功率低。新方案用 HTTP Range 请求分片下载 + 断点续传,中断后可从断点继续,显著提升大文件下载稳定性。
改进
1. UpdateService 分片 + 断点续传下载
重写 downloadUpdate 方法,从单次流式下载改为分片 + 断点续传:
- HEAD 探测:下载前先发 HEAD 请求探测服务器是否支持
Accept-Ranges: bytes,支持则走分片路径,不支持则回退到原流式下载(保证兼容性) - 分片下载:每片 4MB,使用 HTTP Range 请求
bytes=start-end,只下载该范围的数据 - 断点续传:每片下载成功后立即追加写入
.part文件并更新.meta.json元数据(记录 version/sha256/bytesTotal/bytesDownloaded);进程中断后下次启动从断点继续,无需从头下载 - 单片超时保护:单片 30 秒超时(
AbortController),超时自动重试,避免单个分片卡死整个下载 - 单片重试:单片最多重试 5 次,退避 1s/2s/4s/8s/16s
- 整体轮次:最多 6 轮断点续传,每轮从上次中断处继续;连续 20 片失败则中止
- 元数据严格校验:
readDownloadMeta校验 version/sha256/chunkSize/bytesTotal 与.part实际大小一致性,脏断点自动丢弃重下 - 整体 SHA256 校验:下载完成后校验最终文件 SHA256,不匹配则清理所有产物并失败
2. 文件布局与清理策略
- 文件布局:
updates/Recall-{version}-setup.exe— 最终安装包updates/Recall-{version}-setup.exe.part— 下载中分片合并文件updates/Recall-{version}-setup.exe.meta.json— 断点续传元数据
cleanupIncompleteDownloads语义调整:只清理.tmp(流式回退产物),保留.part+.meta.json供断点续传;下次启动readDownloadMeta会校验.part一致性,脏断点自动丢弃重下
验证
- TypeScript 主进程与渲染进程类型检查通过
- 本地构建与 NSIS 安装包打包通过
以下为历史版本发布说明:
v0.5.5 — 截图采集架构重构(单窗口捕获 + 遮挡门禁 + 三后端降级链)
本次版本重构截图采集管线,从"为了拍 1 个窗口打扰整个系统"改为"只碰目标窗口、不打扰任何第三方应用"。旧方案 desktopCapturer 为了抓 1 个窗口会对系统里每个顶层窗口发 WM_PRINT 强制同步渲染,导致钉钉等 GPU 合成应用偶发白屏;新方案用 getDisplayMedia + Windows Graphics Capture 只读 DWM 合成表面,零副作用,并加上遮挡门禁杜绝错误/越权采集。
新增功能
1. WindowFrameGrabber(单窗口 getDisplayMedia 捕获)
替代旧的 desktopCapturer.captureWindow 全窗口抓图,只触及目标窗口:
- 零抓图枚举:
thumbnailSize:{0,0}只取 id/name 不产图像,目标筛选发生在产生任何图像之前 - getDisplayMedia + WGC:用
getDisplayMedia对那一个窗口开捕获会话,底层走 Windows.Graphics.Capture(DWM 读现成合成表面,不发任何WM_PRINT),从根上消除对第三方应用的副作用 - 隐藏 BrowserWindow 抓图宿主:1×1 隐藏窗口通过
setDisplayMediaRequestHandler把"用户选哪个窗口"替换成我们指定的 source,不弹系统选择器;宿主页capture-host.html必须是file://协议(data:/about:blank是 opaque origin 会被 getDisplayMedia 拒绝) - Chromium feature 开关:
app.ts启用AllowWgcWindowCapturer(走 WGC)+ 关闭AllowWgcWindowZeroHz(避免静止内容不产新帧导致超时) - 抓图脚本通过
executeJavaScript注入宿主页(主进程 tsconfig 无 DOM lib,不值得为 40 行脚本给整个项目加 DOM lib)
2. captureOcclusion(遮挡门禁)
纯函数模块,服务于 screen crop 兜底路径,防止目标窗口被遮挡时裁到错误内容或隐私泄露:
findVisibleOccluders:拿active-win.getOpenWindows()的 Z 序,只把 Z 序严格在目标之前的窗口算作遮挡;排除自身进程、最小化窗口(Windows -32000 哨兵坐标)、零面积窗口;目标不在列表返回null(保守不采)computeOccludedRatio:扫描线算矩形并集面积(不是简单累加,避免两个互相重叠的遮挡窗口把比例算过 1)MAX_BENIGN_OCCLUSION_RATIO = 0.35:良性遮挡容忍上限;敏感遮挡(过不了 PrivacyGuard)一律跳过,不看比例
改进
3. CaptureService 三后端降级链
从单一抓图路径改为按"对第三方应用的伤害面从小到大"排序的三后端降级链:
| 优先级 | 后端 | 行为 | 默认 |
|---|---|---|---|
| 1 | window_display_media |
单窗口 getDisplayMedia,只碰目标 | 启用 |
| 2 | screen_crop_fallback |
抓整屏再裁,需先过遮挡门禁 | 启用 |
| 3 | window(旧全窗口缩略图) |
把系统里所有窗口都抓一遍 + WM_PRINT | 禁用,只留应急开关 |
关键新增方法:
captureSingleFrame:依次试三条后端captureViaWindowFrameSource:调用WindowFrameSource接口(结构类型,避免循环依赖 + 测试可注入假实现)checkScreenCropOcclusion:screen crop 前的遮挡门禁analyzeCaptureVisualQuality:检测全黑退化帧(nearBlackRatio / luminanceStdDev / edgeDensity),对付 GDI 抓 GPU 合成窗口返回全黑shouldUseScreenCropFallback:信息量比较,决定是否用裁剪图替换退化的窗口图coalesceCaptureCandidate+runSerializedCapture:候选合并 + 串行化- 新增
CaptureBackend类型 +captureMethod字段落到CaptureBundle;新增occluded/no_safe_backend两个 skip reason pickReportedBackend:多帧落在不同后端时报告"最偏离首选"的那个,便于事后定位问题
核心产品决策:三条路都走不通时跳过这次采集,而不是用伤害用户其它应用的方式硬采——"少一条记忆,好过把用户正在用的应用打白"。
4. ActivityService 增强
新增两个方法直接服务截图采集:
getFreshActiveWindowInfo():屏幕裁剪 fallback 在捕获前后各调用一次,防止窗口切换后裁到其他应用(双重校验:bounds 一致 + windowId/title 一致)getOpenWindowsSnapshot():返回 Z 序窗口列表给遮挡判定用;严格区分 null("不知道"必须保守不采)与空数组("确认没别的窗口")ActivityWindowInfo新增bounds、processId字段
验证
- TypeScript 主进程与渲染进程类型检查通过
- 新增单元测试:
CaptureService.test.ts:守住"对第三方窗口的伤害面"不变量——首选后端只碰目标;整屏裁剪必须先证明矩形归自己;旧路径默认走不到;遮挡门禁四类场景全覆盖captureOcclusion.test.ts:扫描线并集算法边界情况(互相重叠、完全重合、不相交、最小化、零面积、阈值边界 0.34/0.36)WindowFrameGrabber.assets.test.ts:钉住宿主页与加载方的同目录契约ActivityService.test.ts:新增活动服务测试
- 本地构建与 NSIS 安装包打包通过
以下为历史版本发布说明:
v0.5.4 — 混合搜索(FTS5 trigram + BGE 本地 embedding)+ 身份归一化与重复对象审计
本次版本聚焦记忆搜索能力升级与对象身份治理:搜索从单一 LIKE 全表扫描升级为 FTS5 trigram 词法召回 + BGE 本地 embedding 语义召回的双路混合检索,支持中文子串精确匹配与"意思相近但字面不同"的语义召回;同时引入身份归一化工具与只读审计服务,解决历史数据中同名不同人或同人异名的问题。
新增功能
1. 混合搜索(HybridSearchService)
将记忆搜索从单一 LIKE '%关键词%' 全表扫描升级为 FTS5 trigram 词法召回 + BGE 本地 embedding 语义召回的双路混合检索:
- migration 028:重建
memory_search_fts虚拟表,tokenize 改为trigram,支持中文任意 3 字符子串精确匹配,查询从 O(N) 全表扫描降到 O(log N) 索引查找;覆盖 7 类对象(facts/scenes/tasks/projects/decisions/people/reports),每类配 INSERT/UPDATE/DELETE 触发器自动维护索引 - migration 029:新建
memory_embeddings表(512 维 float32 向量 BLOB 存储)+memory_embedding_queue索引队列表 - HybridSearchService:词法保底(FTS5 top 500)+ 向量召回(cosine 相似度 > 0.15,top 100)→ RRF 融合(k=60,词法权重 1.0,向量权重 0.7)→ 精确匹配提权(exact_id +10、exact_title +5)→ 静默降级(向量路超时 1500ms 或失败时自动回退纯 FTS)
- MemorySearchResponse 新增
quality: "strong" | "weak" | "none"字段,区分双路命中、单路命中、无结果 - 观察数据走独立的
observation_search_fts表,同样带 bm25 排序和 LIKE 降级 - 新增
getCandidates+batchGetFtsRowsByKeys批量回填候选详情,消除 N+1 查询
2. BGE 本地 embedding 推理
全本地 CPU 推理的 embedding 服务,无需外网、无需 GPU、无需配置 API Key:
- 模型:
bge-small-zh-v1.5(BGE 中文小模型),512 维,ONNX 量化版(model_quantized.onnx),位于resources/embedding/bge-small-zh-v1.5/ - 进程复用:EmbeddingWorkerClient 复用
rapidocr-worker进程,通过--mode embedding --model-dir <path>参数切换模式,减少打包体积和进程数量 - 进程间协议:stdin 写 JSON 请求(
{id, type: "query"|"document", texts}),stdout 读 JSON 响应;单批最多 32 条文本,超过自动分块 - 三种部署形态自动适配:打包态
rapidocr-worker.exe/ 开发态本地构建 exe / Python 源码rapidocr_worker.py - 进程崩溃自动重建,通过
childGeneration机制隔离旧请求,避免僵尸响应污染新进程 EmbeddingIndexerService:后台索引循环,generation字段防覆盖(旧 embedding 结果不会覆盖已被新内容重新入队的对象)MemoryEmbeddingRepository.listVectors用count:max(updated_at)签名缓存全量向量,避免每次搜索都从 DB 反序列化所有 BLOB
3. 身份归一化与重复对象审计
引入"同名不等于同一身份"的核心约束,解决历史数据中同名不同人或同人异名的问题:
- 共享工具
src/shared/identity.ts:normalizeIdentity:NFKC + trim + 折叠内部空白 + toLowerCase,保留标点(Recall-v1.0与Recall.v1.0不会被误并)comparePersonIdentity:名称 + role/organization 强字段三档判定(strong_field_match/normalized_name_match/conflict_detected)
- IdentityAuditService:只读 dry-run 审计,扫描未归档 projects 和未软删 people,按归一化名称分组,给出三档分类;不运行真实合并、不修改 DB、不把"完全同名"直接判定为同一人物
- MemoryObjectRepository 新增
findProjectByExactIdentity/findPersonByExactIdentity,基于精确身份查询 - MemoryObjectAdmissionService:
admitOrAccumulate从模糊匹配切换到精确身份匹配,保留admissionDecidedBy === "user"的对象不被自动 reassess 覆盖 - cascadeMark.mergeObjects:新增
mergeNormalizedAliases用归一化去重别名;人物合并新增组织信息补全;合并流程包成事务确保原子化
改进
4. BatchProcessor lane 化并发
- 新增 backlog/fresh/wi...
0.5.8
v0.5.6 — 更新下载分片 + 断点续传
v0.5.6 — 更新下载分片 + 断点续传(解决国内 R2 大文件下载不稳定)
本次版本聚焦解决用户反馈的"更新失败"问题:国内访问 Cloudflare R2 下载 180MB+ 安装包时连接不稳定,旧的流式下载一旦中断就必须从头开始,导致更新成功率低。新方案用 HTTP Range 请求分片下载 + 断点续传,中断后可从断点继续,显著提升大文件下载稳定性。
改进
1. UpdateService 分片 + 断点续传下载
重写 downloadUpdate 方法,从单次流式下载改为分片 + 断点续传:
- HEAD 探测:下载前先发 HEAD 请求探测服务器是否支持
Accept-Ranges: bytes,支持则走分片路径,不支持则回退到原流式下载(保证兼容性) - 分片下载:每片 4MB,使用 HTTP Range 请求
bytes=start-end,只下载该范围的数据 - 断点续传:每片下载成功后立即追加写入
.part文件并更新.meta.json元数据(记录 version/sha256/bytesTotal/bytesDownloaded);进程中断后下次启动从断点继续,无需从头下载 - 单片超时保护:单片 30 秒超时(
AbortController),超时自动重试,避免单个分片卡死整个下载 - 单片重试:单片最多重试 5 次,退避 1s/2s/4s/8s/16s
- 整体轮次:最多 6 轮断点续传,每轮从上次中断处继续;连续 20 片失败则中止
- 元数据严格校验:
readDownloadMeta校验 version/sha256/chunkSize/bytesTotal 与.part实际大小一致性,脏断点自动丢弃重下 - 整体 SHA256 校验:下载完成后校验最终文件 SHA256,不匹配则清理所有产物并失败
2. 文件布局与清理策略
- 文件布局:
updates/Recall-{version}-setup.exe— 最终安装包updates/Recall-{version}-setup.exe.part— 下载中分片合并文件updates/Recall-{version}-setup.exe.meta.json— 断点续传元数据
cleanupIncompleteDownloads语义调整:只清理.tmp(流式回退产物),保留.part+.meta.json供断点续传;下次启动readDownloadMeta会校验.part一致性,脏断点自动丢弃重下
验证
- TypeScript 主进程与渲染进程类型检查通过
- 本地构建与 NSIS 安装包打包通过
v0.5.5 — 截图采集架构重构(单窗口捕获 + 遮挡门禁 + 三后端降级链)
v0.5.5 — 截图采集架构重构(单窗口捕获 + 遮挡门禁 + 三后端降级链)
本次版本重构截图采集管线,从"为了拍 1 个窗口打扰整个系统"改为"只碰目标窗口、不打扰任何第三方应用"。旧方案 desktopCapturer 为了抓 1 个窗口会对系统里每个顶层窗口发 WM_PRINT 强制同步渲染,导致钉钉等 GPU 合成应用偶发白屏;新方案用 getDisplayMedia + Windows Graphics Capture 只读 DWM 合成表面,零副作用,并加上遮挡门禁杜绝错误/越权采集。
新增功能
1. WindowFrameGrabber(单窗口 getDisplayMedia 捕获)
替代旧的 desktopCapturer.captureWindow 全窗口抓图,只触及目标窗口:
- 零抓图枚举:
thumbnailSize:{0,0}只取 id/name 不产图像,目标筛选发生在产生任何图像之前 - getDisplayMedia + WGC:用
getDisplayMedia对那一个窗口开捕获会话,底层走 Windows.Graphics.Capture(DWM 读现成合成表面,不发任何WM_PRINT),从根上消除对第三方应用的副作用 - 隐藏 BrowserWindow 抓图宿主:1×1 隐藏窗口通过
setDisplayMediaRequestHandler把"用户选哪个窗口"替换成我们指定的 source,不弹系统选择器;宿主页capture-host.html必须是file://协议(data:/about:blank是 opaque origin 会被 getDisplayMedia 拒绝) - Chromium feature 开关:
app.ts启用AllowWgcWindowCapturer(走 WGC)+ 关闭AllowWgcWindowZeroHz(避免静止内容不产新帧导致超时) - 抓图脚本通过
executeJavaScript注入宿主页(主进程 tsconfig 无 DOM lib,不值得为 40 行脚本给整个项目加 DOM lib)
2. captureOcclusion(遮挡门禁)
纯函数模块,服务于 screen crop 兜底路径,防止目标窗口被遮挡时裁到错误内容或隐私泄露:
findVisibleOccluders:拿active-win.getOpenWindows()的 Z 序,只把 Z 序严格在目标之前的窗口算作遮挡;排除自身进程、最小化窗口(Windows -32000 哨兵坐标)、零面积窗口;目标不在列表返回null(保守不采)computeOccludedRatio:扫描线算矩形并集面积(不是简单累加,避免两个互相重叠的遮挡窗口把比例算过 1)MAX_BENIGN_OCCLUSION_RATIO = 0.35:良性遮挡容忍上限;敏感遮挡(过不了 PrivacyGuard)一律跳过,不看比例
改进
3. CaptureService 三后端降级链
从单一抓图路径改为按"对第三方应用的伤害面从小到大"排序的三后端降级链:
| 优先级 | 后端 | 行为 | 默认 |
|---|---|---|---|
| 1 | window_display_media |
单窗口 getDisplayMedia,只碰目标 | 启用 |
| 2 | screen_crop_fallback |
抓整屏再裁,需先过遮挡门禁 | 启用 |
| 3 | window(旧全窗口缩略图) |
把系统里所有窗口都抓一遍 + WM_PRINT | 禁用,只留应急开关 |
关键新增方法:
captureSingleFrame:依次试三条后端captureViaWindowFrameSource:调用WindowFrameSource接口(结构类型,避免循环依赖 + 测试可注入假实现)checkScreenCropOcclusion:screen crop 前的遮挡门禁analyzeCaptureVisualQuality:检测全黑退化帧(nearBlackRatio / luminanceStdDev / edgeDensity),对付 GDI 抓 GPU 合成窗口返回全黑shouldUseScreenCropFallback:信息量比较,决定是否用裁剪图替换退化的窗口图coalesceCaptureCandidate+runSerializedCapture:候选合并 + 串行化- 新增
CaptureBackend类型 +captureMethod字段落到CaptureBundle;新增occluded/no_safe_backend两个 skip reason pickReportedBackend:多帧落在不同后端时报告"最偏离首选"的那个,便于事后定位问题
核心产品决策:三条路都走不通时跳过这次采集,而不是用伤害用户其它应用的方式硬采——"少一条记忆,好过把用户正在用的应用打白"。
4. ActivityService 增强
新增两个方法直接服务截图采集:
getFreshActiveWindowInfo():屏幕裁剪 fallback 在捕获前后各调用一次,防止窗口切换后裁到其他应用(双重校验:bounds 一致 + windowId/title 一致)getOpenWindowsSnapshot():返回 Z 序窗口列表给遮挡判定用;严格区分 null("不知道"必须保守不采)与空数组("确认没别的窗口")ActivityWindowInfo新增bounds、processId字段
验证
- TypeScript 主进程与渲染进程类型检查通过
- 新增单元测试:
CaptureService.test.ts:守住"对第三方窗口的伤害面"不变量——首选后端只碰目标;整屏裁剪必须先证明矩形归自己;旧路径默认走不到;遮挡门禁四类场景全覆盖captureOcclusion.test.ts:扫描线并集算法边界情况(互相重叠、完全重合、不相交、最小化、零面积、阈值边界 0.34/0.36)WindowFrameGrabber.assets.test.ts:钉住宿主页与加载方的同目录契约ActivityService.test.ts:新增活动服务测试
- 本地构建与 NSIS 安装包打包通过
v0.5.4 — 混合搜索(FTS5 trigram + BGE 本地 embedding)+ 身份归一化与重复对象审计
v0.5.4 — 混合搜索(FTS5 trigram + BGE 本地 embedding)+ 身份归一化与重复对象审计
本次版本聚焦记忆搜索能力升级与对象身份治理:搜索从单一 LIKE 全表扫描升级为 FTS5 trigram 词法召回 + BGE 本地 embedding 语义召回的双路混合检索,支持中文子串精确匹配与"意思相近但字面不同"的语义召回;同时引入身份归一化工具与只读审计服务,解决历史数据中同名不同人或同人异名的问题。
新增功能
1. 混合搜索(HybridSearchService)
将记忆搜索从单一 LIKE '%关键词%' 全表扫描升级为 FTS5 trigram 词法召回 + BGE 本地 embedding 语义召回的双路混合检索:
- migration 028:重建
memory_search_fts虚拟表,tokenize 改为trigram,支持中文任意 3 字符子串精确匹配,查询从 O(N) 全表扫描降到 O(log N) 索引查找;覆盖 7 类对象(facts/scenes/tasks/projects/decisions/people/reports),每类配 INSERT/UPDATE/DELETE 触发器自动维护索引 - migration 029:新建
memory_embeddings表(512 维 float32 向量 BLOB 存储)+memory_embedding_queue索引队列表 - HybridSearchService:词法保底(FTS5 top 500)+ 向量召回(cosine 相似度 > 0.15,top 100)→ RRF 融合(k=60,词法权重 1.0,向量权重 0.7)→ 精确匹配提权(exact_id +10、exact_title +5)→ 静默降级(向量路超时 1500ms 或失败时自动回退纯 FTS)
- MemorySearchResponse 新增
quality: "strong" | "weak" | "none"字段,区分双路命中、单路命中、无结果 - 观察数据走独立的
observation_search_fts表,同样带 bm25 排序和 LIKE 降级 - 新增
getCandidates+batchGetFtsRowsByKeys批量回填候选详情,消除 N+1 查询
2. BGE 本地 embedding 推理
全本地 CPU 推理的 embedding 服务,无需外网、无需 GPU、无需配置 API Key:
- 模型:
bge-small-zh-v1.5(BGE 中文小模型),512 维,ONNX 量化版(model_quantized.onnx),位于resources/embedding/bge-small-zh-v1.5/ - 进程复用:EmbeddingWorkerClient 复用
rapidocr-worker进程,通过--mode embedding --model-dir <path>参数切换模式,减少打包体积和进程数量 - 进程间协议:stdin 写 JSON 请求(
{id, type: "query"|"document", texts}),stdout 读 JSON 响应;单批最多 32 条文本,超过自动分块 - 三种部署形态自动适配:打包态
rapidocr-worker.exe/ 开发态本地构建 exe / Python 源码rapidocr_worker.py - 进程崩溃自动重建,通过
childGeneration机制隔离旧请求,避免僵尸响应污染新进程 EmbeddingIndexerService:后台索引循环,generation字段防覆盖(旧 embedding 结果不会覆盖已被新内容重新入队的对象)MemoryEmbeddingRepository.listVectors用count:max(updated_at)签名缓存全量向量,避免每次搜索都从 DB 反序列化所有 BLOB
3. 身份归一化与重复对象审计
引入"同名不等于同一身份"的核心约束,解决历史数据中同名不同人或同人异名的问题:
- 共享工具
src/shared/identity.ts:normalizeIdentity:NFKC + trim + 折叠内部空白 + toLowerCase,保留标点(Recall-v1.0与Recall.v1.0不会被误并)comparePersonIdentity:名称 + role/organization 强字段三档判定(strong_field_match/normalized_name_match/conflict_detected)
- IdentityAuditService:只读 dry-run 审计,扫描未归档 projects 和未软删 people,按归一化名称分组,给出三档分类;不运行真实合并、不修改 DB、不把"完全同名"直接判定为同一人物
- MemoryObjectRepository 新增
findProjectByExactIdentity/findPersonByExactIdentity,基于精确身份查询 - MemoryObjectAdmissionService:
admitOrAccumulate从模糊匹配切换到精确身份匹配,保留admissionDecidedBy === "user"的对象不被自动 reassess 覆盖 - cascadeMark.mergeObjects:新增
mergeNormalizedAliases用归一化去重别名;人物合并新增组织信息补全;合并流程包成事务确保原子化
改进
4. BatchProcessor lane 化并发
- 新增 backlog/fresh/window 三 lane 调度:
BACKLOG_CONCURRENCY = BATCH_CONCURRENCY - 1 = 4,始终保留 1 个槽给最新数据(fresh lane),防止旧积压拖死今日观察 - 新增
stopAndDrainActive():停止接收新批次并等待已 claiming 批次终态,专供DataLifecycleService.exclusive()清理前调用 drainThroughCapturedAt优先调度窗口内批次,避免窗口外积压占满并发槽
5. DataLifecycleService 事务安全
exclusive()包装器:清理前 pauseSources → drain → suspendAndFlush → drain,避免清理与采集并发;active标志位防止多个清理操作叠加withEmbeddingIndexerPaused():清理期间 stopAndDrain 嵌入索引器,结束后 startBackgroundIndexing,避免索引器读到半删除状态deleteCaptureLedger用COALESCE(json_extract, ...)兼容新旧 bundle 格式forgetRecent/clearAll/clearScreenshots全部走事务,cleanupFiles返回complete/partial/failed三态
6. TimelineHeader 新增"忘掉最近"按钮
- 时间轴头部新增 Eraser 图标按钮,点击弹确认框后调
forgetRecent("30m")清理最近 30 分钟数据 - CSS 配套:
.timeline-header__forget-btn34px 高、13px 字号、hover 态变危险红色调
验证
- TypeScript 主进程与渲染进程类型检查通过
- 新增单元测试:HybridSearchService(双路融合 + 降级)、EmbeddingWorkerClient(进程崩溃重建)、MemoryEmbeddingRepository、IdentityAuditService(三档分类 + 只读断言)、mergeSuggestionsFix(Phase 0 身份归一化)
- migration 028/029 契约测试通过
- 本地构建与 NSIS 安装包打包通过
v0.5.3 — 批次死锁修复 + 残批攒批优化 + 跨天窗口封窗 + 窗口拖动改进
v0.5.3 — 批次死锁修复 + 残批攒批优化 + 跨天窗口封窗 + 窗口拖动改进
本次版本聚焦 v0.5.1 时间轴窗口化落地后暴露的批次与窗口协调类 bug 修复,并顺带把窗口拖动从 CSS app-region 改为 IPC + PointerEvent 自定义实现以解决与 React 事件系统的冲突。
Bug 修复
1. 批次重试耗尽死锁(BatchProcessor + CaptureInboxRepository)
症状:崩溃恢复(recoverRunningBatches)与优雅关闭(checkpointRunning)会把 running 写回 pending 而不动 attempts。若那一次正好是第 maxAttempts 次尝试,这条 batch 就落入死区:listProcessableBatches 因 attempts < maxAttempts 不再挑它,getWindowWatermark 又因为它既非 succeeded 也非 failed 而永久算作 unsettled,于是覆盖它的时间轴窗口再也封不掉。
修复:
CaptureInboxRepository新增failExhaustedBatches(maxAttempts):把重试次数已用尽却仍停在pending/running的 batch 落到终态failed,last_error补retry_exhausted_without_terminal_state- 抽取
BATCH_MAX_ATTEMPTS = 3常量到仓储层,保证BatchProcessor的挑选条件与getWindowWatermark的终态判定用同一个数 getWindowWatermark的 SQL 补attempts >= BATCH_MAX_ATTEMPTS → failed分支,让水位线不再把这种 batch 算成 unsettledBatchProcessor.start()启动时调用一次failExhaustedBatches,避免历史死区 batch 卡死新一天的窗口
2. CaptureBatcher 固定 5 分钟定时器抢切近满批次
症状:ActivityService 长会话间隔 5 分钟、内容变化最小间隔 60 秒,真实到帧节奏约 6070 秒一帧,攒满 6 帧要 6 分钟以上。旧的"从第一帧起算固定 5 分钟"定时器会在批次快满时抢先提交,产生大量 45 帧、甚至 1 帧的批次,拉低模型单批质量。
修复:
- 用"空闲 + 年龄上限"双约束替代固定 5 分钟:
IDLE_FLUSH_MS = 150s(距离最后一帧超过 150s 才认为活动停了)、MAX_BATCH_AGE_MS = 10min(队首最老一帧不允许超过 10 分钟,与时间轴采集窗口对齐) scheduleFlush每次入队都重排,让"空闲"这一半随新帧顺延;delay = Math.max(0, Math.min(IDLE_FLUSH_MS, MAX_BATCH_AGE_MS - oldestAge))- 新增
oldestQueuedAt锚点跟踪队首年龄;flush 后剩下的帧从现在起重新计年龄;队列空了清掉锚点 - flush 失败时帧退回队首,
oldestQueuedAt也要退回previousAnchor,不能让失败重排把它们"变新"
3. TimelineWindowCoordinator 跨天窗口死锁 + 封窗重复排空 + 回调环形等待
症状 A:跨天窗口里有 batch 永远算不上终态(unsettledCount 归不了零),旧实现会在 sealing 上原地打转,把 48 次循环耗光,今天的窗口永远开不出来。
修复 A:跨天窗口用 force: true 强制封窗,把没结算的部分标成 partial;封窗后若状态仍没变(说明推不动),直接退出,不在同一个窗口上耗光循环。
症状 B:sealing 状态每 5 秒轮询一次,旧实现每轮都调用 captureService.drain() + captureBatcher.flush(),把尚未攒满 6 帧的 L0 队列强制 flush 成单帧 batch。
修复 B:用 preparedSealingWindows: Set<string> 标记,只在该窗口首次封窗时排空 producer,后续轮询不再强制 flush;窗口离开 sealing 状态(成功/失败/空窗)时清理标记。
症状 C:onBatchSettled 回调在 advance 正在等待 batch 时触发,会形成 advance → batch → onBatchSettled → advance 的环形等待死锁。
修复 C:onBatchSettled 开头检查 this.running,若已有 advance 在跑则直接返回,避免环形等待。
改进
4. 窗口拖动从 CSS app-region 改为 IPC + PointerEvent
问题:CSS -webkit-app-region: drag 与 Electron + React 事件系统有冲突(按钮点击被拦截、拖动不流畅)。
改进:
- 新增 IPC 通道
window:drag(phase: start|move|end+screenX/screenY),主进程appHandlers.ts维护dragState(pointer 起点 + 窗口原位置),move阶段调用window.setPosition跟随 - 渲染层
AppShell.tsx三个 handler(handleWindowDragStart/Move/End)用setPointerCapture保证拖动不丢失,忽略非左键和button/a/input元素 - 应用于
.app-shell__brand(品牌区)和.app-shell__topbar(顶部状态栏) - CSS 改为
app-region: no-drag+user-select: none,避免与自定义拖动冲突 - 新增 E2E 测试:验证拖动后窗口位置变化但尺寸不变
验证
- TypeScript 主进程与渲染进程类型检查通过
- 新增/更新单元测试:BatchProcessor(reap exhausted + concurrency cap)、CaptureBatcher(idle slide + age cap)、TimelineWindowCoordinator(force seal + no spin + no reentrant advance)
- IPC 契约测试更新:
EXPECTED_INVOKE_CHANNEL_COUNT82 → 83,MIN_VALIDATED_CHANNELS40 → 41 - 本地构建与 NSIS 安装包打包通过
v0.5.2 — 安全存储迁移 + 导航护栏加固 + 渲染层架构重构
v0.5.2 — 安全存储迁移 + 导航护栏加固 + 渲染层架构重构
本次版本聚焦安全加固与内部架构整理:API Key 存储从已归档的 keytar 原生模块迁移到 Electron 内置 safeStorage(DPAPI),降低原生依赖维护成本;BrowserWindow 新增导航白名单护栏,作为 prompt injection 逃逸的纵深防御;数据库写入增加 busy_timeout 与迁移备份自动清理;渲染层将单文件 global.css 与 store.ts 拆分为按页面/组件的 CSS 多文件与 Zustand slice 模式。
新增功能
1. 密钥存储迁移(keytar → Electron safeStorage)
将 API Key 从 keytar(Windows 凭据管理器原生模块)迁移到 Electron 内置 safeStorage,密文落盘到 %APPDATA%/Recall/data/secrets.json:
- 新增
src/main/services/secretsMigration.ts:启动时幂等执行一次迁移,写成功后才删源(避免 key 丢失),全程不记录 key 本身 - keytar 模块缺失时静默跳过(
keytar_unavailable),不挡启动;最坏情况老用户重填一次 API Key - 接入点:
app.ts启动早期、任何模型调用之前跑完 - 迁移动机:keytar 上游已归档不再维护,每次 Electron 大版本升级都要 electron-rebuild;safeStorage 安全等级同源(都基于 DPAPI,按当前用户+机器绑定),密文刻意不进 SQLite 避免被备份/VACUUM INTO 复制时扩大暴露面
- 新增
SecretService.test.ts/secretsMigration.test.ts:9 个用例覆盖正常迁移、写失败保留源、空密码、二次运行幂等、safeStorage 不可用、keytar 缺失等
2. 导航护栏(navigationGuard)
给 BrowserWindow 的 WebContents 安装导航白名单护栏,作为 prompt injection 防御的纵深第二层。renderer 渲染的内容包含不可信输入(OCR 屏幕文字 + 模型生成报告正文),即使模型被绕过吐出可点击链接或触发 window.open,主进程必须兜住:
- 新增
src/main/services/navigationGuard.ts:覆盖四类逃逸面setWindowOpenHandler:一律 deny,外部链接走memory:openSourceUrl → shell.openExternal正规通道will-navigate:主框架导航白名单外preventDefaultwill-frame-navigate:子框架(iframe)导航白名单外preventDefaultwill-attach-webview:一律拒绝(本应用不使用 webview)
- 白名单允许:
dist/renderer目录下的file:资源(含 query/hash)、开发模式 dev server 同源地址、启动占位页精确匹配的data:URL - 显式拒绝:
http(s)外链、其他data:/blob:URL、javascript:/ftp:/about:、renderer 目录外的file:路径(含..穿越与同前缀兄弟目录攻击) - 应用到主窗口与 EndOfDayReview 日报弹窗
- 新增
navigationGuard.test.ts:8 个用例覆盖目录内外导航、穿越、同前缀兄弟目录、生产模式拒绝 http/https、dev server 同源、data: URL 精确匹配等
改进
3. 数据库稳健性
Database.ts:启用busy_timeout = 5000ms,避免并发写撞锁(BatchProcessor / TimelineWindowCoordinator / ReportScheduler / ProjectionInvalidationProcessor / ScreenshotCacheScheduler / 准入后台重评)立即抛 SQLITE_BUSY- 新增
pruneMigrationBackups:迁移成功后自动清理历史备份,只保留最近 2 份(MIGRATION_BACKUP_KEEP);备份文件名内嵌 ISO 时间戳,按名排序即按时间排序;清理失败不影响启动 - 迁移失败时保留全部备份用于人工恢复
4. 渲染层架构重构(内部,用户无感)
将单文件 global.css 与 store.ts 拆分为按职责分层的多文件结构,对外 API 与用户可见行为均保持不变:
- CSS 拆分:
global.css退化为纯@import清单,拆为基础层(base.css/pages-common.css/pages-normalize.css)、布局层(app-shell.css)、页面层(today.css/reminders.css/settings.css/projects.css/people.css/reports.css/memory-search.css/memory-detail.css/debug.css/trust-center.css/unfinished.css)、组件层(components.css/dialogs.css/today-*.css/reports-history.css/update-badge.css等)、两轮精修(refinement.css/refinement-pass2.css) - 状态管理拆分:
store.ts精简为极薄组合器(约 30 行),按域拆为 8 个 slice(shell/today/reminders/search/objects/settings/reports/debug);领域类型抽到state/types.ts,跨 slice 共用初始值与日期工具抽到state/defaults.ts;对外仍是useAppStore((s) => s.someAction),页面无需改 import
5. 工程基础设施
- 新增
eslint.config.mjs:ESLint 9 扁平配置,主进程/共享层与渲染层走不同 tsconfig,仅开"真能抓 bug"的类型感知规则(no-floating-promises/no-misused-promises/await-thenable/no-explicit-any),no-console限制为只允许error/warn - 新增契约测试:
settingsContract.test.ts(设置 IPC 契约)、migrations.contract.test.ts(迁移契约) windows-ci.yml调整:fast 轨道 lint + typecheck + test:coverage + Worker 独立测试,heavy 轨道 build + sqlite + smoke + e2e,concurrency cancel-in-progressActivityService.start()首次轮询吞异常,避免未处理拒绝MemorySearchRepository删除未使用的TYPE_LABELS,FTS 失败分支补注释
验证
- TypeScript 主进程与渲染进程类型检查通过
- 全部单元测试与契约测试通过
- 本地构建与 NSIS 安装包打包通过