Compare commits

...
158 Commits
Author SHA1 Message Date
电摇小子 713232bd62 feat: 新增 API(capabilities / 退群监控 / 自动化 / 群统计) 2026-10-04 02:31:34 +07:00
电摇小子 e83e85fe64 feat: 持久化聊天记录导出设置 2026-10-02 19:37:09 +07:00
电摇小子 e5a1656644 style: 修复图标 样式 #46
Fixes https://github.com/Wxw-Gu/TraceMemo/issues/46
2026-10-01 17:35:47 +07:00
电摇小子 194dd0035b chore: 构建 2026-09-29 18:02:09 +07:00
电摇小子 c3629255ea chore: 更新二维码 2026-09-29 16:24:20 +07:00
电摇小子 345a0db49b chore: 提升版本 整理文档
整理档案首页按钮
2026-09-29 16:13:37 +07:00
电摇小子 5276bce060 chore: 打包配置 测试用例 2026-09-29 13:06:00 +07:00
电摇小子 0abbda1160 Merge branch 'develop_0920' into develop 2026-09-29 10:51:15 +07:00
电摇小子 97c74859c0 fix: 修复联系人 API 因昵称备注未补全导致搜索结果为空 #51
Fixes https://github.com/Wxw-Gu/TraceMemo/issues/51
2026-09-29 10:47:33 +07:00
电摇小子 4d5c6459fe feat: 定时日报并入自动化,统一规则管理与发送链路
发送目标与退群通知统一为 来源群 / 自己 / 文件传输助手 / 指定好友
日报页补「定时日报已并入自动化」指引条
2026-09-28 17:11:50 +07:00
Wxw-Gu c835d5c570 feat: 定时日报归到定制化功能下 2026-09-25 17:44:45 +08:00
Wxw-Gu 59872daf52 feat: 退群监控接入自动化能力统一管理, 支持发送个人/文件传输助手/指定群聊 2026-09-24 16:31:07 +08:00
Wxw-Gu 3bd3935042 fix: 修复mac发送 初始化卡在'正在检查 发送运行时…' 2026-09-24 10:49:34 +08:00
Wxw-Gu 77a6912ade fix: 修复退群监控列表错误, 界面左侧滚动条 2026-09-24 10:36:10 +08:00
Wxw-Gu 4b83aaed61 Merge branch 'develop' into develop_0920
# Conflicts:
#	src/main/index.ts
#	src/preload/index.d.ts
#	src/preload/index.ts
#	src/renderer/src/styles/index.scss
2026-09-24 09:41:53 +08:00
Wxw-Gu 7268188e40 feat: mac发送能力重构为libtmwechat
- 发送改走本地host, 在微信登录时触发
- 删除旧OneBot及对应UI控制面
2026-09-24 03:01:03 +08:00
Wxw-Gu 069d836dea feat: 精确会话自动化规则 2026-09-21 11:06:59 +08:00
Wxw-Gu 95384a4a92 feat: 自动化 2026-09-21 00:35:32 +08:00
Wxw-Gu 9b82ca663b fix: 修复Windows语音消息取错 2026-09-21 00:33:02 +08:00
Wxw-Gu f5633a29cb feat: 新增群员统计与退群事件,并把本地索引集中到设置页
- 群员统计:档案中按群统计发言排行与未发言成员,可复制纯文本、按平台能力发送
- 退群事件永久保存,并以本地推断条目并入档案消息流
- 设置新增本地索引页,集中管理聊天记录索引与图片文字索引
2026-09-20 21:47:06 +08:00
Wxw-Gu 766cd94788 feat: 新增群员统计与退群事件,并把本地索引集中到设置页
- 群员统计:档案中按群统计发言排行与未发言成员,可复制纯文本、按平台能力发送
- 退群事件永久保存,并以本地推断条目并入档案消息流
- 设置新增本地索引页,集中管理聊天记录索引与图片文字索引
2026-09-20 21:46:37 +08:00
wuyouMaster b6d287a299 fix: make packaging checks platform safe 2026-09-20 18:16:41 +08:00
wuyouMaster 4c25236070 fix: ad-hoc sign packaged macOS key helpers and app bundle
electron-builder 26 skips macOS signing entirely when no Developer ID identity is configured, so the packaged key helpers can ship unsigned or with a broken signature and macOS kills them even with SIP disabled. afterPack now verifies and ad-hoc re-signs the packaged xkey helpers and the outer app bundle, failing the build when a signature cannot be repaired.
2026-09-20 18:16:41 +08:00
qingmao c1224cb612 Merge pull request #45 from mmhh256/fix/query-agent-system-messages
fix: 合并 Query Agent 的连续 system messages
2026-09-20 16:11:08 +08:00
Wxw-Gu 5036639aa1 test: 测试用例 2026-09-20 15:56:27 +08:00
Wxw-Gu 9593ca0f54 fix: 修复语音消息取错、时长精度与转写不刷新
- 时长解析:改取 <voicemsg voicelength>(毫秒),不再误取 length(SILK 字节数)
- 时长显示:四舍五入对齐微信口径,有原生时长时不被解码时长覆盖
- 转写:重新识别改为强制重算,跳过身份级缓存短路(音频级缓存仍生效)
- 日报:语音累计秒数显示取整,不再出现小数
2026-09-20 15:40:01 +08:00
mmhh256 b441287d54 fix: 合并 Query Agent 的连续 system messages 2026-09-19 14:01:39 +08:00
Wxw-Gu 9b82d29037 fix: 兼容微信新版系统消息模板格式,入群通知不再显示成「撤销」 2026-09-18 14:41:50 +08:00
Wxw-Gu 0f270366aa feat: 图片文字索引按时间分段优先处理最近图片
- 首次索引先处理最近 7 天,再依次回溯 30 天 / 近一年 / 更早历史
 - 完成后新到的图片单独补齐,不受历史回填影响
 - 覆盖度增加时间维度,可区分「最近已完整」与「更早仍在补齐」
