Compare commits

..
315 Commits
Author SHA1 Message Date
电摇小子 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 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
Wxw-Gu 5018054cfd chore: package 2026-08-12 10:06:50 +08:00
Wxw-Gu 3f019faa38 feat: 完成 TraceMemo v2.2.0 品牌身份与数据迁移升级
- 将 WechatExplorer 产品身份升级为 TraceMemo
- 更新 appId、Runtime Identity、Reader Skill 和 API 环境变量
- 增加旧用户数据、Knowledge、Token 与 AI Provider 安全迁移
- 保留 Windows WeFlow 和旧版配置兼容
- 完善首次启动迁移测试及 v2.2.0 发布文档
- 移除 macOS Intel x64 构建与发布支持
2026-08-11 17:52:57 +08:00
Wxw-Gu b490dc4098 chore: 补充 2026-08-11 15:59:35 +08:00
Wxw-Gu 072387562a Merge branch 'develop' into feat/tracememo-v2.2.0 2026-08-11 14:44:58 +08:00
qingmao f72a7c8025 Merge pull request #15 from Michael-py001/develop
fix: 修复日报生成时群成员名称显示错误
docs: 添加本地启动排障文档
chore: 添加electron下载源配置
2026-08-11 14:43:51 +08:00
Wxw-Gu 632af4f409 feat: 完成 TraceMemo v2.2.0 品牌升级并保留旧数据兼容
- 将用户可见品牌升级为 TraceMemo(迹忆)
- 增加最早期 userData/sessionData 兼容路径选择
- 保留 WechatExplorer runtime identity 以兼容 safeStorage
- 继续使用旧 Knowledge、Settings、API Token 和 Provider 配置
- 新日志写入 TraceMemo 目录并保留历史日志
- 保留旧 API、Skill、环境变量和导出目录兼容标识
- 更新相关文档、界面文案与自动化测试
2026-08-11 14:42:12 +08:00
wushili 4c765568a2 Merge branch 'develop' of https://github.com/Wxw-Gu/WechatExplorer into develop 2026-08-11 13:13:13 +08:00
wushili fcba05aff9 fix: 修复日报生成时群成员名称显示错误 2026-08-11 13:12:22 +08:00
wushili 6b9358b69b Merge branch 'develop' of https://github.com/Wxw-Gu/WechatExplorer into develop 2026-08-11 11:23:45 +08:00
wushili 36a9cffc82 docs: 添加本地启动排障文档 & 添加electron下载源配置 2026-08-11 11:23:29 +08:00
Wxw-Gu e595e240eb feat: 增加退出选择任务栏 2026-08-11 11:22:45 +08:00
Wxw-Gu e62b5ef138 feat: 完善群聊日报语音转写缓存
支持空内容的微信语音消息并兼容历史缓存转写结果
将系统通知标记为微信系统消息,排除活跃成员与发言排行
2026-08-11 11:08:55 +08:00
Wxw-Gu f9e1ba9372 Merge branch 'main' into develop 2026-08-11 09:55:37 +08:00
电摇小子 92d5f5ab47 feat: 完善日报语音转写与全部聊天分目录导出 2026-08-11 02:59:54 +08:00
电摇小子 03870ca78b chore: 修改windows打包报错 2026-08-11 02:59:43 +08:00
电摇小子 7ec0e4f539 fix: 完善交互、知识库目录与 Windows 运行库提示 2026-08-11 02:59:30 +08:00
Wxw-Gu bf80cbc14d docs: 更新文档 2026-08-10 10:44:48 +08:00
电摇小子 cc67b7074f docs: 更新文档 2026-08-09 19:03:34 +08:00
电摇小子 1edea7d3cc docs: 更新文档 2026-08-09 10:12:18 +08:00
Nanin 0e96c146d5 修复bug,第一次导出缩略图,后面有了高清图后再次导出应该覆盖
按原图、中图和缩略图对本地图片资源分级,始终优先选择高清变体。

增量导出仅复用满足清晰度要求的图片,重新探测旧缩略图并在高清图出现后替换;仅缩略图降级时显示提示。
2026-08-08 20:53:40 +08:00
Nanin 7904d42442 修复bug,针对部分没有MD5名字的视频也能支持导出 2026-08-08 19:41:14 +08:00
电摇小子 7eefc59a0f docs: 更新文档 2026-08-08 14:54:54 +08:00
Wxw-Gu 33c0f15665 docs: 更新文档 2026-08-07 22:52:56 +08:00
Wxw-Gu ac82b50571 feat: 为本地 HTTP API 增加 Token 鉴权与安全加固
- 使用 Electron safeStorage 加密存储并自动初始化 API Token
- 为 health 以外的接口增加 Bearer Token 鉴权
- 限制 CORS 仅允许可信本地 Origin
- 增加鉴权、Token rotation、safeStorage 和手动验收测试
2026-08-07 17:48:05 +08:00
Wxw-Gu 1d9987181b Merge branch 'main' into develop 2026-08-07 16:21:48 +08:00
Wxw-Gu 3bef4cecc1 fix: 修复公众号消息读取与 Windows 中文路径兼容
- 支持从 biz_message 分片读取公众号聊天记录
- 在左侧栏增加独立的公众号折叠分组
- 为 Windows 中文数据目录建立 ASCII 路径桥接 (#12)
- 补充公众号分片、侧栏分类和路径桥接测试
2026-08-07 16:21:39 +08:00
Wxw-Gu 2775cc424e feat: 优化问问微信检索性能与分析交互
- 补充 Worker、WCDB、sender、IPC、序列化时间账
- 增加 Agent 增量覆盖统计和重复检索停止条件
- 补充性能与交互回归测试
2026-08-07 15:28:45 +08:00
Wxw-Gu fb5ebfad68 chore: 更新二维码 2026-08-07 14:43:23 +08:00
Wxw-Gu 1c6a908499 Merge remote-tracking branch 'origin/nanin/develop' into develop 2026-08-07 09:45:35 +08:00
电摇小子 7c4508bf29 feat: 完善首次连接账号身份展示
首次连接前读取当前账号昵称和头像
从账号目录安全推导其他账号 wxid 避免账号串号
2026-08-07 01:39:11 +08:00
电摇小子 a6a004e403 feat: 完善日报图片与公众号消息展示
修复日报图片读取格式与模型不支持时的友好降级
恢复话题关键词回退,确保词云在缺少顶层关键词时仍可展示
支持公众号多文章消息在聊天、日报、检索和 HTML 导出中完整呈现
迁移账号身份缓存并优化首次连接的账号占位信息
补充图片、词云、账号缓存与多文章消息回归测试
2026-08-07 00:00:52 +08:00
Nanin 9789cdb72e fix: 优化移动端导出消息布局与计数
- 调整移动端消息栈宽度,统一约束来源标签与消息内容\n- 精简档案计数文案,移除虚拟窗口已显示数量\n- 补充搜索前后消息保持单行展示的移动端回归测试
2026-08-06 21:32:17 +08:00
电摇小子 b5f67c47e5 feat: 知识库更新,优化批量语音转写 - 合并索引 - 增加会话级批量转写处理 - 补充语音转写回归测试 2026-08-06 20:31:08 +08:00
电摇小子 eee84f35df feat: 收敛 AI Search 检索边界与问答体验 2026-08-06 20:29:48 +08:00
电摇小子 4967cff3c0 test: 暂存代码 2026-08-06 20:29:25 +08:00
Nanin 17417aa9d6 fix: 优化导出档案系统消息宽度
- 系统消息气泡按内容自然展开,减少短消息不必要换行
- 保留容器最大宽度约束,避免窄屏横向溢出
- 增加撤回消息桌面端单行显示回归测试
2026-08-06 00:23:22 +08:00
Nanin ce67107128 feat: 优化聊天导出进度与媒体处理
1. 拆分读取、解析、转写、媒体处理、写入和压缩阶段,完善任务进度展示。

2. 增量导出复用语音资源与转写结果,并为缺失转写补充识别。

3. 稳定远程头像文件名并保留同源头像版本更新。

4. 支持音频附件直接播放、新窗口打开附件,并解码分享标题 XML 实体。

5. 补充导出进度、语音、头像、附件和消息解析回归测试。
2026-08-05 23:22:47 +08:00
Wxw-Gu c36590839b Merge branch 'nanin/develop' into develop 2026-08-05 18:44:25 +08:00
Nanin 8a5f3bbd00 fix: 修复聊天档案搜索误匹配微信内部 ID
- 移除隐藏 senderId 搜索字段,避免 xi 等关键词误命中 wxid
- 保留发送者昵称、消息正文及结构化可见内容的搜索能力
- 新增 wxid 与可见正文匹配的回归测试
2026-08-05 18:19:55 +08:00
Nanin e001074392 feat: 优化聊天档案移动端与账号切换体验
- 限制移动端横向滚动并修复发送消息头像裁切
- 调整顶部搜索、筛选与独立计数布局,七类筛选保持单行
- 移除聊天选项消息数和顶部更新时间,简化单聊天展示
- 使用头像、名称、切换图标与自定义弹层替代原生账号下拉控件
- 补充桌面及移动端的布局、切换和无横向溢出回归测试
2026-08-05 18:03:25 +08:00
majun.jason 7bf79a7424 feat: 优化聊天档案搜索体验
- 高亮展示搜索结果中的命中词,覆盖普通文本与结构化消息内容
- 为全部及分类搜索结果提供聊天定位,定位后清空搜索并回到完整消息上下文
- 修复移动端搜索框聚焦自动放大,并补充桌面与移动端测试覆盖
2026-08-05 15:24:36 +08:00
majun.jason 40d9cb3fad feat: 优化聊天档案浏览与增量导出
1. 增加档案加载状态、错误提示和延迟数据加载。
2. 优化移动端工具栏、消息布局及横向滚动控制。
3. 支持按年份折叠时间轴,并同步可见月份定位。
4. 使用自定义文件名作为档案标题。
5. 增量导出时保留历史头像,仅在视觉变化后新增头像版本。
6. 修复附件消息被误判为合并转发的问题。
7. 补充单元、集成及端到端测试覆盖。
2026-08-05 15:00:31 +08:00
Wxw-Gu eaa70dd8c6 fix: 语音模型下载 2026-08-05 10:21:13 +08:00
电摇小子 9e79790d41 feat: 导出html页面 2026-08-05 09:46:10 +08:00
电摇小子 580c7019d7 feat: 语音 2026-08-05 09:45:59 +08:00
majun.jason d4bd7be51c feat: 完善聊天导出与档案浏览
1. 修复数据量较大时,历史数据可能无法完整导出的问题
2. 优化分享 Tab 的信息展示,支持小程序、链接、地图等消息
3. 优化系统 Tab 的信息展示,支持红包、转账、拍一拍、撤回等消息
4. 修复默认进入时,时间轴不随消息自动定位的问题
5. 增加“定位到聊天位置”功能
2026-08-05 00:14:28 +08:00
majun.jason f0662a23c2 fix: 修复视频定位与运行时依赖打包 2026-08-04 21:00:13 +08:00
电摇小子 79ed94bc57 fix: test 2026-08-04 20:41:06 +08:00
电摇小子 f07f8aeb6c fix:test 2026-08-04 20:32:50 +08:00
电摇小子 5a53640b6f fix: 修复跨平台导出测试与 Playwright 浏览器安装 2026-08-04 20:26:04 +08:00
qingmao a8737fc6f7 Merge pull request #9 from nanin/nanin/develop
feat: 支持多聊天合并导出
2026-08-04 20:18:44 +08:00
majun.jason 87ef43575e fix: 修复 macOS 端到端测试 2026-08-04 20:13:07 +08:00
majun.jason 24227b5df2 feat: 支持多聊天合并导出 2026-08-04 19:26:18 +08:00
Wxw-Gu c9522f71d2 chore: 修改打包配置 2026-08-04 17:36:14 +08:00
Wxw-Gu 0c38e22c09 fix: 修复媒体导出与 HTML 聊天档案体验
- 修复打包版图片、语音解析不可用,内置 FFmpeg 与必要运行时依赖
- 修复 HTML 导出原图查找、缩略图回退及增量档案媒体解析问题
- 修复本人昵称在软件和 HTML 导出中显示为微信号的问题,并兼容旧档案昵称迁移
- 修复 HTML 语音播放器超出消息气泡的问题
- 优化 HTML 双向滚动加载,大量消息时最多渲染 240 条,避免页面卡顿
- 移除图片解密页面中手动配置 FFmpeg 的相关提示
- 补充图片解密、语音运行时、打包资源和 HTML 导出相关测试
2026-08-04 17:03:07 +08:00
Wxw-Gu 58906da35c feat: 增强 HTML 聊天档案导出
- 增加时间轴、消息筛选、搜索和完整时间显示
- 使用窗口化懒加载优化大消息档案
- 支持同名档案增量合并并安全复用媒体资源
2026-08-04 12:29:38 +08:00
Wxw-Gu 903e98820a fix: 完善聊天解析与导出体验
- 修复引用消息名称和图片布局
- 明确单会话图片测试日志范围
- 支持导出文件附件
- 完善图片批测、会话刷新和安全退出
2026-08-04 12:03:07 +08:00
Wxw-Gu 7f7ebc8811 chore: 提升版本 2026-08-03 14:12:00 +08:00
Wxw-Gu 4ac1dbb87f feat: 完善多账号连接诊断与聊天媒体导出
- 新增微信账号发现、环境诊断和分步数据库连接引导
- 支持按账号安全保存数据库密钥及快速切换账号
- 完善 WCDB 历史消息分片读取和分页状态提示
- 支持导出图片、视频和语音,提供原图优先及缩略图回退
- 更新安装指引、兼容版本说明和相关自动化测试
2026-08-03 10:43:14 +08:00
电摇小子 603b790682 fix: 修复 macOS 会话昵称显示
(cherry picked from commit be092c64e528cfc363f0ddd32c480eea5532799f)
2026-08-03 10:08:59 +08:00
电摇小子 8ec3237ef9 test: 建立桌面端自动化回归测试体系并完善跨平台 CI
(cherry picked from commit 74267ae2f63f8256c3da84b04f5b330e6e9d4c67)
2026-08-03 10:08:28 +08:00
电摇小子 e8cbfaf223 fix: 修复语音首播并完善消息解析与会话兼容性
(cherry picked from commit 214090192f5dfc91cdec0269bad434d3e22394d8)
2026-08-03 10:04:45 +08:00
电摇小子 d7a2dd3709 fix: 优化数据库启动与设置页响应性能
(cherry picked from commit f6614548b0e742bc703b2a5aaa5060720985ed27)
2026-08-03 10:04:19 +08:00
电摇小子 b779dc9838 fix: 支持 wxgf 原图解密并修复缩略图缓存刷新
- 增加 wxgf/HEVC 图片转换支持
- 增加 FFmpeg 跨平台安装、目录填写与能力检测
- 避免缩略图占用原图缓存,下载原图后可即时刷新
- 移除聊天图片的缩略图角标

(cherry picked from commit 4cee159a65496bd30dd690b568c47a120f3fff30)
2026-08-03 10:03:55 +08:00
电摇小子 7d509ae33b fix: 优化 Windows 启动性能与聊天图片缓存
- 增加聊天图片磁盘持久化缓存,重启后直接复用
- 将 DAT 图片解密移至 Worker,避免阻塞主进程
- 增加图片加载优先级和并发控制
- 优化会话目录与图片文件的异步查找
- 使用本地媒体协议加载缓存图片,减少 Base64 IPC 开销
- 优化启动缓存与数据库初始化流程,降低窗口未响应时间

(cherry picked from commit 82bcc8d32ca674de38c745cc9925ed2309887dc7)
2026-08-03 10:03:19 +08:00
Wxw-Gu af03e398e7 chore: 提升版本 修改打包命令 2026-07-31 11:40:41 +08:00
Wxw-Gu 6df271a173 feat: 重构README 新增引导功能 2026-07-31 11:26:23 +08:00
Wxw-Gu e1a174ade9 feat: 设置功能 2026-07-30 09:49:56 +08:00
Wxw-Gu 3c0e5a94cb refactor: 代码拆分 2026-07-29 10:50:27 +08:00
Wxw-Gu 303333dbc3 docs: 修改README 2026-07-28 16:50:13 +08:00
Wxw-Gu bfc6271af3 perf: 优化图片消息后台加载与解密缓存
- 缩略图优先展示并在后台准备原图
- 增加图片请求去重与受控并发队列
- 缓存图片路径、账号目录和解密结果
2026-07-28 16:11:59 +08:00
Wxw-Gu 20ceaf3b8a feat: 完善 AI 智能检索与定位
新增 AI 查询规划和主题变体多轮检索
修复无结果时回退全量消息导致的错误结论
优化目标成员优先级和大数据量消息匹配性能
新增检索诊断日志、任务中心和持久化缓存
支持证据按时间定位档案并闪烁提示
2026-07-28 15:52:31 +08:00
Wxw-Gu b5c0239b65 feat: 修改缓存加载逻辑 2026-07-28 09:56:50 +08:00
电摇小子 fb906a02c1 feat: 完善微信小程序与红包消息解析
- 新增小程序和红包结构化消息及专用卡片
- 修复嵌套类型、拍一拍和表情包误判
- 补充常见 AppMsg 类型分类
2026-07-28 03:20:11 +08:00
电摇小子 488460cd82 feat: 优化聊天记录缓存、虚拟分页与群聊媒体展示
- 使用持久化缓存加速启动并异步读取 WCDB 消息
- 修复空群缓存、头像和群成员名称丢失问题
- 修复引用图片缩略图、虚拟卸载缓存和图片预览
- 修正引用消息发送者显示为群 ID 的问题
2026-07-28 02:45:20 +08:00
电摇小子 306c7d99c2 feat: 修复导出任务状态并优化聊天记录导出体验 2026-07-27 22:06:08 +08:00
Wxw-Gu 0084b92703 feat: 提交 2026-07-27 20:34:57 +08:00
Wxw-Gu b7fba40754 feat: 表情包优化 2026-07-27 17:55:11 +08:00
Wxw-Gu 7f25920965 feat: 导出功能 2026-07-27 17:43:36 +08:00
Wxw-Gu 7f6b9a0948 chore: 删除无用代码 2026-07-27 10:13:13 +08:00
Wxw-Gu 1394208421 Merge branch 'develop' 2026-07-27 09:42:25 +08:00
Wxw-Gu 44ec8c4bb9 chore: 提升版本号 2026-07-27 09:41:49 +08:00
电摇小子 a3fb425061 fix(paths): 同步 imageKeyRoot 与 dbRoot,仅识别 xwechat_files
之前的状态面板(chat.getCurrentAccountRoot())和自动获取图片密钥(settings.imageKeyRoot)
走两个完全不同的来源,二者漂移导致状态显示 D 盘但实际扫描到 C 盘。

index.ts:
- key:autoGetImageKey 优先级调整为 chat.getCurrentAccountRoot() → self.accountRoot →
  settings.imageKeyRoot → settings.dbRoot,让运行时识别的目录永远最优先。
- db:init:保存 dbRoot 时同步更新 imageKeyRoot,避免 settings 缓存漂移。
- db:reopenWithRoot:用户手动切换根目录时同步更新 imageKeyRoot。

settings-store.ts:
- loadSettings:dbRoot 修正时同步 imageKeyRoot;若 imageKeyRoot 指向旧路径或不存在,
  回退到 dbRoot。
- 新增 redirectLegacyWeChatFilesToXwechat():当 settings 里的 dbRoot/imageKeyRoot 仍指向
  V3 时代的 "Documents\WeChat Files" 路径时,自动重定向到同目录下的 xwechat_files(如果存在)。
  老用户升级时无需手动迁移。

wcdb4-client / windows-db-root-discovery / key-service-win:
- 候选目录列表只保留 xwechat_files 形态(Documents\xwechat_files、
  AppData\Roaming\Tencent\xwechat_files、macOS 的 Library/Containers/.../xwechat_files),
  移除 V3 时代的 "WeChat Files" 候选。
- DB_ROOT_NAMES 只包含 xwechat_files,自动扫描也只认 V4 路径。

V3 数据(V1 头 dat / WeChat Files 目录结构)如果用户机器上还存在,仅作为残留,
不会被 WechatExplorer 当成有效数据源。
2026-07-26 20:46:51 +08:00
电摇小子 b466f4555e fix(image-decrypt): 仅支持 WeChat 4.0,移除 V3 兜底 + 模板诊断
WeChat 4.0 dat 文件头为 07 08 56 32 08 07。V3 / 老版本(V1 头)不在支持范围。

image-decrypt-service:
- 移除 defaultV1AesKey ('cfcd208495d565ef') 字段。
- 移除 if (version === 1) 的 V1 默认 key 分支。
- getDatVersion 只识别 V2 头;其他返回 0 走 unsupported。
- 注释说明 decryptDatV4 方法名里的 V4 是历史命名,跟协议版本无关。

key-service-win._findTemplateData / autoGetImageKeyByMemoryScan:
- 统计扫描诊断信息:搜索到的 _t.dat 数量、V2 头数量、非 V2 数量、扫描根目录。
- 把模糊的「未找到 V2 模板文件」拆成三种具体提示:
  1) 目录下完全没有 _t.dat → 让用户先在微信里点开图片大图(缩略图未生成)。
  2) 有 _t.dat 但都不是 V2 头 → 让用户查看更多图片。
  3) 有 V2 但没有有效长度 → 让用户查看更多图片。
- 每条都附带实际扫描根目录,便于远程排查时一眼看出路径是否正确。
2026-07-26 20:46:34 +08:00
电摇小子 0846e432cd refactor(image-decrypt): 设置面板 UI 重构 + 测试步骤联动
页面拆分两个目录语义:
- 上方「图片解密状态」的「图片资源目录」始终等于 chat 实时识别的默认目录(chat.getCurrentAccountRoot()),
  不再被用户输入覆盖。
- 下方「图片密钥管理」移除「图片资源目录」输入框,只保留 XOR / AES 两个 Key。
  该字段之前会被保存但实际不影响图片解密(数据库查询走默认根目录),保留只会让用户困惑。

UI 改动:
- ImageKeyConfiguration:移除 resourceRoot 输入框,edit() 签名收窄为 xorKey | aesKey。
- ImageTestSection:测试结果由 3 列横排改为步骤条样式(pending / ok / fail / skipped),
  任意一步 ✗ 后续步骤自动标记「跳过」并灰掉,前端语义上变成「要么都成功要么都失败」。
- AutoDetectImageKeySection:加一行小字提示「仅支持 WeChat 4.0,V3 及以下无法解析」。

后端改动:
- testImageDecryption:解密失败时把 fileFound/decrypted/readable 统一置 false,
  拆出「解密成功但不可读」中间态,让 UI 步骤联动准确反映。