2026-09-18 14:39:05 +08:00
Wxw-Gu 12b8fb34c6 fix: 增加sip文案 2026-09-18 09:38:33 +08:00
Wxw-Gu 774346d503 fix: 日报头像补齐下载重试 2026-09-17 19:24:27 +08:00
Wxw-Gu b97e1f4bc6 fix: 问问微信恢复引用编号,日报修复成员头像与 hero 溢出
- 问问微信:Host 分配稳定 citationId 并注入模型上下文,正文 [E#] 与证据按钮同号
- 问问微信:回答返回前校验引用,幻觉编号移除并提示,不再出现不可信的引用按钮
- Agent Hub:收紧「最近会话」快捷路由,内容查询交回 Query Agent
2026-09-17 18:56:32 +08:00
Wxw-Gu 4b2395a8d2 feat: 重写agent hub连接器,新增对话记录和微信输入状态
- 新增 Agent Hub 对话记录面板,按会话回看机器人与微信用户的完整收发内容
- 新增微信原生正在输入状态,长任务维持 typing,异常路径强制收尾
2026-09-17 17:32:37 +08:00
Wxw-Gu 1337bcb7de feat: 新增mac ocr转文字,增加到问一问微信 图片索引优化速度
- 图片文字索引性能与进度诚实化
- 问问微信:证据卡区分「消息类型」与「派生来源」,派生命中内容自报来源
- 问问微信:回答规则禁止未真实执行的多轮承诺
- 本地图片文字识别:支持 macOS 系统 OCR(Apple Vision)
2026-09-17 13:11:20 +08:00
电摇小子 b8f08d54c8 feat: 新增微信图片文字索引与问问微信图片检索能力 2026-09-16 10:42:52 +08:00
电摇小子 24399f1d70 feat: 新增 Windows 本地图片文字识别能力 2026-09-16 00:23:23 +08:00
Wxw-Gu e6db4de711 chore: 提升版本号 修改文案, 以及 macos 首次登录页面文案修改 2026-09-15 16:47:56 +08:00
Wxw-Gu adff6021fc test: 修改测试用例 2026-09-15 15:32:40 +08:00
qingmao 75004375df Merge pull request #41 from Wxw-Gu/codex/develop-xkey-4-1-13
Codex/develop xkey 4 1 13
2026-09-15 15:23:47 +08:00
qingmao 75de616527 Merge pull request #42 from yimizilu/fix/http-image-media-identifiers
fix: prevent HTTP image media ID collisions across conversations
2026-09-15 14:57:26 +08:00
Wxw-Gu f5158fd3e4 feat: 完全支持 mac Intel 环境
抽离 Intel 专用包、connector 与跨架构语音库
修改 publish 脚本,发布先落草稿
修复重装依赖后 Electron 二进制缺失
2026-09-15 14:12:00 +08:00
Wxw-Gu ed4c21a68f docs: 新增提交规范 2026-09-15 11:41:53 +08:00
Wxw-Gu f35ca1778a docs: 移除已下线的防撤回说明 2026-09-15 11:26:11 +08:00
Wxw-Gu 21b2b57d28 feat: 隐藏设置里的防撤回入口,历史开启强制收敛为 false 2026-09-15 11:26:06 +08:00
Wxw-Gu 67dc45819e feat: 修改社区模板市场入口 放到日报二级菜单下, 增加跳转github 2026-09-15 11:03:00 +08:00
Wxw-Gu ff212d594c fix: 增加手动复制退群信息 以及设置页面文案 2026-09-15 10:36:09 +08:00
yimizilu 353df30ff8 fix: align media errors and serialize bigint server IDs 2026-09-15 08:28:19 +08:00
yimizilu 4f8435d0e2 fix: scope HTTP image media handles to database and conversation 2026-09-15 08:03:05 +08:00
Wxw-Gu 389a853560 fix: 日报截图窗口隐藏化、搜索缓存兼容读取与定时日报单测等待窗口 2026-09-14 22:25:55 +08:00
wuyouMaster c437be349e fix: preserve legacy macOS key capture 2026-09-14 18:44:15 +08:00
wuyouMaster 488966d771 fix: expose macOS login-time key detection 2026-09-14 18:20:20 +08:00
wuyouMaster 00095ae627 feat: add WeChat 4.1.13 macOS key capture 2026-09-14 18:20:11 +08:00
Wxw-Gu ffbe78f74a test: 测试用例 2026-09-14 18:09:43 +08:00
Wxw-Gu 7e9a79486e test: 测试用例 2026-09-14 17:41:55 +08:00
Wxw-Gu 4191d4d5f5 test: 固定本地时间标签的测试时区 2026-09-14 17:17:23 +08:00
Wxw-Gu 29c1166eca fix: 按本机时区和可变模板目录收口测试 2026-09-14 17:11:45 +08:00
Wxw-Gu bf78055b69 fix: 收口发布候选版本的时间和配置问题 2026-09-14 16:58:53 +08:00
Wxw-Gu 550e3b104c feat: 抽离保留进程功能到设置里 2026-09-14 16:44:53 +08:00
Wxw-Gu cf9ea8a83b feat: 优化 macOS 微信发送消息能力
参考 wechat_chatter PR #36:
https://github.com/yincongcyincong/wechat_chatter/pull/36

补充 WeChat 4.1.11.53 的 GetService / CdnManager 地址,
并适配绑定后的发送能力检测逻辑。
2026-09-14 15:59:08 +08:00
qingmao 5efae86fcf Merge pull request #35 from huangzhenhao90/fix/opencode-go-session-header
fix(ai): 修复 OpenCode Go 缺少会话 header 导致日报生成失败
2026-09-14 10:05:18 +08:00
电摇小子 70c55442f3 feat: 支持Mac Intel获取密钥, 提供完整支持 2026-09-13 20:08:40 +08:00
Wxw-Gu 9288256a5e feat: 支持mac intel 2026-09-13 13:39:03 +08:00
Wxw-Gu e5bd162a01 fix: 优化微信发送日志展示 2026-09-13 11:22:47 +08:00
Wxw-Gu cef1ad493c feat: 实时刷新档案会话列表
收到新消息后增量更新会话摘要和时间

自动按最新消息重新排序并保留当前选择状态
2026-09-13 11:15:30 +08:00
Wxw-Gu add16885fe fix: 修复导出日期、语音和联系人名字问题
- 导出的 HTML:点月份先看日期,选中日期后才跳到对应消息。
- 语音转文字:不同账号、不同聊天里的语音不会再串到一起。
- 联系人名字:正常昵称显示不变;昵称异常时使用可见的备注或账号,导出文件名也能正常使用。
2026-09-13 11:15:30 +08:00
Wxw-Gu ab3b3731b7 feat: 重构问问微信并完善知识库增量检索
统一问问微信与 Agent Hub 的查询链路
完善查询覆盖度、新鲜度和部分结果表达,避免索引滞后产生错误结论
基于会话实现真正的增量追新与历史补齐
支持后台同步、取消恢复、重启续传以及同步期间继续查询
优化知识库跨会话检索、同步状态、进度展示和侧栏布局
2026-09-11 17:44:48 +08:00
Wxw-Gu 0c4932d740 feat: 完善agent查询和检索 2026-09-11 10:32:21 +08:00
Wxw-Gu fe8619d56d fix: 完善查询agent时间与检索契约
增加调试与诊断
2026-09-10 17:45:12 +08:00
Wxw-Gu 376ea79ff6 feat: 优化问问微信工具规划 2026-09-10 15:35:12 +08:00
Wxw-Gu dc6d1eb6ac fix: 修复查询工具消息引用与参数校验 2026-09-10 11:10:47 +08:00
Wxw-Gu 532e135406 chore: 更换main分支二维码 2026-09-10 09:38:54 +08:00
Wxw-Gu 7ae0d63a7f feat: 新增本地查询模型调用 2026-09-10 09:35:42 +08:00
Wxw-Gu c759bcd30e feat: 退群监控增加重新发送功能 2026-09-09 17:58:14 +08:00
Wxw-Gu 0bad419150 feat: 新增本地微信查询工具 API 2026-09-09 16:52:39 +08:00
Wxw-Gu facc292443 feat: 完善问问微信查询理解与检索链路 2026-09-09 16:51:31 +08:00
Wxw-Gu 7554de7d72 feat: 新增日报生产片段 UI 契约与视觉保护 2026-09-09 11:17:29 +08:00
Wxw-Gu 1bc66e33fd docs: README 2026-09-08 17:58:30 +08:00
Wxw-Gu 976aac32c5 feat: 日报新增模板市场,支持社区模板安装与使用 2026-09-08 11:37:38 +08:00
zhenhao 3634f49a48 fix(ai): send stable session headers for OpenCode Go 2026-09-07 15:07:22 +08:00
Wxw-Gu 1156362c3c test: 完善 2.3.0 跨平台测试与 CI 稳定性
- 修复 Windows 与 macOS 单元测试环境差异\n- 完善组件、集成与 Electron E2E 测试\n- 修复异步等待、平台路径和环境依赖问题\n- 统一 E2E 窗口尺寸与 Visual 测试策略\n- 提升 GitHub Actions 跨平台 CI 稳定性
2026-09-05 10:38:41 +08:00
Wxw-Gu 60528ffe53 Merge branch 'develop' of github.com:Wxw-Gu/TraceMemo into develop 2026-09-04 17:52:53 +08:00
Wxw-Gu f28480ffbe docs: 整理文档 2026-09-04 17:52:49 +08:00
qingmao 8e784d8125 Merge pull request #33 from Lazy-CZ/fix/export-audio-fix
fix: 修复批量导出 wav 音频语速变快、音调升高(采样率不匹配)问题
2026-09-04 16:14:35 +08:00
yy2257 3489535750 test: 恢复表情导出保护并补充语音回归测试 2026-09-04 15:55:22 +08:00
DESKTOP-QMBPBO5\Lazy 5e268cd8f4 fix: 修复导出的音频文件变调、变速的问题 2026-09-04 15:09:06 +08:00
Wxw-Gu 454f449c41 feat: 统一手动发送入口为文字转语音
- 移除普通文本、图片和本地语音的手动发送入口
- 保留文字转语音的生成、试听和发送能力
2026-09-04 15:05:04 +08:00
电摇小子 517e650942 fix: 修复用户已加载的历史记录数量 2026-09-04 10:45:41 +08:00
电摇小子 21e299942b refactor: 重构一下档案搜索功能 支持拼音微信号等 2026-09-03 23:38:12 +08:00
Wxw-Gu bde3bc537f Merge branch 'develop' of github.com:Wxw-Gu/TraceMemo into develop 2026-09-03 18:12:34 +08:00
Wxw-Gu 719ffa0a24 feat: 优化档案搜索与头像
新增退群监控开关
发送限速
2026-09-03 18:12:29 +08:00
qingmao 8fa4dd50e7 Merge pull request #31 from njueeRay/fix/windows-v4-image-key-derivation
fix: derive Windows image keys from local metadata
2026-09-03 17:53:15 +08:00
电摇小子 3462d49516 perf: 重构退群监控,持久化快照并降低 CPU 占用
- 持久化群成员
- 支持 TraceMemo 重启后恢复离线期间的退群检测
- 使用 Batch Membership 替代逐群成员查询
- 降低大量群聊监控时的 CPU 和数据库查询开销
2026-09-03 01:50:34 +08:00
Ray 7ef991aa67 fix: derive Windows image keys from local metadata 2026-09-03 01:00:37 +08:00
电摇小子 4d758b2aca Merge branch 'main' into develop 2026-09-02 19:45:45 +08:00
Wxw-Gu 27f4108cf9 feat: 统一发送能力 接入退群通知与定时日报 2026-09-02 19:43:30 +08:00
电摇小子 4075e52eec Merge branch 'main' of https://github.com/Wxw-Gu/WechatExplorer 2026-09-02 19:42:24 +08:00
电摇小子 f7d619292b chore: 更新二维码 2026-09-02 19:10:13 +08:00
Wxw-Gu e1f4ec79dd feat: 建立发送网关与动作审计 2026-09-02 18:00:39 +08:00
Wxw-Gu 76e9e683c4 chore: 统一日报时间逻辑 2026-09-02 16:02:05 +08:00
Wxw-Gu 7c8b45a24a feat: 修复定时日报错误通知微信机器人功能
增加定时日报debug
修复定时日报历史头像显示并区分定时日报标题
2026-09-02 15:01:03 +08:00
Wxw-Gu 1c8bbae1f0 fix: 增加thinking disabled
修改退群监控模板
定时日报保存日报历史
2026-09-01 11:40:14 +08:00
电摇小子 25b72c3d76 feat: 定时日报支持Windows 2026-09-01 11:36:15 +08:00
Wxw-Gu f65dc4ac32 feat: 刷新消息列表时间改为350ms 2026-08-31 23:27:22 +08:00
Wxw-Gu e59332b514 feat: 增加退群监控功能 2026-08-31 21:06:53 +08:00
Wxw-Gu 6cbf9af662 feat: mac语音转换silk格式修复 2026-08-31 10:55:52 +08:00
电摇小子 5acfa14475 feat: 增加windows发送能力
抽离功能到设置页面
抽离文字转语音功能
2026-08-31 02:44:24 +08:00
Wxw-Gu 698251796f feat: 增加一个查找语音文件 2026-08-28 15:04:37 +08:00
Wxw-Gu a007e110ac feat: 扩展 Skill 支持定时日报操作 2026-08-27 20:27:51 +08:00
Wxw-Gu 7cf043e3c3 feat: 完善定时日报 2026-08-27 19:03:07 +08:00
Wxw-Gu 949e6bc868 feat: 新增定时日报与微信发送能力底座 2026-08-27 17:08:09 +08:00
Wxw-Gu 62abd872ee Merge branch 'develop' of https://github.com/Wxw-Gu/WechatExplorer into develop 2026-08-27 14:45:48 +08:00
Wxw-Gu 0d68217dda fix: 修复转换微信语音发送逻辑 2026-08-27 14:43:09 +08:00
qingmao ee5f50db83 Merge pull request #25 from Wxw-Gu/nanin/develop
修复部分自定义表情导出后图裂的问题
2026-08-27 09:55:58 +08:00
Nanin 36e709d7a6 修复部分自定义表情导出后图裂的问题
校验应用缓存、微信 Emoticon 缓存及 CDN 响应的真实图片格式,无效缓存自动回退下载。

增量导出会识别并替换旧归档中的无效表情资源;刷新失败时移除破图引用并保留明确错误。

补充 StickerService 单元测试和 HTML 媒体增量导出回归测试。
2026-08-26 22:17:25 +08:00
qingmao b26d4dd1a7 Merge pull request #24 from ZipperWang/main
添加response API支持,添加流式传输支持
2026-08-26 17:29:54 +08:00
Zipper_Wang 8f9a0c44da Merge upstream origin/main 2026-08-26 16:49:47 +08:00
Wxw-Gu 7016ca1237 chore: 提升版本 2026-08-26 15:34:46 +08:00
Wxw-Gu fc42b18f4a feat: 本地api增加图片理解 2026-08-26 15:21:05 +08:00
Wxw-Gu 1e25ae4065 feat: 修改发送消息流程 2026-08-26 11:23:45 +08:00
Wxw-Gu d26af6462f Merge branch 'main' into develop 2026-08-26 09:37:32 +08:00
Wxw-Gu 4ffac08775 chore: 更新二维码 2026-08-26 09:37:08 +08:00
Wxw-Gu 9dee0bbd3c feat: 增加更新页 2026-08-25 21:07:25 +08:00
Zipper_Wang 5ff67edaef feat: 支持 OpenAI Responses API 2026-08-25 17:15:07 +08:00
Wxw-Gu eec6286a58 fix: 修复样式, 调用接口方法 2026-08-25 15:14:30 +08:00
Wxw-Gu 47e0d7cd1e fix: 更新打包配置并修复群昵称刷新
更新跨平台 WCDB 和运行时命名。
优化消息获取速度
修复群昵称刷新回退与消息发送者显示,并增加可控的消息读取诊断日志和有界查询优化。
2026-08-25 14:26:29 +08:00
电摇小子 fc54c3b946 chore: 优化windows打包 2026-08-25 11:52:52 +08:00
电摇小子 8e5b9790f6 chore: 更新二维码 2026-08-22 22:35:01 +08:00
Wxw-Gu a0c8696987 chore: 移除没用的包 2026-08-21 17:23:49 +08:00
Wxw-Gu 77e7e54949 Merge branch 'develop' of https://github.com/Wxw-Gu/WechatExplorer into develop 2026-08-21 17:00:41 +08:00
longhuan1999andqingmao ce27829811 fix(ai): 让严格执行max_tokens的API恢复工作,如DeepSeek (#19)
* fix(ai): 让严格执行`max_tokens`的API恢复工作,如DeepSeek

- AI 请求优先使用当前模型配置的`Max Tokens`,并依次回退到供应商高级设置的`Max Tokens`和默认值 4096
- 透传`OpenAI-compatible`与`Anthropic`的结束原因,补充`preload`类型声明
- 群日报生成和修复遇到模型异常结束生成时终止处理,并针对token上限给出配置提示
- 优化 AI 未返回有效内容时的错误信息
- 统一行尾配置

Signed-off-by: longhuan1999 <2114467924@qq.com>

* Update .gitignore

---------

Signed-off-by: longhuan1999 <2114467924@qq.com>
Co-authored-by: qingmao <84499436+Wxw-Gu@users.noreply.github.com>
2026-08-21 16:52:19 +08:00
Wxw-Gu 17e3b51629 fix: 加固退出清理
退出应用时终止后台 进程
移除运行时动态安装 Python 依赖
2026-08-21 15:37:01 +08:00
Wxw-Gu 313d0afff7 feat: 优化全局 UI 交互与页面视觉体验
统一按钮、输入框、选择控件和开关的交互状态
优化日报、导出、设置、Agent Hub 和 API Center 的UI
完善组件、单元及 Electron E2E 测试
2026-08-21 15:05:06 +08:00
Wxw-Gu c83d109a75 feat: 统一按钮 2026-08-21 00:07:00 +08:00
Wxw-Gu 72d2b17842 feat: 统一界面视觉规范并优化导出交互 2026-08-20 20:13:23 +08:00
Wxw-Gu d93caa3539 refactor(ui): 统一迁移原生按钮、输入框及交互控件 2026-08-20 10:18:59 +08:00
Wxw-Gu 49d4e17d67 feat: 完成 UI 基础层与核心页面迁移 2026-08-19 17:43:07 +08:00
Wxw-Gu c0b710e56f refactor: 抽离 AI 搜索运行生命周期 2026-08-18 17:46:14 +08:00
Wxw-Gu d23a83d066 fix: 优化windows多目录报错 2026-08-18 17:34:50 +08:00
Wxw-Gu 10ea43dc9f refactor: 拆分 AI 搜索证据集合 Hook 2026-08-18 17:29:31 +08:00
Wxw-Gu c7f2105495 refactor: 拆分 AI 搜索知识库与授权 Hook 2026-08-18 17:07:07 +08:00
Wxw-Gu 14f3b85865 feat: 拆分代码 2026-08-18 16:49:25 +08:00
Wxw-Gu e75e2ebb5e test: 添加AISearch测试回归 2026-08-18 15:49:49 +08:00
Wxw-Gu 316b8c7583 feat: 新增组件库 2026-08-18 15:29:13 +08:00
Wxw-Gu c89c108098 fix: 修复 AI 检索 Evidence 覆盖与加载更多问题
修复问一问微信查询遗漏群聊的问题
区分会话覆盖与发送者覆盖策略
新增问一问微信加载更多功能
2026-08-18 14:19:28 +08:00
Wxw-Gu 4058a817dd feat: 新增MAC微信发送消息 文字转语音 功能 2026-08-17 18:16:14 +08:00
电摇小子 9b7f5114bd fix: 修复ai模型页面loop循环问题 (#16)
修改获取密钥文案
新增模型跳转等问题
2026-08-15 21:17:18 +08:00
电摇小子 55f91370be feat: 支持导出自定义目录并优化聊天记录读取
接入导出保存目录选择功能
修复导出路径摘要未同步更新
按时间范围查询聊天记录,避免全量扫描历史消息
增加跨分片消息查询支持验证
2026-08-15 02:44:07 +08:00
Wxw-Gu 412a509b2b fix: windows打包 2026-08-13 16:11:30 +08:00
Wxw-Gu 63a11a334e chore: 提升版本 2026-08-13 15:09:47 +08:00
Wxw-Gu d2402c8cfa Merge branch 'nanin/develop' into develop 2026-08-13 15:08:42 +08:00
Wxw-Gu e34567e58f fix: 优化体验 2026-08-13 15:05:48 +08:00
Wxw-Gu 7b07561a7f docs: 更新readme 2026-08-13 15:05:36 +08:00
Wxw-Gu 7f3eff8f03 docs: 修改 2026-08-13 11:04:20 +08:00
Wxw-Gu cd2fb09e02 feat: 新增实验性微信卡片分享与自动部署能力
补充 Cloudflare Worker、R2 存储、微信 JS-SDK 签名与上传鉴权
增加自动部署 Skill 和配置引导文档
优化报告工具栏、微信卡片弹窗及窄屏响应式布局
补充 Worker 鉴权、卡片生成、过期清理与安全转义测试
2026-08-13 10:48:07 +08:00
Nanin dcba9496c6 fix: 修复语音文件缺失误报
批量语音读取失败时,并行回退到兼容的单条读取接口,避免批量接口异常导致全部语音被误判为缺失。

增量合并恢复可播放语音或转写后,清理矛盾的媒体及转写错误状态,并补充批量回退与增量合并回归测试。
2026-08-12 22:30:11 +08:00
Nanin af56f19863 feat: 支持按导出媒体文件名精确搜索
离线 HTML 搜索支持使用不含扩展名的完整图片或视频哈希文件名进行精确匹配,并保持其他消息字段的模糊搜索行为。\n\n补充图片和视频命中、大小写兼容、哈希片段及带扩展名不命中的 DOM 回归测试。
2026-08-12 22:02:43 +08:00
Nanin c1224530c4 perf: 加速语音转写缓存命中
迁移旧版语音转写缓存,并将补迁失败降级为一次性失败状态。

按账号和消息标识优先命中兼容缓存,未命中时才读取音频并计算哈希;导出缓存命中后合并异步刷新知识索引。

补充迁移、缓存快速路径、批量语音读取和导出流程测试。
2026-08-12 20:01:40 +08:00
Wxw-Gu eb4cb30fb5 feat: 增加日报模板 2026-08-12 16:10:22 +08:00
739 changed files with 122978 additions and 14241 deletions
+11
View File
@@ -5,6 +5,9 @@ VITE_DB_KEY=
# Set to true/1/yes/on for local development. Default is disabled.
VITE_AUTO_LOGIN=false
# Show the scheduled report debug notification test action. Default is disabled.
VITE_SCHEDULED_REPORT_DEBUG=false
# AI API Configuration (Optional, can be entered in UI)
# 注意:发布版本不再自动读取以下环境变量。
# 如果你只是本地开发想用默认值,可以在自己机器的 .env.local 里填,
@@ -26,3 +29,11 @@ VITE_IMAGE_AES_KEY=
# Electron E2E test window close delay in milliseconds.
# Local default: 2000 (2 seconds). Set to 0 for immediate close.
WXE_E2E_CLOSE_DELAY_MS=2000
# Experimental self-hosted WeChat share-card service
# Copy these placeholders to .env. Never commit real AppSecret or UPLOAD_TOKEN values.
WECHAT_SHARE_DOMAIN=share.example.com
WECHAT_SHARE_APP_ID=
WECHAT_SHARE_APP_SECRET=
# Leave empty to let docs/skill/setup-wechat-share-card/scripts/setup.sh generate one.
WECHAT_SHARE_UPLOAD_TOKEN=
+3 -10
View File
@@ -25,16 +25,10 @@ jobs:
node-version: 22
cache: pnpm
- uses: actions/setup-go@v5
with:
go-version-file: services/wechat-connector/go.mod
cache-dependency-path: services/wechat-connector/go.sum
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install
- name: Install Playwright Chromium
if: runner.os == 'macOS'
run: pnpm exec playwright install chromium
- name: Type check
@@ -52,9 +46,6 @@ jobs:
- name: Skill installation instruction tests
run: pnpm test:skill-install
- name: WeChat connector tests
run: pnpm test:wechat-connector
- name: Build Electron test application
run: pnpm test:e2e:build
@@ -64,6 +55,8 @@ jobs:
WXE_E2E_CLOSE_DELAY_MS: 0
- name: Platform visual regression
# GitHub-hosted Windows 无法稳定容纳项目统一的 1400×800 Electron 窗口,Windows visual regression 改由可控 Windows 桌面环境执行。
if: matrix.os != 'windows-latest'
run: pnpm exec playwright test tests/e2e/visual.spec.ts
env:
WXE_E2E_CLOSE_DELAY_MS: 0
+12 -5
View File
@@ -1,5 +1,6 @@
node_modules
*.tsbuildinfo
electron.vite.config.[0-9]*.mjs
dist
out
.env
@@ -9,13 +10,19 @@ out
coverage/
playwright-report/
test-results/
resources/connectors/wechat/
resources/runtime/darwin-arm64
.native-runtime-source
.omc
.codex/
docs/design/
docs/ui-redesign-plan.md
docs/ui-redesign-spec.md
AGENTS.md
skills-lock.json
*__screenshots__
/AGENTS.md
/CLAUDE.md
/.claude/
/.workbuddy/
/.ai-local/
findings.md
progress.md
task_plan.md
+6
View File
@@ -1,2 +1,8 @@
shamefully-hoist=true
electron_mirror=https://npmmirror.com/mirrors/electron/
# Keep the Windows x64 sherpa-onnx optional runtime available when packaging
# Windows from macOS/Linux hosts.
supportedArchitectures.os[]=darwin
supportedArchitectures.os[]=win32
supportedArchitectures.cpu[]=arm64
supportedArchitectures.cpu[]=x64
+1
View File
@@ -2,3 +2,4 @@ singleQuote: true
semi: false
printWidth: 100
trailingComma: none
endOfLine: auto
+1 -1
View File
@@ -8,7 +8,7 @@
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.CMD"
},
"runtimeArgs": ["--sourcemap"],
"env": {
+2 -1
View File
@@ -7,5 +7,6 @@
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
},
"files.eol": "\n"
}
+112
View File
@@ -0,0 +1,112 @@
# 参与贡献
> 这份文档同时写给人和 AI Agent。文末有给 Agent 的英文硬性规则。
> 如果你只是想把 TraceMemo 跑起来,看[第一次使用](./docs/user-guide/getting-started.md)就够了。
## 一句话规则
**从 `develop` 拉分支,把 PR 提给 `develop`。**
## 为什么不是 main
| 分支 | 是什么 | 接受 PR 吗 |
| --------- | ------------------------------------------------------- | -------------- |
| `main` | 稳定版。只在发版时更新,对应 GitHub Releases 里的安装包 | 不接受 |
| `develop` | 开发主线。所有改动先进这里,**随下一个版本一起发布** | 唯一的目标分支 |
指向 `main` 的 PR、或者不是基于 `develop` 拉出来的分支,会被直接关闭。不是不欢迎贡献,而是因为冲突合并 提交落后较多等原因
## 提 PR 的完整流程
```bash
# 1. 基于 develop 拉分支(不要基于 main)
git fetch origin
git checkout -b feat/your-change origin/develop
# 2. 改代码,只改与本任务相关的文件
# 3. 自检
pnpm install # 需要 Node 22 与 pnpm 7.33.7
pnpm typecheck
pnpm test:unit # 再按改动范围补跑 component / integration
# 4. 提交
git commit -m "feat: 一句话说明改了什么"
# 5. 提 PR,目标分支必须是 develop
gh pr create --base develop --head feat/your-change
```
## 提交信息
默认**只写一行标题**:类型前缀 + 一句话说明。
```
feat: 新增本地查询能力
fix: 修复查询工具时间契约
docs: 整理开发文档
```
确实包含多个功能点时,标题之后每个功能点各写一行纯文本 —— 不要用列表符号,也不要写
「设置侧:」「测试:」这类分节标题:
```
feat: 统一手动发送入口为文字转语音
移除普通文本、图片和本地语音的手动发送入口
保留文字转语音的生成、试听和发送能力
```
不要在提交信息里堆文件名清单、测试结果或实现过程叙述,那些属于 PR 描述。
## 分支命名
`feat/…`、`fix/…`、`docs/…`、`refactor/…`,后面接简短的英文或拼音描述。
## PR 前自检
| 你的改动 | 至少跑这些 |
| -------------------------- | ----------------------------------------------------------------- |
| 一般代码 / 服务 / 工具函数 | `pnpm typecheck` + 相关 `pnpm test:unit` |
| 界面 / 交互 | `pnpm typecheck` + 相关 `pnpm test:component`,或针对该功能的 E2E |
| preload / IPC 契约 | `pnpm typecheck` + 相关 contract 测试或 `pnpm test:integration` |
| 只改文档 | 不需要跑测试 |
全部测试命令见 `package.json` 的 `scripts`;本地开发环境的说明见[开发概览](./docs/development/overview.md)。
## 请不要提交这些东西
- **构建产物**:`out/`、`dist/`、`build/` 下的二进制、`node_modules/`、`test-results/`、`playwright-report/`
- **真实数据**:微信聊天内容、真实的 wxid / 群名 / 联系人名、聊天截图
- **密钥**:API Key、Token、数据库密钥、图片解密密钥
- **本机绝对路径**:`/Users/…`、`C:\Users\…`
测试用的假数据请用一眼能看出是合成的命名,例如 `fixture-group`、`wxid_fixture_member`。
## 合入之后
PR 合进 `develop` 不会立刻出现在下载页,它随**下一个版本**发布。想提前用上,可以自行从
`develop` 构建。
## 如果你是 AI Agent
请按顺序执行,不要凭直觉选分支:
1. **基线**:`git fetch origin && git checkout -b <branch> origin/develop`。永远不要基于 `main`。
2. **PR 目标**:`gh pr create --base develop`。无法确定时默认 `develop`;任何情况下都不要把
`main` 当 PR 目标。
3. **改动范围**:只改与本任务相关的文件。不要顺手格式化、重排 import、升级依赖。
4. **提交前**:跑 `pnpm typecheck` 和与本次改动相关的测试(见上表)。
5. **提交信息**:一行标题,`type: 描述`。不要写文件清单、测试输出或过程叙述。
6. **PR 描述**:说明改了什么、为什么改、怎么验证的;关联 Issue 用 `closes #123`。
7. **禁止**:真实聊天数据、密钥、Token、本机绝对路径、构建产物。
### Hard rules for AI agents (English)
- Base branch: `origin/develop`. Never branch off `main`.
- Open pull requests with base branch `develop`. PRs targeting `main` are closed without review.
- One PR = one logical change. No drive-by reformatting, import reordering, or dependency upgrades.
- Before opening a PR, run `pnpm typecheck` plus the tests relevant to your change.
- Commit subject: a single line, `type: summary`. No file lists, no test logs, no process narration.
- Never commit build output (`out/`, `dist/`, `test-results/`, `playwright-report/`), real WeChat
data, keys, tokens, or absolute local paths.
- Changes merged into `develop` ship with the next release.
+165 -234
View File
@@ -4,62 +4,135 @@
<img src="./build/icon.png" width="120" alt="TraceMemo Logo" />
</p>
<h2 align="center">把微信聊过的事,找回来、问清楚、留下来</h2>
<h2 align="center">把微信里的信息,记住、理解、监控,并在需要时行动</h2>
<p align="center">本地优先的微信数据、AI 分析与自动化工作台</p>
<p align="center">
本地优先的微信聊天记录工作台:查看、搜索、提问、总结和导出<br />
查看聊天 · 找回信息 · AI 问答 · 群聊日报总结 · 语音转写 · 导出 · 微信机器人 · Agent 接入
<img src="https://img.shields.io/github/stars/Wxw-Gu/TraceMemo?style=for-the-badge" alt="GitHub stars" />
<img src="https://img.shields.io/github/downloads/Wxw-Gu/TraceMemo/total?style=for-the-badge" alt="GitHub downloads" />
<img src="https://img.shields.io/github/v/release/Wxw-Gu/TraceMemo?style=for-the-badge" alt="Latest release" />
</p>
<p align="center">
<img src="https://img.shields.io/github/stars/Wxw-Gu/WechatExplorer?style=for-the-badge" alt="GitHub stars" />
<img src="https://img.shields.io/github/downloads/Wxw-Gu/WechatExplorer/total?style=for-the-badge" alt="GitHub downloads" />
<img src="https://img.shields.io/github/v/release/Wxw-Gu/WechatExplorer?style=for-the-badge" alt="Latest release" />
</p>
<p align="center">
<a href="https://github.com/Wxw-Gu/WechatExplorer/releases"><b>下载 TraceMemo</b></a>
<a href="https://github.com/Wxw-Gu/TraceMemo/releases"><b>下载 TraceMemo</b></a>
·
<a href="./docs/user-guide/getting-started.md"><b>第一次使用</b></a>
·
<a href="./docs/README.md"><b>完整文档</b></a>
·
<a href="./docs/concepts/how-it-works.md"><b>TraceMemo 如何工作</b></a>
</p>
<p align="center">
<img src="./public/software-1.png" alt="TraceMemo 主界面" />
<img src="./public/日报.png" alt="TraceMemo 日报" />
</p>
<p align="center">
<img src="./public/机器人.png" alt="TraceMemo 主界面" />
<img src="./public/自动化.png" alt="TraceMemo 自动化" />
</p>
## TraceMemo(迹忆)是什么
TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。
---
TraceMemo(迹忆)原名 WechatExplorer,是一次从“微信聊天记录探索工具”向“可追溯的本地 AI 知识工作台”演进后的正式品牌升级。
## 🎨 社区日报模板
## 为什么叫 TraceMemo(迹忆)
TraceMemo 日报除了内置版式,也支持从社区模板市场安装更多样式。社区模板与默认日报读取同一份真实日报数据,只改变展示方式,适合手机长图分享、桌面归档、团队复盘等不同场景。
`Trace` 代表聊天记录留下的痕迹、可以追溯的信息来源、AI 搜索过程,以及从结果回到原始聊天上下文并核对证据的能力。
<p align="center">
<a href="https://github.com/Wxw-Gu/TraceMemo-Templates"><b>浏览 TraceMemo 模板社区</b></a>
·
<a href="https://github.com/Wxw-Gu/TraceMemo-Templates/tree/main/skills/tracememo-template-contributor"><b>用 AI 制作并投稿模板</b></a>
</p>
`Memo` 代表记忆、知识沉淀和长期保存:让聊天中产生的信息逐渐形成个人知识。
在 TraceMemo 中打开:
“迹忆”可以理解为“留下痕迹的记忆”。TraceMemo 不是单纯查看微信聊天记录的工具,而是希望让聊天中产生的信息留下痕迹,并能够被再次找到、理解、验证和沉淀。
**日报 → 社区模板市场**
> **品牌说明**
>
> TraceMemo(迹忆)原名 WechatExplorer。WechatExplorer 最初是一个用于查看和探索微信聊天记录的工具。随着本地搜索、AI 问答、来源追溯、知识库、日报、语音转写和 Agent 能力逐渐形成,项目已经从单纯的聊天记录查看器发展为本地 AI 知识与分析工作台,因此在 v2.2.0 正式更名为 TraceMemo(迹忆)。
即可查看、预览、安装和切换已发布的社区模板。
它可以帮你浏览、搜索和整理微信历史,也可以让 AI 帮你找回聊过的内容,并回到原始消息核对答案。
如果你有一张喜欢的日报长图、网页或前端项目,也可以把它交给 Codex、ChatGPT 或其他能够读取 GitHub 仓库的 AI,并让它读取 [TraceMemo Template Contributor Skill](https://github.com/Wxw-Gu/TraceMemo-Templates/tree/main/skills/tracememo-template-contributor)。AI 可以帮助你完成模板转换、真实预览,并在你确认满意后向 [TraceMemo-Templates](https://github.com/Wxw-Gu/TraceMemo-Templates) 提交 Pull Request。
你可以直接浏览聊天,也可以用自然语言提问:
模板通过审核并正式发布后,其他 TraceMemo 用户即可在模板市场中安装使用。
> “上个月我们讨论过哪些发布问题?”
> “张三之前发过的项目地址在哪里?”
> “技术交流群今天有哪些结论和待办?”
---
它和普通聊天记录查看器最大的不同,是 AI 不只是告诉你答案,还会告诉你答案来自哪里。你可以看到答案参考了哪些内容、来自哪个会话和时间,再回到原始消息确认它有没有理解错。
## TraceMemo 是什么
TraceMemo(迹忆)原名 **WechatExplorer** 是一款本地优先的微信数据、AI 分析与自动化工作台,把聊天变成可浏览、可搜索、可理解、可追溯的信息。
先用档案找原话,再按需要使用 AI Search、日报、监控或 Agent。普通浏览、搜索和导出不需要 AI。
## 核心能力
- 💬 **聊天档案与搜索**:浏览会话,按关键词、备注、昵称或 wxid 查找消息。
- 🔍 **AI Search / 问问微信**:用自然语言找回模糊记忆,并查看来源。
- 🧠 **本地知识库**:在本机建立索引,让跨会话、跨时间的查询更稳定。
- 🖼️ **图片文字索引**:在本机识别微信图片里的文字(截图、公告、报价图),识别结果可以在搜索和「问问微信」里被检索。识别全程不联网,原始图片不会因为本地识别而上传。
- 📊 **群聊日报**:生成今日、昨日或近 7 天的群聊总结,可保存为 HTML 与 PNG。
- 🗣️ **群发言统计**:统计群成员的发言量和沉默成员,看清一个群里谁在说、谁一直没说。
- 👀 **退群监控**:用成员快照对比记录群成员退出事件,支持多群与事件历史。
- ⚙️ **自动化**:把上面几步按规则串起来——定时生成并发送日报、成员退群时发送通知;能发到哪里取决于当前的发送能力。
- 🔊 **文字转语音**:把文字生成语音,试听后发送到当前会话。
- 🤖 **Agent Hub**:在微信里向本机 TraceMemo 提问。
- 🔌 **外部 Agent / Local HTTP API**:让 Codex 等外部 Agent 查询本机微信历史。
## 💻 平台支持
TraceMemo 2.5.0 支持:
- **Windows x64**
- **macOS Apple Silicon(M 系列 / arm64)**
- **macOS Intel(x64)**
Windows 与 macOS 均支持微信本地数据库连接与数据库 Key 获取。
### 关于“发送能力”
浏览、搜索、日报生成、导出、知识库和图片文字索引都不需要额外的发送组件。只有**把内容真正发回微信**这一步——自动发送日报、退群通知、把语音发到会话——依赖本机发送能力:
发送能力未就绪、未绑定或发送失败时,报告本身仍会正常生成并保存在本机,执行记录会显示为“已生成,但未发送”或“已生成,发送失败”,可以稍后重试。
## 项目缘起
<details>
TraceMemo 最早叫 **WechatExplorer**。
**2025 年 12 月**,我做出了第一个版本。当时功能很简单:解析微信 3.0 的聊天记录,再用 AI 生成群聊日报。最初只是给自己用,想把散落在微信里的信息重新找出来,也方便看看群里每天聊了什么。
第一个版本完成后,项目搁置了一段时间。后来重新捡起来,我还是想继续做群聊日报,但微信已经更新到 4.x,原来的微信 3.0 数据解析方案不再适用。
为了支持微信 4.x,我开始重新研究数据访问。这部分工作最初得到了 **WeFlow** 很大的帮助。早期 TraceMemo 曾参考 WeFlow 历史版本中的实现和思路,借此解决了数据库消息、密钥获取等微信 4.x 数据访问问题。
随着项目继续发展,我逐步把这部分底层能力从原有实现中抽离,并重新实现了一套独立的数据访问兼容层。目前会继续保持与 WeFlow 历史接口和行为的兼容,以减少上层业务迁移成本。
也就是说,**WeFlow 是 TraceMemo 进入微信 数据访问领域的重要起点。没有 WeFlow,就没有今天的 TraceMemo。**
在此基础上,项目陆续加入了:
- 本地知识库
- 消息来源追溯
- 群聊日报
- 语音消息也参与知识库等问答
- 微信机器人
- Local HTTP API
- Reader Skill
- Agent 接入
- 多种聊天记录导出能力
- 退群监控
- 文字转语音
- 持续监控自动化能力
群聊日报后来被一些人看到,项目也开始有了 Star、Fork、使用反馈和功能建议。说实话,我一开始没想到,这个原本只给自己用的小工具,会得到这么多人的关注。
这些关注和反馈让我决定认真把项目继续做下去。WechatExplorer 就这样一步一步变成了今天的 **TraceMemo(迹忆)**。
感谢每一位使用、关注和反馈过的人。
</details>
---
## 💬 交流与反馈
@@ -69,235 +142,93 @@ TraceMemo(迹忆)原名 WechatExplorer,是一次从“微信聊天记录
## 从你的任务开始
| 我现在想做什么 | 在应用里打开 | 需要准备什么 |
| ----------------------------------------- | ------------------------------------------------------- | ------------------------------------ |
| 找一句记得原文或关键词的聊天 | [档案](./docs/user-guide/chat-archive.md) | 连接微信数据,不需要 AI |
| 找一件记得大意、但不知道在哪聊过的事 | [问问微信](./docs/user-guide/ai-search.md) | 配置 AI 服务,并选择会话和时间范围 |
| 让长期、跨群聊查找更稳定 | [问问微信 → 本地知识库](./docs/user-guide/knowledge.md) | 主动建立本地索引;不会自动创建 |
| 快速了解一个群今天、昨天或近 7 天聊了什么 | [日报](./docs/user-guide/report.md) | 选择群聊并配置 AI 服务 |
| 把微信语音变成可搜索的文字 | [设置 → 语音转文字](./docs/user-guide/voice.md) | 准备本地语音模型 |
| 把聊天保存成 HTML、Markdown、CSV 或 JSON | [导出](./docs/user-guide/export.md) | 选择聊天、时间和格式,不需要 AI |
| 尽量保留之后捕获到的撤回消息 | [设置 → 防撤回](./docs/user-guide/recall-protection.md) | 默认关闭;开启前先了解写入和性能边界 |
| 直接在微信里向 TraceMemo 提问 | [微信机器人](./docs/agent/agent-hub.md) | 扫码连接机器人;总结类任务需要 AI |
| 让 Codex 等外部 Agent 查询微信历史 | [外部 Agent](./docs/agent/overview.md) | 安装 Reader Skill 并配置本机 Token |
## 最核心的三个能力
### 浏览和搜索微信历史
- 浏览联系人、群聊、折叠群聊和公众号消息。
- 查看文本、图片、视频、语音、文件、链接、引用、小程序等内容。
- 搜索会话或当前聊天中的关键词。
- 从 AI 结果跳回对应聊天位置。
详细说明:[聊天档案与普通搜索](./docs/user-guide/chat-archive.md)
### AI 帮你找回聊过的内容
打开“问问微信”,选择搜索范围和时间,然后像提问一样描述你想找的内容。
TraceMemo 会先在本机查找候选消息,再把整理后的少量来源交给你配置的 AI 模型生成回答。你可以查看答案参考了哪些聊天、来自哪个人和时间,并从来源标记跳回原始消息核对;“查看检索详情”还会展示本次查找经历了哪些阶段。
<p align="center">
<img src="./public/问一问.png" alt="问问微信与聊天来源" />
</p>
详细说明:[使用 AI 查找聊天信息](./docs/user-guide/ai-search.md)
### 直接在微信里问你的历史聊天
打开应用中的“Agent”入口(页面标题为“Agent Hub”,对应微信机器人功能),扫码连接一个微信机器人账号。例如,你可以直接给机器人发送“最近 5 个会话”“张三最近和我聊了什么”,或者让它生成指定群聊的总结图片。TraceMemo 会在本机读取已连接的聊天数据并把结果回复到微信。
这个入口不要求另外安装 Codex、Claude Code 等外部 Agent。当前主要处理文字消息,不支持群发、定时任务或通用自主操作微信;总结和自然语言理解需要先配置 AI 服务。
详细步骤和能力边界见[在微信里向 TraceMemo 提问](./docs/agent/agent-hub.md)。
## 其他能力
### 本地知识库
“问问微信”里的“本地知识库”会为当前微信账号建立一份留在本机的可检索资料。它把聊天文本、附件信息和已有语音转写整理起来,让跨会话、跨时间查找更稳定。
它只在用户主动建立后工作,可以同步、查看占用并清理;清理不会删除微信原始数据库。
详细说明:[本地知识库](./docs/user-guide/knowledge.md)
### 生成群聊日报
<details>
<summary>查看群聊日报示例、内容和导出方式</summary>
选择群聊和时间范围后,可以让 AI 把聊天整理成报告,并保存为 HTML 与 PNG 长图。报告可能包含热点、重要消息、资源、问答、待办、未解决事项、活跃统计和图片精选;具体内容取决于消息、媒体是否可读以及模型能力。
<p align="center">
<img src="./public/report-template-1.png" alt="群聊日报示例" />
</p>
详细说明:[生成群聊日报](./docs/user-guide/report.md)
</details>
### 转写微信语音
TraceMemo 支持在本机转写单条或批量微信语音,结果可以参与本地知识库检索和 HTML 导出。转写本身不要求把语音文件发送给在线 AI;随后用于 AI 问答或日报时,文字会按对应功能的规则处理。
详细说明:[语音转文字](./docs/user-guide/voice.md)
### 防撤回
可选开启后,TraceMemo 会尽量保留开启期间捕获到的撤回消息。该能力受微信版本和应用运行状态影响,不保证找回所有内容,也不能恢复开启前已经撤回的消息。
详细说明:[防撤回](./docs/user-guide/recall-protection.md)
### 导出长期可用的聊天档案
支持 HTML、CSV、JSON 和 Markdown。HTML 可携带媒体、头像和可选语音转写,支持最多五个会话合并,也可以压缩为 ZIP;增量合并、媒体资源和 ZIP 只适用于 HTML,其他格式主要保留文本内容。
详细说明:[导出聊天](./docs/user-guide/export.md)
### 在外部 Agent 中查询微信历史
通过 Reader Skill 和本机 Local HTTP API,Codex、Claude Code、OpenClaw 等外部 Agent 可以按需查询联系人、群聊和聊天记录。这和微信机器人是两条不同路径:微信机器人收到消息后在微信中回复;外部 Agent 则主动查询历史。
安装和技术说明请看[Agent 接入概览](./docs/agent/overview.md)与[Local HTTP API](./docs/agent/api.md)。
## 它如何工作
```mermaid
flowchart LR
A[本机微信数据] --> B[TraceMemo 读取与解析]
B --> C[聊天档案]
B --> D[本地知识库与搜索]
D --> E[筛选相关聊天来源]
E --> F[用户配置的 AI 模型]
F --> G[带来源的回答]
B --> H[整理日报输入]
H --> F
B --> I[聊天导出]
B --> J[Local HTTP API]
J --> K[外部 Agent]
L[微信机器人消息] --> M[Agent Hub]
M --> B
M --> F
```
- 微信数据库读取、聊天解析、知识库索引和离线语音识别在本机完成。
- 普通浏览、普通搜索和导出不要求配置 AI 服务。
- 使用“问问微信”、群聊日报或图片理解等 AI 功能时,完成任务所需的内容可能发送到你选择的模型服务;具体发送范围和确认方式以对应功能页面为准。
- “问问微信”会先在本机缩小范围,不会默认把整个微信数据库作为一次模型请求发送。
完整边界见:[数据、隐私与安全](./docs/user-guide/privacy.md)
## 支持平台与安装包
| 平台 | 处理器架构 | Releases 安装包 |
| ------- | ------------------------------ | --------------- |
| Windows | x64 | `-setup.exe` |
| macOS | Apple Silicon(M 系列、arm64) | `.dmg` |
当前版本不支持 Intel 芯片的 Mac。当前代码面向微信 4.x 数据结构。实际连接结果仍会受到微信客户端版本、账号数据状态和系统权限影响;macOS 首次连接可能需要按页面提示完成额外授权。
| 想做什么 | 使用入口 |
| ---------------------------------- | ----------------------------- |
| 找记得原文或关键词的消息 | 档案搜索 |
| 找记得大意、但不知道在哪聊过的内容 | 问问微信(AI Search) |
| 找到截图、公告图里写过的文字 | 问问微信 → 图片文字索引 |
| 长期跨群查询历史 | 本地知识库 |
| 了解一个群今天或近 7 天聊了什么 | 日报 |
| 看群里谁最活跃、谁一直没说话 | 档案 → 群聊 → 群发言统计 |
| 持续关注群成员退出 | 退群监控 |
| 按计划自动生成并发送群聊日报 | 自动化 |
| 成员退群时自动发一条通知 | 自动化 → 退群通知 |
| 把文字生成微信语音 | 文字转语音 |
| 在微信里向本机 TraceMemo 提问 | Agent Hub |
| 让 Codex 等工具查询微信历史 | Reader Skill / Local HTTP API |
| 把聊天保存成文件 | 导出 |
## 快速开始
1. 从 [GitHub Releases](https://github.com/Wxw-Gu/WechatExplorer/releases) 下载安装包。
2. 启动 TraceMemo,按照“第一次使用”页面选择微信数据目录。
3. 第一次使用请先点击“开始连接”,按页面提示准备连接组件并获取数据库密钥;只有已经有密钥的高级用户才需要“手动连接”。
4. 连接成功后打开“档案”,确认联系人和聊天消息已经出现。
5. 先在“档案”里搜索一句你记得的原话;这一步不需要 AI。
6. 需要 AI 问答或日报时,在“设置 → AI 模型”添加并测试 AI 服务,再打开“问问微信”或“日报”。
7. 想直接在微信里提问时,打开“Agent”扫码连接微信机器人;想让 Codex 等外部 Agent 查询时,再进入“API”。
1. 从 [GitHub Releases](https://github.com/Wxw-Gu/TraceMemo/releases) 下载对应平台的安装包。
2. 启动应用,按“第一次使用”页面选择微信数据目录并完成连接。
3. 打开“档案”,确认联系人和消息已加载后开始搜索。
4. 需要 AI 时,在“设置 → AI 模型”添加并测试 Provider。
Windows 安装后无法启动时,请先安装 [Microsoft Visual C++ x64 运行库](https://aka.ms/vc14/vc_redist.x64.exe)。当前完整测试过的微信客户端为 Windows `4.1.9.57` 和 macOS `4.1.8.100`;下载地址与连接要求见[第一次使用](./docs/user-guide/getting-started.md)。
从 WechatExplorer v2.1.9 升级时,TraceMemo v2.2.0 会在首次启动检测旧设置、Knowledge、Token、AI Provider 和 Agent 数据,并在用户确认后复制到新的 TraceMemo 数据目录。迁移不会覆盖已有 TraceMemo 数据,也不会删除旧目录;详情见 [v2.2.0 正式品牌身份与安全升级迁移](./docs/agent/release-notes-v2.2.0.md)。
如果 macOS 页面提示处理 SIP,请先阅读对应说明。具体步骤和限制见[第一次使用](./docs/user-guide/getting-started.md)。
完整步骤:[第一次使用 TraceMemo](./docs/user-guide/getting-started.md)
## 配置 AI
需要 AI 问答、群聊日报或图片理解时,在“设置 → AI 模型”添加并测试一个服务。应用支持云端服务、Ollama 等本地服务和自定义接口;具体服务商的配置、计费和数据规则由服务商决定。
使用本地服务可以减少数据离开电脑的路径,但本地服务的日志和配置仍由你自己负责。
开发者和 Agent 用户可以从[Agent 接入概览](./docs/agent/overview.md)开始,再按需要查看[Local HTTP API](./docs/agent/api.md)与[API 安全](./docs/agent/api-security.md)。
详细步骤见[第一次使用 TraceMemo](./docs/user-guide/getting-started.md)。
## 文档
- [文档首页](./docs/README.md)
- [第一次使用](./docs/user-guide/getting-started.md)
- [聊天档案与搜索](./docs/user-guide/chat-archive.md)
- [AI 查找聊天信息](./docs/user-guide/ai-search.md)
- [本地知识库](./docs/user-guide/knowledge.md)
- [群聊日报](./docs/user-guide/report.md)
- [语音转文字](./docs/user-guide/voice.md)
- [导出聊天](./docs/user-guide/export.md)
- [防撤回](./docs/user-guide/recall-protection.md)
- [数据、隐私与安全](./docs/user-guide/privacy.md)
- [Agent 接入](./docs/agent/overview.md)
- [微信机器人与 Agent Hub](./docs/agent/agent-hub.md)
- [Local HTTP API](./docs/agent/api.md)
- [开发与测试](./docs/development/overview.md)
- [用户指南](./docs/README.md#档案与搜索)
- [AI / Knowledge](./docs/README.md#ai-与知识库)
- [日报与自动化](./docs/README.md#日报与自动化)
- [Agent / API](./docs/README.md#agent--api)
- [开发文档](./docs/development/overview.md)
- [隐私与安全](./docs/user-guide/privacy.md)
## 本地开发
完整目录由[文档首页](./docs/README.md)维护。
需要 Node.js、pnpm 7+、对应平台的 Electron/native 构建环境,以及 Go(用于微信连接器)。
## 支持平台
```bash
pnpm install
pnpm dev
```
| 平台 | 架构 | 微信连接 | 安装包 |
| ------- | ------------------------------ | ------------------------------------- | ------------------------------- |
| Windows | x64 | 支持微信 4.x | `tracememo-<version>-setup.exe` |
| macOS | Apple Silicon(M 系列、arm64) | 自动获取数据库 Key,已适配微信 4.1.13 | `tracememo-<version>-arm64.dmg` |
| macOS | Intel(x64) | 自动获取数据库 Key,已适配微信 4.1.13 | `tracememo-<version>-x64.dmg` |
常用检查:
## 参与贡献
```bash
pnpm typecheck
pnpm test:unit
pnpm test:component
pnpm test:integration
pnpm test:e2e:build
```
稳定版在 `main`,只在发版时更新;所有改动都先进 `develop`,随**下一个版本**一起发布。
完整说明:[开发、测试与构建](./docs/development/overview.md)
**提 PR 请基于 `develop` 拉新分支,并把 PR 的目标分支设为 `develop`** —— 指向 `main` 的 PR 会被直接关闭。
## 支持与反馈
遇到问题时,先查看[常见问题与排查](./docs/user-guide/troubleshooting.md)。提交 Issue 时请提供操作系统、微信版本、TraceMemo 版本、复现步骤和已遮挡敏感信息的截图。
请仅处理你有权访问的数据,并遵守适用的法律法规、组织政策和微信使用规则。数据库读取、解密、自动化和机器人能力都可能受平台版本与账号环境影响。
## 许可说明
仓库中的第三方组件、模型和连接器遵循各自的许可证。当前仓库根目录未提供独立的项目 `LICENSE` 文件;贡献、复制或再分发前,请先向维护者确认 TraceMemo 本身的许可范围。
分支流程、提交信息风格、PR 前自检,以及**给 AI Agent 的硬性规则**,都在[参与贡献指南](./CONTRIBUTING.md)。
## 致谢
<details>
<summary>展开致谢与参考项目</summary>
TraceMemo 的诞生离不开开源社区中许多优秀项目的工作。
TraceMemo 在开发过程中参考了多个优秀的开源项目,感谢这些项目作者的工作与分享。
### 特别感谢 WeFlow
特别感谢:
TraceMemo 在早期适配微信 4.x 时,曾参考 **[WeFlow](https://github.com/hicccc77/WeFlow)** 历史版本中的相关实现和思路,包括数据库访问、密钥获取等底层能力。
特别感谢作者 **[hicccc77](https://github.com/hicccc77)**。项目与 WeFlow 的具体关系见[项目缘起](#项目缘起)。
### 其他参考项目
- **[WechatMessageExplorer](https://github.com/svcvit/WechatMessageExplorer)**
- 提供了微信数据库解析相关思路。
- **[WeFlow](https://github.com/hicccc77/WeFlow)**
- 参考了数据库密钥获取、图片解密等实现思路。
- 提供了数据解析相关思路。
- **[chatlog](https://github.com/sjzar/chatlog)**
- 提供了聊天记录导出与数据处理方面的参考。
- 提供了数据处理方面的参考。
在此基础上,TraceMemo 进行了重新设计与实现,包括:
- **[wechat_chatter](https://github.com/yincongcyincong/wechat_chatter)**
- 提供了发送方面的参考。
- AI 问问微信
- AI 群聊日报
- 本地 HTTP API
- Reader Skill
- Agent Hub
- 新手引导
- Electron + React 全新界面
- 本地优先 AI 工作流
感谢所有开源作者,也感谢所有帮助 TraceMemo 发现问题、提出建议和持续使用它的人。
感谢所有开源作者。
---
</details>
## 最后说两句
这个项目起初只是一个一时兴起的项目,所以它大概也不会有一份特别严肃的产品路线图。
我可能会按照自己的兴趣继续折腾,也可能突然加入一些奇奇怪怪、但觉得有意思的功能
也因此,这个项目随时可能继续折腾,也可能因为其他事情暂时搁置。如果你有想要的功能,可以提Issue;如果觉得现有实现不符合你的需求,也欢迎直接 Fork 后自己改。
<p align="center">
<b>TraceMemo(迹忆)</b>
<br />
把微信聊过的事,找回来、问清楚、留下来。
</p>
+5
View File
@@ -0,0 +1,5 @@
provider: github
owner: Wxw-Gu
repo: TraceMemo
releaseType: release
updaterCacheDirName: tracememo-updater
+48 -40
View File
@@ -1,62 +1,70 @@
# TraceMemo 文档
TraceMemo 的文档按“你想完成什么”组织,而不是按源码模块组织。
文档按产品任务和使用场景组织。需要集成或开发时,再看 Agent、概念和开发文档。
## 从这里开始
## 档案与搜索
- [第一次使用](./user-guide/getting-started.md):安装、连接微信、完成第一次搜索和提问。
- [查看和搜索聊天](./user-guide/chat-archive.md):找原话、回看上下文、处理媒体。
- [用 AI 查找聊天信息](./user-guide/ai-search.md):理解普通搜索和 AI Search 的区别,并核对答案来源。
- [第一次使用](./user-guide/getting-started.md):安装、连接微信并完成第一次搜索。
- [Intel Mac 获取微信密钥](./user-guide/intel-mac-key.md):按页面检查结果准备环境并获取密钥。
- [聊天档案与搜索](./user-guide/chat-archive.md):浏览联系人和群聊,按关键词、备注、昵称、微信号或 wxid 查找消息;也包含档案中的文字转语音入口,以及群聊里的「群发言统计」。
## 你可以完成的任务
## AI 与知识库
- [建立本地知识库](./user-guide/knowledge.md)
- [生成群聊日报和总结](./user-guide/report.md)
- [语音转文字](./user-guide/voice.md)
- [导出聊天档案](./user-guide/export.md)
- [防撤回](./user-guide/recall-protection.md)
- [在微信里向 TraceMemo 提问](./agent/agent-hub.md)
- [数据、隐私与安全](./user-guide/privacy.md)
- [常见问题与排查](./user-guide/troubleshooting.md)
- [AI Search / 问问微信](./user-guide/ai-search.md):用自然语言找回记得大意、但不知道在哪个会话里的内容,并查看 Evidence、Citation 和 Search Trace。
- [本地知识库](./user-guide/knowledge.md):主动建立本地索引,提升跨会话、跨时间查询的稳定性;也包括在本机识别图片文字、让截图和公告图变得可搜索的「图片文字索引」。
- [如何核对 AI 的回答来源](./concepts/answer-sources.md):从来源回到原始消息,检查上下文和覆盖范围。
- [从微信数据到回答、日报和导出](./concepts/how-it-works.md):了解哪些步骤在本机完成,哪些 AI 功能可能调用 Provider。
## 如果你想了解 AI 为什么这样回答
## 日报与自动化
- [如何核对 AI 的回答来源](./concepts/answer-sources.md):用用户语言解释依据、来源标记和查找过程。
- [从微信数据到回答、日报和导出](./concepts/how-it-works.md):了解哪些步骤在本机完成,哪些步骤可能调用 Provider。
- [群聊日报](./user-guide/report.md):手动生成今日、昨日或近 7 天的群聊报告,也可以在「自动化」里创建定时日报。
- 「自动化」按三类规则执行:**@我生成日报**、**定时日报**、**退群通知**。定时日报会依次生成报告、保存 Report History,再按当前微信发送能力尝试通知;发送失败时可复用已有 PNG 重试。
- 自动发送和监控动作通过统一执行边界,并保留执行记录;简要说明见[产品工作方式](./concepts/how-it-works.md#动作执行与审计)。
## 微信机器人和外部 Agent
## 退群监控
TraceMemo 有两种不同的接入方式。微信机器人是普通用户可以直接使用的产品能力;Reader Skill 和 Local HTTP API 面向已经在使用 Codex、Claude Code、OpenClaw 等外部 Agent 的用户。
退群监控会比较当前成员与上一份有效快照,记录成员退出事件。它支持多群、Last Good Snapshot 和事件历史;监控关闭期间的变化不会在重新开启后补报。成员退出同时是「自动化 → 退群通知」的触发条件。工作方式见[产品工作方式](./concepts/how-it-works.md#退群监控)。
| 你想做什么 | 应该看哪里 |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| 在微信里给机器人发消息,让本机读取数据、生成总结并回复 | [Agent Hub](./agent/agent-hub.md) |
| 在 Codex、Claude Code、OpenClaw 等外部 Agent 中主动查询过去的微信数据 | [Reader Skill](./agent/reader-skill.md) + [Local HTTP API](./agent/api.md) |
## 语音能力
### 在微信里提问
- [语音转文字](./user-guide/voice.md):在本机转写微信语音,结果可用于搜索、Knowledge 和导出。
- [聊天档案与搜索](./user-guide/chat-archive.md#文字转语音):把文字生成微信语音,试听后发送到当前联系人或群聊;实际发送依赖本机发送能力。
打开应用一级导航中的“Agent”,进入“Agent Hub”后扫码登录微信机器人。机器人收到文字消息后,可以查询最近会话、读取联系人聊天、生成群聊总结图片或总结群成员发言,并把结果回复给发消息的人。它需要本地微信数据库已经连接;依赖 AI 的任务还需要配置 AI 服务。
## Agent / API
- [Agent Hub](./agent/agent-hub.md):连接机器人、查看运行状态和了解实时交互边界。
Agent Hub 让微信机器人调用本机 TraceMemo;Reader Skill / Local HTTP API 让外部 Agent 主动查询历史数据。
### 让外部 Agent 查询历史微信
- [Agent 接入概览](./agent/overview.md)
- [Agent Hub](./agent/agent-hub.md)
- [Reader Skill](./agent/reader-skill.md)
- [Local HTTP API](./agent/api.md)
- [API 安全](./agent/api-security.md)
连接 Reader Skill 后,你可以询问:
## 导出与隐私
> “总结今天技术交流群讨论了什么。”
> “过去一周有没有人提到这个项目?”
- [导出聊天](./user-guide/export.md):导出 HTML、Markdown、CSV 或 JSON 档案。
- [数据、隐私与安全](./user-guide/privacy.md):本地处理、Provider、媒体和 Token 的数据边界。
- [常见问题与排查](./user-guide/troubleshooting.md):按安装、连接、AI、媒体和 Agent 现象排查。
- [Agent 接入概览](./agent/overview.md):先选择适合你的接入方式。
- [Reader Skill](./agent/reader-skill.md):安装并让外部 Agent 按需读取聊天。
- [Local HTTP API](./agent/api.md):完整端点和请求示例。
- [API 安全](./agent/api-security.md):Bearer Token、CORS、轮换和边界。
## 开发文档
## 开发与平台
- [macOS 数据访问说明](./platform/macos.md)
- [开发、测试与构建](./development/overview.md)
- [界面开发规范:按钮与主题色](./development/ui-guidelines.md)
- [微信系统消息解析与格式兼容](./development/wechat-system-message-parsing.md)
- [Query Agent POC(开发测试入口)](./development/query-agent-poc.md)
- [本地启动排障](./development/local-startup-troubleshooting.md)
- [v2.2.0 正式品牌身份与安全升级迁移](./agent/release-notes-v2.2.0.md)
- [v2.1.9 API 鉴权迁移说明](./agent/release-notes-v2.1.9.md)
- [macOS 数据访问说明](./platform/macos.md)
- [关闭 SIP 教程](./mac-disable-sip.md)
当前工作区版本:**2.2.0**。文档只描述当前代码已经实现的能力;版本兼容性、AI Provider 行为和媒体读取结果可能随系统、微信客户端和服务商变化。
## 实验性功能与第三方
- [实验性:自托管微信分享卡片](./deployment/experimental-wechat-share-card.md)
- [微信分享卡片自动部署 Skill](./skill/setup-wechat-share-card/SKILL.md)
- [TraceMemo Reader Skill 文件](./skill/tracememo-reader/SKILL.md)
## 版本说明
- [v2.2.0 品牌与安全迁移](./agent/release-notes-v2.2.0.md)
- [v2.1.9 API 鉴权迁移](./agent/release-notes-v2.1.9.md)
文档按当前 develop 已实现的能力维护,不在首页固定写死版本号。版本兼容性、AI Provider 行为和媒体读取结果可能随系统、微信客户端和服务商变化。
+12 -11
View File
@@ -21,14 +21,15 @@ Agent Hub 是 TraceMemo 内置的微信机器人入口,也是应用一级导
- “帮我看看最近跟某人聊了些什么。”
- “生成产品交流群今天的群聊总结图片。”
当前已实现的实时任务包括:
Hub 把入站文字分成两类处理。
- 查看最近会话(数量限制为 1–20);
- 查询你和某位联系人的近期聊天;
- 用已配置的 AI 总结你和某位联系人近 7 天的聊天;
- 生成今天、昨天或近 7 天的群聊总结图片;
- 总结指定群成员在群里的近期发言;
- 对不需要读取聊天的普通文字请求返回简短 AI 回复。
**确定性的快捷动作**(不经过模型,命中就执行):
- 查看最近会话:数量限制为 1–20;
- 生成群聊总结图片:今天、昨天或近 7 天(需要同时提到“群”和“图片 / 长图 / 日报 / 报告”);
- 分析某个群成员的近期发言:可以指定“今天 / 昨天 / 最近 N 天”。
**其余问题**交给本机的 Query Agent:它和桌面端“问问微信”使用的是同一个实现,可以按需读取联系人、会话和时间范围来回答,必要时调用你在“设置 → AI 模型”里配置的 AI。例如“帮我看看最近跟某人聊了些什么”“上个月讨论过的项目地址在哪里”。
任务完成后,回复会发送回触发这次请求的微信用户。群聊总结会先发送进度提示,完成后发送图片。
@@ -56,12 +57,12 @@ Agent Hub 页面会记录系统、Agent Hub 和微信连接器日志。日志支
## 安全与边界
- Hub 使用本机通信,不把数据库直接暴露到公网;
- Hub 在主进程内运行,不开放本地监听端口;它不会把数据库暴露到公网;
- 机器人账号和个人微信账号是不同的登录边界,请确认你连接的是正确账号;
- 机器人回复会发送给当前发消息的人;开发者 API 另有受保护的测试发送入口,使用前必须确认接收者;
- Hub 生成群聊总结时仍可能调用你配置的 AI Provider;
- 当前实时自然语言入口主要处理文字消息。底层连接器可以接收图片、语音、文件和视频,但 Agent Hub 尚未为这些媒体提供同等的实时意图处理;
- 当前没有实现群发、广播、定时任务或通用自主操作微信;
- Hub 理解请求或生成总结时,会调用你在“设置 → AI 模型”配置的 Provider;
- 当前实时入口只处理文字消息。连接器会归一化收到的消息条目,但 Agent Hub 只把文本条目当作意图处理,尚未为图片、语音、文件和视频提供同等能力;
- 当前没有实现群发、广播、定时任务或通用自主操作微信(定时日报属于「自动化」,不是 Agent Hub);
- 本页面的“Agent Hub 状态”可以通过 Local HTTP API 查询,但不要把它误认为外部 Agent 的实时消息订阅接口或 MCP Server。
## 无法连接时
+5 -2
View File
@@ -4,9 +4,11 @@
TraceMemo 的本地 API 默认监听 `127.0.0.1:6131`。它面向同一台电脑上的 API Center、Reader Skill、CLI 和 Agent,不是公网网关,也不是带用户账户和细粒度权限 Scope 的服务。
它同时包含**写入型**端点:生成报告并渲染 PNG(`/report`)、通过已连接机器人发送微信消息(`/agent/send`)、创建/修改/删除/启停定时日报任务并触发立即执行(`/scheduled-reports*`)。因此这个 Token 相当于本机敏感凭据,而不是一个只读查询键。
## Bearer Token
新 Agent 配置使用 `TRACEMEMO_API_TOKEN`。v2.2.0 仍兼容读取历史变量 `WECHATEXPLORER_API_TOKEN`,优先级为新变量高于旧变量。
新 Agent 配置使用 `TRACEMEMO_API_TOKEN`。历史变量名 `WECHATEXPLORER_API_TOKEN` 仍被兼容读取,优先级为新变量高于旧变量;当前没有设定旧变量名的移除时间,新配置不要再使用它。
- `/api/v1/health` 是公开健康检查;
- 其他所有端点都要求 `Authorization: Bearer <TOKEN>`;
@@ -14,7 +16,8 @@ TraceMemo 的本地 API 默认监听 `127.0.0.1:6131`。它面向同一台电脑
- Token 由 Electron `safeStorage` 加密保存在用户数据目录的 `local-api-token.bin`;
- 文件权限设置为 `0600`;
- 在“API Center”中可以显示、复制和重新生成;
- 重新生成后旧 Token 立即失效。
- 重新生成后旧 Token 立即失效;
- 服务端只认这个 Token,**不接受用环境变量覆盖**——Agent 一侧的环境变量只是把 Token 交给 Agent 自己的方式,不是鉴权来源。
应用不会自动把 Token 写入 Codex、Claude Code、OpenClaw 或其他 Agent 配置。请把它放进 Agent 自己的本地 secret/environment,例如:
+260 -17
View File
@@ -8,7 +8,9 @@
- API 前缀:`/api/v1`
- 默认只监听 loopback;不要把它当作公网服务。
- `/api/v1/health` 无需 Token;其他端点需要 `Authorization: Bearer <TOKEN>`。
- 请求体使用 JSON;响应为 JSON。
- 请求体使用 JSON,单个请求体最大 `1 MiB`;超限返回 `413`。
- 错误响应包含 `requestId`,响应头包含 `X-Request-Id`。客户端可传入 1-128 位的 `[A-Za-z0-9._:-]` 标识,否则服务端会生成 UUID。
- 不支持的 HTTP method 返回 `405` 和 `Allow` 响应头。
## 最小请求
@@ -24,30 +26,168 @@ curl -H "Authorization: Bearer $TRACEMEMO_API_TOKEN" \
不要把 Token 放入 URL、Skill 文件、仓库或命令历史可被共享的脚本中。
新配置必须优先使用 `TRACEMEMO_API_TOKEN`。已安装的旧 Reader Skill 可在 v2.2.0 兼容期内继续读取 `WECHATEXPLORER_API_TOKEN`;如果两个变量都存在,以新变量为准。
新配置必须优先使用 `TRACEMEMO_API_TOKEN`。应用生成的安装指令仍会提示:尚未升级的旧配置可以继续读取 `WECHATEXPLORER_API_TOKEN`,但新配置必须使用新变量名;如果两个变量都存在,以新变量为准。当前没有设定旧变量名的移除时间。
Token 由应用生成并保存在本机,**不接受用环境变量覆盖**:Agent 侧的环境变量只是把 Token 传给 Agent 自己的方式,不是服务端的鉴权来源。
## 端点
| 方法 | 路径 | 作用 | 参数/请求体 |
| ---- | ---------------------------- | -------------------------------------- | --------------------------------------------------------------- |
| GET | `/api/v1/health` | 服务与数据库健康状态 | 无 |
| GET | `/api/v1/current_time` | 本机时间、时区和 Unix 时间戳 | 无 |
| GET | `/api/v1/contact` | 联系人和群聊列表 | `filter`、`type=user\|group` |
| GET | `/api/v1/chatroom` | 群聊列表 | `keyword` |
| GET | `/api/v1/recent_chat` | 最近会话 | `limit`,默认 50 |
| GET | `/api/v1/chatlog` | 指定会话的聊天记录 | 必填 `talker`;可选 `time` 或 `startTime`/`endTime` |
| GET | `/api/v1/group_snapshot` | 群成员快照 | 必填 `md5` |
| GET | `/api/v1/resolve` | 将昵称、wxid 或 md5 解析为会话 | 必填 `q` |
| POST | `/api/v1/report` | 将结构化日报渲染为 HTML 与 PNG | `GroupReportExportRequest` JSON |
| GET | `/api/v1/agent/status` | Agent Hub、连接器和数据库状态 | 无 |
| POST | `/api/v1/agent/group-report` | 读取群聊并生成总结图片 | `{ "group": "群名或标识", "range": "today\|yesterday\|7days" }` |
| POST | `/api/v1/agent/send` | 通过已连接机器人测试发送文字或本地图片 | `{ "to": "接收者", "text": "...", "media_url": "..." }` |
| 方法 | 路径 | 作用 | 参数/请求体 |
| ------ | --------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------- |
| GET | `/api/v1/health` | 服务与数据库健康状态 | 无 |
| GET | `/api/v1/current_time` | 本机时间、时区和 Unix 时间戳 | 无 |
| GET | `/api/v1/contact` | 联系人和群聊列表 | `filter`、`type=user\|group` |
| GET | `/api/v1/chatroom` | 群聊列表 | `keyword` |
| GET | `/api/v1/recent_chat` | 最近会话 | `limit`,默认 50 |
| GET | `/api/v1/chatlog` | 指定会话的聊天记录 | 必填 `talker`;可选 `time` 或 `startTime`/`endTime` |
| GET | `/api/v1/media/{mediaId}` | 获取图片消息的二进制资源 | 原样使用 `/chatlog` 返回的 `media.url`,不要用消息 `id` 拼接 |
| GET | `/api/v1/group_snapshot` | 群成员快照 | 必填 `md5` |
| GET | `/api/v1/resolve` | 将昵称、wxid 或 md5 解析为会话 | 必填 `q` |
| POST | `/api/v1/report` | 将结构化日报渲染为 HTML 与 PNG | `GroupReportExportRequest` JSON |
| GET | `/api/v1/agent/status` | Agent Hub、连接器和数据库状态 | 无 |
| POST | `/api/v1/agent/group-report` | 读取群聊并生成总结图片 | `{ "group": "群名或标识", "range": "today\|yesterday\|7days" }` |
| POST | `/api/v1/agent/send` | 通过已连接机器人测试发送文字或本地图片 | `{ "to": "接收者", "text": "...", "media_url": "..." }` |
| GET | `/api/v1/wechat-personal/send-capability` | 个人微信发送能力状态 | 无 |
| GET | `/api/v1/scheduled-reports` | 定时日报任务列表 | 无 |
| POST | `/api/v1/scheduled-reports` | 创建定时日报任务 | `ScheduledReportApiCreateRequest` JSON |
| GET | `/api/v1/scheduled-reports/{id}` | 查询单个定时日报任务 | 无 |
| PATCH | `/api/v1/scheduled-reports/{id}` | 修改定时日报任务 | `ScheduledReportApiUpdateRequest` JSON |
| DELETE | `/api/v1/scheduled-reports/{id}` | 删除定时日报任务 | 无 |
| POST | `/api/v1/scheduled-reports/{id}/enable` | 启用定时日报任务 | 无 |
| POST | `/api/v1/scheduled-reports/{id}/disable` | 暂停定时日报任务 | 无 |
| POST | `/api/v1/scheduled-reports/{id}/run` | 立即执行一次并返回 execution | 无 |
| GET | `/api/v1/scheduled-reports/{id}/executions` | 查询某个任务的执行记录 | 无 |
| POST | `/api/v1/scheduled-reports/executions/{executionId}/retry-send` | 兼容占位路由;当前返回 `501 not_supported` | 无 |
| GET | `/api/v1/capabilities` | TraceMemo 应用能力和可用状态 | 无 |
| GET | `/api/v1/automations` | 自动化规则列表 | 可选 `type`、`enabled` |
| POST | `/api/v1/automations` | 创建默认停用的自动化规则 | Automation draft JSON |
| POST | `/api/v1/automations/validate` | 校验规则,不保存、不执行 | Automation draft JSON |
| GET | `/api/v1/automations/{id}` | 查询单条自动化规则 | 无 |
| PATCH | `/api/v1/automations/{id}` | 更新规则配置 | 可变配置字段 JSON |
| DELETE | `/api/v1/automations/{id}` | 删除自定义规则 | 系统内置规则受保护 |
| POST | `/api/v1/automations/{id}/enable` | 校验并启用规则 | 无 |
| POST | `/api/v1/automations/{id}/disable` | 停用规则 | 无 |
| GET | `/api/v1/automations/executions` | 查询自动化执行记录 | `ruleId`、`status`、`since`、`until`、`limit` |
| GET | `/api/v1/monitors/group-exits` | 查看退群监控状态 | 无 |
| PATCH | `/api/v1/monitors/group-exits` | 配置监控群范围或启停 | `enabled`、`monitoredConversationIds` |
| GET | `/api/v1/monitors/group-exits/events` | 查询退群事件历史 | `conversationId`、`since`、`until`、`limit` |
| GET | `/api/v1/groups/{conversationId}/member-stats` | 查询群成员活跃统计 | 必填 `conversationId`、`start`、`end` |
`/api/v1/query/*` 是一组结构化的 Query 端点,见下方[LLM-friendly Query Tool API](#llm-friendly-query-tool-api)。
## Application Capabilities
`GET /api/v1/capabilities` 描述 TraceMemo 应用级能力和当前运行环境;`GET /api/v1/query/capabilities` 只描述结构化 Query primitive,两者不是同一份目录。应用能力使用 `supported` 和 `available` 分开表示“代码支持”与“当前可用”;运行时原因使用稳定的简短 code,不返回 Token、数据库路径、微信密钥或 sender 诊断路径。
响应包含应用版本、数据库 readiness、Query、Automation、退群监控、群统计,以及个人微信/iLink 的能力状态。`groupExitMonitor.operations` 当前声明 `read_state`、`configure_scope`、`enable`、`disable`、`list_events`;`groupStats.operations` 当前声明 `member_stats`。能力声明不会触发监控扫描或群统计查询。
```bash
: "${TRACEMEMO_API_TOKEN:?Set TRACEMEMO_API_TOKEN from API Center}"
BASE="http://127.0.0.1:6131/api/v1"
AUTH="Authorization: Bearer $TRACEMEMO_API_TOKEN"
curl -H "$AUTH" "$BASE/capabilities"
```
## Group Exit Monitor API
退群监控只负责“监测哪些群、发现了哪些退群事实”。退群后是否通知、通知到哪里以及通知模板,仍由 `leave_notification` Automation singleton 负责;修改监控范围不会隐式修改该 Automation。
### 查看和配置监控
```bash
curl -H "$AUTH" "$BASE/monitors/group-exits"
curl -X PATCH -H "$AUTH" -H 'Content-Type: application/json' \
"$BASE/monitors/group-exits" \
-d '{"enabled":true,"monitoredConversationIds":["123@chatroom"]}'
```
`monitoredConversationIds` 只接受当前联系人列表中精确存在的群 `roomId`(例如 `xxx@chatroom`),不接受群名、md5、个人联系人、重复或空 ID。请求至少提供 `enabled` 或 `monitoredConversationIds` 其中一个;传空数组表示清空监控范围。服务会先校验全部群,再执行一次原子配置。PATCH 返回最终完整状态。
状态中的 `eventCount` 是持久化退群事件总数,`lastCheckedAt`/`lastReadAt` 为空时返回 `null`。GET 不会调用 `checkNow()`,也不会触发通知发送。
### 查询退群事件
```bash
curl -G -H "$AUTH" "$BASE/monitors/group-exits/events" \
--data-urlencode 'conversationId=123@chatroom' \
--data-urlencode 'since=2026-10-01T00:00:00+07:00' \
--data-urlencode 'until=2026-10-02T23:59:59+07:00' \
--data-urlencode 'limit=50'
```
时间参数必须是带 offset 的 ISO-8601;默认 `limit=50`,最大 200。事件按 `detectedAt` 升序返回。事件 DTO 使用 `eventId`、稳定的 `conversationId` 和 `memberId`,并把时间输出为 ISO-8601;当前整体已读状态不会伪造成 event-level `read` 字段。当前未开放 clear events、markRead 或 checkNow HTTP 路由。
一个典型 Agent 工作流是:先通过 `/resolve` 或 `/contact` 找到稳定群 ID,再 PATCH monitor scope;如需通知,再单独 PATCH `leave_notification` Automation,调用 `/automations/validate`,最后启用规则。
## Group Member Stats API
```bash
curl -G -H "$AUTH" "$BASE/groups/123%40chatroom/member-stats" \
--data-urlencode 'start=2026-09-01T00:00:00+07:00' \
--data-urlencode 'end=2026-10-01T00:00:00+07:00'
```
`conversationId` 必须是当前联系人列表中精确存在的群 `roomId`;不存在返回 `NOT_FOUND`,个人联系人返回 `NOT_GROUP_CONVERSATION`。`start` 和 `end` 必须同时提供,且使用带 offset 的 ISO-8601,`start` 不能晚于 `end`。HTTP adapter 只负责把稳定群 ID 解析为内部 md5 并调用现有 `GroupStatsService`,不会在 HTTP 层重新统计消息。
响应中的 `activeMembers` 和 `silentMembers` 都只描述当前成员名单;成员使用 `memberId`,活跃成员的 `lastMessageAt` 和 `range` 时间均为 ISO-8601。`freshness`、`complete`、`limitations` 必须原样保留,`unattributedMessages` 与 `excludedSystemMessages` 用于诊断,`firstMessageAt` 没有消息时为 `null`。当前成员统计不等于完整历史成员统计,`limitations` 表达的退群成员或未归档时段不能从文本中推导成额外的 `formerMembers`,也不会伪造 `totalMessageCount`。
## Automation API
`/api/v1/automations*` 是 Automation 的 canonical HTTP API,读写唯一的 `AutomationRuleStore`。它支持当前真实规则类型:`daily_report`、`scheduled_report`、`leave_notification`。本 API 不提供立即执行、重试或清理执行记录。
旧 `/api/v1/scheduled-reports*` 保持兼容,不设移除日期;它是面向旧 DTO 的受限 compatibility API,不是第二份存储,也不能表示所有新的定时日报目标和配置。新的 Agent 集成应使用 `/automations`。
创建和校验规则时 `enabled` 只能缺省或为 `false`。创建成功后必须调用 `/automations/{id}/enable` 才会启用。启用会重新校验当前规则;数据库未就绪、目标无法解析或配置无效时不会启用。`PATCH` 只接受规则配置字段,不可改 `ruleType`、`id`、创建/更新时间或 `enabled`;启停必须使用独立 endpoint。未知字段和未知枚举会被拒绝。
会话范围优先传 `wxid`、`roomId`(如 `xxx@chatroom`)或 canonical conversation ID。唯一匹配的联系人名可被解析为稳定 ID;重名会返回 `ambiguous_contact`,不会猜测。`daily_report.conditions.conversationIds` 在对外 API 中使用稳定 ID,Store 内部仍沿用既有 md5 口径。
校验请求不落盘、不发消息,也不执行规则。`valid: false` 时查看 `issues`;有效时 `normalized` 是经 ID 解析后的草稿,`effects` 描述启用后的动作,定时日报另外返回按本机时区计算的 `nextRunAt`。
```json
{
"name": "产品群每日日报",
"ruleType": "scheduled_report",
"scheduledReport": {
"schedule": { "time": "20:00" },
"report": {
"sourceConversationId": "wxid_product@chatroom",
"range": "today",
"messageTypes": ["text", "image"],
"templateId": "v1",
"memberNameMode": "groupNickname",
"timeoutSeconds": 300
},
"target": { "type": "file_transfer" },
"postfixText": ""
}
}
```
执行历史只读,默认最多返回 50 条,`limit` 范围是 1-200。`since` 和 `until` 接受带时区的 ISO-8601 时间;execution 本身最多留存 200 条。`running` 记录的 `finishedAt` 为 `null`。
新 Agent API 的错误格式:
```json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "自动化规则校验失败",
"details": []
},
"requestId": "..."
}
```
常见错误码包括 `UNAUTHORIZED`、`METHOD_NOT_ALLOWED`、`PAYLOAD_TOO_LARGE`、`INVALID_ARGUMENT`、`NOT_FOUND`、`NOT_GROUP_CONVERSATION`、`DATABASE_NOT_READY`、`VALIDATION_FAILED`、`SINGLETON_RULE`、`PROTECTED_RULE` 和 `PERSISTENCE_FAILED`。`leave_notification` 是固定单例:可读取、修改和启停,但不能创建第二条或删除。内置 `@我生成日报` 同样不能通过 HTTP 删除。
### 这些端点与实时机器人有什么关系
- `/api/v1/agent/status` 只用于查询 Agent Hub、微信连接器和数据库状态;
- `/api/v1/agent/group-report` 由外部 Agent 或脚本主动请求生成群聊总结图片;
- `/api/v1/agent/send` 是受 Bearer Token 保护的开发者/测试发送入口,用于通过已经连接的机器人发送文字或本地图片;它不是任意群发能力,也不是实时消息订阅接口;
- `/api/v1/scheduled-reports*` 会**写入**应用状态:创建、修改、删除、启停定时日报任务,以及立刻执行一次。加上 `/report` 和 `/agent/send`,这个 API 并非只读接口——拿到 Token 就能改配置、生成报告并发送微信消息,请按本机敏感凭据对待;
- `POST /api/v1/scheduled-reports/{id}/run` 与定时触发共用同一条链路:读取群聊 → 生成报告 → 保存 Report History → 尝试发送;
- 当前 API 没有对外暴露实时入站 webhook。微信消息由应用内部的 Agent Hub 和微信连接器接收、处理和回复。
## 时间查询
@@ -82,15 +222,118 @@ curl -H "$AUTH" "$BASE/chatlog?talker=技术交流群&time=2026-08-07"
## 响应与错误
- `200`:请求成功;
- `201`:定时日报任务创建成功;
- `401`:缺少、错误或已失效的 Bearer Token;
- `400`:参数或 JSON 请求体无效;
- `422`:媒体标识格式错误,或目标消息不是可读取的图片(`NOT_IMAGE`);
- `403`:浏览器 Origin 不在允许的 loopback 列表;
- `404`:端点、会话或群聊不存在;
- `409`:定时日报任务重复(`error === "duplicate"`,响应里会带回已存在的任务),或群聊名称匹配到多个目标(`ambiguous_contact`);
- `404`:端点、会话或群聊不存在;媒体标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。媒体请求遇到此状态时,先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在;
- `503`:数据库或 Agent Hub 尚未就绪;
- `500`:服务端处理或报告渲染失败。
成功响应会返回端点对应的 JSON 对象,例如 `chatlog` 包含 `contact`、`query`、`count` 和 `messages`,`contact` 返回 `count` 与 `contacts`。
图片消息在 `messages` 中保留原有字段,并额外提供 `media`:
```json
{
"type": "图片",
"content": "",
"media": {
"type": "image",
"available": true,
"url": "/api/v1/media/image%3A0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
}
```
当用户要求查看或理解图片时,使用 `media.url` 获取 `image/jpeg`、`image/png` 等真实二进制;不要根据 `[图片]` 猜测内容,也不要向 API 传入本地路径。
`media.url` 包含当前数据库连接内的独立媒体标识,不等同于消息 `id`。不同会话的消息 `id` 可能重复,调用方应原样使用返回的地址,不自行拼接或解析。重启、重连或切换账号后须重新读取 `/chatlog` 获取新地址;旧的纯消息 ID 地址仅在无歧义时兼容。`available` 只表示消息带有图片定位信息,不保证本地图片文件仍存在或可以解密。
## 与 MCP 的关系
当前实现没有把 `6131` 暴露为 MCP Server。需要在 Agent 中使用时,请安装随应用提供的 Reader Skill,并让 Skill 通过普通 HTTP 请求调用本 API。
## LLM-friendly Query Tool API
这些端点提供稳定的结构化 Query primitive,不接收自然语言问题,也不会调用 AI。它们与现有 API 共用端口、Bearer Token、loopback 和 CORS 安全策略。
```bash
BASE="http://127.0.0.1:6131/api/v1"
AUTH="Authorization: Bearer ${TRACEMEMO_API_TOKEN:-$WECHATEXPLORER_API_TOKEN}"
# 能力目录
curl -H "$AUTH" "$BASE/query/capabilities"
# BOBO 的第一条真实互动
curl -X POST -H "$AUTH" -H 'Content-Type: application/json' "$BASE/query/messages" \
-d '{"target":{"query":"BOBO"},"timeRange":{"kind":"all"},"direction":"any","order":"asc","limit":1,"excludeSystem":true}'
# 上个月 BOBO 发来的文件
curl -X POST -H "$AUTH" -H 'Content-Type: application/json' "$BASE/query/messages" \
-d '{"target":{"query":"BOBO"},"timeRange":{"kind":"previous_month"},"direction":"from_target","messageTypes":["file"],"order":"desc","limit":1}'
# 受限语义关键词检索(最多 4 个 variants)
curl -X POST -H "$AUTH" -H 'Content-Type: application/json' "$BASE/query/search" \
-d '{"target":{"query":"BOBO"},"timeRange":{"kind":"all"},"query":"答应之后给我或者帮我完成某件事情","variants":["我给你","我发你","弄好给你"],"limit":20}'
# 按会话和时间范围提取可供总结的证据
curl -X POST -H "$AUTH" -H 'Content-Type: application/json' "$BASE/query/conversation-overview" \
-d '{"target":{"query":"BOBO"},"timeRange":{"kind":"previous_month"}}'
```
`query/messages` 的 `messageRef` 是服务端生成的不透明引用,可直接传给 `query/message-context` 获取前后文;不要自行构造 wxid、md5 或数据库路径。
每条消息都会返回 `messageType`(`text`、`image`、`voice`、`video`、`file`、`link`、`sticker`、`system` 或 `other`)。非文本消息不会伪造 `text`;可识别的图片、视频、贴纸和文件会返回不含密钥或本地路径的 `attachment` 元数据。
`conversation-overview` 同时返回 `sourceCoverage` 与 `selection`:前者描述时间范围内源消息是否完整及 `sourceMessageCount`,后者描述从源消息中选出的 Evidence 数量及是否抽样。`evidence` 最终按 `timestamp` 升序返回,`messageRef` 是唯一推荐的消息引用。
`conversation-overview` 另有一个 `origin` 字段:`wcdb` 表示这次证据直接来自本机聊天数据库(会话概览的事实来源),`knowledge` 表示来自本地索引。
### 搜索范围(scope)
`query/messages`、`query/search`、`query/message-context` 和 `query/conversation-overview` 都接受一个可选的 `scope`,用来把检索限制在一个确定的语料边界内:
| scope | 含义 |
| ----------------------------------------- | -------------------------------------------------------------------- |
| `{"kind":"all"}` | 所有可读会话(默认;省略 `scope` 等价于此) |
| `{"kind":"groups"}` | 只搜群聊语料,**且包含群成员实际发送的消息**(不是群名称或群元数据) |
| `{"kind":"contact","conversationId":"…"}` | 只搜该一对一会话 |
| `{"kind":"current","conversationId":"…"}` | 只搜指定的那个会话(单聊或群聊) |
`conversationId` 是会话标识,可用 `/api/v1/resolve` 或 `/api/v1/contact` 得到。`scope` 一旦给出就是**权威边界**:`target` 落在范围之外会被拒绝(`status: "invalid_tool_arguments"`、`constraint: "target_outside_scope"`),不会静默扩大范围;范围里包含多个会话时,`query/messages` 与 `query/conversation-overview` 必须显式指定 `target`(`constraint: "target_required_for_scope"`)。
响应会回显实际生效的边界:
```json
{ "scope": { "kind": "groups", "conversationCount": 243 } }
```
跨会话检索时,`evidence` 的每一项都会带上它所属的会话,便于把结果归属到具体群 / 联系人与具体成员:
```json
{
"messageRef": "…",
"conversationName": "某个群",
"conversationType": "group",
"sender": "某成员",
"timestamp": 1789099069000,
"text": "…"
}
```
### 索引新鲜度(freshness)
`query/search` 依赖本地索引,而本地索引是异步建立的派生数据,可能落后于聊天数据库。因此它的响应会显式给出覆盖口径:
| 字段 | 含义 |
| ------------------- | ------------------------------------------------------------------- |
| `indexLatestAt` | 索引目前覆盖到的源数据时间(epoch ms),`null` 表示无法判定 |
| `sourceLatestAt` | 聊天数据库里最新的活跃时间(epoch ms),`null` 表示无法判定 |
| `coverage.state` | `complete` 只在索引确实覆盖了所请求的时间范围时出现 |
| `freshness.catchUp` | 本次为追赶索引做了什么:`none` / `reused` / `completed` / `pending` |
调用方**必须**把 `coverage` 当真:`coverage.state` 不是 `complete` 且 `evidence` 为空时,只能说明"这段范围暂时无法确认",**不能**下"没有找到"的结论。索引落后时服务端会自动请求一次追赶同步,但不会让请求无限等待;`freshness.catchUp` 为 `pending` 表示追赶仍在后台进行,稍后重试即可拿到更新的覆盖。
`query/messages` 与 `query/conversation-overview` 直读聊天数据库,不受索引新鲜度影响。
+6 -1
View File
@@ -8,7 +8,7 @@ Reader Skill 是一份给 Agent 的操作说明。安装后,Codex、Claude Cod
Reader Skill 只负责“外部 Agent 主动查询历史微信数据”。它不负责二维码登录、监听微信实时消息、接收机器人消息或管理 Agent Hub。想让机器人收到微信消息后处理并回复,请阅读[Agent Hub](./agent-hub.md)。
正式 Reader Skill 名称和目录是 `tracememo-reader`,新安装使用 `TRACEMEMO_API_TOKEN`。已安装的旧 `wechatexplorer-reader` 可在 v2.2.0 兼容期内继续使用旧变量。
正式 Reader Skill 名称和目录是 `tracememo-reader`,新安装使用 `TRACEMEMO_API_TOKEN`。已安装的旧 `wechatexplorer-reader` 仍可继续使用旧变量 `WECHATEXPLORER_API_TOKEN`(当前没有设定移除时间),但新安装请使用新名称与新变量名。
## 推荐安装流程
@@ -53,8 +53,13 @@ Reader Skill 可以指导 Agent 使用:
- 指定会话、日期或时间戳范围的聊天记录;
- 群成员快照;
- 结构化日报渲染和按群聊生成总结图片;
- 定时日报任务的查询、创建、修改、启停、删除、立即执行和执行记录;删除不可逆,Skill 要求先列出唯一任务并取得用户明确确认;
- 个人微信发送能力状态查询(`/wechat-personal/send-capability`);
- `query/*` 一组结构化 Query 端点:`messages`、`search`、`message-context`、`conversation-overview`;
- Agent Hub 状态检查与已连接机器人发送测试。这里的发送接口是开发者/测试用途,不是实时机器人入口,也不会让 Reader Skill 自动监听微信消息。
注意这个 API 不只是只读的:`/report`、`/agent/send` 和 `/scheduled-reports*` 会写入状态或真的发出微信消息。
端点、参数、错误码和鉴权细节以[Local HTTP API](./api.md)为准。Skill 文件保持短小,避免在多个文档中复制会变化的完整响应 schema。
## 隐私边界
+2 -2
View File
@@ -28,12 +28,12 @@ AI 回答后,你可以继续查看它参考了哪些聊天内容、这些内
来源覆盖受时间范围、会话范围、索引状态和可读媒体影响。例如:
- Knowledge 正在同步时,新的分析会被暂停;
- Knowledge 还没追到最新时,跨会话检索只覆盖到索引当前的时间点,答案会标注这个范围;
- 语音没有转写时,AI 可能只能看到消息类型;
- 图片无法读取或未启用图片理解时,AI 不应声称知道图片内容;
- 你只选择了一个群,答案不会自动代表所有聊天。
看到“可能遗漏”或“部分覆盖”时,扩大范围、先完成同步或检查原始媒体后再问。
Knowledge 在后台同步时**不会**暂停分析:你仍然可以提问,只是答案基于当前已可用的覆盖范围。看到“可能遗漏”或“部分覆盖”时,扩大范围、等同步追上或检查原始媒体后再问。
## 这不是事实保证
+83 -24
View File
@@ -4,34 +4,91 @@
```mermaid
flowchart LR
A[本机微信数据] --> B[读取与解析]
B --> C[聊天档案与普通搜索]
B --> D[本地知识索引]
D --> E[筛选相关消息]
E --> F[用户配置的 AI Provider]
F --> G[回答与可核对来源]
B --> H[聊天导出]
B --> I[整理日报输入]
I --> F
F --> J[本地保存 HTML 与 PNG]
B --> K[Local HTTP API]
K --> L[外部 Agent]
M[微信机器人消息] --> N[Agent Hub]
N --> B
N --> F
WX["本机微信数据"] --> PARSE["读取与解析"]
PARSE --> ARCHIVE["聊天档案与普通搜索"]
PARSE --> EXPORT["聊天导出"]
PARSE --> IDX["本机索引"]
IDX --> TEXTIDX["聊天记录索引"]
IDX --> IMGIDX["图片文字索引(本机识别)"]
TEXTIDX --> UNDERSTAND["Understand:AI Search / 问问微信"]
IMGIDX --> UNDERSTAND
UNDERSTAND --> PROVIDER["你配置的 AI Provider"]
PROVIDER --> ANSWER["回答与可核对来源"]
PARSE --> REPORTINPUT["整理日报输入"]
REPORTINPUT --> PROVIDER
PROVIDER --> REPORTFILE["本机保存 HTML 与 PNG"]
PARSE --> MONITOR["Monitor:退群监控 / 成员快照"]
MONITOR --> RULE["自动化规则"]
REPORTFILE --> RULE
RULE --> POLICY["Policy"]
POLICY --> GATEWAY["Action Gateway"]
GATEWAY --> CAP["本机发送能力"]
CAP --> AUDIT["执行记录与审计"]
PARSE --> API["Local HTTP API"]
API --> EXTAGENT["外部 Agent / Reader Skill"]
BOT["微信机器人消息"] --> HUB["Agent Hub"]
HUB --> PARSE
HUB --> PROVIDER
```
## Remember → 图片文字 → Understand → Monitor → Act
TraceMemo 的工作方式可以概括为:
```text
Remember → 图片文字 → Understand → Monitor → Act
```
- **Remember**:读取并解析本机微信数据,建立聊天档案、普通搜索和导出。
- **图片文字**:在本机识别图片里的文字,把截图、公告、报价图也变成可检索的内容。这一步不联网。
- **Understand**:Knowledge、AI Search / 问问微信、群聊日报。需要模型时,只把完成这次任务所需的受控上下文交给 Provider。
- **Monitor**:用成员快照对比发现群成员变化,产出成员退出事件。
- **Act**:自动化规则把前面的步骤串起来(定时日报、退群通知);动作经过统一执行边界,并留下执行记录。
回答和动作结果都应能回到来源或记录核对。
## 退群监控
退群监控使用成员快照判断变化:
```text
Current Membership → Snapshot Diff → Member Event
```
上一份有效快照(Last Good Snapshot)不会被不完整读取覆盖,因此重启后仍可继续监控通知。监控关闭期间发生的变化,不会在重新开启后补报。
成员退出事件同时是「自动化」里「退群通知」规则的触发条件。
## 动作执行与审计
自动发送和监控动作经过统一边界:
```text
Feature → Policy → Gateway → Capability → Execution → Audit
```
Policy blocked 表示策略不允许,Capability unavailable 表示当前发送能力不可用,Send failed 表示已经尝试但执行失败。Action Audit / Logs 会保留执行结果;定时日报即使发送失败,也会保留已生成的报告记录。
这些动作统一由「自动化」管理,当前有三类规则:**@我生成日报**、**定时日报**、**退群通知**。发送目标支持当前群聊、文件传输助手、自己、指定好友,不是任意群发。
## 哪些步骤在本机
- 微信数据库读取与解析;
- 聊天档案浏览和普通搜索;
- Knowledge 索引与增量同步;
- 图片文字索引:识别图片中的文字完全在本机进行,原始图片不会因为本地识别而上传;
- 离线语音转写;
- 聊天导出文件、日报 HTML/PNG 和本地历史记录的保存。
## 哪些步骤可能调用外部服务
当你主动使用 AI Search、群聊日报或图片理解时,应用会把完成任务所需的受控问题和上下文发送给你配置的 Provider。它不会因为打开软件就自动上传完整数据库。
当你主动使用 AI Search、群聊日报或图片理解时,应用会把完成任务所需的受控问题和上下文发送给你配置的 Provider。它不会因为打开软件就自动上传完整数据库,本机 OCR、离线语音转写和普通搜索也不会触发外发。
Agent Hub 收到微信机器人的文字后,也可能为了理解请求或生成总结调用已配置的 Provider。Reader Skill 调用的是本机 API;外部 Agent 是否把读取结果继续交给云端模型,取决于外部 Agent 自己的配置。
@@ -39,13 +96,15 @@ Agent Hub 收到微信机器人的文字后,也可能为了理解请求或生
## 产品名词和用户任务的对应关系
| 用户想做什么 | 产品中可能看到的名称 |
| ------------------------ | ---------------------------- |
| 让 AI 找相关聊天 | AI Search、Retrieval |
| 让答案能回到原消息 | Evidence、Citation |
| 查看 AI 查找过程 | Search Trace |
| 让跨会话查找更稳定 | Knowledge、FTS 索引 |
| 让外部 Agent 读取聊天 | Reader Skill、Local HTTP API |
| 让微信机器人调用本机能力 | Agent Hub |
| 用户想做什么 | 产品中可能看到的名称 |
| ------------------------------ | ---------------------------- |
| 让 AI 找相关聊天 | AI Search、Retrieval |
| 让答案能回到原消息 | Evidence、Citation |
| 查看 AI 查找过程 | Search Trace |
| 让跨会话查找更稳定 | Knowledge、FTS 索引 |
| 搜到截图、公告图里写过的文字 | 图片文字索引、本机 OCR |
| 让日报、退群通知按规则自动执行 | 自动化、Policy、执行记录 |
| 让外部 Agent 读取聊天 | Reader Skill、Local HTTP API |
| 让微信机器人调用本机能力 | Agent Hub |
先按任务使用,再在需要排查或开发集成时阅读术语。
@@ -0,0 +1,447 @@
# 实验性功能:自托管微信分享卡片
![微信卡片分享效果示例](../../public/微信卡片分享.png)
> **实验性功能**
> 该能力需要用户自行准备 Cloudflare、域名和微信公众平台测试号,目前不属于开箱即用的稳定功能。Cloudflare、微信 JS-SDK、测试号权限或微信客户端行为变化,都可能导致分享卡片失效。
## 新手推荐:直接交给 Agent
如果你不熟悉 Cloudflare、Wrangler 或命令行,不需要手动照着整篇文档操作。把下面这个 Skill 文件夹交给 Codex、Claude Code 或其他能够操作项目终端的编程 Agent:
```text
docs/skill/setup-wechat-share-card/
```
然后对 Agent 说:
```text
请使用 setup-wechat-share-card Skill,帮我部署 TraceMemo 的实验性微信分享卡片服务。尽量自动完成,只在缺少必要信息时一次性问我。
```
Agent 会自动:
- 检查 Node.js、pnpm 和 Wrangler;
- 必要时临时下载 Wrangler;
- 打开 Cloudflare 登录并执行 `whoami`;
- 自动生成 `UPLOAD_TOKEN`;
- 创建或复用私有 R2 Bucket;
- 写入 Worker Secret;
- 根据你的域名生成本机 Worker 配置;
- 部署 Worker并执行健康检查和微信签名检查;
- 把上传密钥复制到剪贴板,供你粘贴到 TraceMemo。
Agent 无法替你创建微信测试号或决定使用哪个域名,因此通常只需要你提供:
1. 你准备使用的分享域名,例如 `share.example.com`;
2. 微信测试号页面中的 AppID;
3. 微信测试号页面中的 AppSecret;
4. 浏览器弹出 Cloudflare OAuth 页面时完成一次登录授权。
真实配置保存在被 Git 忽略的本机 `.env` 中,不会写入 `.env.example`。不要把 `.env` 发给别人或提交到仓库。
TraceMemo 可以把已经生成的群聊日报长图发布为一个临时网页,并在微信中分享成带有标题、描述和缩略图的卡片。
TraceMemo **不提供公共卡片服务器**。使用该功能前,需要按照本文部署一套属于你自己的卡片服务。日报图片将上传到你自己的 Cloudflare R2,而不是上传到 TraceMemo 作者的服务器。
## 这个功能解决什么问题
直接把日报 PNG 发到微信,只会显示为一张普通图片。微信卡片还需要:
- 一个域名 (未能备案的话 在微信里点击多次 可能会被微信内置窗口提示需要备案);
- 卡片标题和描述;
- 一张微信可以读取的缩略图;
- 微信 JS-SDK 签名;
- 一个临时保存日报图片的位置。
本项目提供的 Cloudflare Worker 负责这些工作。桌面端上传日报后,会得到一个分享链接和二维码。用微信扫码打开链接,再点击右上角菜单分享,即可生成微信卡片。
## 数据会经过哪里
```mermaid
flowchart LR
A[TraceMemo 本机日报 PNG] -->|带 UPLOAD_TOKEN 上传| B[你的 Cloudflare Worker]
B --> C[你的私有 R2 Bucket]
B -->|AppID + AppSecret| D[微信公众平台接口]
D -->|access_token 与 jsapi_ticket| B
B --> E[临时分享网页]
E --> F[微信 JS-SDK]
F --> G[微信好友或群聊卡片]
```
与 TraceMemo 的本地浏览能力不同,启用分享卡片后,当前日报长图、缩略图、卡片标题和描述会离开本机,上传到你控制的 Cloudflare 账号。
## 你需要准备什么
| 项目 | 用途 | 从哪里获得 |
| ------------------------ | -------------------------------------------- | ---------------------------------------- |
| Cloudflare 账号 | 运行 Worker 和保存 R2 图片 | 自行注册 Cloudflare |
| 托管在 Cloudflare 的域名 | 提供 HTTPS 分享地址 | 使用自己的域名,例如 `share.example.com` |
| R2 Bucket | 临时保存日报和缩略图 | 使用 Wrangler 创建 |
| `UPLOAD_TOKEN` | 阻止陌生人调用你的上传接口 | **由你自己随机生成** |
| 微信测试号 AppID | 标识调用 JS-SDK 的微信应用 | 微信公众平台接口测试号页面 |
| 微信测试号 AppSecret | Worker 获取微信接口凭据 | 微信公众平台接口测试号页面 |
| JS 接口安全域名 | 告诉微信哪些网页可以使用该 AppID 调用 JS-SDK | 在微信测试号页面填写你的分享域名 |
微信公众平台接口测试号入口:
<https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index>
微信 JS-SDK 官方文档:
<https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html>
## 理解三个重要配置
### `UPLOAD_TOKEN` 从哪里来
`UPLOAD_TOKEN` **不是从 Cloudflare 或微信后台领取的**,它是卡片服务部署者自己生成的一段随机密码。
它用于保护 Worker 的上传接口:TraceMemo 上传日报时,会发送:
```http
Authorization: Bearer <UPLOAD_TOKEN>
```
Worker 只有在密钥完全一致时才接受上传。没有它,任何知道接口地址的人都可能向你的 R2 上传文件并消耗资源。
在 macOS 或 Linux 中生成一枚 64 位十六进制随机密钥:
```bash
openssl rand -hex 32
```
示例输出只用于说明格式,不要直接使用:
```text
8a4d...一共 64 个十六进制字符...72ef
```
生成后,同一个值需要配置到两个地方:
1. Cloudflare Worker Secret `UPLOAD_TOKEN`;
2. TraceMemo“生成微信卡片”弹窗中的“上传密钥”。
如果两边不一致,卡片服务会返回 HTTP 401 或“未授权”。
TraceMemo 会使用 Electron `safeStorage` 将服务地址和上传密钥加密保存在本机。不要把密钥提交到 Git,也不要写入 `wrangler.jsonc`。
### AppID 和 AppSecret 从哪里来
打开[微信公众平台接口测试号](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index),使用微信扫码登录。
页面上方会显示:
- `appID`;
- `appsecret`。
将它们分别保存为 Worker Secret:
```text
WECHAT_APP_ID
WECHAT_APP_SECRET
```
它们的作用不同:
- AppID 用于标识这个微信测试应用;
- AppSecret 是高敏感凭据,Worker 用它向微信服务器获取 `access_token`;
- Worker 再使用 `access_token` 获取 `jsapi_ticket`;
- 最后使用 `jsapi_ticket`、当前网页 URL、时间戳和随机串生成 JS-SDK 签名。
AppSecret 只能保存在 Worker Secret 中。不要把它填写到 TraceMemo 的“上传密钥”输入框,不要发送给前端,也不要提交到仓库。怀疑泄露时,应立即在微信后台重置并更新 Worker Secret。
### JS 接口安全域名是干什么的
JS 接口安全域名是微信对网页来源的白名单。
假设你的分享服务地址是:
```text
https://share.example.com
```
那么测试号页面中的“JS 接口安全域名”应填写:
```text
share.example.com
```
填写时:
- 不带 `https://`;
- 不带 `/s/xxx` 等路径;
- 不要填写 Cloudflare Worker 名称;
- 必须与用户实际打开分享页时的域名一致。
它不是用来解析 DNS 的。域名仍然需要先在 Cloudflare 中正确绑定到 Worker。安全域名的作用是告诉微信:允许这个域名下的网页使用当前 AppID 请求 JS-SDK 能力。
如果没有配置、填错域名,或签名 URL 与实际页面 URL 不一致,通常会出现 `invalid signature`、`config:fail` 或分享信息没有生效。
微信可能要求下载一个 TXT 验证文件,并确保它可以通过下面的地址访问:
```text
https://share.example.com/微信提供的文件名.txt
```
项目 Worker 已包含根路径验证文件的实现方式。你需要把自己的文件名和内容加入 `services/share-card-worker/src/index.js` 中的 `WECHAT_DOMAIN_VERIFICATION`,然后重新部署。
## 自托管部署步骤
以下命令均在项目根目录执行。
### 1. 登录 Cloudflare
项目建议使用本地 Wrangler:
```bash
pnpm exec wrangler login
pnpm exec wrangler whoami
```
如果本地版本的 OAuth 登录出现 `invalid_scope` 等问题,可临时使用更新版本:
```bash
pnpm dlx wrangler@latest login
pnpm dlx wrangler@latest whoami
```
登录注意事项:
- 让 Wrangler 自动打开浏览器最稳妥;
- 不要复用以前生成的 OAuth 链接;
- 不要修改链接中的 `state`、`code_challenge` 或回调地址;
- 不建议使用无痕窗口或跨浏览器复制链接;
- 默认回调使用 `localhost:8976`,端口被占用时先结束旧的 Wrangler 登录进程;
- 浏览器提示授权成功后,仍应通过 `whoami` 核对账号。
### 2. 修改 Worker 配置
打开:
```text
services/share-card-worker/wrangler.jsonc
```
至少修改下面两个位置:
```jsonc
{
"routes": [
{
"pattern": "share.example.com",
"custom_domain": true
}
],
"vars": {
"PUBLIC_ORIGIN": "https://share.example.com",
"DEFAULT_EXPIRY_DAYS": "7"
}
}
```
`routes[].pattern` 是 Worker 自定义域名,`PUBLIC_ORIGIN` 是生成分享链接和校验签名来源时使用的完整 HTTPS 地址,两者必须一致。
不要直接照抄仓库维护者的域名。请替换为你自己 Cloudflare 账号中的域名或子域名。
### 3. 创建私有 R2 Bucket
默认配置使用 Bucket 名称:
```text
wechatexplorer-share-reports
```
创建:
```bash
pnpm exec wrangler r2 bucket create wechatexplorer-share-reports \
--config services/share-card-worker/wrangler.jsonc
```
Worker 中的绑定名称是 `REPORTS`。R2 会保存:
```text
cards/<card-id>/card.json
cards/<card-id>/report.png
cards/<card-id>/thumbnail.jpg
```
- `card.json`:标题、描述、创建时间和过期时间;
- `report.png`:完整日报长图;
- `thumbnail.jpg`:微信卡片缩略图。
请保持 R2 Bucket 私有,不要启用公开 `r2.dev` 开发 URL。图片应统一通过 Worker 的随机卡片 URL 读取。
### 4. 生成并配置 `UPLOAD_TOKEN`
```bash
openssl rand -hex 32
```
复制生成结果,然后执行:
```bash
pnpm exec wrangler secret put UPLOAD_TOKEN \
--config services/share-card-worker/wrangler.jsonc
```
Wrangler 提示输入时粘贴密钥。终端不会正常显示 Secret 内容。
### 5. 配置微信 AppID 和 AppSecret
从[微信公众平台接口测试号](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index)复制 AppID:
```bash
pnpm exec wrangler secret put WECHAT_APP_ID \
--config services/share-card-worker/wrangler.jsonc
```
再复制 AppSecret:
```bash
pnpm exec wrangler secret put WECHAT_APP_SECRET \
--config services/share-card-worker/wrangler.jsonc
```
Secret 不会出现在 `wrangler.jsonc` 中。如果你更换 Cloudflare 账号或重新创建 Worker,需要重新配置全部三个 Secret。
### 6. 配置微信测试号
在测试号页面完成:
1. 使用测试微信关注该测试号;
2. 将 `share.example.com` 填入“JS 接口安全域名”;
3. 按页面提示完成 TXT 文件域名验证;
4. 确认 AppID/AppSecret 与刚才写入 Worker 的值来自同一个测试号。
测试号只适合开发和验证。正式公众号的接口权限、认证要求和后台菜单可能不同,请以微信公众平台实际规则为准。
### 7. 部署 Worker
```bash
pnpm exec wrangler deploy \
--config services/share-card-worker/wrangler.jsonc
```
Cloudflare Custom Domain 要求域名已经位于同一 Cloudflare 账号中。如果该子域名已经存在 A、AAAA 或 CNAME 记录,绑定可能失败。删除冲突记录,或者换一个未使用的子域名,例如 `share2.example.com`。
更新 Secret 后,如果线上仍提示旧配置,可再执行一次完整部署。
## 在 TraceMemo 中配置
生成一份日报后,点击“生成微信卡片(实验性)”。首次使用需要填写:
```text
服务地址:https://share.example.com
上传密钥:你自己通过 openssl rand -hex 32 生成的 UPLOAD_TOKEN
```
这里的“上传密钥”绝对不是微信 AppSecret。
配置保存后,TraceMemo 会上传当前日报和缩略图,返回二维码。使用已经关注测试号的微信扫码,打开页面后再通过右上角菜单分享。
## 验证部署
### 健康检查
```bash
curl -fsS https://share.example.com/health
```
正常结果类似:
```json
{ "ok": true, "service": "wechatexplorer-share-card", "storage": "ready" }
```
### JS-SDK 签名检查
```bash
curl -fsS \
'https://share.example.com/api/wx-signature?url=https%3A%2F%2Fshare.example.com%2Fhealth'
```
正常结果应包含:
```text
appId
timestamp
nonceStr
signature
```
响应中不应包含 AppSecret、`access_token` 或 `jsapi_ticket`。
## 常见问题
### HTTP 401 / 未授权
TraceMemo 中保存的上传密钥与 Worker 的 `UPLOAD_TOKEN` 不一致。重新生成或重新配置时,必须同步更新两边。
### “微信 JS-SDK 尚未配置”
Worker 缺少 `WECHAT_APP_ID` 或 `WECHAT_APP_SECRET`。执行两个 `secret put`,再重新部署。
### `invalid signature` 或分享信息不生效
依次检查:
- `PUBLIC_ORIGIN` 是否与浏览器实际访问的 origin 完全一致;
- JS 接口安全域名是否只填写了域名;
- AppID/AppSecret 是否属于同一个测试号;
- AppSecret 是否已被重置但 Worker 仍保存旧值;
- 分享页面是否经过了改变 URL 的代理或重定向;
- 测试微信是否已关注测试号。
### 自定义域名绑定失败
检查同名 A、AAAA、CNAME 记录是否已经存在,域名是否位于当前 Wrangler 登录的 Cloudflare 账号中。
### R2 未配置
确认 Bucket 存在,并且 `wrangler.jsonc` 中的绑定名称为 `REPORTS`、`bucket_name` 与实际 Bucket 一致。
### 卡片过期或图片消失
默认有效期为 7 天。Worker 的定时任务会删除过期卡片的元数据、日报和缩略图,这是设计行为。
## 安全和隐私注意事项
- 日报可能包含敏感群聊内容。只分享你有权分享的内容。
- 获得分享 URL 的人,在过期前可能查看对应日报;当前实现不是按访问者身份授权。
- `UPLOAD_TOKEN` 是整个 Worker 的服务级密钥,不是每个用户独立的账号凭据。
- 不要把 `UPLOAD_TOKEN`、AppSecret 或 Wrangler 凭据提交到 Git。
- R2 保持私有,不要把 Bucket 直接公开。
- 建议定期轮换 `UPLOAD_TOKEN`,怀疑泄露时立即轮换。
- 微信 AppSecret 泄露时,应在微信后台重置,并立即更新 Worker Secret。
- 自托管者自行承担 Cloudflare 用量、域名、数据合规和微信平台规则相关责任。
## 当前实验性限制
- 需要用户自己部署,普通用户无法直接开箱使用;
- 使用一个共享的 `UPLOAD_TOKEN`,没有多用户账号系统;
- 分享链接在有效期内属于“知道链接即可访问”;
- 依赖微信 JS-SDK 和测试号能力,微信侧规则变化可能造成失效;
- 当前仅上传 PNG 日报和 JPEG 缩略图;
- 没有管理后台用于列出、提前删除或审计所有卡片;
- 过期清理由定时任务完成,不保证到期瞬间立即删除。
## 代码入口
- Worker:`services/share-card-worker/src/index.js`
- Worker 配置:`services/share-card-worker/wrangler.jsonc`
- Worker 测试:`services/share-card-worker/test/index.test.js`
- 桌面端上传:`src/main/wechat-share-card-service.ts`
- 本地加密配置:`src/main/wechat-share-config-store.ts`
- 分享弹窗:`src/renderer/src/components/reports/WechatShareCardDialog.tsx`
- 共享类型:`src/shared/wechat-share-card.ts`
## 参考资料
- [微信公众平台接口测试号](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index)
- [微信 JS-SDK 官方文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html)
- [Cloudflare Wrangler 命令文档](https://developers.cloudflare.com/workers/wrangler/commands/)
- [Cloudflare R2 Wrangler 命令](https://developers.cloudflare.com/workers/wrangler/commands/r2/)
- [在 Worker 中绑定和使用 R2](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/)
- [Cloudflare Worker Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/)
@@ -0,0 +1,308 @@
# TraceMemo Local HTTP API / Agent API 能力审计
本报告基于当前代码、文档、IPC、Renderer 调用和相关测试做静态审计,对应当前发布应用版本 `2.5.0`;不连接真实微信数据库,也不修改生产代码、API Center 或测试。
主要代码入口:[http-server.ts](../../src/main/http-server.ts)、[automation-rule-store.ts](../../src/main/services/automation-rule-store.ts)、[group-exit-monitor-service.ts](../../src/main/services/group-exit-monitor-service.ts)、[group-stats-service.ts](../../src/main/services/group-stats-service.ts)、[api.md](../agent/api.md)。
## 1. Executive Summary
当前 HTTP 层声明了 **30 个 Method + Path 模板**:15 个 GET、12 个 POST、1 个 PATCH、1 个 DELETE、1 个 HEAD。共享 `LOCAL_API_ENDPOINTS` 定义 **14 个测试项**,但 Renderer 的 `API_ENDPOINTS` 实际只展示 **12 项**;个人微信能力和 `GET /scheduled-reports` 虽有共享定义,界面没有展示。界面也没有结构化 Query、媒体读取、定时日报写操作、退群监控、群统计、Automation 通用资源或执行日志查询。
当前不缺成熟的 Query primitive:结构化读取消息、Knowledge 搜索、消息前后文、会话概览和图片 OCR 搜索已经有 HTTP 契约。真正的能力缺口集中在 **监控配置、自动化规则通用管理、群员统计、执行历史、全局能力发现、发送状态和报告历史**。
几个影响后续设计的代码事实:
1. 定时日报已迁入 `AutomationRuleStore`。当前 `/scheduled-reports` 是旧 HTTP 契约的兼容投影,不是第二份活动规则存储;但它只表达“生成后发回来源群”,其他当前合法目标不会出现在该兼容 API 列表中。
2. 退群监控只负责监测范围、快照和事件历史;退群后如何通知由唯一的 `leave_notification` Automation 规则负责。二者应分别建资源。
3. 群统计 Service 足以提供活跃成员、当前沉默成员、时间范围、新鲜度和限制说明;它没有结构化的 former member 列表,也没有覆盖所有发言者的群总消息数。
4. 现有微信 Action Gateway 有策略、审计和幂等骨架,但策略目前主要校验收件人,以及 Automation purpose allowlist;手动用户 purpose 默认可放行。它只发个人微信,现有 `/agent/send` 则走 iLink 的 `WechatSendGateway`。两者不能直接视为统一的 Agent 安全边界。
5. HTTP 静态 GET handler 大多没有 method guard;对这些路径发 POST、PATCH 或 DELETE 仍会执行读处理。Automation store 的规则落盘失败会记录日志,但仍将内存中的规则返回为成功。
建议先做应用级能力发现、退群监控资源、严格校验后的 Automation CRUD/执行查询;发送和“立即执行”放到有 dry-run、明确确认、幂等键和 Action 审计的后续阶段。
## 2. Current API Inventory
以下按 `http-server.ts` 的路由分派和动态 route factory 盘点。静态读路由的 `Method` 是当前文档和产品语义的预期方法;实际接受方法的差异见本节末尾。
| Method | Path | 能力 | Read/Write/Execute | Service | API Center | 文档 | Agent 价值 |
|---|---|---|---|---|---|---|---|
| GET | `/api/v1/health` | HTTP 与数据库 ready 状态;唯一免 Token 路径 | Read | `isReady()` | 是 | 是 | 高:连通性 |
| GET | `/api/v1/current_time` | 本机时间、时区、日期 | Read | JavaScript `Date` | 是 | 是 | 中:相对日期换算 |
| GET | `/api/v1/contact` | 联系人和群列表;`filter`、`type`;使用异步 hydration | Read | `chat-service.listContactsAsync` | 是 | 是 | 高:标识发现与 resolve 前置 |
| GET | `/api/v1/chatroom` | 群聊列表;`keyword`;使用异步 hydration | Read | `chat-service.listContactsAsync` | 是 | 是 | 高:群标识发现 |
| GET | `/api/v1/recent_chat` | 最近会话;`limit` 默认 50 | Read | `chat-service.listRecentChat` | 是 | 是 | 高:导航/摘要 |
| GET | `/api/v1/chatlog` | 按 talker 和时间读取原始消息;移除 `contentData.aeskey` | Read | `chat-service.listMessages`、`resolveMd5` | 是 | 是 | 高,但旧式、未做结构化分页 |
| GET | `/api/v1/group_snapshot` | 群成员快照;必填 `md5` | Read | `chat-service.getGroupSnapshot` | 是 | 是 | 高:成员身份解析 |
| GET | `/api/v1/resolve` | 昵称、wxid、md5 解析为会话 | Read | `chat-service.resolveMd5` | 是 | 是 | 高;新资源宜返回稳定 ID 和歧义候选 |
| POST | `/api/v1/report` | 接收完整结构化日报并导出 HTML/PNG;当前拒绝外部 `templateRef` | Write:本地文件 | `group-report-service.exportGroupReport` | 是 | 是 | 低/中:低层渲染契约,Agent 须先拼完整结构 |
| POST | `/api/v1/agent/group-report` | 读群消息、调用 AI 生成群总结、导出 HTML/PNG | Execute:AI/本地文件 | `agent-group-report-service.generateAgentGroupReport` | 是 | 是 | 高但有模型费用/数据出站;结果不等同于报告历史记录 |
| GET | `/api/v1/agent/status` | Agent Hub、connector、数据 API、数据库状态 | Read | `agentHubService.getStatus` | 是 | 是 | 中:只覆盖 Agent Hub,不是应用总能力 |
| POST | `/api/v1/agent/send` | 通过 Agent Hub 连接器发送文字或媒体 | Execute:微信发送 | `agentHubService.testSend` → `WechatSendGateway`/iLink | 是 | 是 | 高但 R2;是测试入口,不含统一 Action policy/幂等确认 |
| GET | `/api/v1/wechat-personal/send-capability` | 个人微信 text/image/voice 能力状态 | Read | `PersonalWechatCapabilityService`,由 `ScheduledReportApiService` 包装 | 否(共享定义有,界面未展示) | 是 | 高但只代表 personal,不代表 iLink |
| GET | `/api/v1/scheduled-reports` | 列出旧 DTO 可表达的定时日报规则 | Read | `ScheduledReportApiService.list` → 规则投影 | 否(共享定义有,界面未展示) | 是 | 高但不完整:仅 `source_chat` 目标 |
| POST | `/api/v1/scheduled-reports` | 创建旧型定时日报,来源群即发送目标 | Write:规则配置 | `ScheduledReportApiService.create` → `AutomationRuleStore` | 否 | 是 | 高;功能受旧 DTO 限制 |
| GET | `/api/v1/scheduled-reports/{id}` | 单条旧型日报规则投影 | Read | `ScheduledReportApiService.get` → `AutomationRuleStore` | 否 | 是 | 中/高:只支持兼容投影规则 |
| PATCH | `/api/v1/scheduled-reports/{id}` | 修改旧型日报规则 | Write:规则配置 | `ScheduledReportApiService.update` → `AutomationRuleStore` | 否 | 是 | 高但只能改旧字段/目标 |
| DELETE | `/api/v1/scheduled-reports/{id}` | 删除旧型日报规则 | Write:删除配置 | `ScheduledReportApiService.delete` → `AutomationRuleStore` | 否 | 是 | 中;新 API 应标 R3 并防护系统规则 |
| POST | `/api/v1/scheduled-reports/{id}/enable` | 启用规则 | Write:配置/未来执行 | `ScheduledReportApiService.setEnabled` → `AutomationRuleStore` | 否 | 是 | 高;启用后未来可能发送微信 |
| POST | `/api/v1/scheduled-reports/{id}/disable` | 暂停规则 | Write:配置 | `ScheduledReportApiService.setEnabled` → `AutomationRuleStore` | 否 | 是 | 高 |
| POST | `/api/v1/scheduled-reports/{id}/run` | 手动触发完整日报规则 | Execute:可能调用 AI、保存历史、发微信 | `ScheduledReportService.runScheduledReportNow` → `AutomationService` | 否 | 是 | 高但 R2;当前无 HTTP 幂等键/确认 |
| GET | `/api/v1/scheduled-reports/{id}/executions` | 旧型 execution 历史和新 Automation execution 投影 | Read | `ScheduledReportService.listExecutions` | 否 | 是 | 高但旧响应模型/有限留存 |
| POST | `/api/v1/scheduled-reports/executions/{executionId}/retry-send` | 旧文档称复用 PNG 重发 | Execute 路由存在但当前固定 `501 not_supported` | `ScheduledReportApiService.retrySend` | 否 | **路径有,语义已失效** | 无:不能重发 |
| GET | `/api/v1/media/{mediaId}` | 读取消息关联的图片二进制 | Read | `http-media-service.readImageMedia` | 否 | 是 | 高:图像证据 |
| HEAD | `/api/v1/media/{mediaId}` | 图片资源存在性/响应头 | Read | `http-media-service.readImageMedia` | 否 | 否 | 低/中 |
| GET | `/api/v1/query/capabilities` | Query Tool 支持的结构化操作、范围和上限 | Read | `LocalQueryApiService.capabilities` | 否 | 是(独立章节) | 高,但不是 TraceMemo 应用能力清单 |
| POST | `/api/v1/query/messages` | 单会话、范围、时间、方向、类型等确定性消息读取 | Read | `LocalQueryApiService.messages` | 否 | 是 | 高:推荐 Query primitive |
| POST | `/api/v1/query/search` | Knowledge 关键词检索,返回覆盖、新鲜度和 OCR 命中 | Read | `LocalQueryApiService.search` + `KnowledgeSearchService` | 否 | 是 | 高:必须读取 coverage/freshness |
| POST | `/api/v1/query/message-context` | 通过 opaque `messageRef` 读取前后文 | Read | `LocalQueryApiService.context` | 否 | 是 | 高:稳定消息引用 |
| POST | `/api/v1/query/conversation-overview` | 单会话范围的概览证据和 source coverage | Read | `LocalQueryApiService.overview` | 否 | 是 | 高:broad summary |
路由来源:[http-server.ts](../../src/main/http-server.ts#L212)、scheduled/query/media route factory(同文件 L404-L670)。总数是代码中声明的业务方法模板,不代表静态 handler 都正确拒绝其他动词:`/health`、`/current_time`、`/contact`、`/chatroom`、`/recent_chat`、`/chatlog`、`/group_snapshot`、`/resolve`、`/agent/status` 没有检查 `req.method`。这些路径携带错误动词仍会走同一 handler;特别是 `POST /health` 也绕过 Token 检查,因为鉴权例外按 pathname 判断。新/旧路由都应 fail closed 并对不支持的方法返回 405。
全局 `OPTIONS` 在路由和鉴权前处理;媒体额外支持 HEAD。非 health 路径要求 `Authorization: Bearer …`,但 `readBody` 没有大小上限。路由外层目前没有统一请求 schema、统一错误 envelope 或 request id。
## 3. Internal Capability Inventory
这里按产品能力追 Service → IPC/Renderer → HTTP → 外部 Agent,而不是按当前 API 名字扩展。
| 业务域 | 已有能力及实现 | IPC / Renderer | HTTP 现状 | Agent 结论 |
|---|---|---|---|---|
| Chat / Contact | 联系人、群聊、最近会话、解析、历史消息、群快照、消息周边上下文、媒体定位;`contact-resolution-service` 可精确匹配别名并返回歧义候选 | `db:getContacts`、`db:getGroupSnapshot`、消息查询/around 等,Chat/Contact/档案 UI | 旧 Reader routes + `/query/*` + 图片 `/media/{id}` | 已有 Read API 基础完整。新配置应使用 `m_nsUsrName` 对应的 wxid/roomId 等稳定 ID,不应把昵称当长期键 |
| Query / Knowledge | `messages`、`search`、`message-context`、`conversation-overview`;Knowledge 索引覆盖/新鲜度;Image OCR 可进入 Knowledge 搜索和证据 | `knowledge:search/getStatus/startIndex/cancelIndex`、AI Search UI、`LocalQueryToolExecutor` | 四个 Query 操作均已 HTTP 化;`query/capabilities` 只描述 Query Tool | 没有需要重做的基础 Query primitive。缺的是统一应用 capability/status、更多外层筛选和 API Center 展示 |
| Group Analytics | 活跃/沉默当前成员、每人 messageCount/lastMessageTime、窗口时间、memberCount、unattributed/system 消息、firstMessageTime、freshness/complete/limitations;索引未新鲜时最多等待 2 秒并如实降级 | `group-stats:getMemberStats`;聊天页群统计 UI | 无 | P1 候选。当前 query 要求群 `userMd5` + epoch 毫秒。former sender 只以限制文案给出数量,没有 formerMembers 数组/结构化计数;没有覆盖 former sender 的群总消息数字段,需扩 DTO 后再承诺 |
| Group Exit Monitor | enabled/running/nativeMonitor 状态、监控 roomId 集合、lastChecked、unread、事件历史;每次回传最多 500 条,但完整事件历史 append-only 长期保存;可立即 check、改范围、启停、筛事件、清历史、mark read | `group-exit-monitor:*`;`GroupExitMonitorWorkspace` | 无 | P0。拆成 monitor state/config、events、check。`checkNow` 可能发现事件并触发自动通知,不是纯读操作;重启监控会重建快照基线,暂停期成员变化不会补报 |
| Automation | 实际规则类型:`daily_report`、`scheduled_report`、`leave_notification`。`daily_report` 是现有消息触发条件/动作链,不是任意流程引擎;退群通知为固定 ID singleton。规则 CRUD、enable、执行记录读写均已有 Service/Store | `automation:*`;AutomationWorkspace 有规则、日志、定时执行、启停、删除和状态 UI | 没有通用 Automation API;仅 scheduled-report 兼容映射 | P0。使用 typed rule union;不要把内部任意 draft 原样开放。规则创建当前缺省 enabled=true,未知值会被归一化成默认;需 HTTP 严格校验,先 disabled + validate,再显式 enable |
| WeChat Send / Action | `WechatSendGateway` 有 personal/iLink transport resolution、text/image/voice/file 统一类型及 Send Log;`WechatActionGateway` 做 capability preflight、Automation purpose allowlist、Action audit、幂等和 Automation 3 秒间隔 | `wechat-personal:send`、`sendGeneratedTtsVoice`、`wechat-action-log:list`、Agent Hub connector 相关 IPC/UI | `/agent/send` 只走 Agent Hub/iLink 测试发送;个人 capability 有独立 GET;两类日志没有 HTTP | 分 transport 公布 capability;R2 send 通过经审计的业务门面,不直接暴露底层 gateway。现有 Action Gateway 还不是普适安全策略:`triggerType=user` 不按 purpose 限制;普通 `/agent/send` 没传 idempotency key,也没有 Action audit |
| Agent Hub | status、connector login/reconnect/disconnect、notification recipient/send、logs、conversation list/detail/clear、入站 inbox retry | `agent-hub:*`;Agent Hub UI | 仅 `/agent/status` 和 `/agent/send`;无 conversation/log HTTP | status 有只读价值。对话记录包含完整收发正文;inbox 包含 context token/raw items。Connector 生命周期、登录 QR/验证码、收件箱和通知 recipient 应保持 internal |
| Reports / Templates | 手动 report render/export;AI group report;本地 Report History list/save/update template/delete;内置/已安装模板和市场 catalog/install/uninstall | `report:*`、`report-template:*`、`report-template-market:*`;Reports 与 Template Market UI | `/report` 低层 export,`/agent/group-report` AI 生成;没有 history/template API | P1:只读 Report History 元数据/资产可分离设计。现有 `listGeneratedReports` 会读取每张 PNG 为 base64,并返回结构快照和本机绝对路径,不可原样直出。模板目录可读列入 P2;安装/卸载涉及网络与本地包写入,不宜第一批开放 |
| Recall Archive | 后台监听撤回变化,最多按会话存归档消息/撤回记录;chat-service 将 archive merge 到历史读结果 | 没有独立 CRUD IPC;设置开关和消息渲染 | 没有独立 archive API;旧 `/chatlog` 可能随底层消息返回 `recalled` 标记;Query DTO 未声明 recalled 字段 | 不开放原始 archive 管理。后续 Query 应明确返回 `recalled`/来源,避免把已撤回归档当普通消息证据 |
| OCR / Image Insight | System OCR 本地识别;image-text-index status/count/start/pause/resume/cancel/clear/repair;Image Insight 读/解密图片并可调用 AI Provider | `system-ocr:*`、`image-text-index:*`、`image:*` IPC;Search/Report UI | OCR 派生文本可通过 `/query/search` 得到;索引管理、单图 AI 分析无 HTTP | 已有搜索能力可用。状态可纳入 capability/status;索引删除、key/decoder 配置、任意图像 AI 分析涉及成本、私密图片和索引破坏,不列第一批 |
| 系统 / 数据 / 其他 | account discovery、DB key 管理、数据库 connect/root 重开、设置写入、cache summary/clear、app update、voice/TTS、export/import、Reader Skill 本地安装信息 | 多组 IPC;Settings、Cache、Export、Update、Voice UI | 无相应 HTTP API | 只读脱敏运行状态可按需求列 P2;DB key/root、通用 settings patch、cache 清理、任意文件路径、更新安装、TTS synthesis 等保持 internal |
### A. Chat / Contact 与 ID 语义
- `/contact`、`/chatroom` 改用 `listContactsAsync`,因为 macOS Session 可能只有原始 wxid/chatroom id,需要 hydrate 显示名;`ScheduledReportApiService` 却使用同步 `listContacts()` 解析群名。稳定 `talker` 可直接解析,但名称输入在需要 hydration 的运行时可能失败/退化。这是可复用 adapter 应统一异步解析的理由。
- ID 现在不是一个口径:旧 Query 的 `scope.conversationId`/`target` 解析为 `Contact.md5`;`group-stats` 传 `userMd5`;监控用 `roomId`(`xxx@chatroom`);新的 scheduled automation 用 `sourceConversationId`(wxid/roomId);老 HTTP 路由混用昵称、wxid、md5。保持已有 Reader 契约不动,新 API facade 应统一对外 canonical `conversationId`(底层当前联系人的稳定 username/wxid 或 roomId),并在 main adapter 转为服务所需 md5。名称只做 resolve,不持久化到规则。
- `chatlog` 时间边界是 Unix 秒,Query `absolute` 内部也是秒,而 group stats IPC 是 epoch 毫秒;新 Agent 资源建议用带时区 ISO-8601 输入/输出,并在 facade 单点转换。
- `/query/messages` 有 200 上限,`messageRef` 是 opaque 稳定引用;图片 OCR 文字和 Coverage 分开呈现。`/chatlog` 则支持旧 talker/time 风格但读取结果没有同等结构边界;作为兼容 Reader 保留,不作为新 Agent 配置/分析的默认接口。
### B. Group Exit Monitor 与 Leave Notification
`GroupExitMonitorService` 的真实 IPC 有 `getState`、`setEnabled`、`setGroups`、`checkNow`、`listEvents`、`clearEvents`、`markRead`。事件是群成员差异事实,包含 roomId、member wxid/name、previous/current count、detectedAt;monitor state 中 `events` 只是最近 500 条快照,`totalEventCount` 对应完整内存历史。
`AutomationService.handleGroupExit` 只处理 `BUILTIN_LEAVE_NOTIFICATION_RULE_ID` 对应的 singleton 规则。规则另有 `notifyScope` / `notifyRoomIds` 二次范围、target、template。Agent 配“监控 A/B/C”需改 monitor 范围;配置通知目标/通知哪些被监控群则另改这条 leave notification automation。两者不能合并为 `/monitors/{id}/notify`。
### C. Automation 与 Scheduled Report
`AutomationRuleStore` 的真实方法有 `listRules/getRule/createRule/updateRule/saveLeaveNotificationRule/deleteRule/setRuleEnabled`;`AutomationExecutionLogService` 提供 `list/record/clear/countSince`。执行日志最多留存 200 条,clear 属于破坏性操作。规则在 `{userData}/automation/rules.json` 中 JSON 持久化。
`scheduled-report-service.ts` 的调度来源是 `automationRuleStore.listRules()`,执行交给 `AutomationService.executeScheduledRule()`,execution 从 Automation Log 投影。迁移后的旧 `tasks.json`/`executions.json` 是只读历史存档。旧 HTTP API 通过 `ScheduledReportApiService` 转换旧 DTO;创建、修改、删除、启停最终也是读写 `AutomationRuleStore`。所以正确方案是保留兼容 facade,并建立 Automation canonical API,不要继续增加第二个 scheduled-report store。
旧 facade 的限制:只列/操作可投影为 `target.type === 'wechat_group'`、且目标等于来源群的规则。如今 scheduled automation 支持 source_chat/self/file_transfer/contact,故通过新 UI 创建为文件传输助手或联系人目标的规则,会从旧 `/scheduled-reports` 列表隐藏。旧 API 输入 schema 也不能表示完整 scheduled config(成员名、消息类型、模板、timeout、postfix 等)。
写 API 前还需处理 `AutomationRuleStore` 的归一化和持久化契约:未知 ruleType 会降成 `daily_report`,大部分错误枚举会静默落安全默认;缺省 enabled 是 true;`persist()` catch 写盘错误后只记 warning,Store 仍返回创建/更新后的对象。HTTP adapter 必须先 strict validate,且 Store 需要可观察的持久化结果,不能把内存态冒充成功。
### D. Group Analytics 确认项
`GroupStatsService.getMemberStats` 已有可直接复用的核心计算;接口具体有:
- 当前群成员:`memberCount`、`activeMembers`、`silentMembers`、各活跃成员 `messageCount`/`lastMessageTime`;
- 查询窗口:`startTime`、`endTime`(epoch ms)、`firstMessageTime`;
- 数据完整性:`freshness = fresh|stale|unknown`、`complete`、`limitations`;
- 诊断:`unattributedMessages`、`excludedSystemMessages`。
成员名单是当前成员集合;知识库统计的 sender 不在当前集合时被排除,只在 `limitations` 中增加“另有 N 位窗口内发言者已不在当前群成员名单”。Service 不返回其身份/每人消息数,也没有 `totalMessageCount`。若 Agent 需要“前成员榜”或全群消息数,需要先扩展 Service/shared type;不能由 API adapter 从 limitation 文案反解析。
## 4. API / Docs / API Center Drift
| 项目 | 代码事实 | 漂移/影响 |
|---|---|---|
| HTTP、共享定义与界面列表 | HTTP 有 30 个 method/path 模板;`LOCAL_API_ENDPOINTS` 定义 14 项,Renderer `API_ENDPOINTS` 实际展示 12 项 | 16 个 HTTP 操作模板没有共享定义;另有 2 个已定义项(个人微信能力、定时日报列表)没有展示。界面仅呈现 12/30 项,不能作为完整 API catalog |
| Scheduled Report 展示 | 共享定义只有 `GET /scheduled-reports`;该项本身也未进入 Renderer 列表 | POST 和 task action 不显示,GET 列表也不显示;Agent 在 API Center 里无法发现这组 API |
| WeChat Capability 展示 | 共享定义有 `GET /wechat-personal/send-capability`;Renderer 列表未包含它 | API Center 看不到个人微信发送能力状态,用户可能误把 Agent Hub 状态当成完整发送能力 |
| Query 展示 | Query 文档在 `api.md` 的独立 LLM-friendly 章节,Service/HTTP 实现完整 | API Center 看不到;用户可能误认为 Reader API 仍只有旧 chatlog |
| Media 方法 | `/media/{mediaId}` 支持 GET、HEAD | 文档仅列 GET;API Center 都未列 |
| Retry Send | 文档表称 retry-send“复用已有 PNG 重试发送” | `ScheduledReportApiService.retrySend()` 当前无条件抛 `501 not_supported`;integration/unit tests 也未覆盖 retry 路由的这项现状 |
| Scheduled Report 完整性 | 旧 facade 只 project `source_chat` | UI 可保存的其他 scheduled target 会从旧 API list/get 隐藏;不是两份存储,但旧 API 不是 Automation API 的完整别名 |
| Health 版本 | `/health` 固定返回 `version: "1.0.0"` | 与当前 package version `2.5.0` 不同,Agent 无法据此判断应用版本 |
| HTTP 动词 | 九个静态 GET 语义路由无 method guard | POST/PATCH/DELETE 等也可能调用读取逻辑;`/health` 任意 method 均免 Token。测试目前未锁定统一 405 契约 |
| 请求/错误 schema | JSON parsing 和错误形状分散:通用 `sendError`、ScheduledReport 专用 error、Query status body、业务自身 result | Agent 要写多套解析逻辑;共享 API schema 和统一错误 code 不存在 |
| 命名 | `/contact`、`/chatroom`、`/recent_chat`、`/group_snapshot` 与 `/scheduled-reports`、`/query/*`、`/agent/*`、`/wechat-personal/*` 并存 | snake_case 旧路径、资源路径和“Agent 为业务 owner”的命名混杂;新接口不能继续沿用此漂移 |
文档 [api.md](../agent/api.md#L35) 基本列出当前 HTTP 路径,Query 在后续单独说明;除 HEAD 外没有发现漏写的当前业务路径,但 retry-send 的成功语义过期。API Center 的来源是单独的 [local-api-test.ts](../../src/shared/local-api-test.ts) 和 [apiEndpoints.ts](../../src/renderer/src/features/api-center/model/apiEndpoints.ts),没有从 HTTP route/schema 派生。测试现有 `local-api-auth` 覆盖鉴权、媒体、部分 Query 和 Agent send;`scheduled-report-api` 覆盖旧生命周期;`local-api-contact-search` 覆盖 hydrate。它们没有自动比对 HTTP route、文档、Catalog 三者,也没有覆盖全部 method guard 和 retry-send。
## 5. Candidate API Matrix
风险按本任务口径:R0 只读;R1 本地配置/应用状态修改;R2 微信发送、AI/provider 调用等外部副作用;R3 删除或清空不可轻易恢复的数据。R1 不代表没有后续行为:enable 一条定时规则会武装未来的 R2 执行。
| Capability | 当前实现 | 当前 API | 建议 | Agent 用例 | Risk | Priority |
|---|---|---|---|---|---|---|
| 联系人/群/会话 resolve | Chat Service + Contact Resolution | 有旧 routes;Query 内 resolve | 保留旧路由;新 resource 返回稳定 ID、歧义候选 | 查找群并取得 roomId | R0 | P0(复用) |
| 结构化消息/搜索/上下文/概览 | LocalQueryApiService + Knowledge | `/query/*` | 保持契约;加 route schema/catalog,后续可升级稳定 ID | 查聊天、关键词/OCR、补上下文 | R0 | P0(复用) |
| 应用 capability discovery | 各 Service 能回答局部状态 | 无;`query/capabilities` 仅 Query Tools | 新 `GET /capabilities`,区分 supported/available/reason/operations | 发现自动化、监控、统计、发送 transport | R0 | P0 |
| Group Exit Monitor 状态/范围 | GroupExitMonitorService | 仅 IPC | GET state + PATCH enabled/roomIds | 查看监控、监控/停止一个群 | R0/R1 | P0 |
| Group Exit events | Monitor JSONL + listEvents | 仅 IPC | GET 带 stable roomId/time/cursor/limit | 最近 7 天谁退群 | R0 | P0 |
| 手动检查退群 | checkNow 会扫描并触发事件 handler | 仅 IPC | 有外部通知时按 R2 操作开放,先 validate effects + confirm | 立即检查一次 | R2 | P1 |
| Automation 规则 CRUD | AutomationRuleStore | 通用 IPC;HTTP 仅旧 scheduled facade | typed union CRUD;create disabled;validate 再 enable;保护 singleton/system rules | 创建、列出、修改、暂停自动化 | R1/R3(delete) | P0 |
| Automation validation/dry-run | 现有编辑器 preview 分散;无通用 validator API | 无 | `POST /automations/validate`;不落盘、不发送 | 确认群、目标、模板、下次运行和能力 | R0 | P0 |
| Automation execution history | AutomationExecutionLogService,最多 200 条 | schedule 专属旧投影 | 规范化 Automation execution 读接口;清日志不开放第一批 | 查看失败、按 rule 过滤 | R0/R3(clear) | P0 |
| Group member stats | GroupStatsService | 仅 IPC | 按稳定 group ID + ISO window 读统计;先补 former/total 语义 | 近 30 天活跃榜 | R0 | P1 |
| WeChat capability | personal capability service;Agent Hub status | personal GET + Agent status | 新全局 capability 含 personal/iLink 和内容能力;旧路由保留 | 检查发送当前是否可用 | R0 | P0 |
| 手动微信发送 | WechatSendGateway + Action Gateway | `/agent/send` iLink test send | 新 send command 经受限 Action facade,强制 stable recipient、confirm、idempotency | 文件助手测试消息 | R2 | P1 |
| Send Log / Action audit | Send Log 500 条;Action audit 500 条;IPC action-log | 无 HTTP | 分层只读分页,preview 脱敏;按 executionId/requestId 关联 | 查最近发送失败、审计规则动作 | R0 | P1 |
| Agent Hub status | AgentHubService.getStatus | `/agent/status` + IPC | 保留 alias,新资源名归 `/agent-hub/status`,与 app capabilities 分开 | 查 Hub/connector online | R0 | P1(复用) |
| Agent Hub 对话内容 | Conversation Store,最多 50 会话×500 条 | 仅 IPC | 默认为 Internal;若产品确认需要,另做显式 opt-in、分页/时间过滤 | 查看机器人与某人的对话 | R0(高隐私) | 不建议第一批 |
| AI 群日报 | AgentGroupReportService + export | `/agent/group-report` | 保留兼容;未来先 validate model/range/group/data egress,再异步 job | 生成临时总结图片 | R2(provider/本地文件) | P1 |
| 日报历史 | ReportHistory Service,IPC CRUD | 无 | 分页 metadata DTO;图片 asset 单独下载;不返回 base64/路径/完整 snapshot | 昨天生成过哪些日报 | R0 | P1 |
| 模板列表 | Template Service + market catalog | 仅 IPC | 仅已安装模板只读列表列 P2 | 有哪些日报模板 | R0 | P2 |
| 模板安装/删除/历史改版 | Template Service/Market + Report History | 仅 IPC | 不开放通用 HTML/路径写入;将来单独授权且保留校验 | 安装或修改模板 | R1/R3 | Maybe/P2 |
| Recall archive 查询 | 内部 archive merge 到历史消息 | 无独立 API | 不单独开放磁盘 Archive;给 Query 增 `recalled` 来源标记 | 找被撤回消息 | R0(敏感/语义风险) | P2 |
| Image OCR index 操作 | image-text-index service | IPC(含 clear/repair) | coverage 状态可汇入 capabilities/status;不让 Agent 操作 clear/reset | 查 OCR 覆盖 | R0/R1/R3 | P2 |
| AI image insight | ImageInsightService 读/解密图片并调 vision provider | IPC | 不暴露任意 hash/message AI 分析,除非有成本/隐私授权 | 理解群图片 | R2 | 不建议第一批 |
| DB key、根目录、settings、cache | 多个设置/DB/cache Service | IPC/UI | 禁止通用 settings patch / 文件路径 / DB key API;只加白名单状态字段 | 修改本机数据库、安全设置 | R1/R3 | 不建议开放 |
| Connector 生命周期/inbox | AgentHubService + WechatInboundInbox | 仅 IPC/内部 | connector 登录、验证码、QR、inbox、context token 不对 Agent 暴露 | 重连或直接拿入站 token | R1/R2 | 不建议开放 |
## 6. P0 Recommendation
第一批目标是“让 Agent 能配置和核验 TraceMemo,但不意外发消息”。建议只包括:
1. `GET /api/v1/capabilities`:应用级 capability,不与现有 `/query/capabilities` 合并。返回版本、DB/readiness、supported vs available、不可用原因和依赖;发送分 personal/iLink 与 text/image/voice 能力。
2. Group Exit Monitor:读取状态、显式配置 monitored roomIds、读取历史事件。`PATCH` 只接 canonical 群 ID,拒绝不存在/非群 ID;修改范围响应明确显示后台 baseline/check 状态。`check` 先列 P1,因为它可能启动退群通知发送。
3. Automation typed CRUD:列/读规则、创建 disabled 规则、更新、显式启停、执行历史读取。Leave Notification 仍使用固定 singleton id,不允许创建重复规则;拒绝未知字段/未知 enum,而不是靠 `normalizeRuleDraft` 静默修正。
4. `POST /automations/validate`:验证目标群、稳定通知 recipient、模板变量、report/template 配置、send capability 和 nextRunAt;只返回 plan,不落盘、不发送。
5. 运行状态与错误契约:一致的 405、最大 body、请求 ID、错误 envelope;这是任何新 Agent 写接口前的 foundation,不是大规模权限系统。
Agent 实现示例(概念流程):
- “监控 A/B/C 退群”:resolve 三个群为 roomId → validate scope → PATCH monitor group IDs。
- “A 群有人退出就通知文件助手”:读取 monitor scope 和 singleton leave rule → validate 类型/notify scope/target → 更新规则但保持 disabled → 用户/Agent 明确 enable。监控与 leave-notification 是两份正交配置。
- “每天 20:00 生成产品群日报”:resolve sourceConversationId → validate scheduled rule(包含 target/transport/模板/时区/next run)→ 创建 disabled → 显式 enable。旧 `/scheduled-reports` 无法表达所有当前 config,不承担新 Agent CRUD。
- “昨天哪些自动化失败”:读 automation executions,按本机 timezone/UTC offset 和 status 查询,不清理日志。
## 7. Proposed Resource Model
采用业务 capability 资源,HTTP server 只负责 transport、auth、body、统一错误;每个 domain route 调用独立的 main-process API facade/Service adapter。Facade 复用当前 Service/Store,不把 UI IPC 当 HTTP RPC 转发层。
| Resource | 职责 | 现有路径的处理 |
|---|---|---|
| `system` | health、版本、应用级 capabilities、运行状态 | `/health` 保留;新增 `/capabilities`;`query/capabilities` 不改语义 |
| `contacts` / `groups` | 稳定 ID 列表、resolve、群成员快照/统计 | `/contact`、`/chatroom`、`/resolve`、`/group_snapshot` 保留兼容 |
| `query` | 消息/搜索/上下文/概览证据 | 现有 `/query/*` 保持;query ID 口径升级需兼容 reader skill |
| `monitors` | 退群监控范围、启停、事件、显式 check | 新 `/monitors/group-exits`,与通知规则分离 |
| `automations` | 规则 typed CRUD、validate、启停和运行 | 新 `/automations` 是 canonical HTTP resource;底层仍由 `AutomationRuleStore` 存储 |
| `executions` | 跨 Automation/Action/Send 的只读运行视图 | 新 `/executions` read model,不合并各自写存储 |
| `wechat` | transport capabilities、受控 send command、Send Log/Action audit | `/agent/send` 与 `/wechat-personal/send-capability` 保留兼容 |
| `reports` | report history 元数据/asset;installed templates read-only | `/report` 与 `/agent/group-report` 保留为不同兼容操作 |
| `agent-hub` | Hub/connector 状态;conversation API 默认 internal | `/agent/status` 保留 alias,不复用 app capability |
| `developer` | 高级 raw request tester 与诊断 | API Center 的 tester;不作为 Agent 业务 API |
共享资源原则:新 API 输入以 stable id 为主、名字只用于 resolve;所有写请求 strict validate;时间对新资源用 offset ISO-8601;分页使用 `limit` + `cursor`;成功/失败使用一个 typed envelope;不直接返回 app userData path、token、context token、AES key 或原始 transport payload。
## 8. Safety Model
### 当前边界
- 默认监听 `127.0.0.1:6131`,但 host/port 由设置和 `api:start` 调用传入,API Center 会警告非 loopback。CORS 只允许 loopback Origin,但不带 Origin 的 curl/Agent 请求仍可用 Token;CORS 不是本地进程授权边界。
- `/health` 公开,其余 endpoint 共用单一 Bearer Token。Token 为 32 random bytes、safeStorage 加密存储并设 `0600`,没有 read/config/send scope。拿到 token 即可读取聊天,也能建/删/启停规则、立即发送。
- HTTP `/agent/send` 的消息进入 `WechatSendGateway`,因此有低层 Send Log;但没有 `WechatActionGateway` 的业务 Action audit/策略决策,也没有调用方 Idempotency-Key。Personal capability 路由只报告个人微信状态。
- `WechatActionGateway` 的请求包含 `purpose`、`triggerType`、recipient、content、`idempotencyKey`、`executionId`;Automation trigger 有 purpose allowlist,sender capability 会先检查,审计记录会保存 content preview/hash。当前 `evaluateWechatActionPolicy` 对 `triggerType=user` 不做 purpose allowlist,`shouldUseAiPolicy` 固定 false;幂等只对显式 key 或历史特定 scheduled request 生效。它目前只发个人微信,不能直接替换 iLink send。
- Action Audit 和 Send Log 各自上限 500;Automation Execution Log 上限 200。三类日志粒度不同,不是重复的同一事实。
### 分级建议
| 风险 | 操作 | 建议控制 |
|---|---|---|
| R0 | 查询聊天/成员/事件/状态/统计/日志/报告元数据 | 保留本地 Token;响应明确 scope、coverage、freshness、隐私字段 |
| R1 | 修改监控群、Automation 配置、启停、模板设置 | typed validation、dry-run plan、显示持久化成功;enable 需确认它武装未来发送 |
| R2 | 微信发送、立即执行日报/退群通知 check、调用 AI Provider 生成报告 | 明确 recipient 和内容/规则;`Idempotency-Key` 必填;统一 Action policy + Action audit + Send Log;重复请求回放原结果;返回发送状态 |
| R3 | 清理退群事件、删除 report、清空执行/发送/Action 日志、清 cache/index、删规则 | 初始不开放;如以后开放,独立权限、预览计数、可恢复备份/本地确认,不接受批量 wildcard |
现有 Bearer Token 不足以支撑“查询、配置、发送、清理”都对一个不受信 Agent 开放。近期不必造完整账户系统,但应先修路由 method/body/validation,提供默认只读或 disabled 配置工作流;后续可加入多个 named token + scope(`read`, `configure`, `send`, `destructive`),R2 请求确认和 durable idempotency。风险分类也需承认本地 artifact write(如 `/report` 导出)不是配置本身,可先按 R1 local-write 处理。
## 9. Compatibility Plan
1. 不 rename/remove 任何现有 route。Reader Skill 依赖旧 contacts/chatlog/media 路径;新 structured query 已经是更合适的 Agent query,但两者并行。
2. 新 `/automations` 读写同一 `AutomationRuleStore`。旧 `/scheduled-reports*` 改为明确标注 Deprecated 的 compatibility adapter;维持现有 request/response shape 和 source_chat 子集,不维护第二个任务存储。响应可加 `Deprecation` header/文档说明,未定 sunset 前不返回 breaking error。
3. `/scheduled-reports` 的投影不能假装覆盖所有 scheduled automation。旧 list/get 只反映 source_chat;新 clients 必须迁到 `/automations?type=scheduled_report`。旧 `retry-send` 保留返回 501,文档明确废弃;不能伪造已发送成功。
4. `/agent/send` 继续表示现有 iLink/Agent Hub 测试发送。新 `/wechat/send` 必须先明确 transport/recipient schema,再通过能统一 personal+iLink 的受控 Action facade;如果不能保留旧发送语义,就将旧路径作为 adapter 而不是简单 alias。
5. `/wechat-personal/send-capability` 保持 personal-only 兼容 view;新全局 capability 返回 transport map。应用能力 `/capabilities` 和 Query Tool 的 `/query/capabilities` 各自有清晰不同的契约。
6. 同一 shared contract/catalog 应供 HTTP 验证、API Center、文档和 route contract tests 使用;把实际路由、文档和 API Center 三者 drift 变成测试失败,而不是发布后人工发现。
## 10. Proposed Phase Plan
### Phase A — HTTP Contract / Facade
给现有 routes 加明确 method guard、body size limit、严格 shared request schema、统一错误 envelope/request id;补 `AutomationRuleStore` 写盘成功/失败结果;建立稳定 conversation ID adapter 和 route contract tests。保留 raw Node HTTP,不需要为第一阶段换 web framework。
### Phase B — Read + Validation P0
增加应用 `/capabilities`、monitor state/events、automation rules/executions 读取、`/automations/validate`、group stats adapter。monitor `check` 因可能触发 notification 暂留 P1 或先加 side-effect confirmation。把新资源路径、schema、风险元数据接进 EndpointCatalog/生成文档。
### Phase C — Automation Configuration
开放 disabled create、PATCH、启停和 DELETE 防护;退群通知专用 singleton upsert 接入同一规则资源;scheduled report 使用真实完整 config;旧 scheduled API 只做 compatibility projection。先做 dry-run 再允许 enable。
### Phase D — Side Effects / Execution Read Model
扩展一层统一 `Action` facade 支持 iLink + personal transports,并有 per-purpose policy、recipient allowlist/validation、确认语义、强幂等、Action audit 到 Send Log correlation。再开放 send/check/run;按需增加 `/executions` read projection,不合并底层日志存储。
### Phase E — API Center
Overview、Query、Monitors、Automations、WeChat Actions、Reports、Developer 分区;按业务任务做 schema-aware 表单/效果预览,Raw Request Tester 留在 Developer。展示 Token scope/host 范围/transport capability,而不只是固定 URL 测试器。
## 11. Concrete Endpoint Proposal
下表是下一阶段建议契约,不表示当前已实现。新写 API 应统一错误:`{"error":{"code":"...","message":"...","details":{...}},"requestId":"..."}`。时间使用带 offset 的 ISO-8601;接口只接受 stable IDs。
| Method | Path | Request → Response | Risk | Underlying Service |
|---|---|---|---|---|
| GET | `/api/v1/capabilities` | 无 → app version/readiness + `query`,`groups.memberStats`,`groupExitMonitor`,`automations`、每种 `wechat.transport/content` 的 `supported/available/reason` | R0 | 新薄 facade 汇总 LocalQuery、GroupStats、Monitor、AutomationStore、personal capability、AgentHub status |
| GET | `/api/v1/monitors/group-exits` | 无 → `{enabled,running,monitoredConversationIds,lastCheckedAt,eventCount}` | R0 | `GroupExitMonitorService.getState` |
| PATCH | `/api/v1/monitors/group-exits` | `{enabled?,monitoredConversationIds?}` → 保存后的 state;监控 ID 必须 resolve 到现有群 | R1 | `setEnabled` / `setMonitoredRoomIds` |
| GET | `/api/v1/monitors/group-exits/events?conversationId=&since=&until=&limit=&cursor=` | ISO 时间和稳定群 ID → `{events,nextCursor}`,事件显式 `eventId`、group/member、counts、detectedAt | R0 | `GroupExitMonitorService.listEvents`;为无 cursor 的现有 list 加稳定分页 adapter |
| POST | `/api/v1/monitors/group-exits/check` | `{confirmSideEffects:true}` + `Idempotency-Key` → checkedAt、新事件数、notification execution refs | R2 | `checkNow`;因检查可发现事件并调用 leave-notification Automation,不应标成纯读 |
| GET | `/api/v1/automations?type=&enabled=` | 无 → typed rules page;包括 singleton leave rule | R0 | `AutomationRuleStore.listRules` |
| POST | `/api/v1/automations` | typed `AutomationRuleDraft`,create 默认 `enabled:false` → `{rule}` | R1 | `AutomationRuleStore.createRule`(需先强化 strict validation/persist result) |
| GET | `/api/v1/automations/{ruleId}` | 无 → `{rule}` | R0 | `AutomationRuleStore.getRule` |
| PATCH | `/api/v1/automations/{ruleId}` | typed partial config → `{rule}`;`ruleType` 不可变 | R1 | `AutomationRuleStore.updateRule` |
| DELETE | `/api/v1/automations/{ruleId}` | 无 → `{deletedId}`;默认拒绝 builtin/system singleton 删除 | R3 | `AutomationRuleStore.deleteRule` + protected-id policy |
| POST | `/api/v1/automations/validate` | typed draft → `{valid,normalizedDraft,issues,effects,resolvedTargets,nextRunAt,capabilities,validationId}`;不保存、不发送 | R0 | 新 validator adapter:contact resolve、template validator、capability services、schedule pure functions |
| POST | `/api/v1/automations/{ruleId}/enable` | `{validationId,confirmFutureEffects:true}` → `{rule}`;validation hash 必须匹配当前规则 | R1(武装未来 R2) | `AutomationRuleStore.setRuleEnabled` + validator |
| POST | `/api/v1/automations/{ruleId}/disable` | 无 → `{rule}` | R1 | `AutomationRuleStore.setRuleEnabled` |
| POST | `/api/v1/automations/{ruleId}/run` | `{confirmSideEffects:true}` + `Idempotency-Key` → `{execution}` | R2 | `AutomationService.executeScheduledRule`;只对具有 `run` 语义的 rule type 开放 |
| GET | `/api/v1/automations/executions?ruleId=&status=&trigger=&since=&until=&limit=&cursor=` | 无 → `{executions,nextCursor}` | R0 | `AutomationExecutionLogService.list`;需加 timestamp/filter/cursor adapter |
| GET | `/api/v1/groups/{conversationId}/member-stats?start=&end=` | ISO 时间窗口 → active/silent/counts/freshness/complete/limitations;未来要 former members 则先扩 shared result | R0 | `GroupStatsService.getMemberStats`;adapter 把 canonical roomId 转 md5 |
| GET | `/api/v1/wechat/capabilities` | 无 → personal/iLink 分 transport 状态与内容能力 | R0 | `PersonalWechatCapabilityService` + `WechatSendGateway.hasIlinkSender`/Agent Hub connector health |
| POST | `/api/v1/wechat/send` | `{recipient:{type,id},content:{type:"text",text},confirm:true}` + 必填 `Idempotency-Key` → `{actionId,status,transport,sendLogRef}` | R2 | 扩展后的 `WechatActionGateway`/统一 Action facade;不得裸调 `WechatSendGateway.send` |
| GET | `/api/v1/wechat/send-logs?status=&since=&limit=&cursor=` | 无 → 只读、脱敏 Send Log page | R0 | `WechatSendGateway.listSendLog` / `WechatSendLogService.list` |
| GET | `/api/v1/wechat/action-logs?executionId=&status=&since=&limit=&cursor=` | 无 → 业务 Action 审计 page;preview 默认截短/可省略 | R0 | `WechatActionLogService.list` / `WechatActionGateway.listAuditRecords` |
| GET | `/api/v1/executions?source=&status=&since=&until=&limit=&cursor=` | 无 → 统一只读 projection,每项保留 source/executionId/trigger/action/status/error/times/correlation IDs | R0 | 组合 `AutomationExecutionLogService`、Action audit、Send Log;不合并其存储 |
| GET | `/api/v1/reports/history?groupId=&since=&until=&limit=&cursor=` | 无 → 仅元数据分页,不带 `generatedImage`、本地绝对路径或完整 report snapshot | R0 | `listGeneratedReports` 经 summary adapter,最好先让 Service 支持 metadata-only |
| GET | `/api/v1/reports/history/{reportId}/image` | 无 → PNG binary | R0 | Report History 的 file resolver;仅按内部 report id 解析,不接收任意路径 |
| GET | `/api/v1/report-templates` | 无 → 已安装模板的 stable id/name/version/available 列表 | R0 | `reportTemplateService.list` |
| GET | `/api/v1/agent-hub/status` | 无 → hub/connector/dataApi/dbReady 的脱敏状态 | R0 | `AgentHubService.getStatus`;`/api/v1/agent/status` 保留 alias |
`POST /wechat/send` 建议先只开放 text + 明确目标;image path/url、voice 和 file 类型各自扩大本机文件/外传风险,需独立 validation/permission,不从现有任意 `msg` 输入自动继承。
## 12. Do Not Expose
- 任意数据库 key、图片 AES key、微信登录凭据、iLink `context_token`/bot token、inbound inbox raw items;这些是密钥或未处理消息,不是业务 API。
- 通用 `settings:set`、任意 `dbRoot`、数据库 disconnect/reopen、cache 全清、Knowledge/OCR 索引 clear/reset;配置或 destructive blast radius 远高于 Agent automation 管理需求。
- Agent Hub QR/login/verify-code/reconnect/disconnect/connector lifecycle。它会影响进程状态/账户登录,也会暴露登录材料。
- `WechatInboundInbox` pending/clear/complete/recordFailure 等队列控制。它是 at-least-once 消息交付内部机制,外部 ack 会破坏不丢消息语义。
- Agent Hub 完整对话正文默认不暴露。若明确产品需要,独立设计用户授权、最小时间窗口、分页、清理和脱敏;现有 Conversation Store 为本机完整收发留档。
- 任意本地 file path、path traversal 类导出/报告资产操作;不要把 IPC 的 file chooser、reveal、delete path 形状直接变 HTTP 参数。
- 单图 AI insight/任意 Vision analyze、批量 TTS synthesis、AI Provider 配置/测试和 app update download/install;它们具有费用、敏感图片/文本上传或应用安装影响。
- 清除退群、Automation、发送/Action、报告历史或批量发送接口作为 P0。清理类不是“管理配置”的必要前提,应使用 R3 独立权限及本地可恢复流程。
- 通用“任意 Automation DAG/任意脚本/任意 purpose”的创建。当前实现只有明确的三类规则和固定动作,不是 workflow engine;扩展功能应有显式 ruleType/schema,而不是暴露内存对象。
## 13. Open Questions
以下属于产品边界选择,无法只从代码决定:
1. 新 Agent API 是否允许同时操作个人微信和 Agent Hub/iLink,还是第一版限定一个 transport?现有 capability 与 send endpoint 分属两套连接状态。
2. Agent 是否默认只拿只读 scope;配置和 R2 send 是否要求独立 token/用户确认?当前只有单一 Bearer Token。
3. 多账号是否属于本阶段?当前 Query/GroupExit 绑定当前活动数据库账号,Agent Hub 有自己的 connector accountId;没有统一 account-scoped API model。
4. Agent Hub 完整对话是否要作为 API capability?它含完整消息正文,和从微信数据库按 query 搜历史是不同隐私边界。
真实微信 runtime 可用性、平台 hydration 和 transport 能力需在后续实现集成测试中验证;静态代码审计无法替代真实数据库/微信连接的运行时验证。
@@ -6,47 +6,43 @@
执行 `pnpm dev` 后,以下状态同时满足,说明本地开发环境已经可用:
- 控制台显示连接器已生成,例如 `resources/connectors/wechat/win32-x64/wechat-connector.exe`;
- Electron 窗口已打开,或 `http://localhost:5173/` 返回 HTTP `200`;
- 控制台显示 Local HTTP API 正在监听 `http://127.0.0.1:6131`。
`6131` 是应用提供给本机集成使用的 API 端口,不是 Vite 的页面端口。
## Go 命令找不到
## WCDB 消息读取诊断
如果 `pnpm dev` 在构建微信连接器时出现 `spawnSync go ENOENT`,先执行:
`WCDB_DEBUG_LOGS` 默认关闭。需要排查消息读取链路时,可以在启动命令前设置为 `1`:
```bash
go version
WCDB_DEBUG_LOGS=1 pnpm dev
```
命令不可用表示当前终端的 `PATH` 没有找到 Go。Windows 默认安装位置是 `C:\Program Files\Go\bin`。确认 Go 已安装并把该目录加入系统 `PATH` 后,关闭并重新打开终端或 IDE,再重新执行 `go version` 和 `pnpm dev`。
如果 Go 刚完成安装,已经打开的终端不会自动继承新的环境变量;重开终端是必要步骤。不要绕过连接器构建直接启动 `electron-vite dev`,否则 Agent Hub 的微信连接器不会生成。
开启后会输出 `GETMSG-xxx` 请求耗时和 native `WCDB-EXPLAIN` 执行计划,不记录聊天正文。取消该环境变量或设为 `0` 即可关闭。
## Electron 二进制缺失或下载失败
`electron-vite dev` 报 `Electron uninstall`,或 Electron 安装器报 `fetch failed`,通常表示 `node_modules/electron/dist` 中的 Electron 二进制缺失或下载未完成。这不是应用业务代码的启动错误。
项目的 [`.npmrc`](../../.npmrc) 已设置:
**先看根因,别急着删 `node_modules` 重装。** `electron@43` 的 npm 包**不再声明 `postinstall`**(其 `package.json` 里 `scripts` 是空对象),下载改为「首次 `require('electron')` 时的懒加载」。因此:
```ini
electron_mirror=https://npmmirror.com/mirrors/electron/
```
- `package.json` 里的 `pnpm.onlyBuiltDependencies: ["electron"]` 对它不起作用——上游没有脚本可执行,pnpm 无从下手;
- `pnpm install` 跑完不会有任何二进制被下载,**只重装依赖解决不了这个问题**。
pnpm 会把该值传给 Electron 安装器,令其从镜像下载与 `package.json` 锁定版本匹配的二进制文件,避免默认 GitHub 下载源在受限网络中不可访问。
项目已自动兜住这条路径:`scripts/ensure-electron-binary.cjs` 挂在 `postinstall` 与 `predev` 上,校验 `path.txt` 指向的可执行文件是否真的存在(只有 `path.txt` 而没有 `dist/` 同样算没装好),缺失时就地补下载。它读取 `.npmrc` 的 `electron_mirror`(当前为 `https://npmmirror.com/mirrors/electron/`),失败后再兜底重试一次该镜像。
依赖安装被中断或 Electron 目录不完整时,删除不完整的 `node_modules` 后重新安装:
正常情况下你不需要做任何事。只有当自动步骤没有执行时(例如安装时带了 `--ignore-scripts`),才需要手动补一次:
```bash
pnpm install --frozen-lockfile
node scripts/ensure-electron-binary.cjs
```
单次安装需要使用其他镜像时,可以临时覆盖项目默认值。PowerShell 示例:
要换用别的镜像时,显式设置环境变量(优先于 `.npmrc`)。PowerShell 示例:
```powershell
$env:ELECTRON_MIRROR = 'https://your-electron-mirror.example/'
pnpm install --frozen-lockfile
node scripts/ensure-electron-binary.cjs
```
该环境变量只影响当前终端,不会改写仓库中的 `.npmrc`。镜像地址必须保留末尾的 `/`,并提供与 Electron 版本对应的目录结构。
@@ -59,4 +55,4 @@ Vite 在某些 Windows 环境中只监听 IPv6 本机回环地址 `::1`。这时
## 仍无法启动时
保留首次错误的完整输出,并同时记录操作系统、Node.js、pnpm 和 Go 版本,以及 `pnpm install --frozen-lockfile` 与 `pnpm dev` 的执行结果。不要提交数据库密钥、AI API Key、微信数据路径或聊天内容。
保留首次错误的完整输出,并同时记录操作系统、Node.js 与 pnpm 版本,以及 `pnpm install --frozen-lockfile` 与 `pnpm dev` 的执行结果。不要提交数据库密钥、AI API Key、微信数据路径或聊天内容。
+25 -16
View File
@@ -6,7 +6,6 @@
- Electron + React + TypeScript;
- pnpm 7+;
- Go(构建微信连接器);
- 平台对应的 Electron/native 构建环境。
产品文档的事实来源优先级是:当前源码 → 当前 UI/Renderer → 测试 → package/config → README/docs → 历史资料。功能、API、版本、隐私和兼容性变更时,不要只改 README。
@@ -18,7 +17,7 @@ pnpm install
pnpm dev
```
本地依赖安装、Go 环境和 Electron 二进制下载异常,请查看[本地启动排障](./local-startup-troubleshooting.md)。
本地依赖安装与 Electron 二进制下载异常,请查看[本地启动排障](./local-startup-troubleshooting.md)。
常用检查:
@@ -30,21 +29,28 @@ pnpm test:integration
pnpm test:e2e:build
```
完整测试入口 `pnpm test` 还会运行 Skill 安装指令、微信连接器、构建和 Playwright 测试;需要对应平台环境。
完整测试入口 `pnpm test` 还会运行 Skill 安装指令、构建和 Playwright 测试;需要对应平台环境。
## 代码变更对应文档
| 代码区域 | 需要同步检查的文档 |
| --------------------------------------------------------- | ---------------------------------------------------------- |
| `src/shared/ai-search.ts`、AI Search pipeline | `user-guide/ai-search.md`、`concepts/answer-sources.md` |
| `src/shared/knowledge.ts`、`src/main/knowledge/` | `user-guide/knowledge.md`、`concepts/how-it-works.md` |
| `src/shared/voice-recognition.ts` | `user-guide/voice.md` |
| `src/shared/group-report.ts`、报告 UI | `user-guide/report.md`、API/Agent 文档 |
| `src/shared/export.ts`、导出服务/UI | `user-guide/export.md` |
| `src/main/services/recall-archive-service.ts`、防撤回设置 | `user-guide/recall-protection.md`、`user-guide/privacy.md` |
| `src/shared/local-api-test.ts`、`src/main/http-server.ts` | `agent/api.md`、`api-security.md`、打包 Skill |
| Agent Hub service/UI | `agent/agent-hub.md`、`user-guide/privacy.md` |
| 设置导航、连接页面 | `user-guide/getting-started.md`、`docs/README.md` |
| 代码区域 | 需要同步检查的文档 |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| `src/shared/ai-search.ts`、AI Search pipeline | `user-guide/ai-search.md`、`concepts/answer-sources.md` |
| `src/shared/knowledge.ts`、`src/main/knowledge/` | `user-guide/knowledge.md`、`concepts/how-it-works.md` |
| `src/shared/voice-recognition.ts` | `user-guide/voice.md` |
| `src/shared/group-report.ts`、报告 UI | `user-guide/report.md`、API/Agent 文档 |
| `src/shared/export.ts`、导出服务/UI | `user-guide/export.md` |
| `src/main/services/system-ocr-service.ts`、`image-text-index-service.ts` | `user-guide/knowledge.md`、`concepts/how-it-works.md`、`user-guide/privacy.md` |
| `src/main/services/image-insight-service.ts`、AI Provider | `user-guide/report.md`、`user-guide/privacy.md` |
| `src/shared/automation.ts`、自动化服务与执行网关 | `user-guide/report.md`、`concepts/how-it-works.md`、`docs/README.md` |
| `src/shared/local-api-test.ts`、`src/main/http-server.ts` | `agent/api.md`、`api-security.md`、打包 Skill |
| Agent Hub service/UI | `agent/agent-hub.md`、`user-guide/privacy.md` |
| 设置导航、连接页面 | `user-guide/getting-started.md`、`docs/README.md` |
两条容易被写错的边界:
- **防撤回已下线**(`src/main/services/recall-archive-service.ts` 保留但不再启动):设置入口隐藏,`recallProtectionEnabled` 在所有读写路径上被强制收敛为 `false`。不要把它写回用户指南。
- **图片文字索引(本机 OCR)与图片理解(需要 Provider)是两条不同的路径**:前者写入本地索引、能被搜索,且不联网;后者只在日报和设置里的模型检测中使用。改其中一条时不要把另一条的隐私口径带过去。
## 文档检查
@@ -52,7 +58,10 @@ pnpm test:e2e:build
```bash
git diff --check
rg -n "v2\.1\.7|TraceMemo|迹忆|mcpServers|无鉴权" README.md docs --glob '*.md' --glob '!DOCUMENTATION_AUDIT.md' --glob '!development/overview.md'
# 过时版本号、旧品牌名、旧结构叙述、MCP 误解
rg -n "v2\.1\.7|2\.4\.0|v2\.2\.0 兼容期|无鉴权|mcpServers" README.md docs --glob '*.md' --glob '!development/overview.md'
# 不存在的产品结构(定时日报已并入自动化)
rg -n "日报 → 定时日报|Monitor / Automation" README.md docs --glob '*.md'
```
历史迁移说明可以出现旧版本号;正式使用指南不要把过时版本写成当前版本。负向澄清“6131 不是 MCP Server”可以保留,以防用户照抄错误配置。
历史迁移说明可以出现旧版本号;正式使用指南不要把过时版本写成当前版本。负向澄清“6131 不是 MCP Server”可以保留,以防用户照抄错误配置。发版前额外确认 `README.md` 里的版本号与 `package.json` 的 `version` 一致。
+84
View File
@@ -0,0 +1,84 @@
# Query Agent POC
这是独立的开发测试入口,不会修改生产“问问微信”执行链。
先启动 TraceMemo,并在 API Center 开启 Local HTTP API。然后在仓库根目录运行:
```bash
pnpm poc:query-agent "我和BOBO第一次聊了什么"
```
## 两个入口
| 命令 | 行为 | 何时用 |
| --- | --- | --- |
| `pnpm poc:query-agent "问题"` | 先执行完整构建,再运行 | 首次运行,或刚改过代码 |
| `pnpm poc:query-agent:run "问题"` | 直接运行已有构建,**不构建** | 连续迭代测试 |
`poc:query-agent:run` 在构建产物不存在时会明确提示先运行 `pnpm poc:query-agent`,**不会自动构建**。
注意:`poc:query-agent` 内部走的是完整 `electron-vite build`(main + preload + renderer),
即使 POC 只需要一个 main entry。连续测试请使用 `poc:query-agent:run` 以免每次都重建整个 renderer。
## 传参
参数按原样转发给入口,可以被 `--` 分隔(`pnpm run` 惯例):
```bash
pnpm poc:query-agent -- "BOBO上个月有没有给我发过文件"
pnpm poc:query-agent:run "BOBO上个月有没有给我发过文件"
```
入口只会移除参数列表**开头**的一个独立 `--`;问题正文中的 `--` 会原样保留。
## Provider
POC 使用设置页当前默认 AI Provider、模型、Base URL 和安全存储中的 API Key。Local Query API 仍使用现有 Bearer Token;POC 输出不会打印 Token、API Key、数据库路径或内部消息 ID。
## 输出
**stdout 是 JSON**(`poc:query-agent` 会在它前面混入构建日志,`poc:query-agent:run` 只多两行 pnpm 横幅)。
需要机器解析时用 `--silent` 拿到纯 JSON:
```bash
pnpm --silent poc:query-agent:run "我和BOBO第一次聊了什么" > result.json
```
JSON 字段:
- `question`、`provider`、`model`
- `modelCallCount`、`toolCallCount`
- `modelDurationsMs`(每次模型调用耗时,含失败的那次)
- `modelDiagnostics`(每次模型调用的请求级诊断:HTTP status、content-type、是否返回 HTML、是否超时、耗时)
- `firstModelMs`、`toolTotalMs`、`finalModelMs`、`totalMs`
- 每次工具调用的名称、脱敏参数、耗时、状态和结果数量
- 最终 `answer` 或错误信息
**stderr 是人类可读摘要**(不参与 JSON 解析):
```text
[Timing]
Model #1 1315 ms
TM Tools(1) 623 ms
Model #2 1598 ms
------------------------
Total 3545 ms
Model total 2913 ms (82.2%)
TM tool total 623 ms (17.6%)
[Provider]
provider DeepSeek
model DeepSeek Chat
host api.deepseek.com
model calls 2
tool calls 1
attempt #1 elapsedMs=1298 status=200 contentType=application/json
attempt #2 elapsedMs=1571 status=200 contentType=application/json
```
`elapsedMs` 是 TTFB(收到响应头),`modelDurationsMs` 是整次调用(含读 body);502 时两者接近,
说明等待发生在上游网关,不是本地读 body 慢。诊断只记录 host,不记录完整 URL 或任何凭据。非 2xx 响应会先记录 status / content-type / elapsedMs,再返回安全错误(例如“模型服务返回了网页而不是 JSON(HTTP 502 Bad Gateway)”),不会把 HTML 正文丢给 JSON 解析器。
## 约束
工具调用最多 5 次,只允许 `query_messages`、`search_messages`、`message_context`、`conversation_overview`。未配置 AI Provider、Local Query API 未启动或当前 Provider 协议不支持 tools 时,POC 会直接返回错误,不会回退到另一套模型配置。
+171
View File
@@ -0,0 +1,171 @@
# 界面开发规范:按钮与主题色
这份规范回答一件事:**为什么同一个产品里,有的按钮是主题色,有的还是浏览器默认的黑白方角。**
先看一个真实案例 —— 同一屏里的两组按钮:
```
主界面:「更新图片文字索引」 ← 主题色(正确)
弹窗里:「取消」「开始索引」 ← 浏览器默认样式(错误)
```
两者渲染出来完全不同,用户会以为是两个不同的产品。根因不是"设计没定颜色",
而是**组件在导出时把样式丢了**。下面写清楚怎么避免。
---
## 1. 永远不要写裸 `<button>`
任何可点的按钮都必须来自 `components/ui/button`:
```tsx
import { Button } from '../ui'
<Button variant="outline" onClick={handleCancel}>取消</Button>
```
**唯一的例外**:结构性控件(导航项、Tab、列表行、图标热区)——它们有自己
成套的布局样式,用原生 `<button>` 是合理的,但**必须**带 `className`,
且样式写在对应的 `.scss` 里,不要在 JSX 里临时拼颜色。
```tsx
// 可以:结构性控件,样式来自 .scss
<button type="button" role="tab" className={active ? 'active' : ''} onClick={...}>
今日日报
</button>
```
**判据**:如果这个按钮在别的界面也会以同样形态出现("取消"、"保存"、"删除"),
它就该是 `Button`;如果它只在某一个位置有意义(侧栏导航项),才考虑原生。
---
## 2. 三种角色,只有三个默认变体
`Button` 提供 6 个变体,但**日常只用其中 3 个**:
| 角色 | `variant` | 长什么样 | 用在哪 |
| --- | --- | --- | --- |
| 主要 | `default` | 主题色实底 | 这一步用户唯一该做的事 |
| 次要 | `outline` / `ghost` | 描边 / 无底色 | 取消、返回、并列的辅助操作 |
| 危险 | `destructive` | 红色实底 | 删除、清空、不可恢复的操作 |
另外两个(`secondary` / `link`)按需用;`link` 只用于正文里的行内跳转。
**一条硬约束:同一个界面(或同一个弹窗)里,`default` 最多出现一次。**
两个主题色实底按钮并排,等于没有主次。
---
## 3. 弹窗按钮:组件已经带样式了,不要再包一层
`AlertDialogCancel` 和 `AlertDialogAction` **自带**按钮样式(分别是 `outline`
和 `default`),直接写文字即可:
```tsx
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction onClick={handleStart}>开始索引</AlertDialogAction>
</AlertDialogFooter>
```
**不要**再套一层 `Button`:
```tsx
// 反面写法:外层已经有样式了,再包一层只会产生重复类名
<AlertDialogCancel asChild>
<Button variant="outline">取消</Button>
</AlertDialogCancel>
```
需要危险动作时,用 `className` 覆盖(`cn` 走 tailwind-merge,同族类后者生效):
```tsx
<AlertDialogAction className="bg-destructive text-destructive-foreground">
删除
</AlertDialogAction>
```
---
## 4. 颜色只能用语义 token,禁止硬编码
颜色全部走 Tailwind 的语义类,它们背后是 `--tm-*` 变量,换主题时自动跟随:
```
背景 bg-primary / bg-surface / bg-accent / bg-destructive
文字 text-foreground / text-primary-foreground / text-muted-foreground
描边 border-border / border-border-subtle / border-disabled-border
```
```tsx
// 对
<Button className="bg-primary text-primary-foreground">保存</Button>
// 错 —— 换主题时这行不会跟着变
<Button className="bg-[#247a63] text-white">保存</Button>
```
**判据**:JSX 里出现 `#` 开头的颜色、`rgb(...)`、或 Tailwind 的调色板名
(`bg-green-600`、`text-slate-500`)—— 都是漏用 semantic token 的信号。
---
## 5. 「默认样式」的三个常见来源
排查界面里冒出来的黑白方角按钮时,按这个顺序找:
**① 组件导出时把样式丢了。** 最常见。把 Radix 的 primitive 原样导出:
```tsx
// 错:渲染出来就是浏览器默认按钮
const AlertDialogCancel = AlertDialogPrimitive.Cancel
```
正确做法是 `forwardRef` 包一层,挂上 `buttonVariants`:
```tsx
const AlertDialogCancel = React.forwardRef<...>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: 'outline' }), className)}
{...props}
/>
))
```
**判据**:`components/ui/` 里凡是导出 Radix primitive 的地方,都要确认它是
"样式化的封装"还是"原样透传"。原样透传只对布局容器(`Root` / `Portal` /
`Group`)成立,对**可点元素**(`Close` / `Action` / `Cancel` / `Item`)不成立。
**② `asChild` 里重复包了一层。** 外层已经带样式、子元素又带一次,虽然因为
同族类后生效而不会出错,但会产生冗余类名。**能去掉一层就去掉。**
**③ 原生 `<button>` 忘写 `className`。** 见第 1 节的例外条款 —— 结构性控件也必须
有样式来源。
---
## 6. 提交前检查清单
- [ ] 新增的可点元素来自 `Button`,不是裸 `<button>`
- [ ] 同一界面里 `default` 变体不超过一个
- [ ] 危险操作走 `destructive`,不是红色硬编码
- [ ] 弹窗按钮没有重复包 `Button`
- [ ] JSX 里没有 `#` 开头的颜色、没有 Tailwind 调色板名
- [ ] `components/ui/` 里新导出的可点 primitive 已经挂上 `buttonVariants`
- [ ] 组件测试覆盖到按钮的可见性与点击行为(testid 用 `xxx-yyy` 连字符命名)
---
## 7. 一个反面案例的复盘
弹窗里的「取消 / 开始索引」显示成浏览器默认样式,原因就是第 5 节第 ① 条:
`alert-dialog.tsx` 把 `Cancel` / `Action` 两个 primitive 原样导出了。
修复是给它们各加一个 `forwardRef` 封装,挂上 `buttonVariants`。**组件本身没坏**,
所有调用方一行不用改,样式自动生效 —— 这正是把样式收在 `components/ui/` 里的价值:
**修一处,全产品对齐。**
如果你发现某个地方的按钮"没跟上主题",先别去改那个界面 ——
**先看它用的组件是不是漏了样式。**
@@ -0,0 +1,92 @@
# 微信系统消息(sysmsg)解析与格式兼容
微信的「系统消息」(入群、撤回、成员变动等)以 XML(`<sysmsg>`)存放在消息内容里,
但**同一类提示的 XML 结构会随客户端版本变化**。本文说明 TraceMemo 的解析方式,
以及在遇到新格式时应当怎么扩展。
## 两类格式
### 旧格式:正文直接放在 `<plain>`
```xml
<sysmsg type="delchatroommember">
<delchatroommember>
<plain><![CDATA["成员昵称"通过扫描你分享的二维码加入群聊]]></plain>
<text><![CDATA["成员昵称"通过扫描你分享的二维码加入群聊]]></text>
<link>
<scene>qrcode</scene>
<text><![CDATA[撤销]]></text>
</link>
</delchatroommember>
</sysmsg>
```
解析:命中 `delchatroommember`,直接取 `<plain>`。
### 新格式:正文在 `<template>`,用 `$名称$` 引用 link
```xml
<sysmsg type="sysmsgtemplate">
<sysmsgtemplate>
<content_template type="tmpl_type_profilewithrevokeqrcode">
<plain><![CDATA[]]></plain>
<template><![CDATA["$adder$"通过扫描你分享的二维码加入群聊 $revoke$]]></template>
<link_list>
<link name="adder" type="link_profile">
<memberlist><member>
<username><![CDATA[wxid_xxxxxxxx]]></username>
<nickname><![CDATA[成员昵称]]></nickname>
</member></memberlist>
</link>
<link name="revoke" type="link_revoke_qrcode" hidden="1">
<title><![CDATA[撤销]]></title>
</link>
</link_list>
</content_template>
</sysmsgtemplate>
</sysmsg>
```
三个要点:
- `<plain>` 变成**空 CDATA**,正文挪进 `<template>`;
- 正文里的 `$名称$` 是占位符,按 `<link_list>` 中 `link[name]` 回填;
- `hidden="1"` 的 link 在微信里是**可点击按钮**,纯文本展示时应省略其文案。
## 解析流程
`src/main/message-parser.ts` 的 `parseSystemMessage()` 按以下顺序尝试:
| 顺序 | 分支 | 处理对象 |
| --- | --- | --- |
| 1 | `extractRecallMessage` | `<revokemsg>` 撤回通知 |
| 2 | `extractSysmsgTemplateText` | `<sysmsgtemplate>` 模板消息 |
| 3 | `extractDelChatroomMemberText` | `<delchatroommember>` 成员变动 |
| 4 | 通用提取(`plain` → `text` → `title`),再退回 `fallbackSystemText` | 其余未覆盖类型 |
第 4 步之前会先调用 `stripSysmsgLinkList()` 剥掉 `<link_list>`。
## 为什么必须显式处理新格式
通用提取链只在第 1~3 步全部落空时才执行,而新格式恰好让它落空:
`<plain>` 是空 CDATA,又没有 `<text>`,于是取到 `<title>` ——
那是 `hidden="1"` 按钮的标题。**结果是整条系统消息只剩一个按钮文案**,
例如把「某某通过扫描你分享的二维码加入群聊」显示成「撤销」。
因此三处约束缺一不可:
1. 模板分支必须排在通用提取之前;
2. 占位符回填必须尊重 `hidden="1"`;
3. 通用提取前先剥 `<link_list>`,作为未知类型的防护。
## 新增一类系统消息时
1. 从真实消息中取出 `content`(`<sysmsg>` 原文),确认 `type` 与承载正文的标签;
2. 在 `parseSystemMessage()` 里加一个**早于通用提取**的分支;
3. 补 `tests/unit/message-parser.test.ts` 用例,**新旧两版各一条**,防止回归;
4. 文档与代码注释只写结构,不粘贴真实会话内容、昵称、wxid 或二维码链接。
## 相关位置
- 解析实现:`src/main/message-parser.ts`
- 单元测试:`tests/unit/message-parser.test.ts`
+60 -3
View File
@@ -1,5 +1,62 @@
# macOS 数据访问说明(兼容入口)
# macOS 关闭 SIP 教程
完整内容已移到[macOS 数据访问与系统权限](./platform/macos.md)。
SIP(System Integrity Protection,系统完整性保护)是 macOS 的系统安全机制。关闭 SIP 会降低系统安全性,只建议在确实需要读取或调试本地微信数据时临时关闭;操作完成后,建议重新开启。
保留此文件是为了兼容应用内已经发布的帮助链接。请不要把“关闭 SIP”当作默认安装步骤;只有当当前连接页面明确要求时才处理,并在完成后恢复系统安全设置。
> 只在连接页面明确提示需要关闭 SIP 时才处理。首次连接失败时,先确认微信版本、账号目录和登录时机,再按本文操作。关闭 SIP 不是 TraceMemo 的常规安装步骤,也不应长期保持关闭。
## 准备
- 一台 Mac 电脑,Intel 芯片和 Apple Silicon 芯片均可。
- 需要进入 macOS 恢复模式。
- 请先保存正在编辑的文件,并预留一次重启时间。
## 关闭 SIP
### Intel Mac
1. 关机。
2. 按下开机键后,立刻按住 `Command + R`。
3. 保持按住,直到进入 macOS 恢复模式。
### Apple Silicon Mac(M1/M2/M3/M4/M5)
1. 关机。
2. 长按开机键不放。
3. 直到出现启动选项界面后松开。
4. 选择"选项",进入 macOS 恢复模式。
### 在恢复模式中执行命令
1. 进入恢复模式后,点击顶部菜单栏的 **Utilities(实用工具)**。
2. 选择 **Terminal(终端)**。
3. 在终端中输入:
```bash
csrutil disable
```
4. 按回车执行。
5. 看到关闭成功提示后,重启电脑。
## 确认是否生效
重启回到正常桌面后,打开"终端",执行:
```bash
csrutil status
```
看到 `System Integrity Protection status: disabled.` 才算关闭成功。
若仍显示 `enabled`,说明没有生效。常见原因是没在恢复模式里执行,或系统刚做过大版本更新——
macOS 大版本更新会把 SIP 重置回开启状态,此前关过也会失效,需要重新按上面的步骤操作。
## 重新开启 SIP
拿到数据库密钥后,建议重新进入恢复模式,在终端中执行:
```bash
csrutil enable
```
然后重启电脑,恢复系统安全设置。
+13 -2
View File
@@ -8,7 +8,7 @@ TraceMemo 需要读取微信本地数据。macOS 会根据系统版本、微信
1. 先启动 TraceMemo,阅读连接页面显示的当前前置条件。
2. 确认微信数据目录指向当前账号。
3. 只在页面明确要求时处理系统授权或 SIP;按页面提示完成密钥获取后,恢复你平时使用的安全设置。
3. 只在页面明确要求时处理系统授权或 SIP;关闭 SIP 的具体步骤见[关闭 SIP 教程](../mac-disable-sip.md),按页面提示完成密钥获取后,恢复你平时使用的安全设置。
4. 返回应用重新检测账号、数据库和图片资源状态。
不要直接复制网上针对其他微信版本的命令。系统授权失败时,记录 macOS 版本、微信版本和页面错误,再按[排障文档](../user-guide/troubleshooting.md#连接微信失败)处理。
@@ -23,5 +23,16 @@ TraceMemo 需要读取微信本地数据。macOS 会根据系统版本、微信
## Intel 与 Apple Silicon
从 Releases 选择与 Mac 处理器匹配的构建。不同架构、微信版本和系统授权状态可能导致连接结果不同;文档不对所有组合做兼容性保证。
TraceMemo 同时支持两种 Mac 架构:
- **Apple Silicon(M 系列、`arm64`)**
- **Intel Mac(`x64`)**
从 Releases 下载与你 Mac 处理器匹配的构建:
- Apple Silicon:`tracememo-<版本号>-arm64.dmg`
- Intel:`tracememo-<版本号>-x64.dmg`
两种架构都可以通过应用内的连接流程自动获取微信数据库密钥。首次连接时,TraceMemo 会根据当前机器架构进入对应的流程,按连接页面提示操作即可。
Apple Silicon 和 Intel 已适配微信 macOS `4.1.13`。不同架构、微信版本和系统授权状态可能导致连接结果不同;文档不对所有组合做兼容性保证。
@@ -0,0 +1,92 @@
---
name: setup-wechat-share-card
description: 自动配置和部署 TraceMemo 实验性微信分享卡片服务。用户要求启用、部署、修复或迁移微信分享卡片,配置 Cloudflare Worker/R2/Wrangler,设置 UPLOAD_TOKEN、微信测试号 AppID/AppSecret、JS 接口安全域名,或希望由 Codex、Claude Code 等 Agent 代替手工阅读部署文档时使用。
---
# 部署微信分享卡片
尽量自行发现项目状态并完成部署。只在自动检查后仍缺少必要信息时,一次性询问用户;不要逐项反复确认。
## 安全边界
- 不把真实 AppSecret、`UPLOAD_TOKEN`、Cloudflare Token 或微信验证内容写入 Git。
- `.env.example` 只保存占位符;真实值写入项目根目录 `.env`,该文件必须被 Git 忽略。
- 不在最终回答中回显 Secret。日志中只报告“已配置/缺失”。
- `UPLOAD_TOKEN` 默认自动生成,不要求用户提供。
- AppID/AppSecret 必须来自用户自己的微信测试号或公众号。无法自动获取时才询问。
- 部署、创建 R2 和写 Secret 属于用户明确请求本 Skill 后的正常动作;不要提交、推送或创建 PR,除非用户另外明确要求。
## 自动工作流
1. 定位 TraceMemo 仓库根目录。确认存在 `services/share-card-worker/wrangler.jsonc`。
2. 运行:
```bash
bash docs/skill/setup-wechat-share-card/scripts/setup.sh doctor
```
3. 检查根目录 `.env`。脚本会自动复用已有配置并生成缺失的 `WECHAT_SHARE_UPLOAD_TOKEN`。
4. 如果以下值缺失,只向用户发起一次集中询问:
- 分享域名,例如 `share.example.com`;
- 微信测试号 AppID;
- 微信测试号 AppSecret。
5. 用户不知道从哪里获取时,告诉他打开:
```text
https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
```
使用微信扫码登录后,复制页面上的 `appID` 和 `appsecret`。提醒用户把分享域名填入“JS 接口安全域名”,不带 `https://` 和路径。
6. 将缺失值交给交互脚本,不要把 Secret 放进命令行参数:
```bash
bash docs/skill/setup-wechat-share-card/scripts/setup.sh configure
```
该命令通过终端交互收集缺项,AppSecret 使用隐藏输入。
7. 执行完整部署:
```bash
bash docs/skill/setup-wechat-share-card/scripts/setup.sh deploy
```
脚本会依次:
- 检查或临时下载 Wrangler;
- 启动 Cloudflare OAuth 登录;
- 执行 `whoami`;
- 生成本地 `wrangler.local.jsonc`;
- 创建或复用 R2 Bucket;
- 写入三个 Worker Secret;
- 部署 Worker;
- 检查 `/health` 和微信签名接口。
8. OAuth 页面出现时,让用户只完成浏览器登录/授权;不要改用 API Token,除非用户主动要求。
9. 部署后把服务地址告诉用户,并提醒他在 TraceMemo 卡片弹窗粘贴 `.env` 中的 `WECHAT_SHARE_UPLOAD_TOKEN`。优先把 Token 复制到剪贴板,不在聊天中展示:
```bash
bash docs/skill/setup-wechat-share-card/scripts/setup.sh copy-token
```
10. 如果微信要求 TXT 验证文件,读取 [references/wechat-domain-verification.md](references/wechat-domain-verification.md),取得用户提供的文件后再修改 Worker。
## 决策规则
- Wrangler 未安装:优先使用项目依赖;否则通过 `pnpm dlx wrangler@latest` 临时下载,不强制全局安装。
- `whoami` 已登录正确账号:不要重复登录。
- R2 已存在:继续,不把“已存在”视为失败。
- 自定义域名有 A/AAAA/CNAME 冲突:报告准确域名并要求用户选择删除冲突记录或换子域名;不要擅自删除 DNS。
- HTTP 401:重新同步 `.env` 中的 `WECHAT_SHARE_UPLOAD_TOKEN` 到 Worker,再让用户更新 TraceMemo。
- “微信 JS-SDK 尚未配置”:重新写入 AppID/AppSecret 并部署。
- 微信返回 AppID/AppSecret 错误:让用户检查是否来自同一个测试号、AppSecret 是否已重置。
- 缺少 JS 接口安全域名或测试号关注:这是微信后台操作,明确告诉用户要填写什么,不要假装已完成。
## 验证结果
完成前必须确认:
- `wrangler whoami` 成功;
- Worker 部署成功;
- `/health` 返回 `storage: ready`;
- `/api/wx-signature` 返回 `appId`、`timestamp`、`nonceStr`、`signature`;
- Git 扫描未发现 `.env`、真实 AppSecret、上传密钥或用户域名被暂存。
详细产品和架构说明见:`docs/deployment/experimental-wechat-share-card.md`。
@@ -0,0 +1,4 @@
interface:
display_name: "部署微信分享卡片"
short_description: "让 Agent 自动完成微信分享卡片服务的配置与部署"
default_prompt: "Use $setup-wechat-share-card to configure and deploy my self-hosted WeChat share-card service with minimal questions."
@@ -0,0 +1,15 @@
# 微信域名验证文件
当微信测试号页面要求下载 TXT 文件时:
1. 向用户索取 TXT 文件本身,或文件名与完整内容。
2. 不把真实验证内容写进公开仓库历史。
3. 优先在本地私有配置中注入;若当前 Worker 只能通过源码 Map 返回,则先提醒用户该值会进入工作区,确认仓库发布前必须移除或改造成 Secret/变量。
4. 验证目标必须是:
```text
https://<分享域名>/<微信提供的文件名>.txt
```
5. 返回内容必须是纯文本且与微信提供内容完全一致,不加空格、HTML 或额外换行。
6. 完成验证后再配置 JS 接口安全域名。安全域名只填主机名,不带协议或路径。
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
ENV_FILE="$REPO_ROOT/.env"
EXAMPLE_FILE="$REPO_ROOT/.env.example"
WORKER_DIR="$REPO_ROOT/services/share-card-worker"
BASE_CONFIG="$WORKER_DIR/wrangler.jsonc"
LOCAL_CONFIG="$WORKER_DIR/wrangler.local.jsonc"
BUCKET_NAME="wechatexplorer-share-reports"
cd "$REPO_ROOT"
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
info() {
printf '[share-card] %s\n' "$*"
}
require_project() {
[[ -f "$BASE_CONFIG" ]] || fail "请在 TraceMemo 仓库根目录运行此脚本"
[[ -f "$EXAMPLE_FILE" ]] || fail "缺少 .env.example"
}
ensure_env_file() {
if [[ ! -f "$ENV_FILE" ]]; then
cp "$EXAMPLE_FILE" "$ENV_FILE"
chmod 600 "$ENV_FILE"
info "已从 .env.example 创建本机 .env"
fi
}
read_env_value() {
local key="$1"
local line
line="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 || true)"
printf '%s' "${line#*=}"
}
write_env_value() {
local key="$1"
local value="$2"
local escaped
escaped="$(printf '%s' "$value" | sed 's/[\\&|]/\\&/g')"
if grep -q -E "^${key}=" "$ENV_FILE"; then
sed -i.bak -E "s|^${key}=.*$|${key}=${escaped}|" "$ENV_FILE"
command rm "$ENV_FILE.bak"
else
printf '\n%s=%s\n' "$key" "$value" >> "$ENV_FILE"
fi
chmod 600 "$ENV_FILE"
}
normalize_domain() {
local value="$1"
value="${value#http://}"
value="${value#https://}"
value="${value%%/*}"
printf '%s' "$value"
}
ensure_upload_token() {
local token
token="$(read_env_value WECHAT_SHARE_UPLOAD_TOKEN)"
if [[ ${#token} -lt 32 ]]; then
token="$(openssl rand -hex 32)"
write_env_value WECHAT_SHARE_UPLOAD_TOKEN "$token"
info "已生成新的 UPLOAD_TOKEN 并安全写入 .env"
fi
}
wrangler() {
if [[ -x "$REPO_ROOT/node_modules/.bin/wrangler" ]]; then
"$REPO_ROOT/node_modules/.bin/wrangler" "$@"
elif command -v pnpm >/dev/null 2>&1; then
pnpm dlx wrangler@latest "$@"
elif command -v npx >/dev/null 2>&1; then
npx --yes wrangler@latest "$@"
else
fail "需要 Node.js 以及 pnpm 或 npm 才能运行 Wrangler"
fi
}
ensure_wrangler_login() {
if wrangler whoami >/dev/null 2>&1; then
wrangler whoami
return
fi
info "即将打开 Cloudflare OAuth 登录,请在浏览器中完成授权"
wrangler login
wrangler whoami
}
validate_required_config() {
local domain app_id app_secret token
domain="$(normalize_domain "$(read_env_value WECHAT_SHARE_DOMAIN)")"
app_id="$(read_env_value WECHAT_SHARE_APP_ID)"
app_secret="$(read_env_value WECHAT_SHARE_APP_SECRET)"
token="$(read_env_value WECHAT_SHARE_UPLOAD_TOKEN)"
[[ -n "$domain" && "$domain" != "share.example.com" ]] || fail "缺少真实 WECHAT_SHARE_DOMAIN"
[[ -n "$app_id" ]] || fail "缺少 WECHAT_SHARE_APP_ID"
[[ -n "$app_secret" ]] || fail "缺少 WECHAT_SHARE_APP_SECRET"
[[ ${#token} -ge 32 ]] || fail "WECHAT_SHARE_UPLOAD_TOKEN 长度不足"
}
configure_interactively() {
ensure_env_file
local domain app_id app_secret
domain="$(read_env_value WECHAT_SHARE_DOMAIN)"
if [[ -z "$domain" || "$domain" == "share.example.com" ]]; then
read -r -p '分享域名(例如 share.example.com,不带 https://):' domain
domain="$(normalize_domain "$domain")"
[[ -n "$domain" ]] || fail "分享域名不能为空"
write_env_value WECHAT_SHARE_DOMAIN "$domain"
fi
app_id="$(read_env_value WECHAT_SHARE_APP_ID)"
if [[ -z "$app_id" ]]; then
read -r -p '微信测试号 AppID:' app_id
[[ -n "$app_id" ]] || fail "AppID 不能为空"
write_env_value WECHAT_SHARE_APP_ID "$app_id"
fi
app_secret="$(read_env_value WECHAT_SHARE_APP_SECRET)"
if [[ -z "$app_secret" ]]; then
read -r -s -p '微信测试号 AppSecret(输入不会显示):' app_secret
printf '\n'
[[ -n "$app_secret" ]] || fail "AppSecret 不能为空"
write_env_value WECHAT_SHARE_APP_SECRET "$app_secret"
fi
ensure_upload_token
info "本机配置已准备完成"
}
generate_local_config() {
local domain
domain="$(normalize_domain "$(read_env_value WECHAT_SHARE_DOMAIN)")"
cat > "$LOCAL_CONFIG" <<EOF
{
"\$schema": "node_modules/wrangler/config-schema.json",
"name": "wechatexplorer-share-card",
"main": "src/index.js",
"compatibility_date": "2026-07-23",
"routes": [{ "pattern": "$domain", "custom_domain": true }],
"r2_buckets": [{ "binding": "REPORTS", "bucket_name": "$BUCKET_NAME" }],
"triggers": { "crons": ["17 3 * * *"] },
"vars": {
"PUBLIC_ORIGIN": "https://$domain",
"DEFAULT_EXPIRY_DAYS": "7"
}
}
EOF
info "已生成本机 Worker 配置 services/share-card-worker/wrangler.local.jsonc"
}
assert_secrets_not_tracked() {
git check-ignore -q .env || fail ".env 未被 Git 忽略,请停止部署并检查 .gitignore"
if git ls-files --error-unmatch .env >/dev/null 2>&1; then
fail ".env 已被 Git 跟踪,请先从索引移除"
fi
if git diff --cached --name-only | grep -Eq '(^|/)\.env$|wrangler\.local\.jsonc$'; then
fail "敏感本机配置已被暂存,请先取消暂存"
fi
}
create_bucket_if_needed() {
local output
set +e
output="$(wrangler r2 bucket create "$BUCKET_NAME" --config "$LOCAL_CONFIG" 2>&1)"
local status=$?
set -e
if [[ $status -eq 0 ]]; then
printf '%s\n' "$output"
elif printf '%s' "$output" | grep -Eqi 'already exists|already owned|10004'; then
info "R2 Bucket 已存在,继续部署"
else
printf '%s\n' "$output" >&2
fail "创建 R2 Bucket 失败"
fi
}
put_secrets() {
local upload_token app_id app_secret
upload_token="$(read_env_value WECHAT_SHARE_UPLOAD_TOKEN)"
app_id="$(read_env_value WECHAT_SHARE_APP_ID)"
app_secret="$(read_env_value WECHAT_SHARE_APP_SECRET)"
printf '%s' "$upload_token" | wrangler secret put UPLOAD_TOKEN --config "$LOCAL_CONFIG"
printf '%s' "$app_id" | wrangler secret put WECHAT_APP_ID --config "$LOCAL_CONFIG"
printf '%s' "$app_secret" | wrangler secret put WECHAT_APP_SECRET --config "$LOCAL_CONFIG"
}
verify_service() {
local domain health signature
domain="$(normalize_domain "$(read_env_value WECHAT_SHARE_DOMAIN)")"
health="$(curl -fsS --retry 5 --retry-delay 2 "https://$domain/health")"
printf '%s' "$health" | grep -q '"storage":"ready"' || fail "健康检查未返回 storage: ready"
signature="$(curl -fsS --retry 3 --retry-delay 2 "https://$domain/api/wx-signature?url=https%3A%2F%2F${domain}%2Fhealth")"
printf '%s' "$signature" | grep -q '"signature"' || fail "微信 JS-SDK 签名检查失败:$signature"
info "服务验证成功:https://$domain"
}
doctor() {
require_project
ensure_env_file
ensure_upload_token
assert_secrets_not_tracked
info "Node: $(node --version 2>/dev/null || printf '未安装')"
info "pnpm: $(pnpm --version 2>/dev/null || printf '未安装')"
if wrangler --version >/dev/null 2>&1; then
info "Wrangler 可用:$(wrangler --version | tail -n 1)"
else
fail "Wrangler 无法运行"
fi
local domain app_id app_secret
domain="$(read_env_value WECHAT_SHARE_DOMAIN)"
app_id="$(read_env_value WECHAT_SHARE_APP_ID)"
app_secret="$(read_env_value WECHAT_SHARE_APP_SECRET)"
[[ -n "$domain" && "$domain" != "share.example.com" ]] && info "分享域名:已配置" || info "分享域名:缺失"
[[ -n "$app_id" ]] && info "微信 AppID:已配置" || info "微信 AppID:缺失"
[[ -n "$app_secret" ]] && info "微信 AppSecret:已配置" || info "微信 AppSecret:缺失"
info "UPLOAD_TOKEN:已配置"
}
deploy() {
require_project
ensure_env_file
ensure_upload_token
validate_required_config
assert_secrets_not_tracked
ensure_wrangler_login
generate_local_config
create_bucket_if_needed
put_secrets
wrangler deploy --config "$LOCAL_CONFIG"
verify_service
}
copy_token() {
ensure_env_file
ensure_upload_token
local token
token="$(read_env_value WECHAT_SHARE_UPLOAD_TOKEN)"
if command -v pbcopy >/dev/null 2>&1; then
printf '%s' "$token" | pbcopy
elif command -v wl-copy >/dev/null 2>&1; then
printf '%s' "$token" | wl-copy
elif command -v xclip >/dev/null 2>&1; then
printf '%s' "$token" | xclip -selection clipboard
else
fail "未找到剪贴板工具;请让用户自行从 .env 读取 WECHAT_SHARE_UPLOAD_TOKEN"
fi
info "UPLOAD_TOKEN 已复制到剪贴板"
}
case "${1:-doctor}" in
doctor) doctor ;;
configure) configure_interactively ;;
deploy) deploy ;;
copy-token) copy_token ;;
*) fail "用法:$0 {doctor|configure|deploy|copy-token}" ;;
esac
+112 -16
View File
@@ -1,6 +1,6 @@
---
name: tracememo-reader
description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的微信聊天数据。当用户要求查看微信消息、查找联系人或群聊、总结聊天、生成群聊总结时使用。此 Skill 由本机 TraceMemo 提供数据,不是 MCP Server。
description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的微信聊天数据和图片媒体,并管理定时日报任务。当用户要求查看微信消息、查找联系人或群聊、总结聊天、查看或理解图片、生成群聊总结、查询或修改定时日报时使用。此 Skill 由本机 TraceMemo 提供数据,不是 MCP Server。
---
# TraceMemo Reader
@@ -27,20 +27,78 @@ description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的
## 端点速查
| 方法 | 路径 | 用途 |
| ---- | --------------------- | ------------------------------------------------- |
| GET | `/health` | 健康和数据库状态 |
| GET | `/current_time` | 本机时间与时区 |
| GET | `/contact` | 联系人/群聊列表;可传 `filter`、`type` |
| GET | `/chatroom` | 群聊列表;可传 `keyword` |
| GET | `/recent_chat` | 最近会话;可传 `limit` |
| GET | `/chatlog` | 会话消息;必填 `talker`,可传 `time` 或时间戳范围 |
| GET | `/group_snapshot` | 群成员快照;必填 `md5` |
| GET | `/resolve` | 昵称、wxid、md5 解析;必填 `q` |
| POST | `/report` | 将已有日报结构渲染为 HTML/PNG |
| GET | `/agent/status` | Agent Hub、连接器和数据库状态 |
| POST | `/agent/group-report` | 按群和 `today`/`yesterday`/`7days` 生成总结图片 |
| POST | `/agent/send` | 已连接机器人发送测试 |
| 方法 | 路径 | 用途 |
| ------ | ------------------------------------------------------- | ------------------------------------------------- |
| GET | `/health` | 健康和数据库状态 |
| GET | `/current_time` | 本机时间与时区 |
| GET | `/contact` | 联系人/群聊列表;可传 `filter`、`type` |
| GET | `/chatroom` | 群聊列表;可传 `keyword` |
| GET | `/recent_chat` | 最近会话;可传 `limit` |
| GET | `/chatlog` | 会话消息;必填 `talker`,可传 `time` 或时间戳范围 |
| GET | `/media/{mediaId}` | 按消息返回的 `media.url` 获取图片二进制资源 |
| GET | `/group_snapshot` | 群成员快照;必填 `md5` |
| GET | `/resolve` | 昵称、wxid、md5 解析;必填 `q` |
| POST | `/query/messages` | 按目标与时间范围取消息(结构化,不调用 AI) |
| POST | `/query/search` | 受限语义关键词检索(依赖本地索引,见 freshness) |
| POST | `/query/message-context` | 用 `messageRef` 取某条消息的前后文 |
| POST | `/query/conversation-overview` | 按会话与时间范围提取可总结的证据 |
| GET | `/query/capabilities` | Query 端点能力目录 |
| GET | `/wechat-personal/send-capability` | 个人微信发送能力状态(文字 / 图片 / 语音) |
| GET | `/scheduled-reports` | 查询全部定时日报任务 |
| GET | `/scheduled-reports/:id` | 查询单个定时日报任务 |
| POST | `/scheduled-reports` | 创建定时日报任务 |
| PATCH | `/scheduled-reports/:id` | 修改定时日报任务 |
| DELETE | `/scheduled-reports/:id` | 删除定时日报任务(执行前必须获得用户确认) |
| POST | `/scheduled-reports/:id/enable` | 启用定时日报任务 |
| POST | `/scheduled-reports/:id/disable` | 暂停定时日报任务 |
| POST | `/scheduled-reports/:id/run` | 立即执行一次并返回 execution |
| GET | `/scheduled-reports/:id/executions` | 查询执行记录 |
| POST | `/scheduled-reports/executions/:executionId/retry-send` | 复用已有 PNG 重试发送 |
| POST | `/report` | 将已有日报结构渲染为 HTML/PNG |
| GET | `/agent/status` | Agent Hub、连接器和数据库状态 |
| POST | `/agent/group-report` | 按群和 `today`/`yesterday`/`7days` 生成总结图片 |
| POST | `/agent/send` | 已连接机器人发送测试(文字或本地图片) |
这个 API **不只是只读的**:`/report` 会渲染并写文件,`/agent/send` 会真的发出微信消息,`/scheduled-reports*` 会创建、修改、删除或立刻执行定时任务。这些调用都要先确认用户意图;`DELETE` 与 `/agent/send` 尤其需要用户明确确认。
## 定时日报管理
定时日报由 TraceMemo 自己持久化和调度。Agent 只负责理解自然语言、解析群聊和时间,再调用上述 API;不要创建 cron、维护任务文件、计算下一次执行时间或自行发送微信。
### 创建任务
用户提出“每天早上 9 点给技术交流群发昨天的日报”时,按以下顺序执行:
1. 调用 `/health`,确认 TraceMemo 和数据库可用。
2. 调用 `/wechat-personal/send-capability`,只有 `capability.status === "ready"` 且 `capability.capabilities.image === true` 才允许继续。
3. 用户使用“今天”“昨天”等相对日期时调用 `/current_time`;日报任务的 `schedule.time` 使用 TraceMemo 本机时区的 `HH:mm`,不要转成 UTC。
4. 调用 `/chatroom` 或 `/contact?type=group` 查找群聊。名称匹配多个结果时,必须把候选项展示给用户并要求选择;不能猜测。
5. 使用唯一群聊的 `talker` 创建:
```json
{
"name": "技术交流群 · 每日日报",
"group": { "talker": "xxx@chatroom", "name": "技术交流群" },
"schedule": { "type": "daily", "time": "09:00" },
"reportRange": "yesterday",
"target": { "type": "wechat_group", "talker": "xxx@chatroom" },
"enabled": true
}
```
如果 API 返回 `409` 且 `error === "duplicate"`,告诉用户相同任务已经存在,不要再次创建。能力状态不是 `ready` 时(`unsupported`、`unconfigured`、`needs_binding`、`initializing` 或 `error`),直接说明需要先在 TraceMemo 的“设置 → 发送能力”里完成个人微信绑定和能力检测,不要继续创建任务。
### 查看、修改和执行
- “我现在有哪些定时日报”调用 `GET /scheduled-reports`,使用返回的 `tasks` 展示任务名称、群聊、每天的时间、范围、目标和启停状态。
- 修改前先查询列表并确认唯一任务,再调用 `PATCH /scheduled-reports/:id`。只提交需要修改的字段,例如 `{"schedule":{"type":"daily","time":"10:00"}}`。
- 暂停调用 `/scheduled-reports/:id/disable`,恢复调用 `/scheduled-reports/:id/enable`。
- “现在执行一次”调用 `/scheduled-reports/:id/run`,不要改用 `/agent/group-report` 后自行发送微信;该接口和定时执行共用同一条链路。
- 查询执行结果调用 `/scheduled-reports/:id/executions`,根据 `status`、`startedAt`、`finishedAt`、`message` 和 `error` 向用户解释结果。
### 删除确认
删除是不可逆操作。收到删除请求后,先用任务列表找到唯一任务,向用户展示任务名称、时间、日报范围和发送目标并明确询问确认;只有用户明确确认后,才调用 `DELETE /scheduled-reports/:id`。
## 时间与上下文规则
@@ -52,6 +110,43 @@ description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的
- 根据多条消息整理出的总结;
- 没有来源支持的推断。
## 结构化查询(query/\*)
需要按目标 + 时间范围稳定取数时,优先使用 `query/*`,而不是自己拼 `chatlog`:
- `query/messages`:按 `target`、`timeRange`、`direction`、`messageTypes` 取消息;
- `query/search`:受限语义关键词检索,依赖本地索引;
- `query/message-context`:用返回的 `messageRef` 取前后文;
- `query/conversation-overview`:按会话与时间范围提取可总结的证据。
两个要点:
- `messageRef` 是服务端生成的不透明引用,**不要**自行构造 wxid、md5 或数据库路径;
- `query/search` 依赖异步建立的本地索引。`coverage.state` 不是 `complete` 且 `evidence` 为空时,只能说“这段范围暂时无法确认”,**不能**下“没有找到”的结论。
## 媒体消息
当 `/chatlog` 返回图片消息时:
1. 如果用户只是询问图片消息是否存在,不需要获取图片。
2. 如果用户要求查看、识别、理解或分析图片,原样使用该消息 `media.url` 获取真实图片;不要用消息 `id` 自行拼接。媒体标识按数据库连接隔离,重启、重连或切换账号后须重新读取 `/chatlog` 获取地址。
3. 不要根据 `[图片]`、消息文本或文件名猜测图片内容。
4. 获取成功后,将图片交给当前 Agent 的视觉能力。
5. 如果图片获取失败,明确说明无法读取图片。
6. 不要声称看到了没有成功获取的图片。
7. 不要向用户暴露 Token、本地文件路径或数据库路径。
### 图片分析
用户:“看看张三昨天发的那张截图。”
1. 调用 `/health`;必要时调用 `/current_time`。
2. 调用 `/resolve`,再调用 `/chatlog` 找到 `type` 为图片的消息。
3. 请求该消息的 `media.url`,将返回的图片交给 Vision。
4. 必要时读取图片消息前后若干条消息,结合聊天上下文回答。
不要只根据 `[图片]` 猜测内容,不要把一次 OCR 当作完整图片理解,也不要直接读取任意本地图片路径。
## 隐私和安全
只读取用户请求所需的会话和时间范围。不要把完整聊天数据库、密钥或 Token 暴露给用户。Reader API 本身不自动把聊天转发到外部服务器,但当前 Agent 可能会把工具结果交给其配置的模型;如有疑问,提醒用户检查 Agent 的数据策略。
@@ -60,6 +155,7 @@ description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的
- `401`:Token 缺失、错误或被轮换;请用户回 API Center 复制最新 Token。
- `403`:浏览器 Origin 不在 loopback 允许列表;CLI/Agent 通常不带 Origin。
- `404`:先用 `/resolve` 确认会话标识。
- `404`:会话查询失败时先用 `/resolve` 确认会话标识;媒体请求表示标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在。
- `422`:媒体标识格式错误,或消息不是可读取的图片(`NOT_IMAGE`)。
- `503`:用户还没有完成数据库连接或对应服务未就绪。
- 空结果:缩小/扩大时间范围,确认账号和会话,再检查媒体或语音是否可读。
+13
View File
@@ -41,6 +41,19 @@
你可以点击来源回到档案中的原始消息。产品内部将这些信息称为 Evidence、Citation 和 Search Trace,用户可以把它们理解为“依据、来源标记和查找过程”。详见[如何核对 AI 的回答来源](../concepts/answer-sources.md)。
## 查找过程和跳回原消息
查找过程中,界面依次显示真实阶段:**理解问题 → 查找相关聊天 → 整理证据 → 生成回答**。跨会话、大范围检索更慢时,副提示会写明“正在搜索较大范围的聊天记录…”。阶段只在真正进入下一步时前进,不使用定时器或百分比伪造进度。
查找结束后,界面给出耗时拆解:**总耗时**,以及其中分别花在 **AI 生成** 和 **本地查询** 上的时间。这样你能判断慢在哪——是模型在写答案,还是本机还在翻聊天记录。
点击来源卡片的 **“跳转到原聊天”** 会真的打开对应会话并定位到那条消息:
- 群聊来源打开的是那个群,而不是群里某个联系人;
- 会加载该消息前后的上下文,并滚动到它、短暂高亮;
- 只加载目标消息附近的一段,不会把整个会话历史全部读出来;
- 如果这条消息已经不在本地(例如已被删除),界面会明确说明“已打开对应会话,但暂时无法定位原消息”,不会假装跳转成功。
## 什么时候不要直接相信答案
- 来源很少,或时间范围与问题不一致;
+18 -6
View File
@@ -4,7 +4,7 @@
## 选择要看的会话
左侧会话列表可以浏览联系人、群聊、折叠群聊和公众号等已读取到的会话。选中会话后,右侧显示消息时间线;滚动到较早位置可以继续加载历史。
左侧会话列表可以浏览已读取到的联系人、群聊和公众号。选中会话后,右侧显示消息时间线;滚动到较早位置可以继续加载历史。
如果你从 AI 回答的来源进入档案,应用会自动切换到对应会话并尽量定位到消息时间。
@@ -19,6 +19,22 @@
关键词搜索速度快、结果直观,但它不会理解“意思相近但没有相同词”的问题。
## 怎么找到联系人
档案搜索会综合多个身份字段匹配联系人或群聊,包括:
- 通讯录备注(remark);
- 微信昵称;
- 当前微信号;
- wxid;
- 拼音全拼和拼音首字母。
因此可以直接输入备注、昵称、微信号或拼音查找。搜索联系人和搜索消息是两步:先确认目标会话,再在会话内查关键词;记得大意但不知道原话时,改用[AI Search](./ai-search.md)。
## 文字转语音
在档案中选择当前联系人或群聊,输入文字后生成语音,试听确认后发送。这个入口只处理明确的文字转语音动作,不是任意文本、图片或本地语音文件发送器。
## 消息和媒体
根据微信数据中实际可用的资源,档案可以展示文本、图片、视频、语音、文件、链接、引用、小程序、表情和系统消息等类型。媒体是否能显示,取决于本机原始资源是否仍然存在、权限是否完整以及当前微信版本的存储方式。
@@ -27,11 +43,7 @@
如果文字正常但图片无法打开,进入“设置 → 图片解密”查看当前状态。可以尝试自动获取,也可以在已经知道正确密钥时手动配置;原文件已经被微信清理时,仅配置密钥也无法恢复图片。
## 可选保留撤回消息
“设置 → 防撤回”提供一个默认关闭的可选功能。开启后,应用会尽量保留之后捕获到的撤回消息,并在气泡旁标记“消息已撤回”。它不能找回开启前已经消失或应用未捕获到的内容,也可能增加加载开销。
该功能与普通只读浏览的数据边界不同。开启前请阅读[防撤回](./recall-protection.md)。
联系人和群聊列表会尽量显示头像、备注和昵称。头像或资料缺失时不影响消息读取;这通常表示本机没有对应资源,或微信没有返回完整资料。
## 保护自己不被误导
+44 -14
View File
@@ -8,32 +8,51 @@
## 1. 开始前准备
| 系统 | 已测试的微信客户端 | 需要注意 |
| ------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| macOS | [微信 macOS `4.1.8.100`](https://github.com/zsbai/wechat-versions/releases/tag/4.1.8.100) | 自动获取数据库密钥前,需要按连接页面提示完成授权;页面明确要求时还需要处理 SIP |
| Windows | [微信 Windows `4.1.9.57`](https://github.com/iibob/wechat-win-archive/releases#release-v4.1.9.57) | 首次使用时请确认微信数据目录;Windows 不需要关闭 SIP |
TraceMemo 需要读取微信本地数据库。首次使用前,请确认已安装受支持的微信客户端,并按照连接页面完成数据库密钥获取。
- 上表是当前实际测试过的客户端版本,不代表只有这些版本可以使用。其他微信 4.x 版本可能可以连接,但尚未逐一验证。
| 系统 | 微信客户端 | 首次连接说明 |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| macOS | **微信 4.1.13 系列**(推荐 [4.1.13.8](https://github.com/zsbai/wechat-versions/releases/tag/4.1.13.8))<br>备选 [4.1.8.100](https://github.com/zsbai/wechat-versions/releases/tag/4.1.8.100) | 支持 Apple Silicon(M 系列)和 Intel Mac 自动获取数据库密钥。首次连接时,请按照 TraceMemo 页面提示完成系统授权及微信登录操作。 |
| Windows | **微信 4.x**(推荐 [4.1.9.57](https://github.com/iibob/wechat-win-archive/releases/tag/v4.1.9.57)) | 支持自动获取数据库密钥。首次使用时请确认微信数据目录正确,无需处理 macOS 的 SIP 设置。 |
### macOS 用户
TraceMemo 已支持两种 Mac 架构:
- **Apple Silicon(M1 / M2 / M3 / M4 等)**
- **Intel Mac(x64)**
两种架构均支持自动获取微信数据库密钥,TraceMemo 会根据当前 Mac 自动选择对应的连接方式。
Apple Silicon 和 Intel 均已适配微信 macOS `4.1.13` 系列。首次获取密钥时,请让微信停留在登录页面,并按照 TraceMemo 中显示的步骤操作。
> 不同 Mac 架构的连接流程可能略有区别,请始终以应用内「第一次使用」页面显示的提示为准。
### 关于微信版本
- 上表中的版本是当前 TraceMemo 已适配或推荐使用的版本,并不代表只有这些版本可以运行。
- TraceMemo 必须取得当前微信账号对应的数据库密钥,才能读取聊天记录。
- 你需要有权访问要读取的微信账号和聊天数据。
- 如果要使用 AI 问答、群聊日报或图片理解,还需要在应用中配置一个 AI 服务。
- 如果要使用 AI 问答、群聊日报或图片理解,还需要在应用中配置一个 AI 服务。「图片文字索引」不在此列——它在本机识别图片里的文字,不需要配置 AI。
当前代码按微信 4.x 数据结构处理。不同微信客户端版本、系统权限和数据迁移状态可能影响自动连接;遇到问题时请查看[常见问题与排查](./troubleshooting.md)。
## 2. 安装并启动
安装包统一从 [GitHub Releases](https://github.com/Wxw-Gu/WechatExplorer/releases) 下载。
安装包统一从 [GitHub Releases](https://github.com/Wxw-Gu/TraceMemo/releases) 下载。
### Windows
1. 从 Releases 下载 Windows x64 的 `TraceMemo-<版本号>-setup.exe` 安装包。
1. 从 Releases 下载 Windows x64 的 `tracememo-<版本号>-setup.exe` 安装包。
2. 双击安装包,按向导完成安装。
3. 启动 TraceMemo。
4. 如果安装完成后软件无法启动,请安装 Microsoft Visual C++ x64 运行库:[vc_redist.x64.exe](https://aka.ms/vc14/vc_redist.x64.exe),安装完成后重新启动 TraceMemo。
### macOS
1. 下载 Apple Silicon(M 系列、`arm64`)版本的 `.dmg`。当前版本不支持 Intel 芯片的 Mac。
1. 从 Releases 下载与你 Mac 处理器架构匹配的 `.dmg`:
- Apple Silicon(M 系列、`arm64`):`tracememo-<版本号>-arm64.dmg`
- Intel(`x64`):`tracememo-<版本号>-x64.dmg`
2. 打开 DMG,将 TraceMemo 拖入“应用程序”文件夹。
3. 如果系统提示“无法打开,因为开发者无法验证”,前往“系统设置 → 隐私与安全性”,点击“仍要打开”。
4. 如果系统提示应用已损坏,可在终端执行:
@@ -42,7 +61,16 @@
xattr -cr "/Applications/TraceMemo.app"
```
5. 启动 TraceMemo。首次自动获取数据库密钥时,按连接页面显示的授权要求操作;只有页面明确提示时才按[关闭 SIP 教程](../mac-disable-sip.md)处理。关闭 SIP 会降低系统安全性,完成密钥配置后应重新开启。
5. 先让微信停在**未登录窗口**(如果微信已经登录,请退出当前账号,**只关闭窗口不算**),再启动 TraceMemo。
- 连接时会按提示自动获取数据库密钥;应用会根据当前架构进入对应流程,按连接页面显示的授权要求操作即可。
- **等第五步 明确提示可以登录后,再回到微信点击登录。** 在此之前不要先点登录,否则这次密钥获取会失败,需要重新开始。
- 只有页面明确提示时才按[关闭 SIP 教程](../mac-disable-sip.md)处理。关闭 SIP 会降低系统安全性,完成密钥配置后应重新开启。
> **注意事项:取密钥时提示监听超时(`CAPTURE_TIMEOUT`,或相关失败)**
>
> 这说明「先让微信停在未登录窗口」这一步操作有误——常见原因是微信当时已经登录,或者还没等连接页面提示就先点了登录
>
> 请先退出微信账号、回到**未登录窗口**,然后按上面的第 5 步**重新来一遍**(等连接页面明确提示可以登录后,再回到微信点击登录)。
更完整的权限和安全边界见 [macOS 数据访问说明](../platform/macos.md)。
@@ -106,7 +134,7 @@
- [生成群聊日报或总结](./report.md)
- [转写微信语音](./voice.md)
- [导出聊天档案](./export.md)
- [可选开启防撤回](./recall-protection.md)
- [让日报、退群通知按规则自动运行](../README.md#日报与自动化)
- [在微信里向 TraceMemo 提问](../agent/agent-hub.md)
- [让外部 Agent 查询微信历史](../agent/overview.md)
@@ -132,17 +160,19 @@ Agent Hub 是普通用户可以直接使用的入口,不需要安装 Reader Sk
## 8. 需要配置 AI 吗?
不一定。浏览聊天、普通关键词搜索、建立本地知识库和导出不要求在线 AI 服务。
不一定。浏览聊天、普通关键词搜索、建立本地知识库、图片文字识别、离线语音转写和导出都不要求在线 AI 服务。
使用“问问微信”、群聊日报或图片理解时,需要在“设置 → AI 模型”中添加并测试 AI 服务。你主动开始并确认远程 AI 功能后,完成任务所需的内容才可能发送给该服务;计费、留存和地区规则由对应服务商决定。
这两种情况容易混淆:**本机识别图片里的文字**(图片文字索引)不联网、不需要 AI 服务;**让模型看图并回答**(图片理解)才需要配置 AI 服务。
## 9. 数据和隐私的最低须知
- 微信数据库、聊天解析和本地索引默认留在本机。
- 微信数据库、聊天解析、本地索引和图片文字识别默认留在本机。
- 离线语音转写使用本地模型;它与在线 AI 请求是两条不同的数据路径。
- 图片文字索引只在本机识别,原始图片不会因为本地识别而上传;识别出的文字会进入本地索引,供搜索和“问问微信”使用。
- 你主动开始并确认 AI 问答或日报后,完成任务所需的受控上下文才可能发送给你选择的 AI 服务;打开应用不会自动上传全部聊天。
- 应用内 Local HTTP API 默认只监听 `127.0.0.1:6131`,受保护接口需要 Token。
- 防撤回默认关闭;首次开启会为微信消息数据库增加本地撤回日志/监听结构,详细边界见[防撤回](./recall-protection.md)。
完整边界见[数据、隐私与安全](./privacy.md)。
+19
View File
@@ -0,0 +1,19 @@
# Intel Mac 首次获取密钥
请先备份微信本地数据。密钥获取和数据库验证都在本机完成,不要把密钥、日志或数据库发给他人。
## 开始使用
1. 启动 TraceMemo,在首次连接页面点击“重新检查环境”。
2. 如果页面显示需要准备连接环境,点击“准备环境”,按页面提示完成。
3. 打开微信,让微信停在未登录页面,不要先点击登录。
4. 回到 TraceMemo,选择微信账号,点击“开始获取密钥”。
5. 等待页面提示可以登录后,立即回到微信点击登录。
6. 等待 TraceMemo 显示“获取成功,验证数据库连接”。
获取成功后,请保存密钥。如果页面提示需要重新准备,按页面的“查看说明”操作,不要连续重试。
## 注意
- 密钥只保存在本机的安全存储中。
- 请不要删除微信本地数据目录。
+39 -2
View File
@@ -13,7 +13,45 @@ Knowledge 不会在第一次连接后自动悄悄建立。进入“问问微信
- **建立本地知识库**:第一次读取当前账号的可检索聊天;
- **同步最新记录**:已有索引时,只补充新增或变化的内容。
同步会在后台运行,完成后页面显示已索引消息、知识片段和磁盘占用。同步期间暂不能开始新的 AI 分析;同步异常时,旧索引仍可能可以继续使用。
同步在后台运行,**期间仍然可以正常提问和分析**,不会被禁用。索引还没追完时,答案会基于当前已经可用的部分给出,并在界面标注覆盖范围。
知识库卡片同时显示两组互相独立的信息:
- **规模**:已索引消息、知识片段、磁盘占用——说明索引有多大;
- **状态与本轮进度**:说明索引现在处于什么状态、这一轮同步在做什么(扫了多少、真正新增了多少、处理到第几个会话)。
`最新索引` 只表示索引已经覆盖到聊天记录的哪个时间点,**不等于**整库已经建完;进度里的计数是**本轮**的数字,不是全部历史的总数。
### 状态怎么读
| 状态 | 含义 |
| ------------------- | ------------------------------------------------ |
| 可用 · 已追至最新 | 索引已覆盖到聊天记录的最新位置,可以直接用 |
| 可用 · 正在追新 | 索引可用,正在后台补充最近新增的消息 |
| 可用 · 正在补齐历史 | 索引可用,正在后台补齐较早的历史内容 |
| 可用 · 同步已取消 | 索引仍然可用;上一轮同步被取消,已建立的部分保留 |
| 可用 · 更新失败 | 索引仍然可用;上一轮同步出错,可以稍后重试 |
只有确实追平、且没有待补齐内容时才会出现“已追至最新”。索引不可查询时不会显示“可用”。
### 取消和继续
同步过程中可以点击 **取消同步**(点击后显示“正在取消…”)。取消只结束当前这一轮,不会删除已经建立的索引,也不会回滚已完成的部分;下次同步会从上次停下的位置继续,不需要从头重扫。中断过的索引仍然可以正常搜索。
## 图片文字索引(另一份索引)
本地索引其实有两份,彼此独立:
- **聊天记录索引**(也就是上面说的 Knowledge):索引文字消息,用于跨会话、跨时间查找;
- **图片文字索引**:在本机识别微信图片里的文字(截图、公告、报价图等),把识别结果也变成可搜索的文字。
“独立”的意思是:聊天记录索引建好了,并不代表图片里的文字就搜得到。建立图片文字索引后,可以在“问问微信”里直接搜截图或公告图里写过的词。
图片文字索引只在本机识别,原始图片不会因为本地识别而上传。它**不等于“图片理解”**:识别文字不联网、不需要 AI 服务;而让模型看图并回答属于图片理解,需要配置 AI 服务,走的是另一条路径。
识别失败的图片可以单独重试,也有“只重建搜索索引、不重新识别图片”的修复入口——修索引不需要重跑几万张图。
两个索引都可以在“设置 → 本地索引”里集中查看状态、建立、同步和清理。
## 账号隔离
@@ -35,4 +73,3 @@ Knowledge 不会在第一次连接后自动悄悄建立。进入“问问微信
## 产品术语(可选)
源码和日志中可能出现 SQLite、FTS、Chunk、索引等词。它们描述的是本地存储和检索实现,不是你开始使用 TraceMemo 的前置知识。
+10 -5
View File
@@ -9,16 +9,15 @@ TraceMemo 的核心路径是本地优先,但“本地优先”不等于所有
- 读取和解析微信数据库;
- 聊天档案浏览和普通关键词搜索;
- 本地 Knowledge 索引及其账号隔离;
- 图片文字索引:识别图片中的文字在本机完成,原始图片不会因为本地识别而上传;
- 离线语音转写;
- 导出文件生成和本地日报历史。
应用不会因为你打开 TraceMemo 就自动把整份微信数据库上传。
防撤回默认关闭,并且和上面的普通读取路径不同。用户第一次明确开启时,当前实现会在微信消息数据库中安装本地撤回日志/监听结构,同时在 TraceMemo 用户数据目录保存必要的恢复记录。v2.1.9 的旧恢复记录会随首次启动迁移复制到 TraceMemo,旧目录仍保留。关闭开关不等于移除已经安装的结构或清空既有记录;当前 UI 没有对应的清理入口。详见[防撤回](./recall-protection.md)。
## 什么时候会请求外部服务
当你主动使用 AI Search、群聊日报或图片理解,并配置了远程 Provider 时,完成任务所需的内容可能发送给该 Provider。当前设置页给出的边界是:
当你主动使用 AI Search、群聊日报或图片理解,并配置了远程 Provider 时,完成任务所需的内容可能发送给该 Provider。图片文字索引、离线语音转写、档案浏览和普通搜索不会触发这一步。当前设置页给出的边界是:
- 当前用户问题;
- 受控检索所需的有限上下文;
@@ -30,7 +29,14 @@ Ollama 等本机 Provider 可以把模型请求留在本机,但本机服务的
## 语音和媒体
离线语音转写在本机进行。图片理解属于 AI 功能:只有你主动启用并使用相关报告/分析路径时,图片才可能按该 Provider 的请求规则被处理。无法读取的媒体不会被自动“猜出来”。
离线语音转写在本机进行。
图片有两条完全不同的路径,不要混为一谈:
- **图片文字索引**:在本机识别图片里的文字,产出的是本地索引数据;原始图片不会因为这一步被上传,也不需要配置 AI 服务。
- **图片理解**:属于 AI 功能。只有你主动启用并使用相关报告/分析路径时,图片才可能按该 Provider 的请求规则被处理。
无法读取的媒体不会被自动“猜出来”。
## Local HTTP API
@@ -58,4 +64,3 @@ Token 由应用生成,使用 Electron `safeStorage` 加密保存在本机 `loc
- 对需要外发的 AI 功能逐项确认 Provider;
- 定期在“设置 → 缓存与清理”清理不再需要的检索、导出和索引缓存;
- 在共享电脑上退出应用并保护系统账户。
- 在开启防撤回前确认你接受其数据库写入、性能和清理边界,并先用微信官方方式备份重要数据。
-37
View File
@@ -1,37 +0,0 @@
# 防撤回
防撤回是一个默认关闭的可选功能。开启后,TraceMemo 会尽量保留它能够捕获到的撤回消息,并在聊天气泡旁标记“消息已撤回”。
它适合希望在本机档案中保留后续聊天上下文的用户,但不能保证找回每一条撤回消息。
## 如何开启
1. 先连接微信数据库,并确认“档案”可以正常读取聊天。
2. 打开“设置 → 防撤回”。
3. 阅读性能和数据提示后,开启“防撤回”。
4. 保持 TraceMemo 与当前微信数据连接;之后捕获到的撤回消息会尽量保留并标记。
防撤回不是第一次使用的必要步骤。只想浏览、搜索、提问或导出时,可以保持关闭。
## 当前能做什么
- 监听应用能够识别到的后续撤回变化;
- 在本地保留必要的消息和撤回关系;
- 将已识别的原消息与撤回状态一起显示在档案中;
- 按微信账号隔离 TraceMemo 保存的恢复记录。
## 当前限制
- 不能恢复开启前已经撤回、且应用从未保存到的消息;
- TraceMemo 未运行、数据库未连接或没有捕获到撤回变化时,消息可能无法保留;
- 微信版本、消息表结构和数据库事件变化都可能让部分消息无法恢复或正确匹配;
- 开启后需要为消息表增加监听,聊天很多或磁盘较慢时可能影响加载性能;
- “消息已撤回”只说明应用识别到了撤回关系,不保证恢复内容完整。
## 数据写入与关闭边界
普通浏览、搜索和 Knowledge 不会修改微信原始聊天数据库;防撤回是一个例外。用户第一次明确开启时,当前实现会在微信消息数据库中安装用于记录撤回的本地日志/监听结构,并在 TraceMemo 的用户数据目录保存必要的本地恢复记录。v2.1.9 的旧恢复记录会在用户确认迁移后复制到 TraceMemo,旧目录不会删除。
关闭设置中的开关,不等同于删除已经安装的日志结构或清空此前保存的恢复记录。当前版本没有在 UI 中提供“移除防撤回日志结构”或“清空防撤回记录”的独立操作。对数据库写入、磁盘占用或完全回滚有要求时,应在开启前先确认这一边界,并使用微信官方方式备份重要数据。
完整的数据边界见[数据、隐私与安全](./privacy.md)。
+20 -4
View File
@@ -6,8 +6,8 @@
典型场景包括:
- 整理今天工作群的讨论重点;
- 回顾昨天错过的决定和资源;
- 整理今日工作群的讨论重点;
- 回顾昨日错过的决定和资源;
- 汇总近 7 天的项目进展、待办和未解决问题;
- 把群里的图片、语音统计和重要消息放进一张长图或 HTML 页面。
@@ -16,7 +16,7 @@
你可以从两个入口开始:打开一级导航“日报”后新建报告,或者在“档案”中选中一个群聊并点击“生成 AI 日报”。
1. 选择一个群聊。当前日报入口只支持群聊,不支持单聊。
2. 选择时间范围:今天、昨天或近 7 天。
2. 选择时间范围:今日、昨日或近 7 天。
3. 按需要选择参与总结的消息类型,先从文字开始最容易核对。
4. 选择报告模板/内容模式并开始生成。
5. 等待“整理输入 → AI 生成 → HTML/PNG 导出”完成。
@@ -33,10 +33,26 @@
生成成功后会保存本地 HTML 与 PNG,并出现在日报历史中。你可以复制图片、打开文件位置或重新生成。删除历史日报只删除本地生成的报告文件,不会影响微信聊天数据库。
## 定时日报(在自动化里)
定时日报现在是「自动化」里的一种规则,不再单独占一个页面:打开一级导航的「自动化」,新建或编辑一条「定时日报」规则,选择群聊、执行时间、日报范围、消息类型和发送目标。日报页顶部的指引条也会直接跳到自动化。
TraceMemo 会按计划执行:
```text
定时触发 → 读取群聊 → 生成报告 → 保存 Report History → 尝试发送
```
生成和发送是两个阶段。当前微信发送能力不可用、未绑定或发送失败时,报告仍会保存,PNG 和执行记录也会保留;这类结果会显示为“已生成,但未发送”或“已生成,发送失败”。
执行记录支持查看已生成的日报。对“等待发送”或“发送失败”的记录,可以直接重试发送,重试会复用已经生成的 PNG,不会重新调用 AI 生成整份报告;完整执行状态和发送边界见[如何把聊天变成可用的信息](../concepts/how-it-works.md#动作执行与审计)。
发送目标当前支持**当前群聊、文件传输助手、自己、指定好友**——还不是任意群发。
## 让报告更可靠
- 先选正确的群和时间范围;
- 不确定时先只选择文字消息;
- 群太活跃时分成“今天”和“近 7 天”两次生成;
- 群太活跃时分成“今日”和“近 7 天”两次生成;
- 看到待办和结论后回到原消息核对上下文;
- AI Provider 不可用时先检查模型配置和网络/本地服务状态。
+5 -5
View File
@@ -12,6 +12,7 @@
### macOS
- 确认下载的构建与 Mac 处理器匹配:Apple Silicon(M 系列)用 `arm64`,Intel 用 `x64`。
- 提示“无法打开,因为开发者无法验证”时,前往“系统设置 → 隐私与安全性”并点击“仍要打开”。
- 提示应用已损坏时,确认应用位于“应用程序”目录,再执行 `xattr -cr "/Applications/TraceMemo.app"`。
@@ -25,10 +26,13 @@
2. 微信版本是否属于当前代码面向的 4.x 数据结构;
3. 微信是否处于页面要求的登录/退出状态;
4. macOS 是否完成页面要求的授权;
5. 连接页面的诊断项是否明确指出密钥、账号或数据库问题。
5. 连接页面的诊断项是否明确指出密钥、账号或数据库问题;
6. macOS 上 Apple Silicon 与 Intel 使用不同的连接流程,按连接页面提示操作;两者都可以自动获取数据库密钥。
重新输入密钥或断开连接不会删除微信原始数据库。macOS 的 SIP 和授权说明见[平台说明](../platform/macos.md)。
仍然失败时,记录 **macOS 版本、CPU 架构(Apple Silicon / Intel)、微信版本、TraceMemo 版本和页面错误提示**后 扫码 README 文档二维码进群提交消息, 或者提交 Issue。
## 连接成功但没有联系人或消息
确认账号身份和数据目录匹配。返回“设置 → 账号与数据库”查看数据库连接状态,重新加载会话后再试。若仍为空,记录系统、微信版本和错误提示后提交 Issue。
@@ -88,7 +92,3 @@ Agent Hub 和外部 Agent 是两条路径。机器人异常时依次确认:
5. 需要总结或自然语言理解时,AI Provider 是否可用。
当前机器人不支持群发、定时任务或与文字同等的图片、语音、文件和视频理解。详细边界见[Agent Hub](../agent/agent-hub.md)。
## 防撤回没有保留消息
防撤回只能尽量保留开启后且应用成功捕获到的撤回变化。确认开启时数据库已经连接、TraceMemo 在撤回发生时保持运行,并检查聊天加载是否明显变慢。开启前已经消失、应用未捕获或微信结构无法识别的消息不能保证恢复;详见[防撤回](./recall-protection.md)。
+16
View File
@@ -0,0 +1,16 @@
extends: ./electron-builder.yml
extraResources:
- from: build/app-update.yml
to: app-update.yml
- from: resources
to: resources
filter:
- '**/*'
- from: docs/skill/tracememo-reader
to: skill/tracememo-reader
filter:
- '**/*'
mac:
target:
- dmg
- zip
+69
View File
@@ -0,0 +1,69 @@
appId: com.tracememo.app
productName: TraceMemo
afterPack: scripts/after-pack.cjs
directories:
buildResources: build
files:
- out
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
extraMetadata:
main: out/main/index.js
asarUnpack:
- resources/**
- node_modules/ffmpeg-static/**
- node_modules/silk-wasm/**
- node_modules/sherpa-onnx-node/**
- node_modules/sherpa-onnx-*/**
- node_modules/@napi-rs/system-ocr/**
- node_modules/@napi-rs/system-ocr-*/**
extraResources:
# Keep the updater provider in every packaged Windows app. electron-builder also
# regenerates this file during publish, using the same release configuration.
- from: build/app-update.yml
to: app-update.yml
# Keep only resources required by the Windows x64 runtime.
- from: resources
to: resources
filter:
- icon.png
- daily_report_templates.html
- mobile_daily_report.html
- mobile_daily_report_v1.html
- mobile_daily_report_v2.html
- key/win32/x64/**
- runtime/win32/**
- wcdb/win32/x64/**
- from: docs/skill/tracememo-reader
to: skill/tracememo-reader
filter:
- '**/*'
win:
icon: icon.ico
executableName: TraceMemo
target:
- target: nsis
arch:
- x64
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
npmRebuild: false
electronLanguages:
- zh-CN
- en-US
publish:
provider: github
owner: Wxw-Gu
repo: TraceMemo
# 一律先上传为草稿 再到 GitHub 上手动 Publish。
# 需要预发布时用 `pnpm release:beta`(EP_PRE_RELEASE 会覆盖这里的 draft)。
releaseType: draft
+17 -11
View File
@@ -19,22 +19,26 @@ asarUnpack:
- node_modules/silk-wasm/**
- node_modules/sherpa-onnx-node/**
- node_modules/sherpa-onnx-*/**
# System OCR(@napi-rs/system-ocr)的 native binding 必须 unpacked,否则
# macOS 的系统 OCR 会在运行时 MODULE_NOT_FOUND。平台本机的 binding 由
# scripts/after-pack.cjs 校验,外架构的同级包在 afterPack 里被裁掉。
- node_modules/@napi-rs/system-ocr/**
- node_modules/@napi-rs/system-ocr-*/**
extraResources:
# Keep the updater provider in every packaged macOS app. electron-builder also
# regenerates this file during publish, using the same release configuration.
- from: build/app-update.yml
to: app-update.yml
# Includes the optional WeChat connector binary for the target platform.
- from: resources
to: resources
filter:
- '**/*'
- '!runtime/darwin-arm64/**'
- from: docs/skill/tracememo-reader
to: skill/tracememo-reader
filter:
- '**/*'
win:
icon: icon.ico
# WCDB's Windows runtime checks the host executable name. The dev runtime is
# electron.exe, so keep the packaged executable compatible while using
# TraceMemo as the product/shortcut name.
executableName: electron
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
@@ -44,11 +48,11 @@ nsis:
createDesktopShortcut: always
mac:
icon: icon.icns
target:
- dmg
- zip
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
# The bundled WCDB bridge still uses Electron as its internal executable
# compatibility name; the public product and bundle identity are TraceMemo.
CFBundleName: Electron
CFBundleDisplayName: TraceMemo
NSCameraUsageDescription: Application requests access to the device's camera.
NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@@ -71,5 +75,7 @@ npmRebuild: false
publish:
provider: github
owner: Wxw-Gu
repo: WechatExplorer
releaseType: release
repo: TraceMemo
# 一律先上传为草稿 再到 GitHub 上手动 Publish。
# 需要预发布时用 `pnpm release:beta`(EP_PRE_RELEASE 会覆盖这里的 draft)。
releaseType: draft
+3 -1
View File
@@ -8,13 +8,15 @@ export default defineConfig({
rollupOptions: {
input: {
index: resolve('src/main/index.ts'),
reportTemplateTest: resolve('src/main/report-template-test-entry.ts'),
queryAgentPoc: resolve('src/main/query-agent-poc-entry.ts'),
voiceRecognitionWorker: resolve('src/main/voice-pipeline/voice-recognition-worker.ts'),
knowledgeWorker: resolve('src/main/knowledge/knowledge-worker.ts')
},
output: {
entryFileNames: '[name].js'
},
external: ['koffi', 'sherpa-onnx-node']
external: ['koffi', 'sherpa-onnx-node', '@napi-rs/system-ocr']
}
}
},
+6
View File
@@ -24,6 +24,12 @@ export default defineConfig(
'react-refresh': eslintPluginReactRefresh
},
rules: {
'prettier/prettier': [
'error',
{
endOfLine: 'auto'
}
],
...eslintPluginReactHooks.configs.recommended.rules,
...eslintPluginReactRefresh.configs.vite.rules
}
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
# TraceMemo 日报模板示例
此目录是可安装的最小日报模板源文件,使用全部虚构数据进行预览。运行 `node scripts/build-report-template-example.cjs` 会生成 `examples/report-template-basic.zip`。
模板只能调整已有日报模块的 HTML/CSS 排版。作者不能加入 JavaScript、事件属性、外部网络资源、嵌套页面、数据库访问、AI Prompt 或新的业务分析。
占位符分为三类:普通文本(会被 HTML 转义)、应用生成的 HTML 片段(只能作为元素内容使用)、受限样式类(只能放进 `class` 属性,并由应用输出合法 token)。`*_MORE_NOTE` 是 HTML 片段,不是样式类。
允许的标签和属性由 `src/shared/report-template-package.ts` 统一定义;图片资源只能是包内 `assets/` 下的 PNG/JPEG/WebP。缺少可选模块时应用输出空字符串和 `*_EMPTY_CLASS`,模板应允许该模块隐藏。
@@ -0,0 +1,14 @@
{
"protocolVersion": "1.0",
"kind": "daily-report",
"id": "community.github.example.basic-feed",
"name": "基础信息流",
"author": { "name": "example" },
"templateVersion": "1.0.0",
"interfaceVersion": "1",
"entry": "template.html",
"preview": "preview.png",
"capture": { "width": 430, "maxWidth": 430, "maxHeight": 20000 },
"license": { "spdx": "MIT" },
"platform": "mobile"
}
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{{REPORT_TITLE}}</title>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; width: 430px; background: #f3f5f7; color: #18202a; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.report { padding: 18px 14px 32px; }
.hero, .section { margin-top: 10px; padding: 16px; border-radius: 14px; background: #fff; }
.hero { margin-top: 0; border-top: 4px solid #1769aa; }
h1 { margin: 0 0 8px; font-size: 23px; }
.meta, .muted { color: #64748b; font-size: 12px; line-height: 1.5; }
.section-title { margin: 0 0 8px; font-size: 14px; }
.topics-grid, .important-list { display: grid; gap: 8px; }
.topic-card, .important-card { padding: 10px; border: 1px solid #dbe4ee; border-radius: 10px; }
.empty-section { display: none !important; }
</style>
</head>
<body>
<main class="report {{REPORT_MODE_CLASS}}">
<section class="hero {{REPORT_MODE_CLASS}}">
<h1>{{REPORT_TITLE}}</h1>
<div class="meta">{{REPORT_DATE}} · {{DATE_RANGE}}</div>
<p>{{HERO_SUMMARY}}</p>
<div class="muted">{{HERO_STATUS_LINE}}</div>
</section>
<section class="section {{TOPICS_EMPTY_CLASS}}">
<h2 class="section-title">今日话题</h2>
<div class="topics-grid">{{TOPIC_CARDS}}</div>
{{TOPICS_MORE_NOTE}}
</section>
<section class="section {{MESSAGES_EMPTY_CLASS}}">
<h2 class="section-title">重要消息</h2>
<div class="important-list">{{IMPORTANT_MESSAGES}}</div>
</section>
<footer class="muted">{{FOOTER_NOTE}}</footer>
</main>
</body>
</html>
+1
View File
@@ -44,6 +44,7 @@ const api = {
startExport: (request) => electron.ipcRenderer.invoke("export:start", request),
cancelExport: (jobId) => electron.ipcRenderer.invoke("export:cancel", jobId),
revealExport: (path) => electron.ipcRenderer.invoke("export:reveal", path),
selectExportDirectory: () => electron.ipcRenderer.invoke("export:selectDirectory"),
onExportProgress: (callback) => {
const listener = (_event, progress) => callback(progress);
electron.ipcRenderer.on("export:progress", listener);
+62 -20
View File
@@ -1,8 +1,8 @@
{
"name": "tracememo",
"version": "2.2.0",
"version": "2.5.0",
"packageManager": "pnpm@7.33.7",
"description": "TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。 原名 WechatExplorer,支持聊天记录搜索、知识库、AI 总结和 Agent 助手。",
"description": "TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。 原名 WechatExplorer,支持聊天记录搜索、知识库、微信群聊总结和 Agent 助手。",
"keywords": [
"wechat",
"wechat chat",
@@ -11,6 +11,7 @@
"windows微信",
"微信聊天记录",
"微信聊天记录搜索",
"微信群聊总结",
"微信AI",
"微信机器人",
"AI聊天搜索",
@@ -24,19 +25,27 @@
},
"main": "./out/main/index.js",
"scripts": {
"test": "pnpm typecheck && pnpm test:unit && pnpm test:component && pnpm test:integration && pnpm test:skill-install && pnpm test:wechat-connector && pnpm test:e2e:build && playwright test",
"test": "pnpm typecheck && pnpm test:unit && pnpm test:component && pnpm test:integration && pnpm test:skill-install && pnpm test:e2e:build && playwright test",
"format": "prettier --write .",
"lint": "eslint --cache .",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"typecheck": "npm run typecheck:node && npm run typecheck:web && npm run typecheck:test",
"typecheck:test": "node scripts/typecheck-tests.cjs",
"test:skill-install": "node scripts/test-skill-install-instruction.cjs",
"cp:env": "node scripts/ensure-env.cjs",
"prepare:env": "node scripts/ensure-env.cjs",
"prepare:ffmpeg:win": "node scripts/prepare-electron-runtime.cjs --platform win32 --arch x64",
"prepare:ffmpeg:mac:arm64": "node scripts/prepare-electron-runtime.cjs --platform darwin --arch arm64",
"prepare:ffmpeg:mac:x64": "node scripts/prepare-electron-runtime.cjs --platform darwin --arch x64",
"prepare:win-runtime": "node scripts/prepare-win-runtime.cjs && npm run prepare:ffmpeg:win",
"prepare:wechat-native": "node scripts/prepare-wechat-native-runtime.cjs",
"start": "electron-vite preview",
"dev": "node scripts/ensure-env.cjs && node scripts/build-wechat-connector.cjs && electron-vite dev",
"test:wechat-connector": "go -C services/wechat-connector test ./... && go -C services/wechat-connector vet ./...",
"predev": "node scripts/ensure-electron-binary.cjs",
"dev": "node scripts/ensure-env.cjs && electron-vite dev",
"dev:update": "cross-env TRACEMEMO_UPDATE_SIMULATION=true pnpm dev",
"poc:query-agent": "electron-vite build && node scripts/run-query-agent-poc.cjs",
"poc:query-agent:run": "node scripts/run-query-agent-poc.cjs",
"test:unit": "vitest run --config vitest.unit.config.ts",
"test:component": "vitest run --config vitest.component.config.ts",
"test:integration": "vitest run --config vitest.integration.config.ts",
@@ -47,30 +56,50 @@
"test:e2e": "pnpm test:e2e:build && playwright test --grep-invert @visual",
"test:visual": "pnpm test:e2e:build && playwright test tests/e2e/visual.spec.ts",
"test:smoke": "node --test tests/smoke/native-environment.test.mjs",
"build:wechat-connector": "node scripts/build-wechat-connector.cjs",
"build:wechat-connector:win": "node scripts/build-wechat-connector.cjs --platform win32 --arch x64,arm64",
"build:wechat-connector:mac": "node scripts/build-wechat-connector.cjs --platform darwin --arch arm64",
"build:native-services": "npm run build:wechat-connector",
"build": "npm run typecheck && npm run build:native-services && electron-vite build",
"postinstall": "electron-builder install-app-deps && node scripts/prepare-electron-runtime.cjs",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps && node scripts/prepare-electron-runtime.cjs && node scripts/ensure-electron-binary.cjs",
"build:unpack": "npm run build && electron-builder --config electron-builder.yml --dir",
"build:win": "npm run typecheck && npm run build:wechat-connector:win && npm run prepare:ffmpeg:win && electron-vite build && electron-builder --config electron-builder.yml --win --x64",
"build:mac:arm64": "npm run typecheck && node scripts/build-wechat-connector.cjs --platform darwin --arch arm64 && electron-vite build && electron-builder --config electron-builder.yml --mac --arm64",
"build:win": "npm run typecheck && npm run prepare:win-runtime && electron-vite build && electron-builder --config electron-builder.win.yml --win --x64",
"build:mac:arm64": "npm run typecheck && npm run prepare:ffmpeg:mac:arm64 && electron-vite build && electron-builder --config electron-builder.yml --mac --arm64",
"build:mac:arm64:send-runtime": "npm run typecheck && npm run prepare:ffmpeg:mac:arm64 && electron-vite build && cross-env TM_SEND_RUNTIME_BUILD=1 electron-builder --config electron-builder.send-runtime.yml --mac --arm64",
"build:mac:x64": "npm run typecheck && npm run prepare:ffmpeg:mac:x64 && electron-vite build && electron-builder --config electron-builder.yml --mac --x64",
"release": "npm run release:mac && npm run release:win",
"release:mac": "npm run typecheck && npm run build:wechat-connector:mac && electron-vite build && electron-builder --config electron-builder.yml --mac --arm64 --publish always",
"release:win": "npm run typecheck && npm run build:wechat-connector:win && npm run prepare:ffmpeg:win && electron-vite build && electron-builder --config electron-builder.yml --win --x64 --publish always",
"release:beta": "cross-env RELEASE_TYPE=prerelease npm run release",
"release:stable": "cross-env RELEASE_TYPE=release npm run release",
"release:mac": "npm run typecheck && electron-vite build && npm run release:mac:arm64 && npm run release:mac:x64",
"release:mac:arm64": "npm run prepare:ffmpeg:mac:arm64 && electron-builder --config electron-builder.yml --mac --arm64 --publish always",
"release:mac:x64": "npm run prepare:ffmpeg:mac:x64 && electron-builder --config electron-builder.yml --mac --x64 --publish always",
"release:win": "npm run typecheck && npm run prepare:win-runtime && electron-vite build && electron-builder --config electron-builder.win.yml --win --x64 --publish always",
"release:beta": "cross-env EP_PRE_RELEASE=true npm run release",
"release:stable": "npm run release",
"build:linux": "electron-vite build && electron-builder --config electron-builder.yml --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@koromix/koffi-darwin-x64": "3.1.0",
"@koromix/koffi-win32-x64": "3.1.0",
"@napi-rs/system-ocr": "1.2.0",
"@napi-rs/system-ocr-darwin-x64": "1.2.0",
"@napi-rs/system-ocr-win32-x64-msvc": "1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.23",
"@radix-ui/react-checkbox": "^1.3.11",
"@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-dropdown-menu": "^2.1.24",
"@radix-ui/react-popover": "^1.1.23",
"@radix-ui/react-progress": "^1.1.16",
"@radix-ui/react-radio-group": "^1.4.7",
"@radix-ui/react-scroll-area": "^1.2.18",
"@radix-ui/react-select": "^2.3.7",
"@radix-ui/react-separator": "^1.1.15",
"@radix-ui/react-switch": "^1.3.7",
"@radix-ui/react-tabs": "^1.1.21",
"@radix-ui/react-toast": "^1.2.23",
"@radix-ui/react-tooltip": "^1.2.16",
"@tanstack/react-virtual": "^3.14.6",
"archiver": "^8.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cross-env": "^10.1.0",
"css-tree": "^3.0.1",
"electron-updater": "^6.6.2",
"ffmpeg-static": "5.3.0",
"fs-extra": "^11.3.2",
@@ -78,8 +107,15 @@
"jsonrepair": "^3.15.0",
"koffi": "^3.1.0",
"openai": "^6.10.0",
"parse5": "^8.0.0",
"pinyin-pro": "^3.26.0",
"qrcode": "^1.5.4",
"sherpa-onnx-darwin-x64": "1.13.3",
"sherpa-onnx-node": "1.13.3",
"sherpa-onnx-win-x64": "1.13.4",
"silk-wasm": "^3.7.1",
"tailwind-merge": "^3.6.0",
"unzipper": "^0.12.0",
"wechat-emojis": "^1.0.2"
},
"devDependencies": {
@@ -95,10 +131,12 @@
"@types/archiver": "^8.0.0",
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.19.1",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.1.10",
"autoprefixer": "^10.5.4",
"electron": "^43.0.0",
"electron-builder": "^26.0.12",
"electron-vite": "^5.0.0",
@@ -107,13 +145,17 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"jsdom": "^30.0.1",
"postcss": "^8.5.26",
"prettier": "^3.7.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"sass": "^1.102.0",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.3",
"vite": "^7.2.6",
"vitest": "^4.1.10"
"vitest": "^4.1.10",
"wrangler": "^4.28.1"
},
"pnpm": {
"supportedArchitectures": {
@@ -132,4 +174,4 @@
"ffmpeg-static"
]
}
}
}
+1964 -28
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 155 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
-32
View File
@@ -464,32 +464,6 @@
font-style: normal;
color: #98a2b3;
}
.gallery-card {
display: grid;
grid-template-columns: 112px 1fr;
gap: 12px;
margin-top: 10px;
padding: 12px;
background: #f7faf9;
border-radius: 14px;
}
.gallery-image {
width: 112px;
height: 112px;
border-radius: 12px;
object-fit: cover;
background: #e5e7eb;
}
.gallery-stats {
display: inline-flex;
margin-top: 7px;
padding: 4px 8px;
border-radius: 999px;
background: #eef5ff;
color: #1677ff;
font-size: 11px;
font-weight: 700;
}
.badge-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -644,7 +618,6 @@
padding: 11px;
}
.compact .important-card,
.compact .gallery-card,
.compact .chat-block {
padding: 10px;
}
@@ -801,11 +774,6 @@
{{VISION_CARDS}}
</section>
<section class="section {{GALLERY_EMPTY_CLASS}}">
<div class="section-title">今日群相册</div>
{{GALLERY_CARDS}}
{{GALLERY_MORE_NOTE}}
</section>
<section class="section {{VOICE_EMPTY_CLASS}}">
<div class="section-title">语音之最</div>
+17 -9
View File
@@ -68,21 +68,29 @@
.overview {
margin-top: 2px;
}
/*
* hero 头像簇的几何必须由 contract 变量驱动,不能再写死容器尺寸。
*
* 生产导出会额外注入 `report-template-fragment-contract.ts`,其中
* `img.tm-avatar.tm-avatar--hero` 用 !important 把头像钉在
* clamp(28px, var(--tm-avatar-hero-size, 40px), 56px)。
* 旧版这里写死 58x58(单头像 28x28),两个权威打架:头像实际 40px,
* 2 列 x 40px + 3px gap = 83px 塞不进 58px 的盒子,于是头像向右向下溢出容器,
* 视觉上越过卡片内边距、压到卡片边缘之外。
*
* 现在容器尺寸由内容决定(列宽/行高都取同一个变量):头像数 1..4 都不会溢出,
* 主题调整 --tm-avatar-hero-size 时容器与头像也不会分叉。
*/
.avatar-grid {
width: 58px;
height: 58px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(2, var(--tm-avatar-hero-size, 40px));
grid-auto-rows: var(--tm-avatar-hero-size, 40px);
gap: 3px;
flex: 0 0 auto;
}
/* 单头像时不保留空列,簇宽恰好等于一个头像。 */
.avatar-grid.avatar-count-1 {
width: 28px;
height: 28px;
grid-template-columns: 1fr;
}
.avatar-grid.avatar-count-2 {
height: 28px;
grid-template-columns: var(--tm-avatar-hero-size, 40px);
}
.avatar-grid.empty-section {
display: none;
+17 -41
View File
@@ -61,21 +61,29 @@
font-size: 13px;
line-height: 1.55;
}
/*
* hero 头像簇的几何必须由 contract 变量驱动,不能再写死容器尺寸。
*
* 生产导出会额外注入 `report-template-fragment-contract.ts`,其中
* `img.tm-avatar.tm-avatar--hero` 用 !important 把头像钉在
* clamp(28px, var(--tm-avatar-hero-size, 40px), 56px)。
* 旧版这里写死 58x58(单头像 28x28),两个权威打架:头像实际 40px,
* 2 列 x 40px + 3px gap = 83px 塞不进 58px 的盒子,于是头像向右向下溢出容器,
* 视觉上越过卡片内边距、压到卡片边缘之外。
*
* 现在容器尺寸由内容决定(列宽/行高都取同一个变量):头像数 1..4 都不会溢出,
* 主题调整 --tm-avatar-hero-size 时容器与头像也不会分叉。
*/
.avatar-grid {
width: 58px;
height: 58px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(2, var(--tm-avatar-hero-size, 40px));
grid-auto-rows: var(--tm-avatar-hero-size, 40px);
gap: 3px;
flex: 0 0 auto;
}
/* 单头像时不保留空列,簇宽恰好等于一个头像。 */
.avatar-grid.avatar-count-1 {
width: 28px;
height: 28px;
grid-template-columns: 1fr;
}
.avatar-grid.avatar-count-2 {
height: 28px;
grid-template-columns: var(--tm-avatar-hero-size, 40px);
}
.avatar-grid.empty-section {
display: none;
@@ -465,32 +473,6 @@
font-style: normal;
color: #98a2b3;
}
.gallery-card {
display: grid;
grid-template-columns: 112px 1fr;
gap: 12px;
margin-top: 10px;
padding: 12px;
background: #f7faf9;
border-radius: 14px;
}
.gallery-image {
width: 112px;
height: 112px;
border-radius: 12px;
object-fit: cover;
background: #e5e7eb;
}
.gallery-stats {
display: inline-flex;
margin-top: 7px;
padding: 4px 8px;
border-radius: 999px;
background: #eef5ff;
color: #1677ff;
font-size: 11px;
font-weight: 700;
}
.badge-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -645,7 +627,6 @@
padding: 11px;
}
.compact .important-card,
.compact .gallery-card,
.compact .chat-block {
padding: 10px;
}
@@ -801,11 +782,6 @@
{{VISION_CARDS}}
</section>
<section class="section {{GALLERY_EMPTY_CLASS}}">
<div class="section-title">今日群相册</div>
{{GALLERY_CARDS}}
{{GALLERY_MORE_NOTE}}
</section>
<section class="section {{VOICE_EMPTY_CLASS}}">
<div class="section-title">语音之最</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<rect
x="1.5"
y="1.5"
width="15"
height="15"
rx="3.5"
fill="none"
stroke="#000"
stroke-width="1.75"
/>
<circle cx="9" cy="9" r="2.3" fill="#000" />
</svg>

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
a35d3fa67387e049cc30bc073f9e65b077aa1db9c5c2b183125dec695fa013b9 xkey_helper_4_1_13
+438 -69
View File
@@ -1,11 +1,10 @@
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */
const { chmodSync, existsSync, renameSync } = require('node:fs')
const { chmodSync, existsSync, readdirSync, rmSync } = require('node:fs')
const { execFileSync } = require('node:child_process')
const path = require('node:path')
const asar = require('@electron/asar')
const { readBinaryArchitectures } = require('./binary-arch.cjs')
const COMPATIBILITY_NAME = 'Electron'
const HELPER_SUFFIXES = ['', ' (Plugin)', ' (Renderer)', ' (GPU)']
const REQUIRED_RUNTIME_PACKAGES = [
'@electron-toolkit/preload',
'@electron-toolkit/utils',
@@ -17,6 +16,15 @@ const REQUIRED_RUNTIME_PACKAGES = [
'koffi'
]
// electron-builder 26 skips macOS signing entirely when no Developer ID
// identity is configured, so an unpacked bundle can ship without a usable
// signature. macOS kills a helper whose code or signature is missing or
// modified even when SIP is disabled, which is what customers hit on newer
// macOS releases. Ad-hoc re-sign the runtime helpers and the outer bundle so
// every Mach-O verifies strictly; spctl still rejects ad-hoc code, which is
// acceptable for the SIP-disabled customer workflow.
const MACOS_HELPER_NAMES = ['xkey_helper', 'xkey_helper_4_1_13']
function getRuntimeResources(context) {
const productName = context.packager.appInfo.productFilename
return context.electronPlatformName === 'darwin'
@@ -79,11 +87,273 @@ function validateSherpaRuntime(runtimeResources, platform, arch) {
}
}
/**
* System OCR 用 native package(@napi-rs/system-ocr)。它是 external + asarUnpack,
* 打包后必须以 unpacked 形式存在,否则运行时会 MODULE_NOT_FOUND / native binding missing。
* Windows 与 macOS 都是 supported target,都要做硬校验(Linux 不是)。
*/
function systemOcrTarget(platform, arch) {
return platform === 'win32' ? `${platform}-${arch}-msvc` : `${platform}-${arch}`
}
function validateSystemOcrRuntime(runtimeResources, platform, arch) {
if (platform !== 'win32' && platform !== 'darwin') return
const target = systemOcrTarget(platform, arch)
const basePath = path.join(
runtimeResources,
'app.asar.unpacked',
'node_modules',
'@napi-rs',
'system-ocr'
)
const nativePath = path.join(
runtimeResources,
'app.asar.unpacked',
'node_modules',
'@napi-rs',
`system-ocr-${target}`
)
const requiredFiles = [
path.join(basePath, 'package.json'),
path.join(basePath, 'index.js'),
path.join(nativePath, 'package.json'),
path.join(nativePath, `system-ocr.${target}.node`)
]
const missingFiles = requiredFiles.filter((filePath) => !existsSync(filePath))
if (missingFiles.length > 0) {
throw new Error(`Missing unpacked System OCR runtime: ${missingFiles.join(', ')}`)
}
}
/**
* koffi 运行期按 `${process.platform}-${process.arch}` 拼出原生包目录名
* (node_modules/koffi/src/koffi/index.cjs:153/175),找不到就直接抛
* "Cannot find the native Koffi module; did you bundle it correctly?"。
* pnpm 7 不支持 supportedArchitectures,会静默跳过外平台可选依赖,所以每个目标平台的
* koffi 原生包都必须在 package.json 里显式声明;这里再兜一层,缺了就让构建失败,
* 而不是发出一个装得上、却打不开 WCDB 的包。
*/
function koffiNativeTarget(platform, arch) {
if (platform === 'win32') {
return arch === 'x64'
? { label: 'Windows', segments: ['@koromix', 'koffi-win32-x64', 'win32_x64', 'koffi.node'] }
: null
}
if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) {
return {
label: 'macOS',
segments: ['@koromix', `koffi-darwin-${arch}`, `darwin_${arch}`, 'koffi.node']
}
}
return null
}
function validateKoffiRuntime(runtimeResources, platform, arch) {
const target = koffiNativeTarget(platform, arch)
if (!target) return
const nativePath = path.join(
runtimeResources,
'app.asar.unpacked',
'node_modules',
...target.segments
)
if (!existsSync(nativePath)) {
throw new Error(`Missing ${target.label} Koffi native module: ${nativePath}`)
}
}
function normalizeBuilderArch(arch) {
if (typeof arch === 'string') return arch
return { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }[arch] || String(arch)
}
function runCodesign(args) {
try {
execFileSync('/usr/bin/codesign', args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
})
} catch (error) {
const stderr =
error && typeof error === 'object' && 'stderr' in error ? String(error.stderr) : ''
if (stderr.trim() && error instanceof Error) {
error.message += `\n${stderr.trim()}`
}
throw error
}
}
function isMacosCodeValid(targetPath, run = runCodesign) {
try {
run(['--verify', '--strict', targetPath])
return true
} catch {
return false
}
}
function findMacosHelperPaths(runtimeResources) {
return MACOS_HELPER_NAMES.map((name) => path.join(runtimeResources, 'resources', name)).filter(
(helperPath) => existsSync(helperPath)
)
}
function signMacosHelpers(runtimeResources, run = runCodesign) {
const helperPaths = findMacosHelperPaths(runtimeResources)
for (const helperPath of helperPaths) {
chmodSync(helperPath, 0o755)
if (!isMacosCodeValid(helperPath, run)) {
run(['--force', '--sign', '-', helperPath])
}
for (const arch of ['arm64', 'x86_64']) {
try {
run(['--verify', '--strict', '--arch', arch, helperPath])
} catch (error) {
throw new Error(
'macOS helper signature verification failed: ' +
path.basename(helperPath) +
' (' +
arch +
')',
{ cause: error }
)
}
}
}
return helperPaths
}
/**
* codesign 只把这些位置当作「嵌套代码」并要求它们先各自签好,才肯签外层 app。
* 只遍历这一组根目录,而不是整个 bundle:Contents/Resources 下的
* app.asar.unpacked 里成千上万个原生文件不属于嵌套代码,逐个签既慢又无意义。
*/
const MACOS_CODE_LOCATIONS = [
'Frameworks',
'MacOS',
'PlugIns',
'XPCServices',
'Helpers',
'Library/LoginItems'
]
function collectNestedMacosCode(dir, depth, targets) {
let entries
try {
entries = readdirSync(dir, { withFileTypes: true })
} catch {
return
}
for (const entry of entries) {
const entryPath = path.join(dir, entry.name)
// framework 里的 Mantle -> Versions/Current/Mantle 这类符号链接指向真实文件,
// 真实文件会在更深的层级被走到;这里跳过以免重复签名。
if (entry.isSymbolicLink()) continue
if (entry.isDirectory()) {
if (/\.(app|framework|xpc)$/.test(entry.name)) {
targets.push({ path: entryPath, depth, bundle: true })
}
collectNestedMacosCode(entryPath, depth + 1, targets)
continue
}
if (!entry.isFile()) continue
if (readBinaryArchitectures(entryPath).length === 0) continue
targets.push({ path: entryPath, depth, bundle: false })
}
}
/**
* 返回嵌套代码的签名顺序:深度大的先签(framework 内部的 dylib、无扩展名的
* crashpad handler 先于 framework 本身,helper 的可执行文件先于 helper app),
* 同深度时文件先于 bundle。
*/
function findNestedMacosCodePaths(appBundlePath) {
const targets = []
for (const location of MACOS_CODE_LOCATIONS) {
const root = path.join(appBundlePath, 'Contents', ...location.split('/'))
if (existsSync(root)) collectNestedMacosCode(root, 1, targets)
}
return targets
.map((target, index) => ({ ...target, index }))
.sort((a, b) => {
if (a.depth !== b.depth) return b.depth - a.depth
if (a.bundle !== b.bundle) return a.bundle ? 1 : -1
return a.index - b.index
})
.map((target) => target.path)
}
/**
* Electron 43.1.0 的 darwin-x64 官方 zip(sha256 与上游 SHASUMS256.txt 一致)
* 里所有嵌套 Mach-O 都是未签名状态,darwin-arm64 那份则是 linker-signed。
* codesign 签外层 bundle 时要求子组件已签,否则直接报
* "code object is not signed at all" + "In subcomponent: ...",
* 所以 x64 出包时只签外层必然失败,必须先由内向外补签一遍。
*
* 这里不采用 `--deep`(Apple 已标记 deprecated):它会把外层的签名选项套用到
* 所有子组件上,将来接上 Developer ID + entitlements 时会把 app 的 entitlements
* 一并套到 helper 上,属于已知的坑。
*/
function signMacosAppBundle(appBundlePath, run = runCodesign) {
if (isMacosCodeValid(appBundlePath, run)) return appBundlePath
for (const nestedPath of findNestedMacosCodePaths(appBundlePath)) {
try {
run(['--force', '--sign', '-', nestedPath])
} catch (error) {
throw new Error(
'macOS nested code signing failed: ' + path.relative(appBundlePath, nestedPath),
{ cause: error }
)
}
}
run(['--force', '--sign', '-', appBundlePath])
try {
run(['--verify', '--strict', appBundlePath])
} catch (error) {
throw new Error('macOS app bundle signature verification failed: ' + appBundlePath, {
cause: error
})
}
return appBundlePath
}
/**
* A foreign-architecture binary only fails once the user touches the feature
* that needs it, so verify the ones whose filename is shared across
* architectures (ffmpeg-static keeps a single "ffmpeg" per platform) and fail
* the build instead of shipping a broken bundle.
*/
function validateRuntimeBinaryArchitecture(filePath, platform, arch, label) {
if (platform !== 'darwin' && platform !== 'win32') return
if (arch === 'universal') return
const architectures = readBinaryArchitectures(filePath)
if (!architectures.length || architectures.includes(arch)) return
throw new Error(
`${label} is ${architectures.join('/')} but this bundle targets ${arch}: ${filePath}`
)
}
/**
* The Intel Mac key helper is an x86_64 executable that only the x64 (or
* universal) macOS bundle can run. Every other target — Apple Silicon macOS,
* Windows, Linux — would otherwise ship a ~34MB binary it can never execute,
* so it is dropped from those bundles. x64/universal builds fail fast instead
* of silently shipping an Intel Mac app that cannot read keys.
*/
function pruneIntelMacKeyTool(runtimeResources, platform, arch) {
const keyToolDirectory = path.join(runtimeResources, 'resources', 'macos-key-tool')
const usable = platform === 'darwin' && (arch === 'x64' || arch === 'universal')
if (!usable) {
rmSync(keyToolDirectory, { recursive: true, force: true })
return null
}
const helperPath = path.join(keyToolDirectory, 'intel_mac_key_helper')
if (!existsSync(helperPath)) {
throw new Error(`Missing Intel Mac key helper in a ${arch} bundle: ${helperPath}`)
}
return helperPath
}
function validateAsarRuntimeDependencies(runtimeResources) {
const asarPath = path.join(runtimeResources, 'app.asar')
if (!existsSync(asarPath)) throw new Error(`Missing packaged application archive: ${asarPath}`)
@@ -101,10 +371,6 @@ function validateAsarRuntimeDependencies(runtimeResources) {
)
}
}
function setPlistValue(plistPath, key, value) {
execFileSync('/usr/libexec/PlistBuddy', ['-c', `Set :${key} ${value}`, plistPath])
}
function validateReaderSkillRuntime(runtimeResources) {
const skillPath = path.join(runtimeResources, 'skill', 'tracememo-reader', 'SKILL.md')
if (!existsSync(skillPath)) {
@@ -113,81 +379,170 @@ function validateReaderSkillRuntime(runtimeResources) {
return skillPath
}
/**
* Native runtime packages are published once per platform-arch pair, and pnpm
* installs all of them, so every bundle ends up carrying the native libraries
* of every platform (measured: ~129MB of speech models plus ~16MB of koffi).
* The loaders pick their package from process.platform/arch, so the siblings
* are dead weight — drop them.
*/
// 每个条目返回 platform package 的**完整后缀**(不含 package 前缀与连字符)。
const NATIVE_RUNTIME_PACKAGES = [
{
modules: [],
prefix: 'sherpa-onnx',
platformName: (platform, arch) => `${platform === 'win32' ? 'win' : platform}-${arch}`
},
{
modules: ['@koromix'],
prefix: 'koffi',
platformName: (platform, arch) => `${platform}-${arch}`
},
{
// @napi-rs 的 platform package 目录名带 -msvc 后缀(win32-x64-msvc)。
modules: ['@napi-rs'],
prefix: 'system-ocr',
platformName: (platform, arch) => systemOcrTarget(platform, arch),
foreignPattern: /^system-ocr-[a-z0-9]+-(arm64|x64|ia32|loong64|riscv64)(-msvc)?$/
}
]
function pruneForeignArchNativeRuntimes(runtimeResources, platform, arch) {
if (arch === 'universal') return []
const unpackedRoot = path.join(runtimeResources, 'app.asar.unpacked', 'node_modules')
if (!existsSync(unpackedRoot)) return []
const removed = []
for (const runtime of NATIVE_RUNTIME_PACKAGES) {
const modulesRoot = path.join(unpackedRoot, ...runtime.modules)
if (!existsSync(modulesRoot)) continue
const expected = `${runtime.prefix}-${runtime.platformName(platform, arch)}`
const foreign =
runtime.foreignPattern ||
new RegExp(`^${runtime.prefix}-[a-z0-9]+-(arm64|x64|ia32|loong64|riscv64)$`)
for (const entry of readdirSync(modulesRoot, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name === expected || !foreign.test(entry.name)) continue
rmSync(path.join(modulesRoot, entry.name), { recursive: true, force: true })
removed.push(
runtime.modules.length ? `${runtime.modules.join('/')}/${entry.name}` : entry.name
)
}
}
return removed
}
/**
* Bundled native directories under resources/connectors are named
* "<platform>-<arch>". Cross-building both macOS architectures leaves both on
* disk, but a bundle can only execute its own, so drop the foreign ones
* instead of shipping every connector twice.
*/
function pruneForeignArchConnectors(runtimeResources, platform, arch) {
if (arch === 'universal') return []
const connectorsRoot = path.join(runtimeResources, 'resources', 'connectors')
if (!existsSync(connectorsRoot)) return []
const expected = `${platform}-${arch}`
const removed = []
for (const packageEntry of readdirSync(connectorsRoot, { withFileTypes: true })) {
if (!packageEntry.isDirectory()) continue
const packageRoot = path.join(connectorsRoot, packageEntry.name)
for (const targetEntry of readdirSync(packageRoot, { withFileTypes: true })) {
if (!targetEntry.isDirectory() || targetEntry.name === expected) continue
if (!/^[a-z0-9]+-(arm64|x64|ia32)$/.test(targetEntry.name)) continue
rmSync(path.join(packageRoot, targetEntry.name), { recursive: true, force: true })
removed.push(`${packageEntry.name}/${targetEntry.name}`)
}
}
return removed
}
/**
* 微信发送运行时打包边界
*/
const SEND_RUNTIME_RELATIVE = ['resources', 'runtime', 'darwin-arm64']
const SEND_RUNTIME_ENTRY = 'tm-wechat-host'
function sendRuntimeLocations(runtimeResources) {
return [
path.join(runtimeResources, ...SEND_RUNTIME_RELATIVE),
path.join(runtimeResources, 'app.asar.unpacked', ...SEND_RUNTIME_RELATIVE)
]
}
function findSendRuntime(runtimeResources) {
return (
sendRuntimeLocations(runtimeResources).find((directory) =>
existsSync(path.join(directory, SEND_RUNTIME_ENTRY))
) || null
)
}
function isSendRuntimeBuild() {
return process.env.TM_SEND_RUNTIME_BUILD === '1'
}
function enforceSendRuntimeBoundary(
runtimeResources,
platform,
bundlesSendRuntime = isSendRuntimeBuild()
) {
if (platform !== 'darwin') return null
const found = findSendRuntime(runtimeResources)
if (bundlesSendRuntime) {
if (!found) {
throw new Error(
'This macOS build requires the WeChat send runtime but resources/runtime/darwin-arm64 is missing. ' +
'Run `pnpm prepare:wechat-native` first, or point TM_NATIVE_RUNTIME_DIR at the artifact.'
)
}
return found
}
if (found) {
throw new Error(
'macOS bundle must not include the WeChat send runtime: ' +
found +
'. Build with `pnpm build:mac:arm64:send-runtime` (TM_SEND_RUNTIME_BUILD=1) when it is required, ' +
'or fix the resources filter in electron-builder.yml.'
)
}
return null
}
exports.default = async function afterPack(context) {
const runtimeResources = getRuntimeResources(context)
const arch = normalizeBuilderArch(context.arch)
// 边界先判,越早失败越好。
const sendRuntime = enforceSendRuntimeBoundary(runtimeResources, context.electronPlatformName)
if (context.electronPlatformName === 'darwin') {
console.log(
sendRuntime
? `[afterPack] send runtime bundled at ${sendRuntime}`
: '[afterPack] send runtime excluded'
)
}
validateAsarRuntimeDependencies(runtimeResources)
validateReaderSkillRuntime(runtimeResources)
validateSilkWasmRuntime(runtimeResources)
const ffmpegPath = validateFfmpegRuntime(runtimeResources, context.electronPlatformName)
validateSherpaRuntime(
runtimeResources,
validateRuntimeBinaryArchitecture(
ffmpegPath,
context.electronPlatformName,
normalizeBuilderArch(context.arch)
arch,
'Bundled ffmpeg'
)
validateSherpaRuntime(runtimeResources, context.electronPlatformName, arch)
validateSystemOcrRuntime(runtimeResources, context.electronPlatformName, arch)
validateKoffiRuntime(runtimeResources, context.electronPlatformName, arch)
pruneIntelMacKeyTool(runtimeResources, context.electronPlatformName, arch)
pruneForeignArchConnectors(runtimeResources, context.electronPlatformName, arch)
pruneForeignArchNativeRuntimes(runtimeResources, context.electronPlatformName, arch)
if (context.electronPlatformName === 'darwin') {
execFileSync('/usr/bin/codesign', ['--force', '--sign', '-', ffmpegPath], {
stdio: 'ignore'
})
}
if (context.electronPlatformName === 'win32') {
const koffiNative = path.join(
context.appOutDir,
'resources',
'app.asar.unpacked',
'node_modules',
'@koromix',
'koffi-win32-x64',
'win32_x64',
'koffi.node'
)
if (!existsSync(koffiNative)) {
throw new Error(`Missing Windows Koffi native module: ${koffiNative}`)
}
return
}
if (context.electronPlatformName !== 'darwin') return
const productName = context.packager.appInfo.productFilename
const appPath = path.join(context.appOutDir, `${productName}.app`)
const contentsPath = path.join(appPath, 'Contents')
const sourceExecutable = path.join(contentsPath, 'MacOS', productName)
const targetExecutable = path.join(contentsPath, 'MacOS', COMPATIBILITY_NAME)
if (existsSync(sourceExecutable)) renameSync(sourceExecutable, targetExecutable)
if (!existsSync(targetExecutable)) {
throw new Error(`Missing Electron main executable: ${sourceExecutable}`)
}
const appPlistPath = path.join(contentsPath, 'Info.plist')
setPlistValue(appPlistPath, 'CFBundleExecutable', COMPATIBILITY_NAME)
setPlistValue(appPlistPath, 'CFBundleName', COMPATIBILITY_NAME)
const frameworksPath = path.join(contentsPath, 'Frameworks')
for (const suffix of HELPER_SUFFIXES) {
const sourceName = `${productName} Helper${suffix}`
const targetName = `${COMPATIBILITY_NAME} Helper${suffix}`
const sourceBundle = path.join(frameworksPath, `${sourceName}.app`)
const targetBundle = path.join(frameworksPath, `${targetName}.app`)
if (existsSync(sourceBundle)) renameSync(sourceBundle, targetBundle)
if (!existsSync(targetBundle)) {
throw new Error(`Missing Electron helper bundle: ${sourceBundle}`)
}
const sourceExecutable = path.join(targetBundle, 'Contents', 'MacOS', sourceName)
const targetExecutable = path.join(targetBundle, 'Contents', 'MacOS', targetName)
if (existsSync(sourceExecutable)) renameSync(sourceExecutable, targetExecutable)
if (!existsSync(targetExecutable)) {
throw new Error(`Missing Electron helper executable: ${sourceExecutable}`)
}
const plistPath = path.join(targetBundle, 'Contents', 'Info.plist')
setPlistValue(plistPath, 'CFBundleExecutable', targetName)
setPlistValue(plistPath, 'CFBundleName', targetName)
signMacosHelpers(runtimeResources)
const productName = context.packager.appInfo.productFilename
signMacosAppBundle(path.join(context.appOutDir, productName + '.app'))
}
}
@@ -197,3 +552,17 @@ exports.validateReaderSkillRuntime = validateReaderSkillRuntime
exports.validateFfmpegRuntime = validateFfmpegRuntime
exports.validateSilkWasmRuntime = validateSilkWasmRuntime
exports.validateSherpaRuntime = validateSherpaRuntime
exports.validateSystemOcrRuntime = validateSystemOcrRuntime
exports.validateKoffiRuntime = validateKoffiRuntime
exports.pruneIntelMacKeyTool = pruneIntelMacKeyTool
exports.pruneForeignArchConnectors = pruneForeignArchConnectors
exports.pruneForeignArchNativeRuntimes = pruneForeignArchNativeRuntimes
exports.validateRuntimeBinaryArchitecture = validateRuntimeBinaryArchitecture
exports.findMacosHelperPaths = findMacosHelperPaths
exports.isMacosCodeValid = isMacosCodeValid
exports.signMacosHelpers = signMacosHelpers
exports.signMacosAppBundle = signMacosAppBundle
exports.findNestedMacosCodePaths = findNestedMacosCodePaths
exports.sendRuntimeLocations = sendRuntimeLocations
exports.findSendRuntime = findSendRuntime
exports.enforceSendRuntimeBoundary = enforceSendRuntimeBoundary
+83
View File
@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('node:fs')
const MACHO_MAGIC_32 = 0xfeedface
const MACHO_MAGIC_64 = 0xfeedfacf
const FAT_MAGIC = 0xcafebabe
const FAT_MAGIC_64 = 0xcafebabf
const PE_SIGNATURE = 0x00004550
const PE_MACHINE_X64 = 0x8664
const PE_MACHINE_ARM64 = 0xaa64
const CPU_TYPE_IA32 = 0x00000007
const CPU_TYPE_X86_64 = 0x01000007
const CPU_TYPE_ARM64 = 0x0100000c
/** Only the headers are needed; native binaries can be tens of megabytes. */
const HEADER_BYTES = 64 * 1024
function readHeader(filePath) {
const descriptor = fs.openSync(filePath, 'r')
try {
const buffer = Buffer.alloc(HEADER_BYTES)
const bytesRead = fs.readSync(descriptor, buffer, 0, HEADER_BYTES, 0)
return buffer.subarray(0, bytesRead)
} finally {
fs.closeSync(descriptor)
}
}
function normalizeMachoCpuType(cpuType) {
if (cpuType === CPU_TYPE_X86_64) return 'x64'
if (cpuType === CPU_TYPE_ARM64) return 'arm64'
if (cpuType === CPU_TYPE_IA32) return 'ia32'
return ''
}
/**
* Returns every architecture contained in a Mach-O or PE binary, as
* electron-builder arch names ("x64", "arm64"). Universal binaries report both.
* Returns an empty array for anything that is not a native executable (scripts,
* wasm), so callers can treat "unknown" separately from "wrong architecture".
*/
function readBinaryArchitectures(filePath) {
let buffer
try {
buffer = readHeader(filePath)
} catch {
return []
}
if (buffer.length < 8) return []
const fatMagic = buffer.readUInt32BE(0)
if (fatMagic === FAT_MAGIC || fatMagic === FAT_MAGIC_64) {
const entrySize = fatMagic === FAT_MAGIC_64 ? 32 : 20
const count = Math.min(buffer.readUInt32BE(4), 32)
const architectures = []
for (let index = 0; index < count; index += 1) {
const entryOffset = 8 + index * entrySize
if (entryOffset + 4 > buffer.length) break
const name = normalizeMachoCpuType(buffer.readUInt32BE(entryOffset))
if (name && !architectures.includes(name)) architectures.push(name)
}
return architectures
}
const thinMagic = buffer.readUInt32LE(0)
if (thinMagic === MACHO_MAGIC_32 || thinMagic === MACHO_MAGIC_64) {
const name = normalizeMachoCpuType(buffer.readUInt32LE(4))
return name ? [name] : []
}
if (buffer.readUInt16LE(0) === 0x5a4d) {
const peOffset = buffer.readUInt32LE(0x3c)
if (peOffset + 6 > buffer.length || buffer.readUInt32LE(peOffset) !== PE_SIGNATURE) return []
const machine = buffer.readUInt16LE(peOffset + 4)
if (machine === PE_MACHINE_X64) return ['x64']
if (machine === PE_MACHINE_ARM64) return ['arm64']
}
return []
}
module.exports = { readBinaryArchitectures }
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env node
const fs = require('node:fs')
const path = require('node:path')
const { ZipArchive } = require('archiver')
const root = path.resolve(__dirname, '..')
const source = path.join(root, 'examples', 'report-template-basic')
const outputPath = path.join(root, 'examples', 'report-template-basic.zip')
const output = fs.createWriteStream(outputPath)
const archive = new ZipArchive({ zlib: { level: 9 } })
output.on('close', () => console.log(`wrote ${outputPath} (${archive.pointer()} bytes)`))
archive.on('error', (error) => { throw error })
archive.pipe(output)
archive.file(path.join(source, 'manifest.json'), { name: 'manifest.json' })
archive.file(path.join(source, 'template.html'), { name: 'template.html' })
// 使用 1x1 PNG 作为虚构预览占位图,避免引入真实用户媒体。
archive.append(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64'), { name: 'preview.png' })
archive.finalize()
-65
View File
@@ -1,65 +0,0 @@
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */
const { execFileSync } = require('node:child_process')
const fs = require('node:fs')
const path = require('node:path')
const projectRoot = path.resolve(__dirname, '..')
const sourceDir = path.join(projectRoot, 'services', 'wechat-connector')
const outputRoot = path.join(projectRoot, 'resources', 'connectors', 'wechat')
function normalizePlatform(value) {
if (value === 'win32' || value === 'windows') return 'windows'
if (value === 'darwin' || value === 'macos') return 'darwin'
if (value === 'linux') return 'linux'
throw new Error(`Unsupported connector platform: ${value}`)
}
function normalizeArch(value) {
if (value === 'x64' || value === 'amd64') return 'amd64'
if (value === 'arm64') return 'arm64'
throw new Error(`Unsupported connector architecture: ${value}`)
}
function detectHostArch() {
if (process.platform !== 'darwin') return process.arch
try {
const arm64Supported = execFileSync('sysctl', ['-n', 'hw.optional.arm64'], {
encoding: 'utf8'
}).trim()
return arm64Supported === '1' ? 'arm64' : process.arch
} catch {
return process.arch
}
}
function parseTargets() {
const platformArg = process.argv.indexOf('--platform')
const archArg = process.argv.indexOf('--arch')
const platforms = platformArg >= 0 ? process.argv[platformArg + 1].split(',') : [process.platform]
const arches = archArg >= 0 ? process.argv[archArg + 1].split(',') : [detectHostArch()]
return platforms.flatMap((platform) =>
arches.map((arch) => ({ goos: normalizePlatform(platform), goarch: normalizeArch(arch) }))
)
}
if (!fs.existsSync(path.join(sourceDir, 'go.mod'))) {
throw new Error(`Repository-local WeChat connector source is missing: ${sourceDir}`)
}
for (const target of parseTargets()) {
const directoryName = `${target.goos === 'windows' ? 'win32' : target.goos}-${target.goarch === 'amd64' ? 'x64' : target.goarch}`
const outputDir = path.join(outputRoot, directoryName)
const outputPath = path.join(
outputDir,
target.goos === 'windows' ? 'wechat-connector.exe' : 'wechat-connector'
)
fs.rmSync(outputDir, { recursive: true, force: true })
fs.mkdirSync(outputDir, { recursive: true })
execFileSync('go', ['build', '-trimpath', '-o', outputPath, '.'], {
cwd: sourceDir,
env: { ...process.env, GOOS: target.goos, GOARCH: target.goarch, CGO_ENABLED: '0' },
stdio: 'inherit'
})
if (target.goos !== 'windows') fs.chmodSync(outputPath, 0o755)
console.log(`[build-wechat-connector] built ${directoryName}: ${outputPath}`)
}

Some files were not shown because too many files have changed in this diff Show More