- inspectImageDecryptionStatus:accountRoot 兜底改为 getCurrentAccountRoot() || config.resourceRoot,
  让状态面板始终显示当前识别到的默认目录。
- sanitizeImageError:把 'no_image_message' / '300' / 'unsupported' / 'dat version' 关键字翻译成具体提示。
- useImageDecryptionController.autoDetect:错误信息原文透传,不再走 sanitizeImageError(后者会
  把扫描阶段的「未找到 V2 模板文件」/「60 秒未找到 AES 密钥」等归类成「无法解析媒体文件」,掩盖真因)。
- image-key-config-service.save():不再写回 imageKeyRoot,下方输入框只用于校验,不再落盘。
2026-07-26 20:46:20 +08:00
电摇小子 fd2febc152 chore(security): 移除 AI 内置 Key fallback + 修正默认模型名
发布版本不再自动从环境变量读取 VITE_DEEPSEEK_API_KEY 创建默认 DeepSeek provider。
ensureEnvironmentMigration() 改为 no-op(保留方法作为占位)。
用户首次启动必须在「设置 → AI 模型」手动配置 API Key。

migrateLegacy() 保留:用于把用户自己之前存在 localStorage 的旧配置迁移到加密存储,
与内置 Key 是两回事。

附带修正 .env.example 默认模型名:deepseek-v4-flash 是已弃用/不存在的标识符,
DeepSeek 官方未发布此模型,统一改为 deepseek-chat。
2026-07-26 20:45:53 +08:00
Wxw-Gu 8083c1c4bd Merge branch 'develop' 2026-07-24 16:36:11 +08:00
Wxw-Gu bb588b1448 docs: 更新文档 2026-07-24 16:19:27 +08:00
Wxw-Gu 4ae5afb888 docs: 修改二维码 2026-07-24 14:46:17 +08:00
Wxw-Gu 26d027f947 feat: 支持微信视频消息解析与播放
支持视频 XML 解析、本地文件映射与拖动播放。

修复源码乱码注释并统一 UTF-8 编码。
2026-07-24 14:16:34 +08:00
Wxw-Gu 864c01b345 docs: 修改Readme 2026-07-24 11:28:10 +08:00
Wxw-Gu fa1ccc341e feat: 添加微信消息防撤回功能 2026-07-24 11:27:44 +08:00
Wxw-Gu 39afb0b8c2 chore: 提升版本 2026-07-23 16:25:08 +08:00
Wxw-Gu dabdb61919 fix: 容错修复群聊日报 JSON, 图片样式问题 2026-07-23 16:20:45 +08:00
Wxw-Gu 7238b4a01c chore: 添加应用诊断日志 2026-07-23 16:09:20 +08:00
Wxw-Gu 3c2aa00dc6 chore: 删除图片 2026-07-17 14:46:11 +08:00
Wxw-Gu fd52aa4f5c docs: 添加交流二维码并升级版本 2026-07-17 14:44:50 +08:00
Wxw-Gu 0c1197b7a9 feat: 重构数据库登录与路径发现 2026-07-17 14:35:53 +08:00
Wxw-Gu 7fbd9f3af2 perf: 优化聊天渲染与后台任务性能 2026-07-17 11:37:02 +08:00
Wxw-Gu e7a4ed738e merge: 合并日报名称与图片识别修复 2026-07-15 21:18:02 +08:00
Wxw-Gu 840ad51900 fix: 修复日报成员名称与图片识别兼容性 2026-07-15 21:17:54 +08:00
Wxw-Gu 6d8ed43f65 merge: 合并 AI 微信 Agent 能力 2026-07-15 20:50:04 +08:00
Wxw-Gu 5f341357d1 feat: 完善跨平台发布与生产密钥隔离 2026-07-15 20:49:47 +08:00
Wxw-Gu c21fda590b feat: 内置微信连接器并完善 Agent 查询能力 2026-07-15 20:27:05 +08:00
电摇小子 ba9e08d406 feat: 集成 Agent Hub 微信机器人能力 2026-07-15 17:26:15 +08:00
Wxw-Gu 6070d3b92d merge: 合并群聊日报与图片理解功能 2026-07-15 16:32:35 +08:00
Wxw-Gu 5cda0c8c18 feat: 完善群聊日报模板与图片理解 2026-07-15 16:29:25 +08:00
Wxw-Gu bf41745cbd merge: feat/report 集成进 feat/newReport (M1)
合并 feat/report 的日报功能 + main 的图片识别基建:
- 保留 main 分支的图片识别能力(ai-provider-service + key-store + settings/ai-model)
- 保留 feat/report 的日报功能(ChatWindow.tsx + group-report* + mobile_daily_report.html)
- index.ts/preload 同时保留两边的 IPC handler(api:chat / ai:testVision / report:export / db:getImage)

冲突解决:
- main/index.ts ai:chat:采用 main 版本(AIProviderService)
- group-report-service.ts values:采用 feat/report 版本(完整 hero/section 占位符)
- 整文件替换:ChatWindow.tsx / group-report.ts / mobile_daily_report.html
  (采用 feat/report 版本,与 group-report-facts.ts 协同)
2026-07-15 10:34:19 +08:00
电摇小子 888a708476 feat: 临时加一个windows 目录输入框 2026-07-14 14:33:40 +08:00
电摇小子 959a4fefd1 chore: 提升版本号 2026-07-14 11:10:42 +08:00
电摇小子 7c8aafaf78 docs: 修改README 2026-07-14 11:07:35 +08:00
电摇小子 3a9dbec1f1 fix: 修复登录报错key问题 2026-07-14 11:07:35 +08:00
电摇小子 b15f23d728 feat: 统一应用品牌图标并修复 macOS 打包路径 2026-07-14 11:07:35 +08:00
电摇小子 85d66f2da2 实现 AI 图片理解能力测试 2026-07-14 11:07:35 +08:00
电摇小子 a0028fc57d 重构 AI 模型配置中心 2026-07-14 11:07:35 +08:00
电摇小子 24879a2639 实现图片解密设置页 2026-07-14 11:07:35 +08:00
电摇小子 54ebe01fa3 实现数据库密钥设置页 2026-07-14 11:07:35 +08:00
电摇小子 e586c5f571 精简账号与数据库设置页 2026-07-14 11:07:35 +08:00
电摇小子 6b20e819aa fix: 修改登录报错, error改为warn 2026-07-14 11:07:35 +08:00
电摇小子 c8be398243 feat: 设置页面新UI 未做完 2026-07-14 11:07:35 +08:00
电摇小子 18e2b90583 feat: 完成本地 API 中心与 Reader Skill 流程 2026-07-14 11:07:35 +08:00
电摇小子 e70bddea82 fix: 完善 AI 日报详情状态与解析容错 2026-07-14 11:07:35 +08:00
电摇小子 1e6bffa6ee feat: 优化 AI 日报结果中心信息架构 2026-07-14 11:07:35 +08:00
电摇小子 762af8dd95 feat: 实现 AI 日报历史资产化 2026-07-14 11:07:35 +08:00
电摇小子 1d3a2ee181 重构 UI-04 AI 群聊日报工作区 2026-07-14 11:07:35 +08:00
电摇小子 d7c5ff1335 完成 UI-02 和 UI-03 聊天档案界面 2026-07-14 11:07:35 +08:00
电摇小子 1eb96f3d56 增加自动登录环境开关 2026-07-14 11:07:35 +08:00
电摇小子 716597f089 完成 UI-01 应用外壳与设计令牌 2026-07-14 11:07:35 +08:00
电摇小子 65d0d05daa 修复 Windows 消息游标回退查询 2026-07-14 11:07:35 +08:00
电摇小子 85728a47f4 修复 Windows 会话名称和密钥显示图标 2026-07-14 11:07:35 +08:00
电摇小子 606d05aa7a feat: 支持点击图片优先打开原图 2026-07-14 11:07:35 +08:00
电摇小子 8368f7f7f2 feat: 优化 Windows 缓存加载和图片显示 2026-07-14 11:07:35 +08:00
电摇小子 e67d893458 feat: 支持微信内置表情渲染 2026-07-14 10:20:45 +08:00
电摇小子 383ace8474 feat: 支持图片密钥配置与内存扫描 2026-07-14 10:20:23 +08:00
电摇小子 8f2d83dead docs: 更新 Windows 支持说明与 SIP 教程 2026-07-14 10:20:00 +08:00
电摇小子 52d2e63bc5 fix: 优化群聊展示与群日报生成 2026-07-14 10:19:40 +08:00
电摇小子 4fedc8c387 feat: 支持 Windows WCDB 解密与打包 2026-07-14 10:15:36 +08:00
电摇小子 295d77f585 fix: gitignore 2026-07-14 10:12:05 +08:00
电摇小子 ae561e6340 feat: 更换日报功能模板
- ChatWindow 模型下拉框新增 deepseek-v4-pro /
  deepseek-v4-flash
  - 主进程 model 兜底值改为 deepseek-v4-flash
  - 同步更新 .env.example 默认模型
  - 修复旧 localStorage 中 gpt-5.5 等不支持的模型导致的
  400 报错
2026-07-10 15:40:35 +08:00
电摇小子 3e37550ff9 Merge branch 'fix/xkey' 2026-07-10 10:38:39 +08:00
电摇小子 359ff23faf Merge branch 'feat/local-http-api' 2026-07-09 10:37:20 +08:00
电摇小子 cedc67c36e fix: 修复登录 2026-07-09 10:37:11 +08:00
电摇小子 e4c708cdf0 feat: 群日报 9 宫格头像自动反推 + 持续时长语义 + 自动登录
- group-report-service: enrichAvatarsFromGroup 从群成员快照反推真头像
  - group-report-service: 修 SVG data URL 正则,fallback 现在能正常嵌入
  - group-report (shared): GroupReportMetadata/Result 加 talker/warnings 字段
  - timeSpan 改为持续时长(\"1 h\" / \"30 min\" / \"2 d\" 紧凑半角)
  - App.tsx 启动自动连接(env var + safeStorage)
  - Wcdb4Client 父目录自动解析为最新 wxid
  - HTTP server EADDRINUSE 友好提示 + 指数退避
  - installSafeConsole 修 EPIPE crash
  - SettingsPanel 测试连接后自动更新 dbRoot
2026-07-08 16:40:03 +08:00
电摇小子 42393e4796 feat: 本地 HTTP API + 设置面板 + 账号自助入口
让 WechatExplorer 既能用图形界面浏览聊天记录,也能作为本机 MCP 数据源被
Claude / Codex 等客户端通过 127.0.0.1:6131 直接拉取。

本机 HTTP API
  - 新增 http-server: 8 个端点,覆盖 health / current_time / contact /
    chatroom / recent_chat / chatlog / group_snapshot / resolve / report
  - 时间参数支持 YYYY-MM-DD / YYYY-MM-DD/HH:mm / Unix 秒级;日期单独使用
    时自动补到 00:00:00~23:59:59,避免漏消息
  - apiServer 单例支持动态启停,启动失败返回 friendlyMessage(把
    EADDRINUSE 翻译成"端口已被占用"的中文错误并附 4 次重试)

数据库根目录与自服务入口
  - Wcdb4Client 接受 accountRoot 时会自动解析:父目录下找最新含
    db_storage 的 wxid 子目录;设置面板"测试连接"成功后回写解析后的
    精确路径
  - 抽 chat-service.ts:IPC 和 HTTP 共享 listContacts / listMessages /
    getGroupSnapshot / searchMessages / getSelfAccountInfo / testConnection
    / reopenWithRoot
  - WechatDb 接受可选 accountRoot;设置面板新增"应用并重新初始化"
    按钮,改完 dbRoot 立即生效
  - 新增 settings-store.ts,dbRoot / apiEnabled / apiHost / apiPort 落到
    userData/settings.json

主进程健壮性
  - 新增 safe-log.ts 包一层 console.log/warn/error,electron-vite 关闭
    子进程 stderr 后写 EPIPE 不再炸 IPC handler(原 main build 启动时
    即 installSafeConsole)
2026-07-07 13:58:10 +08:00
电摇小子 e6927d7cad docs: README 2026-07-07 13:52:47 +08:00
电摇小子 767a85ee5f chore: 修改打包名推送 2026-07-03 14:11:49 +08:00
电摇小子andClaude Opus 4.7 04925828d9 refactor: 移除微信 3.0 解密路径, 统一走 4.0 WCDB
- wechat-db.ts 删除 SQLCipher 回退、connectDb、getUser、tryOpenWechat4 等 3.0 专属逻辑
- image-decrypt-service.ts 删除 decryptDatV3 与 version === 0 分支
- index.ts 去除 wcdb4Client 冗余空值检查
- 移除 better-sqlite3-multiple-ciphers 依赖
- README 版本前置改为 4.0+

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 10:37:51 +08:00
电摇小子 42c6cce0c9 fix: 修复 macOS 打包版 WCDB 初始化 2026-07-03 10:05:50 +08:00
电摇小子 cca5284977 style: 修改样式, 去除日志 2026-07-02 10:24:31 +08:00
电摇小子 3b05bcbe29 feat: 实现退群监控 2026-06-30 16:04:01 +08:00
电摇小子andClaude Opus 4.7 1ddfe5f87e chore: bump version to 2.0.0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 14:39:52 +08:00
电摇小子 e0e0105e8b feat: 添加微信数据库实时监控,聊天记录自动刷新 2026-06-30 14:36:45 +08:00
电摇小子 44bf1bcaef feat: 支持自动获取并安全保存数据库密钥 2026-06-30 10:12:25 +08:00
电摇小子 a184bb1da1 feat: 修改群聊日报生成与群成员信息展示
- 支持结构化 AI 群聊日报及移动端长图导出
- 支持日报时间范围和消息类型筛选
- 支持群成员及当前用户群昵称解析
- 移除消息加载更多并优化图片懒加载
- 增强头像处理与敏感密钥日志保护
2026-06-29 16:43:01 +08:00
电摇小子 5f92e1d410 feat: 支持微信 4.0 数据库解密与富媒体消息查看
- 接入微信 4.0 WCDB 数据库解密,兼容微信 3.0 解密方式
- 支持图片解密及图片预览、缩放、旋转和拖动
- 支持语音解密与播放
- 支持表情包、引用、分享、名片、位置及通话消息解析
- 支持联系人和群聊真实头像
- 优化群聊、联系人分类及排序
- 修复复合消息类型和压缩消息内容解析
- 完善原生解密库打包及数据库错误提示
2026-06-29 14:51:45 +08:00
电摇小子 a12ec593da chore: 去除release脚本 2026-06-01 11:36:04 +08:00
电摇小子andClaude Opus 4.7 9a7549505f fix: 使用 pnpm/action-setup@v4 安装 pnpm
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 11:04:19 +08:00
电摇小子andClaude Opus 4.7 bcef842bb3 fix: 使用 corepack 启用 pnpm 替代 npm install -g
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 11:01:37 +08:00
电摇小子andClaude Opus 4.7 d151f36dd3 fix: 使用 pnpm@7 匹配项目要求
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:39:23 +08:00
电摇小子andClaude Opus 4.7 a3e3f8edbf fix: 修复 release workflow - 改用 npm 安装 pnpm 和 artifacts 上传
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:38:03 +08:00
电摇小子andClaude Opus 4.7 7154921d05 chore: 添加 GitHub Actions 自动构建 release workflow
- 推送 v* 标签时自动在 macOS 多架构构建 .dmg
- 自动创建 GitHub Release 并附加构建产物
- 升级版本至 v1.1.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:20:32 +08:00
电摇小子andClaude Opus 4.7 e11b7b1aa2 docs: 更新 README,添加多模型服务支持说明
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:12:57 +08:00
电摇小子andClaude Opus 4.7 bdccead5b4 feat: 支持多模型服务配置 (Key/Base URL/模型名)
- 后端 ai:chat 支持 baseURL 参数,可配置任意 OpenAI 兼容 API
- 前端 UI 增加 Base URL 输入框和模型选择下拉 (DeepSeek/GPT-4o/Claude/Moonshot)
- 配置通过 localStorage 持久化,支持从 .env 文件读取默认値
- 统一错误提示,移除 DeepSeek 硬编码引用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:11:25 +08:00
电摇小子 6e6224c017 doc: 更新文档 2026-04-08 11:01:03 +08:00
电摇小子 1f84a0a4b9 chore: 提升版本更新README 2026-01-14 16:28:37 +08:00
电摇小子 f61482afd8 docs: 更新README 2026-01-04 16:08:37 +08:00
847 changed files with 196433 additions and 1892 deletions
+35 -4
View File
@@ -1,8 +1,39 @@
# WeChat Database Key (Optional, can be entered in UI)
VITE_DB_KEY=
# DeepSeek API Key (Optional, can be entered in UI)
VITE_DEEPSEEK_API_KEY=
# Auto login on startup with VITE_DB_KEY or saved key.
# Set to true/1/yes/on for local development. Default is disabled.
VITE_AUTO_LOGIN=false
# Message types to filter out (comma separated)
VITE_FILTER_MSG_TYPES=分享消息,图片,表情包,视频
# 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 里填,
# 然后在「设置 → AI 模型」里手动完成"添加供应商"流程。
VITE_DEEPSEEK_API_KEY=
VITE_AI_BASE_URL=https://api.deepseek.com
VITE_AI_MODEL=deepseek-chat
# Message types to filter out (comma separated). Empty means show all message types.
VITE_FILTER_MSG_TYPES=
# Image Decryption Keys (Optional, for WeChat 4.0+ image decryption)
# These are dev fallbacks. End users can fill or auto-fetch them in Settings.
# XOR Key: hex format like 0x40, 0x53 etc.
# AES Key: 16-character string, derived from wxid and code
VITE_IMAGE_XOR_KEY=
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=
+73
View File
@@ -0,0 +1,73 @@
name: Tests
on:
push:
pull_request:
jobs:
desktop-tests:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 7.33.7
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Install Playwright Chromium
run: pnpm exec playwright install chromium
- name: Type check
run: pnpm typecheck
- name: Unit tests
run: pnpm test:unit
- name: Component tests
run: pnpm test:component
- name: IPC integration tests
run: pnpm test:integration
- name: Skill installation instruction tests
run: pnpm test:skill-install
- name: Build Electron test application
run: pnpm test:e2e:build
- name: Electron E2E tests
run: pnpm exec playwright test --grep-invert="@visual"
env:
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
- name: Upload Playwright diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-${{ matrix.os }}
path: |
test-results/
playwright-report/
if-no-files-found: ignore
retention-days: 14
+21
View File
@@ -1,7 +1,28 @@
node_modules
*.tsbuildinfo
electron.vite.config.[0-9]*.mjs
dist
out
.env
.DS_Store
.eslintcache
*.log*
coverage/
playwright-report/
test-results/
resources/runtime/darwin-arm64
.native-runtime-source
.omc
.codex/
skills-lock.json
*__screenshots__
/AGENTS.md
/CLAUDE.md
/.claude/
/.workbuddy/
/.ai-local/
findings.md
progress.md
task_plan.md
+7
View File
@@ -1 +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": {
+3 -2
View File
@@ -6,6 +6,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"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.
+211 -28
View File
@@ -1,41 +1,224 @@
# WechatExplorer
# TraceMemo(迹忆)
MAC系统 获取微信聊天记录 AI一键生成群聊总结
是一个基于 Electron + React + TypeScript 开发的微信聊天记录查看与分析工具。它支持查看解密后的微信数据库内容,提供聊天记录搜索、导出以及 AI 智能总结功能。
<p align="center">
<img src="./build/icon.png" width="120" alt="TraceMemo Logo" />
</p>
## 📸 预览
<h2 align="center">把微信里的信息,记住、理解、监控,并在需要时行动</h2>
<img src="./public/example1.png" alt="预览图1" />
<img src="./public/example2.png" alt="预览图2" />
<img src="./public/example3.png" alt="预览图3" />
<p align="center">本地优先的微信数据、AI 分析与自动化工作台</p>
## [点击这里下载](https://github.com/Wxw-Gu/WechatExplorer/releases/tag/v1.0.0)
<p align="center">
<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">
<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>
- **聊天记录查看**: 浏览微信好友和群聊的聊天记录。
- **全局搜索**: 快速搜索聊天内容。
- **AI 智能总结**: 集成 DeepSeek AI,一键总结群聊精华内容,生成话题报告。
- **图片生成**: 将 AI 总结的内容生成精美图片,方便分享。
- **数据导出**: 支持导出聊天记录为 CSV 文件(今日、昨日、近7天或全部)。
- **安全隐私**: 所有数据仅在本地处理,AI 功能需自行配置 API Key。
<p align="center">
<img src="./public/日报.png" alt="TraceMemo 主界面" />
</p>
## 🚀 快速开始
<p align="center">
<img src="./public/问问微信.png" alt="TraceMemo 问问微信" />
</p>
### 使用前置要求
<p align="center">
<img src="./public/退群监控.png" alt="TraceMemo 退群监控" />
</p>
---
- 微信>=4.0 无法使用
- 微信<=4.0 需要获取自己微信本地数据库的密码, 获取方式参考: [Mac 导出微信聊天记录](https://blog.vcvit.me/2024/08/02/mac-export-wechat-chat-records/)
- 如果无法获取本地数据库密码 则无法使用当前项目
- Node.js (推荐 v16+)
- pnpm@7
- 解密后的微信数据库文件 (`.db`) 和对应的密钥
- [DeepSeek API Key](https://www.deepseek.com/) (用于 AI 总结功能)
## 🎨 社区日报模板
## ⚠️ 免责声明
TraceMemo 日报除了内置版式,也支持从社区模板市场安装更多样式。社区模板与默认日报读取同一份真实日报数据,只改变展示方式,适合手机长图分享、桌面归档、团队复盘等不同场景。
本项目仅供学习和研究使用。请勿用于非法用途。开发者不对使用本项目造成的任何后果负责。请遵守相关法律法规和微信使用协议。
<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>
## 🔗 参考
在 TraceMemo 中打开:
- [WechatMessageExplorer](https://github.com/svcvit/WechatMessageExplorer)
**日报 → 今日日报 → 日报模板 → 模板市场**
即可查看、预览、安装和切换已发布的社区模板。
如果你有一张喜欢的日报长图、网页或前端项目,也可以把它交给 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 用户即可在模板市场中安装使用。
---
## TraceMemo 是什么
TraceMemo(迹忆)原名 **WechatExplorer** 是一款本地优先的微信数据、AI 分析与自动化工作台,把聊天变成可浏览、可搜索、可理解、可追溯的信息。
先用档案找原话,再按需要使用 AI Search、日报、监控或 Agent。普通浏览、搜索和导出不需要 AI。
## 核心能力
- 💬 **聊天档案与搜索**:浏览会话,按关键词或身份信息查找。
- 🔍 **AI Search / 问问微信**:用自然语言找回模糊记忆,并查看来源。
- 🧠 **本地知识库**:建立索引,提升跨会话查询稳定性。
- 📊 **群聊日报**:生成今日、昨日或近 7 天的群聊总结。
- 👀 **群成员变化监控**:记录指定群聊的退群动态。
- 🔊 **文字转语音**:生成语音,试听后发送到选定会话。
- 🤖 **Agent Hub**:在微信里调用本机 TraceMemo。
- 🔌 **外部 Agent / Local HTTP API**:让外部 Agent 查询本机微信历史。
## 💻 平台支持
TraceMemo 2.4.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>
---
## 💬 交流与反馈
<p align="center">
<img src="./public/二维码.jpg" alt="TraceMemo 交流与售后群二维码" width="280" />
</p>
## 从你的任务开始
| 想做什么 | 使用入口 |
| ---------------------------------- | ----------------------------- |
| 找记得原文或关键词的消息 | 档案搜索 |
| 找记得大意、但不知道在哪聊过的内容 | AI Search / 问问微信 |
| 长期跨群查询历史 | 本地知识库 |
| 了解一个群今天或近 7 天聊了什么 | 群聊日报 |
| 持续关注群成员退出 | 退群监控 |
| 按计划生成并发送群聊日报 | 定时日报 |
| 把文字生成微信语音 | 文字转语音 |
| 在微信里向本机 TraceMemo 提问 | Agent Hub |
| 让 Codex 等工具查询微信历史 | Reader Skill / Local HTTP API |
| 把聊天保存成文件 | 导出 |
## 快速开始
1. 从 [GitHub Releases](https://github.com/Wxw-Gu/TraceMemo/releases) 下载对应平台的安装包。
2. 启动应用,按“第一次使用”页面选择微信数据目录并完成连接。
3. 打开“档案”,确认联系人和消息已加载后开始搜索。
4. 需要 AI 时,在“设置 → AI 模型”添加并测试 Provider。
详细步骤见[第一次使用 TraceMemo](./docs/user-guide/getting-started.md)。
## 文档
- [用户指南](./docs/README.md#用户指南)
- [AI / Knowledge](./docs/README.md#ai-与知识库)
- [Monitor / Automation](./docs/README.md#日报与自动化)
- [Agent / API](./docs/README.md#agent--api)
- [开发文档](./docs/development/overview.md)
- [隐私与安全](./docs/user-guide/privacy.md)
完整目录由[文档首页](./docs/README.md)维护。
## 支持平台
| 平台 | 架构 | 微信连接 | 安装包 |
| ------- | ------------------------------ | ------------------------------------- | ------------------------------- |
| 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` |
## 参与贡献
稳定版在 `main`,只在发版时更新;所有改动都先进 `develop`,随**下一个版本**一起发布。
**提 PR 请基于 `develop` 拉新分支,并把 PR 的目标分支设为 `develop`** —— 指向 `main` 的 PR 会被直接关闭。
分支流程、提交信息风格、PR 前自检,以及**给 AI Agent 的硬性规则**,都在[参与贡献指南](./CONTRIBUTING.md)。
## 致谢
TraceMemo 的诞生离不开开源社区中许多优秀项目的工作。
### 特别感谢 WeFlow
TraceMemo 在早期适配微信 4.x 时,曾参考 **[WeFlow](https://github.com/hicccc77/WeFlow)** 历史版本中的相关实现和思路,包括数据库访问、密钥获取等底层能力。
特别感谢作者 **[hicccc77](https://github.com/hicccc77)**。项目与 WeFlow 的具体关系见[项目缘起](#项目缘起)。
### 其他参考项目
- **[WechatMessageExplorer](https://github.com/svcvit/WechatMessageExplorer)**
- 提供了数据解析相关思路。
- **[chatlog](https://github.com/sjzar/chatlog)**
- 提供了数据处理方面的参考。
- **[wechat_chatter](https://github.com/yincongcyincong/wechat_chatter)**
- 提供了发送方面的参考。
感谢所有开源作者,也感谢所有帮助 TraceMemo 发现问题、提出建议和持续使用它的人。
---
## 最后说两句
这个项目起初只是一个一时兴起的项目,所以它大概也不会有一份特别严肃的产品路线图。
我可能会按照自己的兴趣继续折腾,也可能突然加入一些奇奇怪怪、但觉得有意思的功能—— 比如让AI给某个好友, 某个群发一个语音条(逗逗群友) 或者定时生成群聊日报并做成微信卡片。
也因此,这个项目随时可能继续折腾,也可能因为其他事情暂时搁置。如果你有想要的功能,可以提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
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 21 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# 获取当前版本号(从 package.json 读取)
VERSION=$(jq -r '.version' package.json)
# 格式化为新的 tag(在版本号前加上 "v")
TAG="v${VERSION}"
# 输出当前生成的 tag
echo "生成的 tag: $TAG"
# 创建并推送 tag 到远程
git tag $TAG
git push origin $TAG
echo "Tag $TAG 已经推送到远程仓库!"
+70
View File
@@ -0,0 +1,70 @@
# TraceMemo 文档
文档按产品任务和使用场景组织。需要集成或开发时,再看 Agent、概念和开发文档。
## 档案与搜索
- [第一次使用](./user-guide/getting-started.md):安装、连接微信并完成第一次搜索。
- [Intel Mac 获取微信密钥](./user-guide/intel-mac-key.md):按页面检查结果准备环境并获取密钥。
- [聊天档案与搜索](./user-guide/chat-archive.md):浏览联系人和群聊,按关键词、备注、昵称、微信号或 wxid 查找消息;也包含档案中的文字转语音入口。
## AI 与知识库
- [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。
## 日报与自动化
- [群聊日报](./user-guide/report.md):手动生成今日、昨日或近 7 天的群聊报告,也可以创建定时日报。
- 定时日报会依次生成报告、保存 Report History,再按当前微信发送能力尝试通知;发送失败时可复用已有 PNG 重试。
- 自动发送和监控动作通过统一执行边界,并保留执行记录;简要说明见[产品工作方式](./concepts/how-it-works.md#动作执行与审计)。
## Monitor
退群监控会比较当前成员与上一份有效快照,记录成员退出事件。它支持多群、Last Good Snapshot 和事件历史;监控关闭期间的变化不会在重新开启后补报。工作方式见[产品工作方式](./concepts/how-it-works.md#退群监控)。
## 语音能力
- [语音转文字](./user-guide/voice.md):在本机转写微信语音,结果可用于搜索、Knowledge 和导出。
- [聊天档案与搜索](./user-guide/chat-archive.md#文字转语音):把文字生成微信语音,试听后发送到当前联系人或群聊。
## Agent / API
Agent Hub 让微信机器人调用本机 TraceMemo;Reader Skill / Local HTTP API 让外部 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)
## 导出与隐私
- [导出聊天](./user-guide/export.md):导出 HTML、Markdown、CSV 或 JSON 档案。
- [数据、隐私与安全](./user-guide/privacy.md):本地处理、Provider、媒体和 Token 的数据边界。
- [常见问题与排查](./user-guide/troubleshooting.md):按安装、连接、AI、媒体和 Agent 现象排查。
## 开发文档
- [开发、测试与构建](./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)
- [macOS 数据访问说明](./platform/macos.md)
- [关闭 SIP 教程](./mac-disable-sip.md)
## 实验性功能与第三方
- [实验性:自托管微信分享卡片](./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 行为和媒体读取结果可能随系统、微信客户端和服务商变化。
+69
View File
@@ -0,0 +1,69 @@
# 在微信里向 TraceMemo 提问(Agent Hub)
Agent Hub 是 TraceMemo 内置的微信机器人入口,也是应用一级导航中的“Agent”页面。你先扫码登录一个微信机器人账号,再用微信账号向机器人发送文字;本机 Agent Hub 会接收消息、读取已经连接的微信数据,必要时调用已配置的 AI,再把结果回复给发送者。
普通用户不需要安装 Reader Skill,也不需要配置 API Token。先连接微信数据库,再扫码登录机器人即可开始;需要总结或自然语言理解的任务还要配置 AI Provider。
它和 Reader Skill 是两条不同的路径:
- Reader Skill / Local HTTP API:外部 Agent 主动查询历史微信数据;
- Agent Hub / 微信机器人:机器人收到实时消息后处理并回复。
## 连接器和 Agent Hub 是什么关系
你不需要单独部署这些组件。扫码后,后台的微信连接器负责登录机器人、保持连接、接收微信消息和发送回复;Agent Hub 负责判断消息要做什么、查询 TraceMemo 本地数据、调用 AI 并组织结果。可以把它理解为:连接器负责“和微信通信”,Hub 负责“处理任务”。
## 你能做什么
连接 Agent Hub 后,可以在微信中询问:
- “最近 5 个会话”;
- “帮我看看最近跟某人聊了些什么。”
- “生成产品交流群今天的群聊总结图片。”
当前已实现的实时任务包括:
- 查看最近会话(数量限制为 1–20);
- 查询你和某位联系人的近期聊天;
- 用已配置的 AI 总结你和某位联系人近 7 天的聊天;
- 生成今天、昨天或近 7 天的群聊总结图片;
- 总结指定群成员在群里的近期发言;
- 对不需要读取聊天的普通文字请求返回简短 AI 回复。
任务完成后,回复会发送回触发这次请求的微信用户。群聊总结会先发送进度提示,完成后发送图片。
这些任务会在后台查询联系人、群聊和聊天记录,但当前机器人没有单独的“列出所有联系人”或“列出所有群聊”命令;需要完整浏览或按条件查询时,请使用档案页面或 Reader Skill / Local HTTP API。
## 连接步骤
1. 打开应用主导航中的“Agent”;页面标题为“Agent Hub”。
2. 确认 Hub 显示“运行中”,数据库状态为“可查询”。
3. 点击“扫码登录微信机器人”。
4. 用微信扫描二维码;如果页面显示“已扫码,等待手机确认”,在手机上确认。
5. 状态变为“在线”后,用另一个微信账号向机器人发送测试问题。
可以重新扫码登录或断开连接。登录凭证失效时,需要重新扫码。
## 运行日志
Agent Hub 页面会记录系统、Agent Hub 和微信连接器日志。日志支持筛选、复制和清空,并会隐藏 Token 和二维码数据,不记录微信密码。
## 需要满足的条件
- TraceMemo 的微信数据库已经连接,并且数据 API 可以查询;
- 依赖总结或自然语言理解的任务,需要在“设置 → AI 模型”配置可用的 AI 服务;
- TraceMemo 和 Agent Hub 需要保持运行,机器人才能接收和回复消息。
## 安全与边界
- Hub 使用本机通信,不把数据库直接暴露到公网;
- 机器人账号和个人微信账号是不同的登录边界,请确认你连接的是正确账号;
- 机器人回复会发送给当前发消息的人;开发者 API 另有受保护的测试发送入口,使用前必须确认接收者;
- Hub 生成群聊总结时仍可能调用你配置的 AI Provider;
- 当前实时自然语言入口主要处理文字消息。底层连接器可以接收图片、语音、文件和视频,但 Agent Hub 尚未为这些媒体提供同等的实时意图处理;
- 当前没有实现群发、广播、定时任务或通用自主操作微信;
- 本页面的“Agent Hub 状态”可以通过 Local HTTP API 查询,但不要把它误认为外部 Agent 的实时消息订阅接口或 MCP Server。
## 无法连接时
先检查 Hub、连接器和数据库三项状态,再查看日志。二维码过期、连接器不存在、凭证失效和数据 API 未就绪分别需要重新扫码、修复安装、重新登录或先完成微信数据库连接。
+52
View File
@@ -0,0 +1,52 @@
# Local HTTP API 安全
## 当前安全边界
TraceMemo 的本地 API 默认监听 `127.0.0.1:6131`。它面向同一台电脑上的 API Center、Reader Skill、CLI 和 Agent,不是公网网关,也不是带用户账户和细粒度权限 Scope 的服务。
## Bearer Token
新 Agent 配置使用 `TRACEMEMO_API_TOKEN`。v2.2.0 仍兼容读取历史变量 `WECHATEXPLORER_API_TOKEN`,优先级为新变量高于旧变量。
- `/api/v1/health` 是公开健康检查;
- 其他所有端点都要求 `Authorization: Bearer <TOKEN>`;
- Token 由应用生成,使用 32 个随机字节编码;
- Token 由 Electron `safeStorage` 加密保存在用户数据目录的 `local-api-token.bin`;
- 文件权限设置为 `0600`;
- 在“API Center”中可以显示、复制和重新生成;
- 重新生成后旧 Token 立即失效。
应用不会自动把 Token 写入 Codex、Claude Code、OpenClaw 或其他 Agent 配置。请把它放进 Agent 自己的本地 secret/environment,例如:
```bash
export TRACEMEMO_API_TOKEN="<TOKEN>"
```
## CORS 与 Origin
带浏览器 `Origin` 的请求只允许精确的 HTTP loopback Origin:
- `http://localhost` 及其端口;
- `http://127.0.0.1` 及其端口;
- `http://[::1]` 及其端口。
不带 `Origin` 的 curl、Node、本地脚本和 Agent 请求不受浏览器 CORS 规则限制,但仍必须携带 Token(health 除外)。
## 不要做的事
- 不要把 Token 放入 URL query、日志、截图、公开 Skill 或 Git;
- 不要把服务反向代理到公网;
- 不要把“health 能访问”误认为数据端点无需授权;
- 不要把 Bearer Token 当成跨用户权限系统;当前服务没有细粒度 Scope;
- 不要在共享机器上让不可信进程继承 Token 环境变量。
## Token 不可用时
如果系统安全存储不可用,API Token 会无法生成或读取,本地 API 会安全停用。先修复系统钥匙串/凭据服务,再回到 API Center 重试。不要手动编辑 `local-api-token.bin`。
## 相关文档
- [Agent 接入概览](./overview.md)
- [Reader Skill](./reader-skill.md)
- [数据、隐私与安全](../user-guide/privacy.md)
- [v2.1.9 鉴权迁移说明](./release-notes-v2.1.9.md)
+198
View File
@@ -0,0 +1,198 @@
# TraceMemo Local HTTP API
本文面向需要自己写集成的开发者。普通用户请先阅读[Agent 接入概览](./overview.md)。
## 基本信息
- 默认地址:`http://127.0.0.1:6131`
- API 前缀:`/api/v1`
- 默认只监听 loopback;不要把它当作公网服务。
- `/api/v1/health` 无需 Token;其他端点需要 `Authorization: Bearer <TOKEN>`。
- 请求体使用 JSON;响应为 JSON。
## 最小请求
```bash
# 健康检查
curl http://127.0.0.1:6131/api/v1/health
# 读取数据
export TRACEMEMO_API_TOKEN="<从 API Center 复制的 Token>"
curl -H "Authorization: Bearer $TRACEMEMO_API_TOKEN" \
"http://127.0.0.1:6131/api/v1/recent_chat?limit=20"
```
不要把 Token 放入 URL、Skill 文件、仓库或命令历史可被共享的脚本中。
新配置必须优先使用 `TRACEMEMO_API_TOKEN`。已安装的旧 Reader Skill 可在 v2.2.0 兼容期内继续读取 `WECHATEXPLORER_API_TOKEN`;如果两个变量都存在,以新变量为准。
## 端点
| 方法 | 路径 | 作用 | 参数/请求体 |
| ---- | ---------------------------- | -------------------------------------- | --------------------------------------------------------------- |
| 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": "..." }` |
### 这些端点与实时机器人有什么关系
- `/api/v1/agent/status` 只用于查询 Agent Hub、微信连接器和数据库状态;
- `/api/v1/agent/group-report` 由外部 Agent 或脚本主动请求生成群聊总结图片;
- `/api/v1/agent/send` 是受 Bearer Token 保护的开发者/测试发送入口,用于通过已经连接的机器人发送文字或本地图片;它不是任意群发能力,也不是实时消息订阅接口;
- 当前 API 没有对外暴露实时入站 webhook。微信消息由应用内部的 Agent Hub 和微信连接器接收、处理和回复。
## 时间查询
`chatlog` 的 `time` 支持:
- `YYYY-MM-DD`:当天;
- `YYYY-MM-DD~YYYY-MM-DD`:日期闭区间;
- `YYYY-MM-DD/HH:mm`:从该分钟开始的 60 秒;
- 也可以使用 Unix 秒级 `startTime` 和 `endTime`。
时间按运行 TraceMemo 的本机时区解析。用户说“今天”“昨天”时,先调用 `current_time`,再根据返回的 `localDate` 计算日期,避免使用 Agent 自己的时区。
## 常用工作流
### 查找并读取一个会话
```bash
BASE="http://127.0.0.1:6131/api/v1"
AUTH="Authorization: Bearer ${TRACEMEMO_API_TOKEN:-$WECHATEXPLORER_API_TOKEN}"
curl -H "$AUTH" "$BASE/resolve?q=技术交流群"
curl -H "$AUTH" "$BASE/chatlog?talker=技术交流群&time=2026-08-07"
```
当标识不确定时,先用 `resolve` 或 `contact`,再调用 `chatlog`。对重要问题,先宽范围定位,再针对关键时间点读取前后文,不要只凭一次粗查回答。
### 生成群聊总结图片
优先使用 `/api/v1/agent/group-report`,因为它会读取指定群聊并按 `today`、`yesterday` 或 `7days` 生成总结。`/api/v1/report` 是更底层的渲染接口,要求调用方已经准备好 `report` 和 `metadata` 结构;完整 TypeScript 类型以 `src/shared/group-report.ts` 为准。
## 响应与错误
- `200`:请求成功;
- `401`:缺少、错误或已失效的 Bearer Token;
- `400`:参数或 JSON 请求体无效;
- `422`:媒体标识格式错误,或目标消息不是可读取的图片(`NOT_IMAGE`);
- `403`:浏览器 Origin 不在允许的 loopback 列表;
- `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` 直读聊天数据库,不受索引新鲜度影响。
+52
View File
@@ -0,0 +1,52 @@
# 在微信机器人或外部 Agent 中使用 TraceMemo
TraceMemo 提供两条不同路径。先按你实际想做的事选择,不需要先理解 Agent、Skill 或 API 等术语。
| 你想做什么 | 使用方式 | 需要什么 |
| ---------------------------------------------------- | ----------------------------- | --------------------------------------------------------- |
| 直接在微信里发文字,让本机查询聊天并回复 | 微信机器人(Agent Hub) | 在应用“Agent”页面扫码登录机器人;部分任务需要 AI Provider |
| 在 Codex、Claude Code、OpenClaw 等工具里查询微信历史 | Reader Skill + Local HTTP API | 安装 Skill,并配置本机 API Token |
## 直接在微信里提问
打开应用一级导航中的“Agent”,进入“Agent Hub”,扫码登录一个微信机器人账号。之后用另一个微信账号向机器人发送文字,它会调用 TraceMemo 的本机数据,必要时使用已配置的 AI,再把结果回复给发送者。
可以先尝试:
- “最近 5 个会话”;
- “帮我看看最近跟张三聊了些什么”;
- “生成产品交流群今天的群聊总结图片”。
这条路径不要求安装 Reader Skill,也不要求用户配置 API Token。它主要处理文字请求,不支持群发、定时任务或与文字同等的任意媒体理解。
连接步骤、当前任务清单和安全边界见[Agent Hub](./agent-hub.md)。
## 在外部 Agent 中查询历史微信
Reader Skill 是给外部 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以通过 TraceMemo Local HTTP API 按需读取联系人、群聊、最近会话、指定时间范围的聊天和群成员信息。
典型问题包括:
- “总结今天技术交流群讨论的内容。”
- “帮我找上个月讨论过的项目地址。”
- “过去一周有没有人提到退款?”
外部 Agent 不会直接打开微信数据库文件,但它能取得本机 API 返回的聊天内容。Agent 是否继续把结果发送给云端模型,取决于 Agent 自己的模型和工具配置。
## 外部 Agent 的安装步骤
1. 启动 TraceMemo 并完成微信数据库连接。
2. 打开一级导航“API”(页面为“API Center”),确认本地 API、数据库和 Reader Skill 都可用。
3. 选择目标 Agent,点击“复制安装指令”。
4. 在 Agent 自己的 Skill/配置目录执行或粘贴指令。
5. 在 API Center 复制当前 Token,并在 Agent 运行环境中设置 `TRACEMEMO_API_TOKEN`。
6. 先让 Agent 调用 health,再尝试查询最近会话。
详细说明:[Reader Skill](./reader-skill.md)、[Local HTTP API](./api.md)、[API 安全](./api-security.md)。
## 不要混淆两条路径
- Agent Hub:微信机器人收到实时文字后处理并回复;
- Reader Skill/API:外部 Agent 主动查询历史数据;
- `127.0.0.1:6131` 是 Local HTTP API,不是 MCP Server;
- Local HTTP API 当前没有对外提供实时入站消息订阅。
+62
View File
@@ -0,0 +1,62 @@
# Reader Skill:让外部 Agent 读取微信
## 先理解它能做什么
Reader Skill 是一份给 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以按需调用 TraceMemo,读取联系人、群聊、最近会话、指定时间的聊天和群成员信息。
它使用的是 TraceMemo Local HTTP API,不是 MCP Server。
Reader Skill 只负责“外部 Agent 主动查询历史微信数据”。它不负责二维码登录、监听微信实时消息、接收机器人消息或管理 Agent Hub。想让机器人收到微信消息后处理并回复,请阅读[Agent Hub](./agent-hub.md)。
正式 Reader Skill 名称和目录是 `tracememo-reader`,新安装使用 `TRACEMEMO_API_TOKEN`。已安装的旧 `wechatexplorer-reader` 可在 v2.2.0 兼容期内继续使用旧变量。
## 推荐安装流程
1. 启动 TraceMemo 并完成数据库连接。
2. 打开“API Center”,确认 API 服务和数据库状态正常。
3. 在 Reader Skill 区域选择目标 Agent,点击“复制安装指令”。
4. 把指令粘贴到对应 Agent 的 Skill/配置目录;应用会根据本机路径生成适合 Codex、Claude Code、OpenClaw 或通用 Agent 的说明。
5. 在 API Center 复制 Token,在 Agent 自己的本地环境设置:
```bash
export TRACEMEMO_API_TOKEN="<YOUR_API_TOKEN>"
```
6. 先执行 health 检查,再读取数据端点。
TraceMemo 不会自动把 Token 写进 Agent 配置。重新生成 Token 后,必须同步更新 Agent 环境。
## Agent 的读取顺序
当用户使用“今天”“昨天”“本周”等相对时间时:
1. 调用 `/api/v1/current_time` 获取本机时区和日期;
2. 将相对时间换算为 `chatlog` 支持的 `time` 或时间戳;
3. 调用 `/api/v1/resolve`、`contact` 或 `chatroom` 确认会话;
4. 调用 `/api/v1/chatlog` 读取目标范围;
5. 对重要结论再读取关键消息前后文,不要只凭一次粗查。
## 最小请求
```bash
curl http://127.0.0.1:6131/api/v1/health
curl -H "Authorization: Bearer $TRACEMEMO_API_TOKEN" \
"http://127.0.0.1:6131/api/v1/recent_chat?limit=20"
```
## 当前能力范围
Reader Skill 可以指导 Agent 使用:
- 联系人、群聊、最近会话和会话解析;
- 指定会话、日期或时间戳范围的聊天记录;
- 群成员快照;
- 结构化日报渲染和按群聊生成总结图片;
- Agent Hub 状态检查与已连接机器人发送测试。这里的发送接口是开发者/测试用途,不是实时机器人入口,也不会让 Reader Skill 自动监听微信消息。
端点、参数、错误码和鉴权细节以[Local HTTP API](./api.md)为准。Skill 文件保持短小,避免在多个文档中复制会变化的完整响应 schema。
## 隐私边界
Reader Skill 本身不会把聊天数据自动上传到其他服务器;它只是让 Agent 调用本机 API。Agent 读取结果是否继续发送给云端模型,取决于 Agent 自己的模型和工具配置。请同时阅读[数据、隐私与安全](../user-guide/privacy.md)。
+12
View File
@@ -0,0 +1,12 @@
# TraceMemo 2.1.9:Local HTTP API 鉴权迁移
2.1.9 为 Local HTTP API 增加 Bearer Token 鉴权。这是一次有意的兼容性变化:除健康检查外,数据接口不再接受裸请求。
- 历史版本中,`GET /api/v1/contact` 等数据请求可能直接返回内容;
- 2.1.9 中,相同请求必须携带 `Authorization: Bearer <TOKEN>`,否则返回 `401`;
- `GET /api/v1/health` 保持公开;
- 升级后应用会生成并安全保存 Token,原有 API 启用状态、监听地址和端口设置保持不变;
- Token 可在 TraceMemo → API Center 中显示、复制和重新生成;
- Reader Skill、Codex、Claude Code、OpenClaw 和其他本地 Agent 需要在自己的环境中设置 `WECHATEXPLORER_API_TOKEN`。
如果旧 Agent 无法访问,请先从 API Center 复制当前 Token,再确认每个非 health 请求都带有 Bearer header。完整规则见[API 安全](./api-security.md)。
+69
View File
@@ -0,0 +1,69 @@
# TraceMemo 2.2.0:正式品牌身份与安全升级迁移
TraceMemo(迹忆)原名 WechatExplorer。v2.2.0 不只更新用户可见名称,也正式启用新的应用身份、数据目录、Reader Skill 和默认 Agent 环境变量,同时为 v2.1.9 用户提供一次安全迁移路径。
## 新的产品身份
- 产品名与 Electron runtime name:`TraceMemo`;
- bundle/app identifier:`com.tracememo.app`;
- macOS userData:`~/Library/Application Support/TraceMemo`;
- macOS 日志:`~/Library/Logs/TraceMemo`;
- Reader Skill:`tracememo-reader`;
- Agent API Token 环境变量:`TRACEMEMO_API_TOKEN`;
- Agent Hub 凭据目录:`~/.tracememo/wechat-connector/accounts`。
## v2.1.9 升级迁移
首次启动 TraceMemo 时,如果检测到包含有效用户资产的旧数据目录,应用会询问是否立即迁移:
- `WechatExplorer`;
- v2.1.9 在区分大小写文件系统上可能使用的 `wechatexplorer`。
两个旧目录都有效时,应用确定性优先选择 `WechatExplorer` 并写入诊断日志,不合并目录。选择“以后迁移”不会删除或修改旧数据,下次启动仍可继续处理。
迁移遵循以下安全边界:
- 只复制明确列出的用户资产,不复制整个 Application Support;
- TraceMemo 已存在的文件或目录绝不覆盖;
- 每一项迁移可重复执行,已完成项会跳过;
- 迁移失败只清理本次创建的 staging,旧目录和旧文件始终保留;
- 不移动、不删除旧目录,不修改微信数据库或 Knowledge schema。
## 迁移的用户资产
- 设置、微信数据库连接路径和 AI Provider 元数据;
- Knowledge 本地索引;
- 报告历史、防撤回归档、图片理解结果和 Renderer Local Storage;
- Local HTTP API Token;
- AI Provider Key、微信数据库 Key 和图片解密 Key;
- Agent Hub credential 与同步状态。
Chromium Cache、Code Cache、GPUCache、临时文件、语音模型和其他可重建运行缓存不会为了品牌升级强制复制。
## Knowledge
Knowledge 以完整目录为单位复制。每个账号的 `knowledge.sqlite`、`knowledge.sqlite-wal` 和 `knowledge.sqlite-shm` 会一起进入同一个 staging;复制后先核对主库及 companion 文件,再对 staging 数据库执行 SQLite `integrity_check`。只有验证通过后才放入 TraceMemo 数据根。
迁移过程不会打开、修改或删除真实旧 Knowledge。失败时旧索引仍可用于重新迁移,不要求用户重新建立 2.47GB 级别的索引。
## Token 与加密 Key
旧 `safeStorage` 密文不会原样复制到新数据目录。TraceMemo 会启动一个隔离的 legacy helper:macOS 使用旧 `WechatExplorer` identity,helper 只在内存中解密并校验旧 Token/Key,再通过专用进程管道交给主进程重新加密;macOS 主进程使用 TraceMemo identity. 明文不会写入磁盘、环境变量或日志。
Token 格式、随机熵、加密方式和 rotation 行为没有变化。如果旧 API Token 因系统安全存储限制无法迁移,应用不会静默生成替代 Token,本地 API 会安全停用并提示用户重试迁移或在 API Center 主动重新生成。AI Provider Key、数据库 Key 和图片 Key 失败时也会明确记录为部分迁移,旧密文保持不变。
## API、Agent 与 Skill 兼容
Local HTTP API 继续使用 `127.0.0.1:6131` 和 `/api/v1/*`,Bearer Token 格式不变。
新安装和新文档默认使用 `TRACEMEMO_API_TOKEN`。已安装的旧 Reader Skill 可以在一个兼容版本内继续使用 `WECHATEXPLORER_API_TOKEN`。正式随应用分发的 Skill 已更名为 `tracememo-reader`,资源解析仍可读取旧 `wechatexplorer-reader` 目录作为 fallback。
Agent Hub 新凭据写入 `~/.tracememo`。如果迁移尚未完成且新目录没有凭据,connector 会只读回退到 `~/.wechatexplorer`;新版本不会清理或删除旧目录。
## 日志与 Documents
TraceMemo 新日志写入新的日志目录,“设置 → 关于 → 打开诊断日志目录”会打开当前 TraceMemo 日志。历史 WechatExplorer 日志保持原位置,不搬迁、不重命名、不删除。
`Documents/TraceMemo` 用于新导出和 Emoji 数据;历史 `Documents/WechatExplorer` 不删除,并继续提供兼容读取。
更多安全边界见[数据、隐私与安全](../user-guide/privacy.md)和[API 安全](./api-security.md)。
+41
View File
@@ -0,0 +1,41 @@
# 如何核对 AI 的回答来源
## 先记住一件事
AI 回答后,你可以继续查看它参考了哪些聊天内容、这些内容来自哪个会话和时间,并跳回原始消息检查上下文。
这让 TraceMemo 和只给一段摘要的聊天机器人不同:答案不是终点,来源也应该能被你检查。
## 三类来源信息
在产品界面和检索详情中,你可能看到这些名称:
- **Evidence**:AI 回答所依据的原始聊天片段。
- **Citation**:回答中某个结论对应的来源标记。
- **Search Trace**:本次查找经历了哪些阶段、每一步用了多久、覆盖是否完整。
普通用户不需要记住英文名。判断一个回答是否可信时,按“来源 → 原消息 → 上下文”检查即可。
## 推荐的核对顺序
1. 先看回答是否明确区分事实、推断和不确定信息;
2. 打开来源,检查发送者、会话和时间;
3. 跳回档案,查看消息前后文,确认是否存在引用、转发或后续修正;
4. 检查提示中是否有未转写语音、缺失媒体或只覆盖部分范围;
5. 对重要决定、金额、日期和责任人,不要只依据 AI 摘要。
## 为什么来源可能不完整
来源覆盖受时间范围、会话范围、索引状态和可读媒体影响。例如:
- Knowledge 还没追到最新时,跨会话检索只覆盖到索引当前的时间点,答案会标注这个范围;
- 语音没有转写时,AI 可能只能看到消息类型;
- 图片无法读取或未启用图片理解时,AI 不应声称知道图片内容;
- 你只选择了一个群,答案不会自动代表所有聊天。
Knowledge 在后台同步时**不会**暂停分析:你仍然可以提问,只是答案基于当前已可用的覆盖范围。看到“可能遗漏”或“部分覆盖”时,扩大范围、等同步追上或检查原始媒体后再问。
## 这不是事实保证
Evidence 和 Citation 能告诉你“模型看到了什么”,不能保证模型没有误读。最终判断仍应回到原始消息,尤其是涉及隐私、法律、财务、医疗或工作决策时。
+88
View File
@@ -0,0 +1,88 @@
# TraceMemo 如何把聊天变成可用的信息
你可以把一次任务想成下面这条路径:
```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
B --> O[Monitor / Snapshot]
O --> P[Proposed Action]
F --> P
P --> Q[Policy]
Q --> R[Action Gateway]
R --> S[Personal WeChat Send Capability]
S --> T[Action Audit / Logs]
```
## Remember → Understand → Monitor → Act
TraceMemo 的工作方式可以概括为:
```text
Remember → Understand → Monitor → Act
```
先读取和整理微信信息,再由 AI、Knowledge 或日报帮助理解;Monitor 负责发现成员变化,明确的业务动作再进入执行边界。回答和动作结果都应能回到来源或记录核对。
## 退群监控
退群监控使用成员快照判断变化:
```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。它不会因为打开软件就自动上传完整数据库。
Agent Hub 收到微信机器人的文字后,也可能为了理解请求或生成总结调用已配置的 Provider。Reader Skill 调用的是本机 API;外部 Agent 是否把读取结果继续交给云端模型,取决于外部 Agent 自己的配置。
如果 Provider 是 Ollama 等本机服务,请把它视为本机的另一个进程;如果是云服务,数据处理和留存规则由该服务商决定。
## 产品名词和用户任务的对应关系
| 用户想做什么 | 产品中可能看到的名称 |
| ------------------------ | ---------------------------- |
| 让 AI 找相关聊天 | AI Search、Retrieval |
| 让答案能回到原消息 | Evidence、Citation |
| 查看 AI 查找过程 | Search Trace |
| 让跨会话查找更稳定 | Knowledge、FTS 索引 |
| 让外部 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,58 @@
# 本地启动排障
本文面向运行源码开发环境的贡献者。常规启动顺序和测试入口请先阅读[开发、测试与构建](./overview.md)。
## 启动成功的判断标准
执行 `pnpm dev` 后,以下状态同时满足,说明本地开发环境已经可用:
- Electron 窗口已打开,或 `http://localhost:5173/` 返回 HTTP `200`;
- 控制台显示 Local HTTP API 正在监听 `http://127.0.0.1:6131`。
`6131` 是应用提供给本机集成使用的 API 端口,不是 Vite 的页面端口。
## WCDB 消息读取诊断
`WCDB_DEBUG_LOGS` 默认关闭。需要排查消息读取链路时,可以在启动命令前设置为 `1`:
```bash
WCDB_DEBUG_LOGS=1 pnpm dev
```
开启后会输出 `GETMSG-xxx` 请求耗时和 native `WCDB-EXPLAIN` 执行计划,不记录聊天正文。取消该环境变量或设为 `0` 即可关闭。
## Electron 二进制缺失或下载失败
`electron-vite dev` 报 `Electron uninstall`,或 Electron 安装器报 `fetch failed`,通常表示 `node_modules/electron/dist` 中的 Electron 二进制缺失或下载未完成。这不是应用业务代码的启动错误。
**先看根因,别急着删 `node_modules` 重装。** `electron@43` 的 npm 包**不再声明 `postinstall`**(其 `package.json` 里 `scripts` 是空对象),下载改为「首次 `require('electron')` 时的懒加载」。因此:
- `package.json` 里的 `pnpm.onlyBuiltDependencies: ["electron"]` 对它不起作用——上游没有脚本可执行,pnpm 无从下手;
- `pnpm install` 跑完不会有任何二进制被下载,**只重装依赖解决不了这个问题**。
项目已自动兜住这条路径:`scripts/ensure-electron-binary.cjs` 挂在 `postinstall` 与 `predev` 上,校验 `path.txt` 指向的可执行文件是否真的存在(只有 `path.txt` 而没有 `dist/` 同样算没装好),缺失时就地补下载。它读取 `.npmrc` 的 `electron_mirror`(当前为 `https://npmmirror.com/mirrors/electron/`),失败后再兜底重试一次该镜像。
正常情况下你不需要做任何事。只有当自动步骤没有执行时(例如安装时带了 `--ignore-scripts`),才需要手动补一次:
```bash
node scripts/ensure-electron-binary.cjs
```
要换用别的镜像时,显式设置环境变量(优先于 `.npmrc`)。PowerShell 示例:
```powershell
$env:ELECTRON_MIRROR = 'https://your-electron-mirror.example/'
node scripts/ensure-electron-binary.cjs
```
该环境变量只影响当前终端,不会改写仓库中的 `.npmrc`。镜像地址必须保留末尾的 `/`,并提供与 Electron 版本对应的目录结构。
## 页面地址无法通过 IPv4 访问
Vite 在某些 Windows 环境中只监听 IPv6 本机回环地址 `::1`。这时直接访问 `http://127.0.0.1:5173/` 可能失败,但 `http://localhost:5173/` 仍然正常,Electron 也会使用后者加载页面。
排查时优先访问 `http://localhost:5173/`;需要显式验证 IPv6 时,使用 `http://[::1]:5173/`。不要因为 IPv4 回环地址不可用就判断 Electron 或 Vite 启动失败。
## 仍无法启动时
保留首次错误的完整输出,并同时记录操作系统、Node.js 与 pnpm 版本,以及 `pnpm install --frozen-lockfile` 与 `pnpm dev` 的执行结果。不要提交数据库密钥、AI API Key、微信数据路径或聊天内容。
+57
View File
@@ -0,0 +1,57 @@
# 开发、测试与构建
本文面向希望参与 TraceMemo 开发、验证文档或维护集成的贡献者。普通用户请从[第一次使用](../user-guide/getting-started.md)开始。
## 技术基线
- Electron + React + TypeScript;
- pnpm 7+;
- 平台对应的 Electron/native 构建环境。
产品文档的事实来源优先级是:当前源码 → 当前 UI/Renderer → 测试 → package/config → README/docs → 历史资料。功能、API、版本、隐私和兼容性变更时,不要只改 README。
## 本地开发
```bash
pnpm install
pnpm dev
```
本地依赖安装与 Electron 二进制下载异常,请查看[本地启动排障](./local-startup-troubleshooting.md)。
常用检查:
```bash
pnpm typecheck
pnpm test:unit
pnpm test:component
pnpm test:integration
pnpm test:e2e:build
```
完整测试入口 `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/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` |
## 文档检查
提交文档变更前至少执行:
```bash
git diff --check
rg -n "v2\.1\.7|TraceMemo|迹忆|mcpServers|无鉴权" README.md docs --glob '*.md' --glob '!development/overview.md'
```
历史迁移说明可以出现旧版本号;正式使用指南不要把过时版本写成当前版本。负向澄清“6131 不是 MCP Server”可以保留,以防用户照抄错误配置。
+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`
+62
View File
@@ -0,0 +1,62 @@
# macOS 关闭 SIP 教程
SIP(System Integrity Protection,系统完整性保护)是 macOS 的系统安全机制。关闭 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
```
然后重启电脑,恢复系统安全设置。
+38
View File
@@ -0,0 +1,38 @@
# macOS 数据访问与系统权限
## 你什么时候会看到这些提示
TraceMemo 需要读取微信本地数据。macOS 会根据系统版本、微信状态和安全设置,要求应用完成授权;自动获取数据库密钥时,页面可能提示暂时调整系统安全设置。
## 推荐步骤
1. 先启动 TraceMemo,阅读连接页面显示的当前前置条件。
2. 确认微信数据目录指向当前账号。
3. 只在页面明确要求时处理系统授权或 SIP;关闭 SIP 的具体步骤见[关闭 SIP 教程](../mac-disable-sip.md),按页面提示完成密钥获取后,恢复你平时使用的安全设置。
4. 返回应用重新检测账号、数据库和图片资源状态。
不要直接复制网上针对其他微信版本的命令。系统授权失败时,记录 macOS 版本、微信版本和页面错误,再按[排障文档](../user-guide/troubleshooting.md#连接微信失败)处理。
## SIP 风险
关闭 System Integrity Protection 会降低 macOS 对系统文件和进程的保护。它不是日常使用 TraceMemo 的功能开关,也不应长期保持关闭。只有在你理解风险、确认页面要求且完成必要操作时才处理;完成后按 Apple 官方方式重新启用。
## 应用无法打开
如果 macOS 阻止未验证的应用,使用系统“隐私与安全性”中的“仍要打开”选项。不要为了绕过提示下载来历不明的补丁或替换应用文件。
## Intel 与 Apple Silicon
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
+139
View File
@@ -0,0 +1,139 @@
---
name: tracememo-reader
description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的微信聊天数据和图片媒体。当用户要求查看微信消息、查找联系人或群聊、总结聊天、查看或理解图片、生成群聊总结时使用。此 Skill 由本机 TraceMemo 提供数据,不是 MCP Server。
---
# TraceMemo Reader
你是一个通过本机 TraceMemo 读取微信历史的 Agent。先确认用户已经在 TraceMemo 中完成数据库连接,再按需调用 API;不要假设数据库已就绪,也不要声称读取了没有调用过的消息。
## 连接信息
- Base URL 默认是 `http://127.0.0.1:6131/api/v1`。
- `GET /health` 不需要 Token。
- 其他端点必须带 `Authorization: Bearer $TRACEMEMO_API_TOKEN`。
- 新配置优先读取 `TRACEMEMO_API_TOKEN`;为兼容已安装的旧 Reader,可在新变量缺失时回退到 `WECHATEXPLORER_API_TOKEN`。
- Token 由用户在 TraceMemo → API Center 显示/复制,并放在 Agent 自己的本地环境中。
- 不要把 Token 放到 URL、回答、日志、Skill 文件或仓库。
- 6131 是普通 Local HTTP API,不是 MCP Server;不要生成 `mcpServers` 配置。
## 每次任务前
1. 调用 `/health`,确认服务和数据库状态。
2. 用户说“今天”“昨天”“本周”等相对时间时,先调用 `/current_time`,按返回的本机时区换算日期。
3. 用 `/resolve`、`/contact` 或 `/chatroom` 确认会话标识。
4. 用 `/chatlog` 读取最小必要的时间范围。
5. 对重要结论读取关键消息前后文;不要只凭一次宽范围粗查回答。
## 端点速查
| 方法 | 路径 | 用途 |
| ------ | ----------------------------------- | ------------------------------------------------- |
| 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` |
| 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 | `/report` | 将已有日报结构渲染为 HTML/PNG |
| GET | `/agent/status` | Agent Hub、连接器和数据库状态 |
| POST | `/agent/group-report` | 按群和 `today`/`yesterday`/`7days` 生成总结图片 |
| POST | `/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"`,告诉用户相同任务已经存在,不要再次创建。能力状态为 `unsupported`、`unconfigured`、`needs_binding`、`needs_verification` 或 `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`。
## 时间与上下文规则
`/chatlog` 的 `time` 支持 `YYYY-MM-DD`、日期闭区间和分钟范围;也可以使用 Unix 秒级 `startTime`/`endTime`。时间按 TraceMemo 所在机器的本机时区解释。
当用户问“某个话题是谁说的、后来结论是什么”时,先定位会话和时间,再读取关键消息前后文。回答时区分:
- 原消息明确写出的内容;
- 根据多条消息整理出的总结;
- 没有来源支持的推断。
## 媒体消息
当 `/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 的数据策略。
## 常见错误
- `401`:Token 缺失、错误或被轮换;请用户回 API Center 复制最新 Token。
- `403`:浏览器 Origin 不在 loopback 允许列表;CLI/Agent 通常不带 Origin。
- `404`:会话查询失败时先用 `/resolve` 确认会话标识;媒体请求表示标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在。
- `422`:媒体标识格式错误,或消息不是可读取的图片(`NOT_IMAGE`)。
- `503`:用户还没有完成数据库连接或对应服务未就绪。
- 空结果:缩小/扩大时间范围,确认账号和会话,再检查媒体或语音是否可读。
+74
View File
@@ -0,0 +1,74 @@
# 用 AI 查找你以前聊过的信息
## AI Search 是什么
你可以把它理解成“会帮你翻聊天记录的 AI”。
普通搜索需要你猜关键词;AI Search 更适合这些问题:
- “我们上个月为什么决定延期?”
- “谁提过这个项目,后来结论是什么?”
- “过去一周有哪些待跟进事项?”
它会先在本机查找相关聊天,再把受控范围内的内容交给你选择的 AI Provider 生成回答。它不是凭空记忆,也不是把整库聊天一次性上传。
## 第一次使用
1. 进入“设置 → AI 模型”,添加一个 Provider,填写服务地址、模型和认证信息,然后测试连接。
2. 打开“问问微信”。
3. 选择所有聊天、群聊、单聊或当前会话,并选择今天、近 7 天、近 30 天或不限时间;范围越明确,答案越容易核对。
4. 输入问题并开始分析。
如果知识库尚未建立,页面会提示你建立或同步;你也可以先直接使用当前可用的搜索路径。
## 怎么提问更容易得到好结果
把“谁、什么时候、在哪个群、想找什么结果”写出来。例如:
> “在产品交流群里,查找 2026 年 7 月讨论发布延期的消息,列出结论和待办。”
尽量避免只写“总结一下”。如果你只记得模糊含义,也可以先提问,再根据来源缩小范围继续追问。
## AI 回答后先看什么
不要只看结论。回答区域通常还会展示:
- 参考了哪些聊天内容;
- 来源来自哪个会话、发送者和时间;
- 哪一段回答对应哪条来源;
- 本次查找经过了哪些阶段、耗时和覆盖情况;
- 是否存在未转写语音、媒体不可用或结果不完整的提示。
你可以点击来源回到档案中的原始消息。产品内部将这些信息称为 Evidence、Citation 和 Search Trace,用户可以把它们理解为“依据、来源标记和查找过程”。详见[如何核对 AI 的回答来源](../concepts/answer-sources.md)。
## 查找过程和跳回原消息
查找过程中,界面依次显示真实阶段:**理解问题 → 查找相关聊天 → 整理证据 → 生成回答**。跨会话、大范围检索更慢时,副提示会写明“正在搜索较大范围的聊天记录…”。阶段只在真正进入下一步时前进,不使用定时器或百分比伪造进度。
查找结束后,界面给出耗时拆解:**总耗时**,以及其中分别花在 **AI 生成** 和 **本地查询** 上的时间。这样你能判断慢在哪——是模型在写答案,还是本机还在翻聊天记录。
点击来源卡片的 **“跳转到原聊天”** 会真的打开对应会话并定位到那条消息:
- 群聊来源打开的是那个群,而不是群里某个联系人;
- 会加载该消息前后的上下文,并滚动到它、短暂高亮;
- 只加载目标消息附近的一段,不会把整个会话历史全部读出来;
- 如果这条消息已经不在本地(例如已被删除),界面会明确说明“已打开对应会话,但暂时无法定位原消息”,不会假装跳转成功。
## 什么时候不要直接相信答案
- 来源很少,或时间范围与问题不一致;
- 回答提到了来源中没有的细节;
- 关键内容来自未转写语音、无法读取的图片或转发消息;
- 页面提示只覆盖了部分聊天。
这些情况下,打开原消息,扩大或缩小范围,再重新提问。必要时把问题改成“只列出原文明确说过的内容”。
## 取消、失败和降级
分析过程中可以取消当前任务。检索或模型请求失败时,页面可能保留已找到的来源或切换到备用路径;这不代表一定得到了完整答案。请查看提示、检索详情和[排障文档](./troubleshooting.md#ai-没有结果或回答失败)。
## 数据会发到哪里
本地解析、索引和候选消息查找在本机完成。只有完成 AI 任务所需的用户问题、受控检索上下文和最终用于总结的来源内容,才可能发送到你配置的 Provider;具体边界见[数据、隐私与安全](./privacy.md)。
使用远程 Provider 时,当前界面会在本次请求发出前显示接收方和发送范围,等待你确认。当前实现最多发送 8 条最终来源,不会发送完整微信数据库、数据库密钥、绝对文件路径或内部会话/消息引用 ID;这次确认不会自动授权之后的其他请求。
+69
View File
@@ -0,0 +1,69 @@
# 查看和搜索聊天
“档案”是你直接阅读微信历史的地方。适合查原文、回看上下文、确认 AI 来源,也适合在你已经知道关键词时快速定位。
## 选择要看的会话
左侧会话列表可以浏览已读取到的联系人、群聊和公众号。选中会话后,右侧显示消息时间线;滚动到较早位置可以继续加载历史。
如果你从 AI 回答的来源进入档案,应用会自动切换到对应会话并尽量定位到消息时间。
## 普通关键词搜索什么时候最好用
当你记得以下任意信息时,优先使用档案搜索:
- 一段原话或关键词;
- 人名、群名、项目名;
- 链接、文件名或订单号;
- 大致知道在哪个联系人或群里。
关键词搜索速度快、结果直观,但它不会理解“意思相近但没有相同词”的问题。
## 怎么找到联系人
档案搜索会综合多个身份字段匹配联系人或群聊,包括:
- 通讯录备注(remark);
- 微信昵称;
- 当前微信号;
- wxid;
- 拼音全拼和拼音首字母。
因此可以直接输入备注、昵称、微信号或拼音查找。搜索联系人和搜索消息是两步:先确认目标会话,再在会话内查关键词;记得大意但不知道原话时,改用[AI Search](./ai-search.md)。
## 文字转语音
在档案中选择当前联系人或群聊,输入文字后生成语音,试听确认后发送。这个入口只处理明确的文字转语音动作,不是任意文本、图片或本地语音文件发送器。
## 消息和媒体
根据微信数据中实际可用的资源,档案可以展示文本、图片、视频、语音、文件、链接、引用、小程序、表情和系统消息等类型。媒体是否能显示,取决于本机原始资源是否仍然存在、权限是否完整以及当前微信版本的存储方式。
不要把“消息类型已读取”理解成“所有媒体都一定能解码”。遇到图片或视频空白时,请先检查[媒体与导出排查](./troubleshooting.md#媒体显示或导出异常)。
如果文字正常但图片无法打开,进入“设置 → 图片解密”查看当前状态。可以尝试自动获取,也可以在已经知道正确密钥时手动配置;原文件已经被微信清理时,仅配置密钥也无法恢复图片。
联系人和群聊列表会尽量显示头像、备注和昵称。头像或资料缺失时不影响消息读取;这通常表示本机没有对应资源,或微信没有返回完整资料。
## 保护自己不被误导
档案中的原始消息是核对 AI 结果的最终依据。看到 AI 的总结、日报或来源时,建议:
1. 打开来源对应的会话;
2. 查看消息前后几条上下文;
3. 注意消息时间、发送者和是否存在转发/引用;
4. 对未转写的语音、无法读取的图片保持不确定判断。
## 常见问题
### 会话列表为空
确认数据库连接成功、连接的是正确微信账号,并重新加载数据。若仍为空,查看[连接微信失败](./troubleshooting.md#连接微信失败)。
### 搜索不到明明存在的消息
先缩小到正确会话,再尝试更短的关键词或原文片段。对于“以前讨论过什么”这类语义问题,改用[AI Search](./ai-search.md)。
### 想跨多个会话查找
使用“问问微信”,并在问题中写清时间范围、人物或群聊范围。需要更稳定的跨会话查找时,先建立[本地知识库](./knowledge.md)。
+39
View File
@@ -0,0 +1,39 @@
# 导出聊天档案
导出适合把微信里的重要讨论保存成可阅读、可分享或可继续处理的文件。
## 支持的格式
| 格式 | 适合什么任务 | 当前边界 |
| -------- | ------------------------ | ---------------------------------------------------------- |
| HTML | 完整阅读和长期归档 | 可包含媒体、头像和可选语音转写;支持多会话、增量合并和 ZIP |
| Markdown | 笔记、版本管理和再次编辑 | 主要保留文本内容,不复制 HTML 资源文件 |
| CSV | 表格分析 | 主要保留文本内容,不复制 HTML 资源文件 |
| JSON | 程序处理和数据归档 | 主要保留文本内容,不复制 HTML 资源文件 |
ZIP 是 HTML 资源包的压缩选项,不是第五种内容格式。
## 导出步骤
可以打开一级导航“导出”,也可以在“档案”的聊天顶部点击“导出”并选择时间范围。
1. 选择一个或多个联系人/群聊。
2. 选择时间范围和消息类型。
3. 选择格式;只有 HTML 可以配置媒体资源、语音转写和 ZIP。
4. 按需要设置头像、原图/缩略图和缺失资源处理。
5. 设置文件名并开始导出。
6. 在导出任务中心查看读取、解析、媒体处理、转写、写入和压缩进度;完成后打开文件位置。
## 多会话和增量导出
HTML 支持把最多五个会话合并到一个档案中;选择多个会话后,其他格式会不可用。再次使用相同名称导出 HTML 时,可以把新消息增量合并到已有档案;这不会删除之前已导出的消息。
## 媒体怎么处理
原图、缩略图、缺失资源和头像都可能影响导出大小与可读性。想要小文件时关闭媒体或选择缩略图;想要长期保存时,确认原始媒体目录仍可访问,并考虑 ZIP 归档。
HTML 导出可以选择在任务中执行本地语音转写,并把成功结果显示在语音气泡下方。语音模型不可用或识别失败时,导出不会把失败内容当成已转写文本。
## 导出和原始数据的关系
导出是复制/整理结果,不会修改微信原始数据库。删除导出文件也不会影响应用内聊天记录或本地知识库。
+177
View File
@@ -0,0 +1,177 @@
# 第一次使用 TraceMemo
如果你刚下载 TraceMemo,只需要完成一条主线:
> 安装应用 → 连接微信数据 → 确认聊天已加载 → 搜索或提问。
这篇文档不要求你先学习内部术语;先把第一个问题问出来,之后再按需要深入了解产品名称和进阶功能。
## 1. 开始前准备
TraceMemo 需要读取微信本地数据库。首次使用前,请确认已安装受支持的微信客户端,并按照连接页面完成数据库密钥获取。
| 系统 | 微信客户端 | 首次连接说明 |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| 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 服务。
当前代码按微信 4.x 数据结构处理。不同微信客户端版本、系统权限和数据迁移状态可能影响自动连接;遇到问题时请查看[常见问题与排查](./troubleshooting.md)。
## 2. 安装并启动
安装包统一从 [GitHub Releases](https://github.com/Wxw-Gu/TraceMemo/releases) 下载。
### Windows
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. 从 Releases 下载与你 Mac 处理器架构匹配的 `.dmg`:
- Apple Silicon(M 系列、`arm64`):`tracememo-<版本号>-arm64.dmg`
- Intel(`x64`):`tracememo-<版本号>-x64.dmg`
2. 打开 DMG,将 TraceMemo 拖入“应用程序”文件夹。
3. 如果系统提示“无法打开,因为开发者无法验证”,前往“系统设置 → 隐私与安全性”,点击“仍要打开”。
4. 如果系统提示应用已损坏,可在终端执行:
```bash
xattr -cr "/Applications/TraceMemo.app"
```
5. 先让微信停在**未登录窗口**(如果微信已经登录,请退出当前账号,**只关闭窗口不算**),再启动 TraceMemo。
- 连接时会按提示自动获取数据库密钥;应用会根据当前架构进入对应流程,按连接页面显示的授权要求操作即可。
- **等第五步 明确提示可以登录后,再回到微信点击登录。** 在此之前不要先点登录,否则这次密钥获取会失败,需要重新开始。
- 只有页面明确提示时才按[关闭 SIP 教程](../mac-disable-sip.md)处理。关闭 SIP 会降低系统安全性,完成密钥配置后应重新开启。
> **注意事项:取密钥时提示监听超时(`CAPTURE_TIMEOUT`,或相关失败)**
>
> 这说明「先让微信停在未登录窗口」这一步操作有误——常见原因是微信当时已经登录,或者还没等连接页面提示就先点了登录
>
> 请先退出微信账号、回到**未登录窗口**,然后按上面的第 5 步**重新来一遍**(等连接页面明确提示可以登录后,再回到微信点击登录)。
更完整的权限和安全边界见 [macOS 数据访问说明](../platform/macos.md)。
## 3. 让应用读取微信数据
首次启动会自动进入“第一次使用”页面。页面会根据当前系统显示连接方式和注意事项:
<p align="center">
<img src="../../public/setup-page.png" alt="第一次使用连接页面" width="820" />
</p>
通常按下面三步操作即可:
1. **确认微信数据目录**:自动识别不准确时,打开微信设置中的缓存/存储管理,复制实际路径并在页面中修改。
2. **让微信停在登录页面**:如果微信已经登录,先退出当前微信账号,不只是关闭微信窗口。
3. **点击“开始连接”并按提示获取密钥**:软件准备好连接组件后会提示你登录微信;回到微信完成登录,再等待数据库、账号和联系人检查完成。
只有已经通过其他方式取得当前账号数据库密钥的高级用户,才需要选择“手动连接”。Windows 不需要关闭 SIP;macOS 是否需要额外授权或处理 SIP,以当前连接页面提示为准。
连接页面会显示微信状态、数据库状态和诊断结果。连接失败时先不要反复删除数据,优先查看[连接问题排查](./troubleshooting.md#连接微信失败)。
## 4. 确认第一次连接成功
连接成功后会进入“档案”页面。你可以用下面三个信号确认已经准备好:
- 左侧出现联系人或群聊列表;
- 选中一个会话后,右侧能看到历史消息;
- 搜索框可以在当前会话中定位文字。
如果联系人列表为空,先检查是否连到了正确账号和数据目录,再重新加载会话。
文字消息正常但图片打不开时,不代表数据库连接失败。打开“设置 → 图片解密”查看状态并尝试自动获取;图片原文件缺失、权限不足或密钥不匹配时,部分图片仍可能无法显示。
## 5. 完成你的第一个任务
### 只是想找一句话
进入“档案”,选择联系人或群聊,在会话内搜索关键词。适合你记得原话、姓名、链接或大致关键词的情况。
### 想找一个模糊的结论
先在“设置 → AI 模型”添加并测试一个 Provider,再进入“问问微信”描述问题,例如:
- “上个月技术群讨论过哪些发布问题?”
- “张三之前发过的项目地址在哪里?”
- “过去一周有没有人提到退款?”
这就是 AI Search:它会先帮你从本机聊天中找出相关内容,再让你配置的模型组织答案。你不需要知道关键词在哪,但问题越具体,结果越容易核对。
### 想让 AI 的答案可核对
回答生成后,打开来源或检索详情,查看它参考的聊天内容、会话、时间和原始消息。你可以从来源直接跳回“档案”检查上下文。
产品把这些来源信息分别称为 Evidence、Citation 和 Search Trace;普通使用时只需要记住“答案可以回到原消息核对”即可。详见[如何核对 AI 的回答来源](../concepts/answer-sources.md)。
## 6. 接下来可以做什么
- [查看和搜索聊天](./chat-archive.md)
- [使用 AI 查找聊天信息](./ai-search.md)
- [建立本地知识库,让后续查找更稳定](./knowledge.md)
- [生成群聊日报或总结](./report.md)
- [转写微信语音](./voice.md)
- [导出聊天档案](./export.md)
- [在微信里向 TraceMemo 提问](../agent/agent-hub.md)
- [让外部 Agent 查询微信历史](../agent/overview.md)
## 7. 想直接在微信里提问
如果你希望直接在微信里向 TraceMemo 提问,而不是另外配置 Codex 等外部 Agent,请使用 Agent Hub:
1. 先完成上面的微信数据库连接,并确认“档案”里能看到聊天。
2. 打开应用主导航中的“Agent”;页面标题为“Agent Hub”。
3. 确认 Agent Hub 显示“运行中”,数据 API/数据库状态可以查询。
4. 点击“扫码登录微信机器人”,用微信扫描二维码,并在手机上确认登录。
5. 状态变为“在线”后,向这个机器人发送文字消息。
可以先试试这些真实支持的请求:
- “最近 5 个会话”;
- “帮我看看最近跟张三聊了些什么”;
- “生成产品交流群今天的群聊总结图片”。
机器人会把处理结果回复给发消息的人。联系人聊天总结、群聊总结和需要理解自然语言的请求依赖“设置 → AI 模型”中已经配置好的 AI 服务。当前实时入口主要处理文字消息;它不是支持任意图片、语音、文件理解、群发或定时任务的通用机器人。机器人账号扫码登录与读取你微信数据库是两条独立流程,都需要分别确认账号和权限。
Agent Hub 是普通用户可以直接使用的入口,不需要安装 Reader Skill 或配置 API Token。Reader Skill 和 API 只用于让外部 Agent 主动查询历史微信。
## 8. 需要配置 AI 吗?
不一定。浏览聊天、普通关键词搜索、建立本地知识库和导出不要求在线 AI 服务。
使用“问问微信”、群聊日报或图片理解时,需要在“设置 → AI 模型”中添加并测试 AI 服务。你主动开始并确认远程 AI 功能后,完成任务所需的内容才可能发送给该服务;计费、留存和地区规则由对应服务商决定。
## 9. 数据和隐私的最低须知
- 微信数据库、聊天解析和本地索引默认留在本机。
- 离线语音转写使用本地模型;它与在线 AI 请求是两条不同的数据路径。
- 你主动开始并确认 AI 问答或日报后,完成任务所需的受控上下文才可能发送给你选择的 AI 服务;打开应用不会自动上传全部聊天。
- 应用内 Local HTTP API 默认只监听 `127.0.0.1:6131`,受保护接口需要 Token。
完整边界见[数据、隐私与安全](./privacy.md)。
## 10. 如果你卡住了
按现象进入[常见问题与排查](./troubleshooting.md):连接失败、聊天为空、AI 没有结果、语音模型不可用、导出失败和 Agent 无法访问分别有不同处理方式。
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

+19
View File
@@ -0,0 +1,19 @@
# Intel Mac 首次获取密钥
请先备份微信本地数据。密钥获取和数据库验证都在本机完成,不要把密钥、日志或数据库发给他人。
## 开始使用
1. 启动 TraceMemo,在首次连接页面点击“重新检查环境”。
2. 如果页面显示需要准备连接环境,点击“准备环境”,按页面提示完成。
3. 打开微信,让微信停在未登录页面,不要先点击登录。
4. 回到 TraceMemo,选择微信账号,点击“开始获取密钥”。
5. 等待页面提示可以登录后,立即回到微信点击登录。
6. 等待 TraceMemo 显示“获取成功,验证数据库连接”。
获取成功后,请保存密钥。如果页面提示需要重新准备,按页面的“查看说明”操作,不要连续重试。
## 注意
- 密钥只保存在本机的安全存储中。
- 请不要删除微信本地数据目录。
+61
View File
@@ -0,0 +1,61 @@
# 把聊天变成更容易再次找到的本地资料
## 你为什么需要 Knowledge
如果你经常查同一批工作群、项目讨论或长期联系人,只靠每次临时翻聊天会越来越慢。Knowledge 会在本机建立一份可重复查找的索引,让“以前聊过什么”这类问题更容易跨会话、跨时间找到相关内容。
它不是另一个聊天窗口,也不会替你修改微信原始数据库;它是 TraceMemo 为当前账号维护的本地加速资料。
## 建立和同步
Knowledge 不会在第一次连接后自动悄悄建立。进入“问问微信”后,在“本地知识库”区域点击:
- **建立本地知识库**:第一次读取当前账号的可检索聊天;
- **同步最新记录**:已有索引时,只补充新增或变化的内容。
同步在后台运行,**期间仍然可以正常提问和分析**,不会被禁用。索引还没追完时,答案会基于当前已经可用的部分给出,并在界面标注覆盖范围。
知识库卡片同时显示两组互相独立的信息:
- **规模**:已索引消息、知识片段、磁盘占用——说明索引有多大;
- **状态与本轮进度**:说明索引现在处于什么状态、这一轮同步在做什么(扫了多少、真正新增了多少、处理到第几个会话)。
`最新索引` 只表示索引已经覆盖到聊天记录的哪个时间点,**不等于**整库已经建完;进度里的计数是**本轮**的数字,不是全部历史的总数。
### 状态怎么读
| 状态 | 含义 |
| ---- | ---- |
| 可用 · 已追至最新 | 索引已覆盖到聊天记录的最新位置,可以直接用 |
| 可用 · 正在追新 | 索引可用,正在后台补充最近新增的消息 |
| 可用 · 正在补齐历史 | 索引可用,正在后台补齐较早的历史内容 |
| 可用 · 同步已取消 | 索引仍然可用;上一轮同步被取消,已建立的部分保留 |
| 可用 · 更新失败 | 索引仍然可用;上一轮同步出错,可以稍后重试 |
只有确实追平、且没有待补齐内容时才会出现“已追至最新”。索引不可查询时不会显示“可用”。
### 取消和继续
同步过程中可以点击 **取消同步**(点击后显示“正在取消…”)。取消只结束当前这一轮,不会删除已经建立的索引,也不会回滚已完成的部分;下次同步会从上次停下的位置继续,不需要从头重扫。中断过的索引仍然可以正常搜索。
## 账号隔离
每个微信账号使用独立的本地索引。切换账号时,应用不会把一个账号的索引混入另一个账号的搜索结果。
## 什么时候值得建立
- 你要跨多个群查过去几个月的内容;
- 你反复查同一个项目、客户或主题;
- 你希望 AI 先从更稳定的本地资料中找来源;
- 你想减少每次搜索都重新读取大量原始记录的等待。
只偶尔查一条原话时,直接使用档案搜索通常更快。
## 清理和重建
在“设置 → 缓存与清理”中可以清理本地知识库索引、检索记录和导出任务缓存。清理索引不会删除微信原始聊天记录或数据库密钥;之后可以回到“问问微信”重新建立。
## 产品术语(可选)
源码和日志中可能出现 SQLite、FTS、Chunk、索引等词。它们描述的是本地存储和检索实现,不是你开始使用 TraceMemo 的前置知识。
+58
View File
@@ -0,0 +1,58 @@
# 数据、隐私与安全
TraceMemo 的核心路径是本地优先,但“本地优先”不等于所有功能都完全离线。是否有数据离开电脑,取决于你是否启用了对应的 AI、Agent 或机器人能力。
## 默认留在本机的内容
以下处理由应用在本机完成:
- 读取和解析微信数据库;
- 聊天档案浏览和普通关键词搜索;
- 本地 Knowledge 索引及其账号隔离;
- 离线语音转写;
- 导出文件生成和本地日报历史。
应用不会因为你打开 TraceMemo 就自动把整份微信数据库上传。
## 什么时候会请求外部服务
当你主动使用 AI Search、群聊日报或图片理解,并配置了远程 Provider 时,完成任务所需的内容可能发送给该 Provider。当前设置页给出的边界是:
- 当前用户问题;
- 受控检索所需的有限上下文;
- 最终用于总结的 Evidence。
不会发送完整微信数据库、全量聊天记录、未选中的聊天范围、数据库密钥、内部索引结构或内部会话/消息引用 ID。Provider 的日志、保留、计费和跨境规则不由 TraceMemo 控制,请查看你所选服务商的政策。
Ollama 等本机 Provider 可以把模型请求留在本机,但本机服务的日志和配置仍由你负责。
## 语音和媒体
离线语音转写在本机进行。图片理解属于 AI 功能:只有你主动启用并使用相关报告/分析路径时,图片才可能按该 Provider 的请求规则被处理。无法读取的媒体不会被自动“猜出来”。
## Local HTTP API
- 默认监听地址为 `127.0.0.1:6131`,不是公网服务;
- `/api/v1/health` 为公开健康检查;
- 其他端点需要 `Authorization: Bearer <TOKEN>`;
- 浏览器 CORS 只允许 HTTP 的 `localhost`、`127.0.0.1` 和 `[::1]` Origin;
- 不带 Origin 的本地 CLI/Agent 请求可以使用 Token 访问;
- API 不适合直接转发到公网或绑定到不受信任的网络接口。
Token 由应用生成,使用 Electron `safeStorage` 加密保存在本机 `local-api-token.bin`,文件权限为仅当前用户可读写。你可以在“API Center”中显示、复制或重新生成 Token;重新生成会立即使旧 Token 失效。具体配置见[API 安全](../agent/api-security.md)。
## Agent 访问时发生什么
外部 Agent 通过 Reader Skill 调用本机 API,按需读取联系人、会话或时间范围内的聊天;它不会因此获得数据库文件路径或任意文件系统权限。Agent 是否把读取结果再次发送给模型,取决于 Agent 本身及其配置。
应用内 Agent Hub 是另一条路径:微信机器人通过本机 Hub 调用 TraceMemo,并且可能使用已配置的 AI 来理解问题。请把机器人账号、发送权限和日志视为独立的安全边界。
机器人收到的文字会先进入本机 Agent Hub;如果任务需要总结或自然语言理解,受控上下文可能发送给你配置的 AI Provider。机器人账号扫码登录、个人微信数据库连接和外部 Agent/API Token 是不同的边界,使用前请分别确认账号与权限。
## 你可以主动做的事
- 不要把 API Token 放进 Git、截图、URL 或公开 Skill 文件;
- 只连接你有权访问的微信数据;
- 对需要外发的 AI 功能逐项确认 Provider;
- 定期在“设置 → 缓存与清理”清理不再需要的检索、导出和索引缓存;
- 在共享电脑上退出应用并保护系统账户。
+54
View File
@@ -0,0 +1,54 @@
# 生成群聊日报和总结
如果你每天在多个群里聊天,晚上不想重新翻几十个群,可以让 TraceMemo 根据一个群的聊天内容整理出一份可阅读、可保存的报告。
## 报告适合做什么
典型场景包括:
- 整理今日工作群的讨论重点;
- 回顾昨日错过的决定和资源;
- 汇总近 7 天的项目进展、待办和未解决问题;
- 把群里的图片、语音统计和重要消息放进一张长图或 HTML 页面。
## 生成步骤
你可以从两个入口开始:打开一级导航“日报”后新建报告,或者在“档案”中选中一个群聊并点击“生成 AI 日报”。
1. 选择一个群聊。当前日报入口只支持群聊,不支持单聊。
2. 选择时间范围:今日、昨日或近 7 天。
3. 按需要选择参与总结的消息类型,先从文字开始最容易核对。
4. 选择报告模板/内容模式并开始生成。
5. 等待“整理输入 → AI 生成 → HTML/PNG 导出”完成。
报告可能包含主题、重要消息、问答、资源、待办、未解决事项、关键词、活跃统计,以及可用媒体的精选内容。具体展示内容会随消息类型、资源可用性和模型能力变化。
## 如何检查报告
报告中的重点结论会关联来源消息。对于重要决定、金额、时间和责任人,打开对应原消息核对,不要把 AI 生成的摘要当成新的事实来源。
图片无法读取时,报告可能只保留消息类型和上下文;模型未通过图片理解验证时,图片精选会被跳过。语音在日报中可参与数量和活跃度统计,但不要把统计当成语音内容已经被完整转写。
## 保存、查看和删除
生成成功后会保存本地 HTML 与 PNG,并出现在日报历史中。你可以复制图片、打开文件位置或重新生成。删除历史日报只删除本地生成的报告文件,不会影响微信聊天数据库。
## 定时日报
在“日报 → 定时日报”中可以创建每天运行的任务。选择群聊、执行时间、日报范围、消息类型和模板后,TraceMemo 会按计划执行:
```text
定时触发 → 读取群聊 → 生成报告 → 保存 Report History → 尝试发送
```
生成和发送是两个阶段。当前微信发送能力不可用、未绑定或发送失败时,报告仍会保存,PNG 和执行记录也会保留;这类结果会显示为“已生成,但未发送”或“已生成,发送失败”。
执行记录支持查看已生成的日报。对“等待发送”或“发送失败”的记录,可以直接重试发送,重试会复用已经生成的 PNG,不会重新调用 AI 生成整份报告;完整执行状态和发送边界见[如何把聊天变成可用的信息](../concepts/how-it-works.md#动作执行与审计)。
## 让报告更可靠
- 先选正确的群和时间范围;
- 不确定时先只选择文字消息;
- 群太活跃时分成“今日”和“近 7 天”两次生成;
- 看到待办和结论后回到原消息核对上下文;
- AI Provider 不可用时先检查模型配置和网络/本地服务状态。
+94
View File
@@ -0,0 +1,94 @@
# 常见问题与排查
先按现象定位,不要为了“重置”而直接删除微信数据库或整个应用目录。
## 安装后软件无法打开
### Windows
1. 确认下载的是 GitHub Releases 中的 Windows x64 `-setup.exe`,并已完成安装。
2. 安装 [Microsoft Visual C++ x64 运行库](https://aka.ms/vc14/vc_redist.x64.exe)。
3. 安装完成后重新启动 TraceMemo;如果仍无响应,再重新运行安装包进行覆盖安装。
### macOS
- 确认下载的构建与 Mac 处理器匹配:Apple Silicon(M 系列)用 `arm64`,Intel 用 `x64`。
- 提示“无法打开,因为开发者无法验证”时,前往“系统设置 → 隐私与安全性”并点击“仍要打开”。
- 提示应用已损坏时,确认应用位于“应用程序”目录,再执行 `xattr -cr "/Applications/TraceMemo.app"`。
完整安装步骤见[第一次使用 TraceMemo](./getting-started.md#2-安装并启动)。
## 连接微信失败
依次检查:
1. 数据目录是否指向当前登录账号,而不是旧备份或迁移前目录;
2. 微信版本是否属于当前代码面向的 4.x 数据结构;
3. 微信是否处于页面要求的登录/退出状态;
4. macOS 是否完成页面要求的授权;
5. 连接页面的诊断项是否明确指出密钥、账号或数据库问题;
6. macOS 上 Apple Silicon 与 Intel 使用不同的连接流程,按连接页面提示操作;两者都可以自动获取数据库密钥。
重新输入密钥或断开连接不会删除微信原始数据库。macOS 的 SIP 和授权说明见[平台说明](../platform/macos.md)。
仍然失败时,记录 **macOS 版本、CPU 架构(Apple Silicon / Intel)、微信版本、TraceMemo 版本和页面错误提示**后 扫码 README 文档二维码进群提交消息, 或者提交 Issue。
## 连接成功但没有联系人或消息
确认账号身份和数据目录匹配。返回“设置 → 账号与数据库”查看数据库连接状态,重新加载会话后再试。若仍为空,记录系统、微信版本和错误提示后提交 Issue。
## AI 没有结果或回答失败
- 先在“设置 → AI 模型”测试 Provider;
- 检查问题的时间范围和会话范围是否过窄;
- 确认 Knowledge 没有正在同步;
- 打开检索详情,查看是本地查找为空、Provider 失败还是来源被过滤;
- 把问题改成要求“只根据来源原文回答”。
AI Search 失败时可能仍保留部分来源;不要把部分结果当成完整覆盖。
## AI 答案看起来不对
打开来源和原始消息,检查发送者、时间和上下文。若来源不支持结论,扩大或缩小范围后重问。涉及未转写语音、缺失图片、转发和引用时,优先以原消息为准。
## Knowledge 一直在同步
首次建立或增量同步会在后台运行。查看“已索引消息、知识片段、磁盘占用”和同步详情;同步期间暂不能开始新的 AI 分析。若出现错误,旧索引可能仍可用,重启应用或在“缓存与清理”清理后重新建立。
## 语音转写失败
检查本地模型是否已准备、磁盘空间是否足够、单条语音是否仍有原始资源。批量任务可能部分成功;先处理失败项,不必重复转写已缓存内容。
## 媒体显示或导出异常
原图/缩略图目录缺失、权限不足或微信资源已被清理都会导致图片、视频或语音不可用。导出时可以切换缩略图、关闭媒体或保留缺失项,先确认文本档案是否正常。
文字正常但图片打不开时,进入“设置 → 图片解密”查看状态并尝试自动获取。密钥正确也不能恢复已经被微信清理的原图文件。
## 日报生成失败
日报只支持群聊。确认已选择群聊、时间范围内确实有消息、Provider 可用,并尝试先只选择文字消息。图片理解失败不会自动变成图片内容;报告可能跳过图片精选但仍生成文字日报。
## Agent 无法读取
确认:
1. TraceMemo 正在运行且 API Center 显示本地服务在线;
2. Agent 使用的是当前 Reader Skill,而不是旧的 MCP 配置;
3. 请求地址为 `http://127.0.0.1:6131`;
4. 非 health 请求带有最新 `Authorization: Bearer <TOKEN>`;
5. Token 重新生成后,Agent 配置已同步更新。
详细步骤见[Agent 接入概览](../agent/overview.md)和[API 安全](../agent/api-security.md)。
## 微信机器人无法连接或不回复
Agent Hub 和外部 Agent 是两条路径。机器人异常时依次确认:
1. “Agent”页面中的 Agent Hub、微信连接器和数据库状态是否正常;
2. 二维码是否过期,手机是否已经确认登录;
3. 是否由另一个微信账号向已登录的机器人账号发送文字;
4. 请求是否属于当前支持的最近会话、联系人聊天、近 7 天联系人总结、群聊总结或群成员发言总结;
5. 需要总结或自然语言理解时,AI Provider 是否可用。
当前机器人不支持群发、定时任务或与文字同等的图片、语音、文件和视频理解。详细边界见[Agent Hub](../agent/agent-hub.md)。
+37
View File
@@ -0,0 +1,37 @@
# 语音转文字
TraceMemo 可以把微信语音转换成可搜索的文字,适合你不想逐条播放、希望把语音内容带入后续查找或导出的场景。
## 使用前准备
1. 打开“设置 → 语音识别”。
2. 按页面提示准备或下载本地语音模型。
3. 等待模型状态显示可用。
语音识别使用本地 SenseVoice/sherpa-onnx 运行时。首次准备模型可能需要下载文件和占用额外磁盘空间;模型文件可以从设置中删除,之后需要重新准备。
## 转写单条语音
在聊天档案中找到语音消息,点击转写入口。完成后,转写文本会与该消息关联,并可用于后续查看或检索。失败时查看消息提示和模型状态。
## 批量转写
在语音设置中选择联系人或群聊,再选择范围:
- 最近 30 天;
- 当前年份;
- 选择的历史范围。
开始前页面会显示语音条数、已缓存数量、待处理数量和预计耗时。批量任务支持进度、取消、缓存复用,并可能以“部分失败”结束;部分失败时可以根据列表重新处理未成功内容。
## 和 AI、知识库、导出的关系
- 本地转写结果可以参与本地知识库检索;
- 导出时可选择是否包含已有语音转写;
- AI Search 可能提示某些语音尚未转写,这意味着答案覆盖不完整;
- 群聊日报默认会统计语音数量和时长,但不等于已经理解了每条语音的具体内容。
## 隐私提示
离线转写本身在本机完成。若你主动把转写结果用于 AI Search、日报或其他 AI 功能,受控文本可能按对应功能的规则发送给你配置的 Provider;详见[数据、隐私与安全](./privacy.md)。
+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
+45 -11
View File
@@ -1,5 +1,6 @@
appId: com.electron.app
productName: wechatexplorer
appId: com.tracememo.app
productName: TraceMemo
afterPack: scripts/after-pack.cjs
directories:
buildResources: build
files:
@@ -14,24 +15,53 @@ extraMetadata:
main: out/main/index.js
asarUnpack:
- resources/**
win:
executableName: wechatexplorer
- node_modules/ffmpeg-static/**
- 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:
- '**/*'
- from: docs/skill/tracememo-reader
to: skill/tracememo-reader
filter:
- '**/*'
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
icon: icon.icns
target:
- dmg
- zip
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
CFBundleDisplayName: TraceMemo
NSCameraUsageDescription: Application requests access to the device's camera.
NSMicrophoneUsageDescription: Application requests access to the device's microphone.
NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
artifactName: ${name}-${version}-${arch}.${ext}
linux:
icon: icon.png
target:
- AppImage
- snap
@@ -42,5 +72,9 @@ appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
provider: github
owner: Wxw-Gu
repo: TraceMemo
# 一律先上传为草稿 再到 GitHub 上手动 Publish。
# 需要预发布时用 `pnpm release:beta`(EP_PRE_RELEASE 会覆盖这里的 draft)。
releaseType: draft
+17 -1
View File
@@ -3,7 +3,23 @@ import { defineConfig } from 'electron-vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
main: {},
main: {
build: {
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', '@napi-rs/system-ocr']
}
}
},
preload: {},
renderer: {
resolve: {
+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>
+140
View File
@@ -0,0 +1,140 @@
"use strict";
const electron = require("electron");
const preload = require("@electron-toolkit/preload");
const api = {
writeAppLog: (entry) => electron.ipcRenderer.invoke("app-log:write", entry),
getAppLogPath: () => electron.ipcRenderer.invoke("app-log:getPath"),
revealAppLog: () => electron.ipcRenderer.invoke("app-log:reveal"),
getAppUpdateState: () => electron.ipcRenderer.invoke("app-update:getState"),
checkAppUpdate: () => electron.ipcRenderer.invoke("app-update:check"),
downloadAppUpdate: () => electron.ipcRenderer.invoke("app-update:download"),
installAppUpdate: () => electron.ipcRenderer.invoke("app-update:install"),
onAppUpdateState: (callback) => {
const listener = (_event, state) => callback(state);
electron.ipcRenderer.on("app-update:state", listener);
return () => electron.ipcRenderer.removeListener("app-update:state", listener);
},
getCacheSummary: () => electron.ipcRenderer.invoke("cache:getSummary"),
clearCache: (scope) => electron.ipcRenderer.invoke("cache:clear", scope),
initDb: (key) => electron.ipcRenderer.invoke("db:init", key),
getBootstrapCache: () => electron.ipcRenderer.invoke("db:getBootstrapCache"),
getStartupCache: () => electron.ipcRenderer.invoke("db:getStartupCache"),
getContacts: (filter) => electron.ipcRenderer.invoke("db:getContacts", filter),
getContactAvatars: (usernames) => electron.ipcRenderer.invoke("db:getContactAvatars", usernames),
getCachedMessages: (userMd5, startTime, endTime) => electron.ipcRenderer.invoke("db:getCachedMessages", userMd5, startTime, endTime),
getCachedMessagePage: (userMd5, startTime, endTime) => electron.ipcRenderer.invoke("db:getCachedMessagePage", userMd5, startTime, endTime),
getMessages: (userMd5, startTime, endTime, options) => electron.ipcRenderer.invoke("db:getMessages", userMd5, startTime, endTime, options),
getGroupSnapshot: (userMd5) => electron.ipcRenderer.invoke("db:getGroupSnapshot", userMd5),
search: (keyword) => electron.ipcRenderer.invoke("db:search", keyword),
aiChat: (messages, options) => electron.ipcRenderer.invoke("ai:chat", messages, options),
listAIProviders: () => electron.ipcRenderer.invoke("ai:listProviders"),
getAIRuntimeConfig: () => electron.ipcRenderer.invoke("ai:getRuntimeConfig"),
saveAIProvider: (provider) => electron.ipcRenderer.invoke("ai:saveProvider", provider),
deleteAIProvider: (providerId) => electron.ipcRenderer.invoke("ai:deleteProvider", providerId),
setDefaultAIProvider: (providerId) => electron.ipcRenderer.invoke("ai:setDefaultProvider", providerId),
testAIProvider: (providerId) => electron.ipcRenderer.invoke("ai:testProvider", providerId),
testAIVision: (request) => electron.ipcRenderer.invoke("ai:testVision", request),
migrateLegacyAIConfig: (config) => electron.ipcRenderer.invoke("ai:migrateLegacy", config),
copyImage: (base64String) => electron.ipcRenderer.invoke("copy-image", base64String),
getVoiceData: (sessionId, localId, createTime, svrId) => electron.ipcRenderer.invoke("db:getVoiceData", sessionId, localId, createTime, svrId),
parseMessage: (content, messageType) => electron.ipcRenderer.invoke("db:parseMessage", content, messageType),
getImage: (imageMd5, imageDatNameOrThumb, sessionId, options) => electron.ipcRenderer.invoke("db:getImage", imageMd5, imageDatNameOrThumb, sessionId, options),
getVideo: (hashes) => electron.ipcRenderer.invoke("db:getVideo", hashes),
getSticker: (cdnUrl, md5) => electron.ipcRenderer.invoke("db:getSticker", cdnUrl, md5),
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);
return () => electron.ipcRenderer.removeListener("export:progress", listener);
},
exportGroupReport: (request) => electron.ipcRenderer.invoke("report:export", request),
listGeneratedReports: () => electron.ipcRenderer.invoke("report:listGenerated"),
saveGeneratedReport: (request) => electron.ipcRenderer.invoke("report:saveGenerated", request),
deleteGeneratedReport: (reportId) => electron.ipcRenderer.invoke("report:deleteGenerated", reportId),
revealGroupReport: (filePath) => electron.ipcRenderer.invoke("report:reveal", filePath),
getSavedDbKey: () => electron.ipcRenderer.invoke("key:getSavedDbKey"),
getDatabaseKeyEnvironment: () => electron.ipcRenderer.invoke("key:getEnvironment"),
readDatabaseKeyClipboard: () => electron.ipcRenderer.invoke("key:readClipboardDbKey"),
autoGetDbKey: (options) => electron.ipcRenderer.invoke("key:autoGetDbKey", options),
autoGetImageKey: (options) => electron.ipcRenderer.invoke("key:autoGetImageKey", options),
getImageKeyConfig: () => electron.ipcRenderer.invoke("image:getConfig"),
getImageDecryptionStatus: () => electron.ipcRenderer.invoke("image:getStatus"),
saveImageKeyConfig: (request) => electron.ipcRenderer.invoke("image:saveConfig", request),
testImageDecryption: (request) => electron.ipcRenderer.invoke("image:testConfig", request),
clearImageKeyConfig: () => electron.ipcRenderer.invoke("image:clearConfig"),
pasteAndSaveDbKey: () => electron.ipcRenderer.invoke("key:pasteAndSaveDbKey"),
saveDbKey: (key) => electron.ipcRenderer.invoke("key:saveDbKey", key),
clearSavedDbKey: () => electron.ipcRenderer.invoke("key:clearSavedDbKey"),
onWcdbChange: (callback) => {
const listener = (_event, payload) => callback(payload);
electron.ipcRenderer.on("wcdb-change", listener);
return () => electron.ipcRenderer.removeListener("wcdb-change", listener);
},
onDbKeyStatus: (callback) => {
const listener = (_event, payload) => callback(payload);
electron.ipcRenderer.on("key:dbKeyStatus", listener);
return () => electron.ipcRenderer.removeListener("key:dbKeyStatus", listener);
},
onImageKeyStatus: (callback) => {
const listener = (_event, payload) => callback(payload);
electron.ipcRenderer.on("key:imageKeyStatus", listener);
return () => electron.ipcRenderer.removeListener("key:imageKeyStatus", listener);
},
getSettings: () => electron.ipcRenderer.invoke("settings:get"),
setSettings: (patch) => electron.ipcRenderer.invoke("settings:set", patch),
getSelf: () => electron.ipcRenderer.invoke("settings:getSelf"),
testConnection: (key, accountRoot) => electron.ipcRenderer.invoke("db:testConnection", key, accountRoot),
reopenWithRoot: (accountRoot) => electron.ipcRenderer.invoke("db:reopenWithRoot", accountRoot),
selectDbRoot: () => electron.ipcRenderer.invoke("settings:selectDbRoot"),
openAccountRoot: () => electron.ipcRenderer.invoke("settings:openAccountRoot"),
disconnectDb: (options) => electron.ipcRenderer.invoke("db:disconnect", options),
apiStatus: () => electron.ipcRenderer.invoke("api:getStatus"),
apiStart: (host, port) => electron.ipcRenderer.invoke("api:start", host, port),
apiStop: () => electron.ipcRenderer.invoke("api:stop"),
apiToggle: (enabled) => electron.ipcRenderer.invoke("api:toggle", enabled),
getReaderSkillStatus: () => electron.ipcRenderer.invoke("api:skillStatus"),
readReaderSkill: () => electron.ipcRenderer.invoke("api:readSkill"),
revealReaderSkill: () => electron.ipcRenderer.invoke("api:revealSkill"),
openReaderSkillGithub: () => electron.ipcRenderer.invoke("api:openSkillGithub"),
testLocalApiRequest: (request) => electron.ipcRenderer.invoke("api:testLocalRequest", request),
copyText: (text) => electron.ipcRenderer.invoke("api:copyText", text),
// ============================================================
// AI 图片理解基础设施(ImageInsightService)
// ============================================================
imageListCandidates: (query) => electron.ipcRenderer.invoke("image:listCandidates", query),
imageAnalyze: (request) => electron.ipcRenderer.invoke("image:analyze", request),
getImageInsight: (imageHash) => electron.ipcRenderer.invoke("image:getInsight", imageHash),
listImageInsights: (sessionId, limit) => electron.ipcRenderer.invoke("image:listInsights", sessionId, limit),
getAgentHubStatus: () => electron.ipcRenderer.invoke("agent-hub:getStatus"),
getAgentHubLogs: () => electron.ipcRenderer.invoke("agent-hub:getLogs"),
clearAgentHubLogs: () => electron.ipcRenderer.invoke("agent-hub:clearLogs"),
startAgentHubLogin: () => electron.ipcRenderer.invoke("agent-hub:startLogin"),
cancelAgentHubLogin: () => electron.ipcRenderer.invoke("agent-hub:cancelLogin"),
reconnectAgentHub: () => electron.ipcRenderer.invoke("agent-hub:reconnect"),
disconnectAgentHub: () => electron.ipcRenderer.invoke("agent-hub:disconnect"),
selectAgentHubTestImage: () => electron.ipcRenderer.invoke("agent-hub:selectTestImage"),
onAgentHubStatus: (callback) => {
const listener = (_event, status) => callback(status);
electron.ipcRenderer.on("agent-hub:status", listener);
return () => electron.ipcRenderer.removeListener("agent-hub:status", listener);
},
onAgentHubLog: (callback) => {
const listener = (_event, entry) => callback(entry);
electron.ipcRenderer.on("agent-hub:log", listener);
return () => electron.ipcRenderer.removeListener("agent-hub:log", listener);
}
};
if (process.contextIsolated) {
try {
electron.contextBridge.exposeInMainWorld("electron", preload.electronAPI);
electron.contextBridge.exposeInMainWorld("api", api);
} catch (error) {
console.error(error);
}
} else {
window.electron = preload.electronAPI;
window.api = api;
}
+125 -18
View File
@@ -1,65 +1,172 @@
{
"name": "wechatexplorer",
"version": "1.0.2",
"description": "mac 版本获取微信聊天记录, AI群聊总结助手",
"name": "tracememo",
"version": "2.4.0",
"packageManager": "pnpm@7.33.7",
"description": "TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。 原名 WechatExplorer,支持聊天记录搜索、知识库、微信群聊总结和 Agent 助手。",
"keywords": [
"wechat",
"chat",
"wechat chat",
"wechat history",
"mac微信",
"windows微信",
"微信聊天记录",
"AI群聊总结助手"
"微信聊天记录搜索",
"微信群聊总结",
"微信AI",
"微信机器人",
"AI聊天搜索",
"AI群聊总结",
"本地AI"
],
"author": "Qingmao",
"repository": {
"type": "git",
"url": "https://github.com/Wxw-Gu/TraceMemo.git"
},
"main": "./out/main/index.js",
"scripts": {
"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": "electron-vite dev",
"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",
"benchmark:knowledge": "vitest run --config vitest.knowledge-benchmark.config.ts --reporter=verbose",
"benchmark:knowledge:capacity": "cross-env KNOWLEDGE_CAPACITY=1 vitest run --config vitest.knowledge-benchmark.config.ts --reporter=verbose",
"test:e2e:build": "electron-vite build",
"test:knowledge-worker": "pnpm test:e2e:build && node scripts/test-knowledge-worker.cjs",
"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": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux"
"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 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: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 && 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",
"better-sqlite3-multiple-ciphers": "^12.5.0",
"@koromix/koffi-win32-x64": "3.1.0",
"@napi-rs/system-ocr": "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",
"html-to-image": "^1.11.13",
"openai": "^6.10.0"
"fzstd": "^0.1.1",
"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-node": "1.13.3",
"silk-wasm": "^3.7.1",
"tailwind-merge": "^3.6.0",
"unzipper": "^0.12.0",
"wechat-emojis": "^1.0.2"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^2.0.0",
"@playwright/test": "^1.62.1",
"@rollup/rollup-darwin-arm64": "^4.62.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@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",
"electron": "^39.2.6",
"@vitest/coverage-v8": "^4.1.10",
"autoprefixer": "^10.5.4",
"electron": "^43.0.0",
"electron-builder": "^26.0.12",
"electron-vite": "^5.0.0",
"eslint": "^9.39.1",
"eslint-plugin-react": "^7.37.5",
"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"
"vite": "^7.2.6",
"vitest": "^4.1.10",
"wrangler": "^4.28.1"
},
"pnpm": {
"supportedArchitectures": {
"os": [
"current",
"win32"
],
"cpu": [
"current",
"x64"
]
},
"onlyBuiltDependencies": [
"electron",
"esbuild"
"esbuild",
"ffmpeg-static"
]
}
}
+22
View File
@@ -0,0 +1,22 @@
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
testMatch: /.*\.spec\.ts/,
timeout: 45_000,
expect: { timeout: 8_000 },
fullyParallel: false,
workers: 1,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
reporter: process.env.CI
? [['line'], ['html', { outputFolder: 'playwright-report', open: 'never' }]]
: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
outputDir: 'test-results',
snapshotPathTemplate: 'tests/e2e/__screenshots__/{platform}/{testFilePath}/{arg}{ext}',
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
}
})
+4109 -293
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.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 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.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+808
View File
@@ -0,0 +1,808 @@
<!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;
}
::-webkit-scrollbar {
width: 0;
height: 0;
}
html {
width: 430px;
scrollbar-width: none;
}
body {
margin: 0;
width: 430px;
background: #f3f5f7;
color: #1f2933;
font-family:
-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.report {
width: 430px;
margin: 0 auto;
padding: 20px 14px 34px;
}
.hero,
.section,
.card {
background: #fff;
border-radius: 18px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.hero {
padding: 20px;
}
.hero-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.hero-top > div:first-child {
min-width: 0;
flex: 1 1 auto;
}
.hero h1 {
margin: 0;
font-size: 23px;
line-height: 1.2;
font-weight: 900;
}
.sub {
margin-top: 8px;
color: #667085;
font-size: 13px;
line-height: 1.55;
}
.mode-tag {
display: inline-flex;
margin-top: 10px;
padding: 4px 10px;
border-radius: 999px;
background: #eef8f2;
color: #07a352;
font-size: 11px;
font-weight: 800;
}
.avatar-grid {
width: 58px;
height: 58px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3px;
flex: 0 0 auto;
}
.avatar-grid img,
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.hero-headline {
margin-top: 14px;
padding: 14px;
border-radius: 16px;
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
}
.hero-headline b {
display: block;
font-size: 17px;
color: #076c39;
}
.hero-headline p {
margin: 8px 0 0;
font-size: 13px;
line-height: 1.65;
color: #1f2933;
}
.hero-inline-notes {
display: grid;
gap: 8px;
margin-top: 10px;
}
.hero-note,
.hero-status {
padding: 10px 12px;
border-radius: 12px;
font-size: 12px;
line-height: 1.5;
}
.hero-status {
margin-top: 10px;
background: #f7faf9;
color: #076c39;
font-weight: 700;
}
.hero-note.takeaway {
background: #eef8f2;
color: #076c39;
}
.hero-note.pending {
background: #fff8e8;
color: #8a5a00;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-top: 16px;
}
.stat {
background: #f7faf9;
border-radius: 12px;
padding: 10px 6px;
text-align: center;
}
.stat b {
display: block;
font-size: 18px;
color: #07a352;
line-height: 1.25;
}
.stat span {
font-size: 11px;
color: #667085;
}
.section {
margin-top: 18px;
padding: 18px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 14px;
font-size: 19px;
font-weight: 900;
}
.section-title::before {
content: '';
width: 5px;
height: 20px;
border-radius: 99px;
background: #07c160;
}
.section-subtitle {
margin: 10px 0 6px;
color: #667085;
font-size: 12px;
font-weight: 700;
}
.section-more {
margin-top: 10px;
color: #98a2b3;
font-size: 11px;
text-align: right;
}
.card {
padding: 14px;
margin-top: 10px;
border: 1px solid #edf0f2;
box-shadow: none;
}
.topic-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.topic-title-row h3 {
margin: 0;
font-size: 16px;
line-height: 1.35;
font-weight: 850;
}
.heat,
.tag {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 999px;
background: #eef8f2;
color: #07a352;
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.hot {
background: #fff4e5;
color: #d46b08;
}
.blue {
background: #eef5ff;
color: #1677ff;
}
.topic-meta {
margin-top: 6px;
color: #8a94a6;
font-size: 12px;
}
.card p {
margin: 10px 0 0;
font-size: 13px;
line-height: 1.65;
}
.topic-conclusions {
display: grid;
gap: 8px;
margin-top: 10px;
}
.topic-conclusion {
padding: 8px 10px;
border-radius: 10px;
background: #edf9f1;
color: #076c39;
font-size: 12px;
line-height: 1.5;
font-weight: 700;
}
.topic-inline-image {
display: grid;
grid-template-columns: 76px 1fr;
gap: 10px;
margin-top: 10px;
padding: 10px;
border-radius: 12px;
background: #f7faf9;
}
.topic-inline-image img {
width: 76px;
height: 76px;
border-radius: 10px;
object-fit: cover;
}
.participants {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.person-chip {
display: inline-flex;
align-items: center;
gap: 5px;
background: #f6f8fa;
border-radius: 999px;
padding: 3px 8px 3px 3px;
}
.person-chip img {
width: 24px;
height: 24px;
border-radius: 50%;
}
.person-chip b {
max-width: 58px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
}
.keywords {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.keywords span {
font-size: 11px;
padding: 4px 8px;
border-radius: 999px;
background: #f2f4f7;
color: #667085;
}
.important-card {
display: flex;
gap: 10px;
background: #f7faf9;
border-radius: 14px;
padding: 12px;
margin-top: 10px;
}
.important-card > .avatar {
width: 36px;
height: 36px;
flex: 0 0 auto;
}
.important-meta {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 12px;
color: #667085;
}
.important-meta b {
color: #1f2933;
}
.important-text {
margin-top: 5px;
font-size: 13px;
line-height: 1.55;
}
.important-note {
margin-top: 8px;
padding: 7px 9px;
border-left: 3px solid #07c160;
background: #fff;
border-radius: 8px;
color: #07a352;
font-size: 12px;
line-height: 1.45;
}
.action-grid {
display: grid;
gap: 10px;
}
.action-card {
border-radius: 14px;
padding: 12px;
}
.todo-card {
background: #eef5ff;
}
.unresolved-card {
background: #fff8e8;
}
.action-card b {
display: block;
color: #1f2933;
font-size: 14px;
}
.action-card div {
margin-top: 6px;
font-size: 12px;
line-height: 1.55;
color: #485465;
}
.action-note {
color: #667085;
}
.chat-block {
background: #f0f2f5;
border-radius: 14px;
padding: 12px;
margin-top: 10px;
}
.chat-msg {
display: flex;
gap: 8px;
margin-top: 8px;
}
.chat-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.chat-name {
font-size: 11px;
color: #667085;
margin-bottom: 4px;
}
.chat-bubble {
background: #fff;
border-radius: 4px 12px 12px 12px;
padding: 9px 10px;
font-size: 13px;
line-height: 1.5;
}
.quote-note {
margin-top: 10px;
padding: 9px 10px;
border-radius: 10px;
background: #fff8e1;
color: #8a5a00;
font-size: 12px;
line-height: 1.5;
}
.qa-card,
.resource {
margin-top: 10px;
padding: 12px;
border-radius: 14px;
background: #f8fafc;
}
.qa-card b,
.resource b {
display: block;
color: #1f2933;
margin-bottom: 5px;
}
.qa-card div,
.resource {
font-size: 13px;
line-height: 1.55;
color: #485465;
}
.storyline-card,
.chain-card {
background: #f8fafc;
}
.storyline-steps {
display: grid;
gap: 8px;
margin-top: 10px;
}
.storyline-step {
display: grid;
grid-template-columns: 50px 1fr;
gap: 10px;
}
.storyline-step span {
color: #8a94a6;
font-size: 12px;
}
.storyline-step b {
font-size: 13px;
line-height: 1.5;
}
.chain-flow {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
margin-top: 10px;
}
.chain-flow span {
display: inline-flex;
align-items: center;
padding: 6px 9px;
border-radius: 999px;
background: #eef8f2;
color: #076c39;
font-size: 12px;
font-weight: 700;
}
.chain-flow i {
font-style: normal;
color: #98a2b3;
}
.badge-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
/* AI 图片识别板块 */
.vision-card {
display: grid;
grid-template-columns: 132px 1fr;
gap: 14px;
margin-top: 10px;
padding: 12px;
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
border: 1px solid #d6efde;
border-radius: 14px;
}
.vision-image {
width: 132px;
height: 132px;
border-radius: 12px;
object-fit: cover;
background: #e5e7eb;
}
.vision-description {
margin-top: 6px;
font-size: 13px;
line-height: 1.5;
color: #1f2933;
}
.vision-ocr {
margin-top: 6px;
padding: 6px 10px;
background: #eef5ff;
color: #1677ff;
font-size: 11px;
border-radius: 8px;
word-break: break-all;
}
.vision-tags {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.vision-tag {
display: inline-block;
padding: 3px 8px;
background: #07c160;
color: #fff;
font-size: 10px;
font-weight: 700;
border-radius: 999px;
}
.vision-label {
margin-top: 6px;
font-size: 10px;
color: #07a352;
font-weight: 600;
}
.badge-card {
background: linear-gradient(180deg, #fdfdfd 0%, #f6fbf8 100%);
border: 1px solid #edf0f2;
border-radius: 14px;
padding: 12px;
}
.badge-card b {
display: block;
margin-top: 8px;
font-size: 15px;
}
.badge-card p {
margin: 8px 0 0;
font-size: 12px;
line-height: 1.55;
}
.data-grid {
display: grid;
gap: 12px;
}
.rank {
display: flex;
align-items: center;
gap: 9px;
padding: 9px 0;
border-bottom: 1px solid #eef0f2;
}
.rank:last-child {
border-bottom: none;
}
.rank img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
}
.rank b {
font-size: 13px;
}
.rank span {
margin-left: auto;
color: #8a94a6;
font-size: 12px;
}
.cloud-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cloud-tags span {
padding: 6px 10px;
border-radius: 999px;
background: #f2f4f7;
color: #485465;
font-weight: 800;
}
.cloud-tags .xl {
font-size: 20px;
color: #07a352;
background: #e9f8ef;
}
.cloud-tags .lg {
font-size: 17px;
color: #1677ff;
background: #eef5ff;
}
.cloud-tags .md {
font-size: 15px;
color: #d46b08;
background: #fff4e5;
}
.footer {
padding: 16px 4px 0;
color: #98a2b3;
text-align: center;
font-size: 11px;
line-height: 1.8;
}
.muted {
color: #8a94a6;
}
.empty-section {
display: none !important;
}
.compact .report {
padding-top: 18px;
}
.compact .section {
margin-top: 12px;
padding: 14px;
}
.compact .card {
padding: 11px;
}
.compact .important-card,
.compact .chat-block {
padding: 10px;
}
.compact .section-title {
margin-bottom: 9px;
font-size: 17px;
}
.compact .hero-headline p,
.compact .card p,
.compact .important-text,
.compact .chat-bubble {
line-height: 1.5;
}
.compact .participants,
.compact .keywords,
.compact .hero-inline-notes {
gap: 6px;
}
.compact .topic-conclusions {
gap: 6px;
}
.compact .topic-inline-image {
grid-template-columns: 64px 1fr;
padding: 8px;
}
.compact .topic-inline-image img {
width: 64px;
height: 64px;
}
.compact .stats {
gap: 6px;
margin-top: 14px;
}
.compact .stat {
padding: 8px 6px;
}
.compact .stat b {
font-size: 17px;
}
@media (max-width: 430px) {
html,
body {
width: 100%;
}
.report {
width: 100%;
padding-left: 12px;
padding-right: 12px;
}
}
</style>
</head>
<body class="{{REPORT_MODE_CLASS}}">
<main class="report">
<header class="hero">
<div class="hero-top">
<div>
<h1>{{GROUP_NAME}}日报</h1>
<div class="sub">{{DATE_RANGE}}<br />{{RECORD_NOTE}}</div>
<div class="mode-tag">{{REPORT_MODE_LABEL}}</div>
</div>
<div class="avatar-grid">{{HERO_AVATARS}}</div>
</div>
<div class="hero-headline">
<b>{{HERO_HEADLINE}}</b>
<p>{{HERO_SUMMARY}}</p>
</div>
<div class="hero-status {{HERO_STATUS_EMPTY_CLASS}}">{{HERO_STATUS_LINE}}</div>
<div class="hero-inline-notes">
<div class="hero-note takeaway {{HERO_TAKEAWAY_EMPTY_CLASS}}">{{HERO_TAKEAWAY}}</div>
<div class="hero-note pending {{HERO_PENDING_EMPTY_CLASS}}">{{HERO_PENDING}}</div>
</div>
<div class="stats">
<div class="stat"><b>{{MESSAGE_COUNT}}</b><span>消息数</span></div>
<div class="stat"><b>{{ACTIVE_USERS}}</b><span>活跃人数</span></div>
<div class="stat"><b>{{TOPIC_COUNT}}</b><span>话题数</span></div>
<div class="stat"><b>{{MEDIA_COUNT}}</b><span>媒体消息</span></div>
</div>
</header>
<section class="section {{TOPICS_EMPTY_CLASS}}">
<div class="section-title">今日讨论热点</div>
{{TOPIC_CARDS}}
{{TOPICS_MORE_NOTE}}
</section>
<section class="section {{MESSAGES_EMPTY_CLASS}}">
<div class="section-title">重要消息</div>
{{IMPORTANT_MESSAGES}}
{{MESSAGES_MORE_NOTE}}
</section>
<section class="section {{ACTIONS_EMPTY_CLASS}}">
<div class="section-title">待办事项和未解决问题</div>
<div class="section-subtitle {{TODO_EMPTY_CLASS}}">待办事项</div>
<div class="action-grid {{TODO_EMPTY_CLASS}}">{{TODO_CARDS}}</div>
<div class="section-subtitle {{UNRESOLVED_EMPTY_CLASS}}">尚未解决</div>
<div class="action-grid {{UNRESOLVED_EMPTY_CLASS}}">{{UNRESOLVED_CARDS}}</div>
{{ACTIONS_MORE_NOTE}}
</section>
<section class="section {{QUOTES_EMPTY_CLASS}}">
<div class="section-title">今日名场面</div>
{{QUOTE_BLOCKS}}
{{QUOTES_MORE_NOTE}}
</section>
<section class="section {{ANALYTICS_EMPTY_CLASS}}">
<div class="section-title">今日群数据</div>
<div class="data-grid">
<div class="card">
<div class="muted" style="font-size: 12px; margin-bottom: 6px">话唠榜 TOP5</div>
{{RANK_ITEMS}}
</div>
<div class="card">
<p><b>最活跃时段:</b>{{ACTIVITY_TIMELINE}}</p>
<p><b>今日状态:</b>形成 {{CONCLUSION_COUNT}} 个结论,待办 {{TODO_COUNT}} 项,未解决 {{UNRESOLVED_COUNT}} 项。</p>
</div>
</div>
</section>
<section class="section {{KEYWORDS_EMPTY_CLASS}}">
<div class="section-title">关键词</div>
<div class="cloud-tags">{{CLOUD_TAGS}}</div>
{{KEYWORDS_MORE_NOTE}}
</section>
<section class="section {{RESOURCES_EMPTY_CLASS}}">
<div class="section-title">实用信息与资源</div>
{{RESOURCE_ITEMS}}
{{RESOURCES_MORE_NOTE}}
</section>
<section class="section {{QA_EMPTY_CLASS}}">
<div class="section-title">问题与解答</div>
{{QA_CARDS}}
{{QA_MORE_NOTE}}
</section>
<section class="section {{STORYLINES_EMPTY_CLASS}}">
<div class="section-title">今日剧情时间线</div>
{{STORYLINE_CARDS}}
{{STORYLINES_MORE_NOTE}}
</section>
<section class="section {{REVERSALS_EMPTY_CLASS}}">
<div class="section-title">群聊反转现场</div>
{{REVERSAL_CARDS}}
{{REVERSALS_MORE_NOTE}}
</section>
<section class="section {{VISION_EMPTY_CLASS}}">
<div class="section-title">{{VISION_TITLE}}</div>
{{VISION_CARDS}}
</section>
<section class="section {{VOICE_EMPTY_CLASS}}">
<div class="section-title">语音之最</div>
{{VOICE_CARDS}}
{{VOICE_MORE_NOTE}}
</section>
<section class="section {{VOICE_RANK_EMPTY_CLASS}}">
<div class="section-title">语音时长榜</div>
<div class="card">{{VOICE_RANK_CARDS}}</div>
</section>
<section class="section {{BADGES_EMPTY_CLASS}}">
<div class="section-title">今日临时人设</div>
<div class="badge-grid">{{BADGE_CARDS}}</div>
{{BADGES_MORE_NOTE}}
</section>
<section class="section {{CHAINS_EMPTY_CLASS}}">
<div class="section-title">话题参与链路</div>
{{CHAIN_CARDS}}
{{CHAINS_MORE_NOTE}}
</section>
<footer class="footer">
数据来源:微信群聊记录<br />
生成时间:{{GENERATED_AT}}<br />
{{FOOTER_NOTE}}
</footer>
</main>
</body>
</html>
+611
View File
@@ -0,0 +1,611 @@
<!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;
}
::-webkit-scrollbar {
width: 0;
height: 0;
}
html {
width: 430px;
scrollbar-width: none;
}
body {
margin: 0;
width: 430px;
background: #f3f5f7;
color: #1f2933;
font-family:
-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.report {
width: 430px;
margin: 0 auto;
padding: 22px 14px 34px;
}
.hero,
.card,
.section {
background: #fff;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.hero {
padding: 20px;
}
.hero-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
min-width: 0;
}
.hero-top > div:first-child {
min-width: 0;
flex: 1 1 auto;
}
.hero h1 {
font-size: 23px;
line-height: 1.2;
margin: 0 0 8px;
font-weight: 900;
}
.sub {
color: #667085;
font-size: 13px;
line-height: 1.5;
}
.record-note {
color: #485465;
font-weight: 650;
}
.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 {
display: grid;
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 {
grid-template-columns: var(--tm-avatar-hero-size, 40px);
}
.avatar-grid.empty-section {
display: none;
}
.avatar-grid img,
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-top: 16px;
}
.stat {
background: #f7faf9;
border-radius: 12px;
padding: 10px 6px;
text-align: center;
min-width: 0;
overflow: hidden;
}
.stat b {
display: block;
font-size: 18px;
color: #07a352;
white-space: nowrap;
line-height: 1.25;
}
.stat span {
font-size: 11px;
color: #667085;
}
.section {
margin-top: 14px;
padding: 18px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 900;
margin-bottom: 12px;
}
.section-title::before {
content: '';
width: 5px;
height: 20px;
border-radius: 99px;
background: #07c160;
}
.card {
padding: 14px;
margin-top: 10px;
box-shadow: none;
border: 1px solid #edf0f2;
}
.topic-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.topic-title-row h3 {
font-size: 16px;
line-height: 1.35;
margin: 0;
font-weight: 850;
}
.heat,
.tag {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 4px 8px;
background: #eef8f2;
color: #07a352;
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.hot {
background: #fff4e5;
color: #d46b08;
}
.blue {
background: #eef5ff;
color: #1677ff;
}
.red {
background: #fff1f0;
color: #ff4d4f;
}
.topic-meta {
margin-top: 6px;
color: #8a94a6;
font-size: 12px;
}
.card p {
font-size: 13px;
line-height: 1.65;
margin: 10px 0 0;
}
.participants {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.person-chip {
display: inline-flex;
align-items: center;
gap: 5px;
min-width: 0;
background: #f6f8fa;
border-radius: 999px;
padding: 3px 8px 3px 3px;
}
.person-chip img {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
.person-chip b {
max-width: 58px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
}
.keywords {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.keywords span {
font-size: 11px;
padding: 4px 8px;
border-radius: 999px;
background: #f2f4f7;
color: #667085;
}
.resource {
padding: 11px 12px;
background: #f7f8fa;
border-radius: 12px;
margin-top: 8px;
font-size: 13px;
line-height: 1.55;
}
.resource b {
color: #1677ff;
}
.important-card {
display: flex;
gap: 10px;
background: #f7faf9;
border-radius: 14px;
padding: 12px;
margin-top: 10px;
}
.important-card > .avatar {
width: 36px;
height: 36px;
flex: 0 0 auto;
}
.important-meta {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 12px;
color: #667085;
}
.important-meta b {
color: #1f2933;
}
.important-text {
font-size: 13px;
line-height: 1.55;
margin-top: 5px;
}
.important-note {
margin-top: 8px;
padding: 7px 9px;
border-left: 3px solid #07c160;
background: #fff;
border-radius: 8px;
color: #07a352;
font-size: 12px;
line-height: 1.45;
}
.chat-block {
background: #f0f2f5;
border-radius: 14px;
padding: 12px;
margin-top: 10px;
}
.chat-msg {
display: flex;
gap: 8px;
margin-top: 8px;
}
.chat-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.chat-name {
font-size: 11px;
color: #667085;
margin-bottom: 4px;
}
.chat-bubble {
background: #fff;
border-radius: 4px 12px 12px 12px;
padding: 9px 10px;
font-size: 13px;
line-height: 1.5;
}
.quote-note {
background: #fff8e1;
border-radius: 10px;
padding: 9px 10px;
margin-top: 10px;
color: #8a5a00;
font-size: 12px;
line-height: 1.5;
}
.qa-card {
background: #f8fafc;
border-radius: 14px;
padding: 12px;
margin-top: 10px;
}
.qa-card b {
display: block;
color: #1f2933;
margin-bottom: 5px;
}
.qa-card div {
font-size: 13px;
line-height: 1.55;
color: #485465;
}
.bar-row {
display: grid;
grid-template-columns: 82px 1fr;
gap: 8px;
align-items: center;
margin-top: 9px;
font-size: 12px;
}
.bar {
height: 10px;
background: #edf1f5;
border-radius: 999px;
overflow: hidden;
}
.bar i {
display: block;
height: 100%;
background: #07c160;
border-radius: 999px;
}
.rank {
display: flex;
align-items: center;
gap: 9px;
padding: 9px 0;
border-bottom: 1px solid #eef0f2;
}
.rank img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
}
.rank b {
font-size: 13px;
}
.rank span {
margin-left: auto;
color: #8a94a6;
font-size: 12px;
}
.cloud-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cloud-tags span {
padding: 6px 10px;
border-radius: 999px;
background: #f2f4f7;
color: #485465;
font-weight: 800;
}
.cloud-tags .xl {
font-size: 20px;
color: #07a352;
background: #e9f8ef;
}
.cloud-tags .lg {
font-size: 17px;
color: #1677ff;
background: #eef5ff;
}
.cloud-tags .md {
font-size: 15px;
color: #d46b08;
background: #fff4e5;
}
.footer {
padding: 16px 4px 0;
color: #98a2b3;
text-align: center;
font-size: 11px;
line-height: 1.8;
}
.muted {
color: #8a94a6;
}
.empty-section {
display: none;
}
@media (max-width: 430px) {
html,
body {
width: 100%;
}
.report {
width: 100%;
padding-left: 12px;
padding-right: 12px;
}
.stats {
gap: 6px;
}
.stat b {
font-size: 16px;
}
}
/* AI 图片识别板块(v1 模板) */
.vision-card {
display: grid;
grid-template-columns: 132px 1fr;
gap: 14px;
margin-top: 10px;
padding: 12px;
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
border: 1px solid #d6efde;
border-radius: 14px;
}
.vision-image {
width: 132px;
height: 132px;
border-radius: 12px;
object-fit: cover;
background: #e5e7eb;
}
.vision-description {
margin-top: 6px;
font-size: 13px;
line-height: 1.5;
color: #1f2933;
}
.vision-ocr {
margin-top: 6px;
padding: 6px 10px;
background: #eef5ff;
color: #1677ff;
font-size: 11px;
border-radius: 8px;
word-break: break-all;
}
.vision-tags {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.vision-tag {
display: inline-block;
padding: 3px 8px;
background: #07c160;
color: #fff;
font-size: 10px;
font-weight: 700;
border-radius: 999px;
}
.vision-label {
margin-top: 6px;
font-size: 10px;
color: #07a352;
font-weight: 600;
}
/* 热度条形图(v1 模板) */
.heat-row {
display: grid;
grid-template-columns: 80px 1fr 40px;
align-items: center;
gap: 10px;
margin-top: 8px;
font-size: 12px;
}
.heat-name {
color: #1f2933;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.heat-bar {
background: #f3f5f7;
border-radius: 999px;
height: 10px;
overflow: hidden;
}
.heat-bar i {
display: block;
height: 100%;
background: linear-gradient(90deg, #07c160 0%, #34d399 100%);
border-radius: 999px;
}
.heat-val {
color: #485465;
font-weight: 700;
font-size: 11px;
text-align: right;
}
</style>
</head>
<body>
<main class="report">
<header class="hero">
<div class="hero-top">
<div>
<h1>{{GROUP_NAME}}日报</h1>
<div class="sub">
<div>{{DATE_RANGE}}</div>
<div class="record-note">{{RECORD_NOTE}}</div>
<div class="overview">{{OVERVIEW}}</div>
</div>
</div>
<div class="avatar-grid {{HERO_AVATAR_CLASS}}">{{HERO_AVATARS}}</div>
</div>
<div class="stats">
<div class="stat"><b>{{MESSAGE_COUNT}}</b><span>消息数</span></div>
<div class="stat"><b>{{ACTIVE_USERS}}</b><span>活跃人数</span></div>
<div class="stat"><b>{{TIME_SPAN}}</b><span>持续时长</span></div>
<div class="stat"><b>{{TOPIC_COUNT}}</b><span>主要话题</span></div>
</div>
</header>
<section class="section topics">
<div class="section-title">今日讨论热点</div>
{{TOPIC_CARDS}}
</section>
<section class="section vision {{VISION_EMPTY_CLASS}}">
<div class="section-title">{{VISION_TITLE}}</div>
{{VISION_CARDS}}
</section>
<section class="section resources {{RESOURCES_EMPTY_CLASS}}">
<div class="section-title">实用信息与资源</div>
{{RESOURCE_ITEMS}}
</section>
<section class="section messages {{MESSAGES_EMPTY_CLASS}}">
<div class="section-title">重要消息汇总</div>
{{IMPORTANT_MESSAGES}}
</section>
<section class="section quotes {{QUOTES_EMPTY_CLASS}}">
<div class="section-title">有趣对话或金句</div>
{{QUOTE_BLOCKS}}
</section>
<section class="section qa {{QA_EMPTY_CLASS}}">
<div class="section-title">问题与解答</div>
{{QA_CARDS}}
</section>
<section class="section analytics">
<div class="section-title">群内数据可视化</div>
{{HEAT_BARS}}
<div class="card">
<div class="muted" style="font-size: 12px; margin-bottom: 6px">
话唠榜 TOP5(基于已读取记录估算)
</div>
{{RANK_ITEMS}}
</div>
<div class="card">
<p><b>活跃时间线:</b>{{ACTIVITY_TIMELINE}}</p>
</div>
</section>
<section class="section cloud">
<div class="section-title">词云/关键词</div>
<div class="cloud-tags">{{CLOUD_TAGS}}</div>
</section>
<footer class="footer">
数据来源:TraceMemo · 微信群聊记录<br />
生成时间:{{GENERATED_AT}}<br />
{{FOOTER_NOTE}}
</footer>
</main>
</body>
</html>
+816
View File
@@ -0,0 +1,816 @@
<!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;
}
::-webkit-scrollbar {
width: 0;
height: 0;
}
html {
width: 430px;
scrollbar-width: none;
}
body {
margin: 0;
width: 430px;
background: #f3f5f7;
color: #1f2933;
font-family:
-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.report {
width: 430px;
margin: 0 auto;
padding: 20px 14px 34px;
}
.hero,
.section,
.card {
background: #fff;
border-radius: 18px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.hero {
padding: 20px;
}
.hero-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.hero-top > div:first-child {
min-width: 0;
flex: 1 1 auto;
}
.hero h1 {
margin: 0;
font-size: 23px;
line-height: 1.2;
font-weight: 900;
}
.sub {
margin-top: 8px;
color: #667085;
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 {
display: grid;
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 {
grid-template-columns: var(--tm-avatar-hero-size, 40px);
}
.avatar-grid.empty-section {
display: none;
}
.avatar-grid img,
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.hero-headline {
margin-top: 14px;
padding: 14px;
border-radius: 16px;
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
}
.hero-headline b {
display: block;
font-size: 17px;
color: #076c39;
}
.hero-headline p {
margin: 8px 0 0;
font-size: 13px;
line-height: 1.65;
color: #1f2933;
}
.hero-inline-notes {
display: grid;
gap: 8px;
margin-top: 10px;
}
.hero-note,
.hero-status {
padding: 10px 12px;
border-radius: 12px;
font-size: 12px;
line-height: 1.5;
}
.hero-status {
margin-top: 10px;
background: #f7faf9;
color: #076c39;
font-weight: 700;
}
.hero-note.takeaway {
background: #eef8f2;
color: #076c39;
}
.hero-note.pending {
background: #fff8e8;
color: #8a5a00;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-top: 16px;
}
.stat {
background: #f7faf9;
border-radius: 12px;
padding: 10px 6px;
text-align: center;
}
.stat b {
display: block;
font-size: 18px;
color: #07a352;
line-height: 1.25;
}
.stat span {
font-size: 11px;
color: #667085;
}
.section {
margin-top: 18px;
padding: 18px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 14px;
font-size: 19px;
font-weight: 900;
}
.section-title::before {
content: '';
width: 5px;
height: 20px;
border-radius: 99px;
background: #07c160;
}
.section-subtitle {
margin: 10px 0 6px;
color: #667085;
font-size: 12px;
font-weight: 700;
}
.section-more {
margin-top: 10px;
color: #98a2b3;
font-size: 11px;
text-align: right;
}
.card {
padding: 14px;
margin-top: 10px;
border: 1px solid #edf0f2;
box-shadow: none;
}
.topic-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.topic-title-row h3 {
margin: 0;
font-size: 16px;
line-height: 1.35;
font-weight: 850;
}
.heat,
.tag {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 999px;
background: #eef8f2;
color: #07a352;
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.hot {
background: #fff4e5;
color: #d46b08;
}
.blue {
background: #eef5ff;
color: #1677ff;
}
.topic-meta {
margin-top: 6px;
color: #8a94a6;
font-size: 12px;
}
.card p {
margin: 10px 0 0;
font-size: 13px;
line-height: 1.65;
}
.topic-conclusions {
display: grid;
gap: 8px;
margin-top: 10px;
}
.topic-conclusion {
padding: 8px 10px;
border-radius: 10px;
background: #edf9f1;
color: #076c39;
font-size: 12px;
line-height: 1.5;
font-weight: 700;
}
.topic-inline-image {
display: grid;
grid-template-columns: 76px 1fr;
gap: 10px;
margin-top: 10px;
padding: 10px;
border-radius: 12px;
background: #f7faf9;
}
.topic-inline-image img {
width: 76px;
height: 76px;
border-radius: 10px;
object-fit: cover;
}
.participants {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.person-chip {
display: inline-flex;
align-items: center;
gap: 5px;
background: #f6f8fa;
border-radius: 999px;
padding: 3px 8px 3px 3px;
}
.person-chip img {
width: 24px;
height: 24px;
border-radius: 50%;
}
.person-chip b {
max-width: 58px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
}
.keywords {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.keywords span {
font-size: 11px;
padding: 4px 8px;
border-radius: 999px;
background: #f2f4f7;
color: #667085;
}
.important-card {
display: flex;
gap: 10px;
background: #f7faf9;
border-radius: 14px;
padding: 12px;
margin-top: 10px;
}
.important-card > .avatar {
width: 36px;
height: 36px;
flex: 0 0 auto;
}
.important-meta {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 12px;
color: #667085;
}
.important-meta b {
color: #1f2933;
}
.important-text {
margin-top: 5px;
font-size: 13px;
line-height: 1.55;
}
.important-note {
margin-top: 8px;
padding: 7px 9px;
border-left: 3px solid #07c160;
background: #fff;
border-radius: 8px;
color: #07a352;
font-size: 12px;
line-height: 1.45;
}
.action-grid {
display: grid;
gap: 10px;
}
.action-card {
border-radius: 14px;
padding: 12px;
}
.todo-card {
background: #eef5ff;
}
.unresolved-card {
background: #fff8e8;
}
.action-card b {
display: block;
color: #1f2933;
font-size: 14px;
}
.action-card div {
margin-top: 6px;
font-size: 12px;
line-height: 1.55;
color: #485465;
}
.action-note {
color: #667085;
}
.chat-block {
background: #f0f2f5;
border-radius: 14px;
padding: 12px;
margin-top: 10px;
}
.chat-msg {
display: flex;
gap: 8px;
margin-top: 8px;
}
.chat-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.chat-name {
font-size: 11px;
color: #667085;
margin-bottom: 4px;
}
.chat-bubble {
background: #fff;
border-radius: 4px 12px 12px 12px;
padding: 9px 10px;
font-size: 13px;
line-height: 1.5;
}
.quote-note {
margin-top: 10px;
padding: 9px 10px;
border-radius: 10px;
background: #fff8e1;
color: #8a5a00;
font-size: 12px;
line-height: 1.5;
}
.qa-card,
.resource {
margin-top: 10px;
padding: 12px;
border-radius: 14px;
background: #f8fafc;
}
.qa-card b,
.resource b {
display: block;
color: #1f2933;
margin-bottom: 5px;
}
.qa-card div,
.resource {
font-size: 13px;
line-height: 1.55;
color: #485465;
}
.storyline-card,
.chain-card {
background: #f8fafc;
}
.storyline-steps {
display: grid;
gap: 8px;
margin-top: 10px;
}
.storyline-step {
display: grid;
grid-template-columns: 50px 1fr;
gap: 10px;
}
.storyline-step span {
color: #8a94a6;
font-size: 12px;
}
.storyline-step b {
font-size: 13px;
line-height: 1.5;
}
.chain-flow {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
margin-top: 10px;
}
.chain-flow span {
display: inline-flex;
align-items: center;
padding: 6px 9px;
border-radius: 999px;
background: #eef8f2;
color: #076c39;
font-size: 12px;
font-weight: 700;
}
.chain-flow i {
font-style: normal;
color: #98a2b3;
}
.badge-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
/* AI 图片识别板块 */
.vision-card {
display: grid;
grid-template-columns: 132px 1fr;
gap: 14px;
margin-top: 10px;
padding: 12px;
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
border: 1px solid #d6efde;
border-radius: 14px;
}
.vision-image {
width: 132px;
height: 132px;
border-radius: 12px;
object-fit: cover;
background: #e5e7eb;
}
.vision-description {
margin-top: 6px;
font-size: 13px;
line-height: 1.5;
color: #1f2933;
}
.vision-ocr {
margin-top: 6px;
padding: 6px 10px;
background: #eef5ff;
color: #1677ff;
font-size: 11px;
border-radius: 8px;
word-break: break-all;
}
.vision-tags {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.vision-tag {
display: inline-block;
padding: 3px 8px;
background: #07c160;
color: #fff;
font-size: 10px;
font-weight: 700;
border-radius: 999px;
}
.vision-label {
margin-top: 6px;
font-size: 10px;
color: #07a352;
font-weight: 600;
}
.badge-card {
background: linear-gradient(180deg, #fdfdfd 0%, #f6fbf8 100%);
border: 1px solid #edf0f2;
border-radius: 14px;
padding: 12px;
}
.badge-card b {
display: block;
margin-top: 8px;
font-size: 15px;
}
.badge-card p {
margin: 8px 0 0;
font-size: 12px;
line-height: 1.55;
}
.data-grid {
display: grid;
gap: 12px;
}
.rank {
display: flex;
align-items: center;
gap: 9px;
padding: 9px 0;
border-bottom: 1px solid #eef0f2;
}
.rank:last-child {
border-bottom: none;
}
.rank img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
}
.rank b {
font-size: 13px;
}
.rank span {
margin-left: auto;
color: #8a94a6;
font-size: 12px;
}
.cloud-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cloud-tags span {
padding: 6px 10px;
border-radius: 999px;
background: #f2f4f7;
color: #485465;
font-weight: 800;
}
.cloud-tags .xl {
font-size: 20px;
color: #07a352;
background: #e9f8ef;
}
.cloud-tags .lg {
font-size: 17px;
color: #1677ff;
background: #eef5ff;
}
.cloud-tags .md {
font-size: 15px;
color: #d46b08;
background: #fff4e5;
}
.footer {
padding: 16px 4px 0;
color: #98a2b3;
text-align: center;
font-size: 11px;
line-height: 1.8;
}
.muted {
color: #8a94a6;
}
.empty-section {
display: none !important;
}
.compact .report {
padding-top: 18px;
}
.compact .section {
margin-top: 12px;
padding: 14px;
}
.compact .card {
padding: 11px;
}
.compact .important-card,
.compact .chat-block {
padding: 10px;
}
.compact .section-title {
margin-bottom: 9px;
font-size: 17px;
}
.compact .hero-headline p,
.compact .card p,
.compact .important-text,
.compact .chat-bubble {
line-height: 1.5;
}
.compact .participants,
.compact .keywords,
.compact .hero-inline-notes {
gap: 6px;
}
.compact .topic-conclusions {
gap: 6px;
}
.compact .topic-inline-image {
grid-template-columns: 64px 1fr;
padding: 8px;
}
.compact .topic-inline-image img {
width: 64px;
height: 64px;
}
.compact .stats {
gap: 6px;
margin-top: 14px;
}
.compact .stat {
padding: 8px 6px;
}
.compact .stat b {
font-size: 17px;
}
@media (max-width: 430px) {
html,
body {
width: 100%;
}
.report {
width: 100%;
padding-left: 12px;
padding-right: 12px;
}
}
</style>
</head>
<body class="{{REPORT_MODE_CLASS}}">
<main class="report">
<header class="hero">
<div class="hero-top">
<div>
<h1>{{GROUP_NAME}}日报</h1>
<div class="sub">{{DATE_RANGE}}<br />{{RECORD_NOTE}}</div>
</div>
<div class="avatar-grid {{HERO_AVATAR_CLASS}}">{{HERO_AVATARS}}</div>
</div>
<div class="hero-headline">
<b>{{HERO_HEADLINE}}</b>
<p>{{HERO_SUMMARY}}</p>
</div>
<div class="hero-status {{HERO_STATUS_EMPTY_CLASS}}">{{HERO_STATUS_LINE}}</div>
<div class="hero-inline-notes">
<div class="hero-note takeaway {{HERO_TAKEAWAY_EMPTY_CLASS}}">{{HERO_TAKEAWAY}}</div>
<div class="hero-note pending {{HERO_PENDING_EMPTY_CLASS}}">{{HERO_PENDING}}</div>
</div>
<div class="stats">
<div class="stat"><b>{{MESSAGE_COUNT}}</b><span>消息数</span></div>
<div class="stat"><b>{{ACTIVE_USERS}}</b><span>活跃人数</span></div>
<div class="stat"><b>{{TOPIC_COUNT}}</b><span>话题数</span></div>
<div class="stat"><b>{{MEDIA_COUNT}}</b><span>媒体消息</span></div>
</div>
</header>
<section class="section {{TOPICS_EMPTY_CLASS}}">
<div class="section-title">今日讨论热点</div>
{{TOPIC_CARDS}}
{{TOPICS_MORE_NOTE}}
</section>
<section class="section {{MESSAGES_EMPTY_CLASS}}">
<div class="section-title">重要消息</div>
{{IMPORTANT_MESSAGES}}
{{MESSAGES_MORE_NOTE}}
</section>
<section class="section {{ACTIONS_EMPTY_CLASS}}">
<div class="section-title">待办事项和未解决问题</div>
<div class="section-subtitle {{TODO_EMPTY_CLASS}}">待办事项</div>
<div class="action-grid {{TODO_EMPTY_CLASS}}">{{TODO_CARDS}}</div>
<div class="section-subtitle {{UNRESOLVED_EMPTY_CLASS}}">尚未解决</div>
<div class="action-grid {{UNRESOLVED_EMPTY_CLASS}}">{{UNRESOLVED_CARDS}}</div>
{{ACTIONS_MORE_NOTE}}
</section>
<section class="section {{QUOTES_EMPTY_CLASS}}">
<div class="section-title">今日名场面</div>
{{QUOTE_BLOCKS}}
{{QUOTES_MORE_NOTE}}
</section>
<section class="section {{ANALYTICS_EMPTY_CLASS}}">
<div class="section-title">今日群数据</div>
<div class="data-grid">
<div class="card">
<div class="muted" style="font-size: 12px; margin-bottom: 6px">话唠榜 TOP5</div>
{{RANK_ITEMS}}
</div>
<div class="card">
<p><b>最活跃时段:</b>{{ACTIVITY_TIMELINE}}</p>
<p><b>今日状态:</b>形成 {{CONCLUSION_COUNT}} 个结论,待办 {{TODO_COUNT}} 项,未解决 {{UNRESOLVED_COUNT}} 项。</p>
</div>
</div>
</section>
<section class="section {{KEYWORDS_EMPTY_CLASS}}">
<div class="section-title">关键词</div>
<div class="cloud-tags">{{CLOUD_TAGS}}</div>
{{KEYWORDS_MORE_NOTE}}
</section>
<section class="section {{RESOURCES_EMPTY_CLASS}}">
<div class="section-title">实用信息与资源</div>
{{RESOURCE_ITEMS}}
{{RESOURCES_MORE_NOTE}}
</section>
<section class="section {{QA_EMPTY_CLASS}}">
<div class="section-title">问题与解答</div>
{{QA_CARDS}}
{{QA_MORE_NOTE}}
</section>
<section class="section {{STORYLINES_EMPTY_CLASS}}">
<div class="section-title">今日剧情时间线</div>
{{STORYLINE_CARDS}}
{{STORYLINES_MORE_NOTE}}
</section>
<section class="section {{REVERSALS_EMPTY_CLASS}}">
<div class="section-title">群聊反转现场</div>
{{REVERSAL_CARDS}}
{{REVERSALS_MORE_NOTE}}
</section>
<section class="section {{VISION_EMPTY_CLASS}}">
<div class="section-title">{{VISION_TITLE}}</div>
{{VISION_CARDS}}
</section>
<section class="section {{VOICE_EMPTY_CLASS}}">
<div class="section-title">语音之最</div>
{{VOICE_CARDS}}
{{VOICE_MORE_NOTE}}
</section>
<section class="section {{VOICE_RANK_EMPTY_CLASS}}">
<div class="section-title">语音时长榜</div>
<div class="card">{{VOICE_RANK_CARDS}}</div>
</section>
<section class="section {{BADGES_EMPTY_CLASS}}">
<div class="section-title">今日临时人设</div>
<div class="badge-grid">{{BADGE_CARDS}}</div>
{{BADGES_MORE_NOTE}}
</section>
<section class="section {{CHAINS_EMPTY_CLASS}}">
<div class="section-title">话题参与链路</div>
{{CHAIN_CARDS}}
{{CHAINS_MORE_NOTE}}
</section>
<footer class="footer">
数据来源:TraceMemo · 微信群聊记录<br />
生成时间:{{GENERATED_AT}}<br />
{{FOOTER_NOTE}}
</footer>
</main>
</body>
</html>
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.

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