feat: 完成 TraceMemo v2.2.0 品牌升级并保留旧数据兼容

- 将用户可见品牌升级为 TraceMemo(迹忆)
- 增加最早期 userData/sessionData 兼容路径选择
- 保留 WechatExplorer runtime identity 以兼容 safeStorage
- 继续使用旧 Knowledge、Settings、API Token 和 Provider 配置
- 新日志写入 TraceMemo 目录并保留历史日志
- 保留旧 API、Skill、环境变量和导出目录兼容标识
- 更新相关文档、界面文案与自动化测试
This commit is contained in:
Wxw-Gu
2026-08-11 14:42:12 +08:00
parent 2354fd0766
commit 55c6fb8cd3
86 changed files with 481 additions and 459 deletions
+35 -21
View File
@@ -1,7 +1,7 @@
# WechatExplorer # TraceMemo(迹忆)
<p align="center"> <p align="center">
<img src="./build/icon.png" width="120" alt="WechatExplorer Logo" /> <img src="./build/icon.png" width="120" alt="TraceMemo Logo" />
</p> </p>
<h2 align="center">把微信聊过的事,找回来、问清楚、留下来</h2> <h2 align="center">把微信聊过的事,找回来、问清楚、留下来</h2>
@@ -18,7 +18,7 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/Wxw-Gu/WechatExplorer/releases"><b>下载 WechatExplorer</b></a> <a href="https://github.com/Wxw-Gu/WechatExplorer/releases"><b>下载 TraceMemo</b></a>
· ·
<a href="./docs/user-guide/getting-started.md"><b>第一次使用</b></a> <a href="./docs/user-guide/getting-started.md"><b>第一次使用</b></a>
· ·
@@ -26,16 +26,30 @@
</p> </p>
<p align="center"> <p align="center">
<img src="./public/software-1.png" alt="WechatExplorer 主界面" /> <img src="./public/software-1.png" alt="TraceMemo 主界面" />
</p> </p>
<p align="center"> <p align="center">
<img src="./public/机器人.png" alt="WechatExplorer 主界面" /> <img src="./public/机器人.png" alt="TraceMemo 主界面" />
</p> </p>
## WechatExplorer 是什么 ## TraceMemo(迹忆)是什么
WechatExplorer 是一本地优先的微信聊天记录搜索与 AI 工作台。 TraceMemo(迹忆)是一本地优先、可追溯的 AI 微信知识与分析工作台。
TraceMemo(迹忆)原名 WechatExplorer,是一次从“微信聊天记录探索工具”向“可追溯的本地 AI 知识工作台”演进后的正式品牌升级。
## 为什么叫 TraceMemo(迹忆)
`Trace` 代表聊天记录留下的痕迹、可以追溯的信息来源、AI 搜索过程,以及从结果回到原始聊天上下文并核对证据的能力。
`Memo` 代表记忆、知识沉淀和长期保存:让聊天中产生的信息逐渐形成个人知识。
“迹忆”可以理解为“留下痕迹的记忆”。TraceMemo 不是单纯查看微信聊天记录的工具,而是希望让聊天中产生的信息留下痕迹,并能够被再次找到、理解、验证和沉淀。
> **品牌说明**
>
> TraceMemo(迹忆)原名 WechatExplorer。WechatExplorer 最初是一个用于查看和探索微信聊天记录的工具。随着本地搜索、AI 问答、来源追溯、知识库、日报、语音转写和 Agent 能力逐渐形成,项目已经从单纯的聊天记录查看器发展为本地 AI 知识与分析工作台,因此在 v2.2.0 正式更名为 TraceMemo(迹忆)。
它可以帮你浏览、搜索和整理微信历史,也可以让 AI 帮你找回聊过的内容,并回到原始消息核对答案。 它可以帮你浏览、搜索和整理微信历史,也可以让 AI 帮你找回聊过的内容,并回到原始消息核对答案。
@@ -50,7 +64,7 @@ WechatExplorer 是一个本地优先的微信聊天记录搜索与 AI 工作台
## 💬 交流与反馈 ## 💬 交流与反馈
<p align="center"> <p align="center">
<img src="./public/二维码.jpg" alt="WechatExplorer 交流与售后群二维码" width="280" /> <img src="./public/二维码.jpg" alt="TraceMemo 交流与售后群二维码" width="280" />
</p> </p>
@@ -65,7 +79,7 @@ WechatExplorer 是一个本地优先的微信聊天记录搜索与 AI 工作台
| 把微信语音变成可搜索的文字 | [设置 → 语音转文字](./docs/user-guide/voice.md) | 准备本地语音模型 | | 把微信语音变成可搜索的文字 | [设置 → 语音转文字](./docs/user-guide/voice.md) | 准备本地语音模型 |
| 把聊天保存成 HTML、Markdown、CSV 或 JSON | [导出](./docs/user-guide/export.md) | 选择聊天、时间和格式,不需要 AI | | 把聊天保存成 HTML、Markdown、CSV 或 JSON | [导出](./docs/user-guide/export.md) | 选择聊天、时间和格式,不需要 AI |
| 尽量保留之后捕获到的撤回消息 | [设置 → 防撤回](./docs/user-guide/recall-protection.md) | 默认关闭;开启前先了解写入和性能边界 | | 尽量保留之后捕获到的撤回消息 | [设置 → 防撤回](./docs/user-guide/recall-protection.md) | 默认关闭;开启前先了解写入和性能边界 |
| 直接在微信里向 WechatExplorer 提问 | [微信机器人](./docs/agent/agent-hub.md) | 扫码连接机器人;总结类任务需要 AI | | 直接在微信里向 TraceMemo 提问 | [微信机器人](./docs/agent/agent-hub.md) | 扫码连接机器人;总结类任务需要 AI |
| 让 Codex 等外部 Agent 查询微信历史 | [外部 Agent](./docs/agent/overview.md) | 安装 Reader Skill 并配置本机 Token | | 让 Codex 等外部 Agent 查询微信历史 | [外部 Agent](./docs/agent/overview.md) | 安装 Reader Skill 并配置本机 Token |
## 最核心的三个能力 ## 最核心的三个能力
@@ -83,7 +97,7 @@ WechatExplorer 是一个本地优先的微信聊天记录搜索与 AI 工作台
打开“问问微信”,选择搜索范围和时间,然后像提问一样描述你想找的内容。 打开“问问微信”,选择搜索范围和时间,然后像提问一样描述你想找的内容。
WechatExplorer 会先在本机查找候选消息,再把整理后的少量来源交给你配置的 AI 模型生成回答。你可以查看答案参考了哪些聊天、来自哪个人和时间,并从来源标记跳回原始消息核对;“查看检索详情”还会展示本次查找经历了哪些阶段。 TraceMemo 会先在本机查找候选消息,再把整理后的少量来源交给你配置的 AI 模型生成回答。你可以查看答案参考了哪些聊天、来自哪个人和时间,并从来源标记跳回原始消息核对;“查看检索详情”还会展示本次查找经历了哪些阶段。
<p align="center"> <p align="center">
<img src="./public/问一问.png" alt="问问微信与聊天来源" /> <img src="./public/问一问.png" alt="问问微信与聊天来源" />
@@ -93,11 +107,11 @@ WechatExplorer 会先在本机查找候选消息,再把整理后的少量来
### 直接在微信里问你的历史聊天 ### 直接在微信里问你的历史聊天
打开应用中的“Agent”入口(页面标题为“Agent Hub”,对应微信机器人功能),扫码连接一个微信机器人账号。例如,你可以直接给机器人发送“最近 5 个会话”“张三最近和我聊了什么”,或者让它生成指定群聊的总结图片。WechatExplorer 会在本机读取已连接的聊天数据并把结果回复到微信。 打开应用中的“Agent”入口(页面标题为“Agent Hub”,对应微信机器人功能),扫码连接一个微信机器人账号。例如,你可以直接给机器人发送“最近 5 个会话”“张三最近和我聊了什么”,或者让它生成指定群聊的总结图片。TraceMemo 会在本机读取已连接的聊天数据并把结果回复到微信。
这个入口不要求另外安装 Codex、Claude Code 等外部 Agent。当前主要处理文字消息,不支持群发、定时任务或通用自主操作微信;总结和自然语言理解需要先配置 AI 服务。 这个入口不要求另外安装 Codex、Claude Code 等外部 Agent。当前主要处理文字消息,不支持群发、定时任务或通用自主操作微信;总结和自然语言理解需要先配置 AI 服务。
详细步骤和能力边界见[在微信里向 WechatExplorer 提问](./docs/agent/agent-hub.md)。 详细步骤和能力边界见[在微信里向 TraceMemo 提问](./docs/agent/agent-hub.md)。
## 其他能力 ## 其他能力
@@ -126,13 +140,13 @@ WechatExplorer 会先在本机查找候选消息,再把整理后的少量来
### 转写微信语音 ### 转写微信语音
WechatExplorer 支持在本机转写单条或批量微信语音,结果可以参与本地知识库检索和 HTML 导出。转写本身不要求把语音文件发送给在线 AI;随后用于 AI 问答或日报时,文字会按对应功能的规则处理。 TraceMemo 支持在本机转写单条或批量微信语音,结果可以参与本地知识库检索和 HTML 导出。转写本身不要求把语音文件发送给在线 AI;随后用于 AI 问答或日报时,文字会按对应功能的规则处理。
详细说明:[语音转文字](./docs/user-guide/voice.md) 详细说明:[语音转文字](./docs/user-guide/voice.md)
### 防撤回 ### 防撤回
可选开启后,WechatExplorer 会尽量保留开启期间捕获到的撤回消息。该能力受微信版本和应用运行状态影响,不保证找回所有内容,也不能恢复开启前已经撤回的消息。 可选开启后,TraceMemo 会尽量保留开启期间捕获到的撤回消息。该能力受微信版本和应用运行状态影响,不保证找回所有内容,也不能恢复开启前已经撤回的消息。
详细说明:[防撤回](./docs/user-guide/recall-protection.md) 详细说明:[防撤回](./docs/user-guide/recall-protection.md)
@@ -152,7 +166,7 @@ WechatExplorer 支持在本机转写单条或批量微信语音,结果可以
```mermaid ```mermaid
flowchart LR flowchart LR
A[本机微信数据] --> B[WechatExplorer 读取与解析] A[本机微信数据] --> B[TraceMemo 读取与解析]
B --> C[聊天档案] B --> C[聊天档案]
B --> D[本地知识库与搜索] B --> D[本地知识库与搜索]
D --> E[筛选相关聊天来源] D --> E[筛选相关聊天来源]
@@ -187,7 +201,7 @@ flowchart LR
## 快速开始 ## 快速开始
1. 从 [GitHub Releases](https://github.com/Wxw-Gu/WechatExplorer/releases) 下载安装包。 1. 从 [GitHub Releases](https://github.com/Wxw-Gu/WechatExplorer/releases) 下载安装包。
2. 启动 WechatExplorer,按照“第一次使用”页面选择微信数据目录。 2. 启动 TraceMemo,按照“第一次使用”页面选择微信数据目录。
3. 第一次使用请先点击“开始连接”,按页面提示准备连接组件并获取数据库密钥;只有已经有密钥的高级用户才需要“手动连接”。 3. 第一次使用请先点击“开始连接”,按页面提示准备连接组件并获取数据库密钥;只有已经有密钥的高级用户才需要“手动连接”。
4. 连接成功后打开“档案”,确认联系人和聊天消息已经出现。 4. 连接成功后打开“档案”,确认联系人和聊天消息已经出现。
5. 先在“档案”里搜索一句你记得的原话;这一步不需要 AI。 5. 先在“档案”里搜索一句你记得的原话;这一步不需要 AI。
@@ -198,7 +212,7 @@ Windows 安装后无法启动时,请先安装 [Microsoft Visual C++ x64 运行
如果 macOS 页面提示处理 SIP,请先阅读对应说明。具体步骤和限制见[第一次使用](./docs/user-guide/getting-started.md)。 如果 macOS 页面提示处理 SIP,请先阅读对应说明。具体步骤和限制见[第一次使用](./docs/user-guide/getting-started.md)。
完整步骤:[第一次使用 WechatExplorer](./docs/user-guide/getting-started.md) 完整步骤:[第一次使用 TraceMemo](./docs/user-guide/getting-started.md)
## 配置 AI ## 配置 AI
@@ -248,20 +262,20 @@ pnpm test:e2e:build
## 支持与反馈 ## 支持与反馈
遇到问题时,先查看[常见问题与排查](./docs/user-guide/troubleshooting.md)。提交 Issue 时请提供操作系统、微信版本、WechatExplorer 版本、复现步骤和已遮挡敏感信息的截图。 遇到问题时,先查看[常见问题与排查](./docs/user-guide/troubleshooting.md)。提交 Issue 时请提供操作系统、微信版本、TraceMemo 版本、复现步骤和已遮挡敏感信息的截图。
请仅处理你有权访问的数据,并遵守适用的法律法规、组织政策和微信使用规则。数据库读取、解密、自动化和机器人能力都可能受平台版本与账号环境影响。 请仅处理你有权访问的数据,并遵守适用的法律法规、组织政策和微信使用规则。数据库读取、解密、自动化和机器人能力都可能受平台版本与账号环境影响。
## 许可说明 ## 许可说明
仓库中的第三方组件、模型和连接器遵循各自的许可证。当前仓库根目录未提供独立的项目 `LICENSE` 文件;贡献、复制或再分发前,请先向维护者确认 WechatExplorer 本身的许可范围。 仓库中的第三方组件、模型和连接器遵循各自的许可证。当前仓库根目录未提供独立的项目 `LICENSE` 文件;贡献、复制或再分发前,请先向维护者确认 TraceMemo 本身的许可范围。
## 致谢 ## 致谢
<details> <details>
<summary>展开致谢与参考项目</summary> <summary>展开致谢与参考项目</summary>
WechatExplorer 在开发过程中参考了多个优秀的开源项目,感谢这些项目作者的工作与分享。 TraceMemo 在开发过程中参考了多个优秀的开源项目,感谢这些项目作者的工作与分享。
特别感谢: 特别感谢:
@@ -272,7 +286,7 @@ WechatExplorer 在开发过程中参考了多个优秀的开源项目,感谢
- **[chatlog](https://github.com/sjzar/chatlog)** - **[chatlog](https://github.com/sjzar/chatlog)**
- 提供了聊天记录导出与数据处理方面的参考。 - 提供了聊天记录导出与数据处理方面的参考。
在此基础上,WechatExplorer 进行了重新设计与实现,包括: 在此基础上,TraceMemo 进行了重新设计与实现,包括:
- AI 问问微信 - AI 问问微信
- AI 群聊日报 - AI 群聊日报
-241
View File
@@ -1,241 +0,0 @@
# WechatExplorer 产品经理交接
> 面向下一任产品经理的产品事实、边界和文档维护说明。
>
> - 最后核验:2026-08-07
> - 仓库版本:`2.1.9`
> - 基准提交:`cd2c3cf``docs: 更新文档`
## 1. 接手时先记住什么
WechatExplorer 当前可以定位为:
> **一个本地优先的微信聊天记录查看、搜索、整理与 AI 分析工具。**
它把几类原本分散的任务放在一起:
- 浏览和搜索本机微信聊天;
- 用 AI 查找历史信息,并回到来源消息核对;
- 建立本地知识库,辅助跨会话、跨时间查找;
- 生成群聊日报,转写语音,导出聊天档案;
- 让外部 Agent 或应用内微信机器人按边界使用本机数据能力。
产品表达应先回答用户能完成什么,再解释 Knowledge、Evidence、Citation、Search Trace、FTS 等内部术语。README 负责定位、主要价值和最短上手路径;完整步骤、限制和安全说明放在 `docs/`
### 事实来源优先级
描述“当前支持”前,按以下顺序核验:
1. 当前源码和 UI
2. 当前测试;
3. `package.json`、构建和发布配置;
4. 正式 `docs/`
5. README
6. 历史说明和产品设想。
历史文档、旧版本描述和聊天记录不能单独证明当前能力。新增事实陈述时,最好同时写清限制并链接到正式文档或实现位置。
## 2. 当前已验证的产品能力
本节是能力地图,不替代正式使用手册。用户步骤和异常处理以链接的正式文档为准。
### 2.1 连接与查看微信数据
当前代码面向微信 4.x 数据结构,支持在 macOS 和 Windows 上连接本机微信数据。连接结果会受到微信版本、账号数据、系统权限和数据迁移状态影响。
连接成功后,“档案”可以浏览已读取到的联系人、群聊、折叠群聊和公众号会话,并显示文本、图片、视频、语音、文件、链接、引用、小程序、表情和系统消息等类型。
边界:消息类型可被读取,不等于对应媒体一定可以解码或显示。来源文件缺失、权限不足或微信存储方式变化都可能导致媒体不可用。
来源:[第一次使用](./docs/user-guide/getting-started.md)、[查看和搜索聊天](./docs/user-guide/chat-archive.md)
### 2.2 普通搜索与 AI Search
档案内关键词搜索适合已知原话、文件名、人名或大致会话的任务。AI Search 适合“记得含义但不记得关键词或位置”的问题。
AI Search 会先在本机查找候选聊天,再把完成任务所需的受控上下文交给用户配置的 AI Provider。回答可以展示来源会话、发送者、时间、来源标记和检索过程,并允许用户回到档案检查上下文。
边界:Evidence、Citation 和 Search Trace 提供核对路径,不保证模型结论正确,也不保证选定范围之外没有遗漏。关键决定仍应回到原始消息确认。
来源:[AI Search](./docs/user-guide/ai-search.md)、[如何核对 AI 回答来源](./docs/concepts/answer-sources.md)
### 2.3 本地知识库
用户可以主动为当前微信账号建立本地索引,并在之后同步新增或变化的记录。不同微信账号使用独立索引;界面会显示索引规模和磁盘占用。
清理知识库不会删除微信原始数据库。知识库同步期间新的 AI 分析会暂停,但已有索引在部分异常情况下仍可能可用。
边界:知识库是本地检索资料,不是新的微信数据库,也不是所有问题都必须先建立。只查一条已知原话时,普通搜索通常更直接。
来源:[本地知识库](./docs/user-guide/knowledge.md)、`src/main/knowledge/``tests/unit/knowledge-*.test.ts`
### 2.4 群聊日报
用户可以选择一个群聊以及今天、昨天或近 7 天的范围,生成可能包含主题、重要消息、问答、资源、待办、未解决事项、关键词、活跃统计和可用媒体精选的报告。成功结果会保存为本地 HTML 和 PNG。
边界:当前日报入口只支持群聊。具体栏目取决于所选消息、媒体可用性和模型能力;图片不可读或模型未通过图片理解验证时,图片精选会跳过。语音数量统计不代表语音内容已经转写或理解。
来源:[群聊日报](./docs/user-guide/report.md)、`src/renderer/src/utils/group-report-facts.ts`
### 2.5 本地语音转写
应用使用本地 SenseVoice/sherpa-onnx 运行时转写微信语音,支持单条转写和按联系人或群聊批量处理。批量任务支持进度、取消、缓存复用和部分失败提示。
成功转写的文本可以用于查看、本地知识库检索和导出。离线转写本身在本机完成;如果用户随后把转写文本用于 AI Search 或日报,文本会按对应 AI 功能的规则处理。
边界:首次使用可能需要下载模型。失败或尚未转写的语音不会被自动当作已理解内容。
来源:[语音转文字](./docs/user-guide/voice.md)、`src/main/voice-pipeline/``tests/unit/voice-*.test.ts`
### 2.6 聊天导出
当前支持 HTML、Markdown、CSV 和 JSON
| 能力 | 当前边界 |
| ------------------- | -------------------------------------------------- |
| HTML | 可包含媒体和头像;支持最多五个会话合并 |
| Markdown、CSV、JSON | 主要保留文本内容,不包含 HTML 资源文件 |
| ZIP | 是 HTML 资源包的压缩选项,不是独立内容格式 |
| 增量合并 | 仅适用于同名 HTML 档案 |
| 语音转写 | 只写入已经成功取得的转写文本,不会自动补齐失败内容 |
导出不会修改微信原始数据库;删除导出文件也不会删除应用中的聊天或知识库。
来源:[导出聊天](./docs/user-guide/export.md)、`src/main/export-service.ts``src/renderer/src/components/export/ExportWorkspace.tsx``tests/integration/export-media-flow.test.ts`
### 2.7 外部 Agent 与 Local HTTP API
外部 Agent 可以安装随应用提供的 Reader Skill,通过本机 Local HTTP API 按需读取联系人、会话、指定时间范围的聊天和群成员信息,也可以请求生成群聊总结图片。
当前 API 默认地址为 `http://127.0.0.1:6131`
- `GET /api/v1/health` 不需要 Token
- 其他端点需要 `Authorization: Bearer <TOKEN>`
- Token 在 API Center 中显示、复制和重新生成;
- 重新生成后旧 Token 立即失效;
- API 没有细粒度用户 Scope,不应转发到公网。
`6131` 是普通 Local HTTP API,当前不是 MCP Server。Reader Skill 不会自动监听微信实时消息。
来源:[Agent 接入概览](./docs/agent/overview.md)、[Local HTTP API](./docs/agent/api.md)、[API 安全](./docs/agent/api-security.md)、[Reader Skill](./docs/agent/reader-skill.md)
### 2.8 Agent Hub 与微信机器人
Agent Hub 是应用内的实时微信入口。用户扫码连接一个微信机器人账号后,机器人收到文字消息,Agent Hub 可以查询本机数据、按需调用已配置的 AI,并把结果回复给触发请求的微信用户。
当前明确支持的实时任务包括:
- 查看最近会话,数量限制为 1 至 20;
- 查询与某位联系人的近期聊天;
- 总结与某位联系人近 7 天的聊天;
- 生成今天、昨天或近 7 天的群聊总结图片;
- 总结指定群成员的近期发言;
- 对不需要读取聊天的普通文字请求给出有限的 AI 回复。
边界:实时自然语言入口主要处理文字。底层连接器可以接收其他媒体,但 Agent Hub 尚未提供同等的图片、语音、文件和视频意图处理。它也没有群发、广播、定时任务或通用自主操作微信的能力。
来源:[Agent Hub](./docs/agent/agent-hub.md)、`src/main/agent/`、Agent Hub 相关测试
## 3. 隐私与安全边界
“本地优先”不能表达成“所有数据永远不会离开电脑”。
默认在本机完成的处理包括:微信数据库读取和解析、档案浏览、普通关键词搜索、本地知识库索引、离线语音转写、导出文件生成和本地日报历史。
当用户主动使用 AI Search、群聊日报或图片理解,并配置远程 Provider 时,用户问题、受控检索上下文和最终用于总结的来源内容可能发送给该 Provider。Provider 的日志、保留、计费和地区规则不由 WechatExplorer 控制。
外部 Agent 是否把 API 读取结果继续发送给云端模型,取决于 Agent 自己的配置。Agent Hub 的机器人账号、个人微信数据库连接和外部 Agent/API Token 是三条不同的安全边界。
来源:[数据、隐私与安全](./docs/user-guide/privacy.md)
## 4. 尚未实现或不能宣称的能力
以下内容不是当前能力。未来讨论这些方向时,必须明确写成“未来场景 / 尚未实现”,且不能据此承诺路线图或发布时间:
- **未来场景 / 尚未实现:**按固定时间自动生成或发送每日群聊总结;
- **未来场景 / 尚未实现:**群发、广播或通用微信自动化;
- **未来场景 / 尚未实现:**Agent Hub 对图片、语音、文件和视频提供与文字相同的实时理解能力;
- **未来场景 / 尚未实现:**将 `127.0.0.1:6131` 作为 MCP Server 使用;
- **未来场景 / 尚未实现:**对外提供实时入站 webhook 或由 Reader Skill 订阅实时微信消息;
- **不能宣称:**所有微信 4.x 版本、所有账号和所有系统组合都能稳定连接;
- **不能宣称:**所有图片、视频、文件或语音都一定能读取、解码或理解;
- **不能宣称:**AI 回答或日报一定完整、准确,或者 Evidence 本身能保证结论正确;
- **不能宣称:**启用远程 AI 后所有数据仍只停留在本机。
## 5. 仅凭当前仓库仍无法确认的事项
以下问题需要真实发布环境、用户研究或外部平台信息,不能仅凭当前源码和测试得出结论:
1. GitHub Releases 中各平台安装包当前是否齐全、可下载,以及在不同系统安全策略下的实际安装成功率;
2. 不同微信 4.x 小版本、历史迁移状态和真实账号规模下的连接成功率与兼容矩阵;
3. 超大聊天历史下,索引、AI Search、日报、语音批处理和导出的真实耗时、容量上限与失败率;
4. 微信机器人账号在长期运行中的登录稳定性、平台规则风险和账号限制;
5. 用户是否真正理解并使用“来源核对”、Knowledge、Reader Skill 和 Agent Hub,以及这些功能是否改善了实际任务结果。
这些事项在得到真实证据前,应写成“待验证”,不能转写成产品优势。
## 6. 文档职责和维护方法
### README
README 只负责:
- 一句话说明产品是什么;
- 展示最重要的用户任务和差异;
- 给出最短上手路径;
- 引导到正式 docs。
不要把完整 API、安全实现、数据库结构、检索原理或所有边缘情况塞进 README。
### 正式 docs
- `docs/user-guide/`:第一次使用、档案、AI Search、Knowledge、日报、语音、导出、隐私和排障;
- `docs/concepts/`:来源核对和工作原理;
- `docs/agent/`Reader Skill、Local HTTP API、安全和 Agent Hub
- `docs/platform/`:平台权限和限制;
- `docs/development/`:开发、测试、构建及代码与文档的对应关系。
文档入口见:[docs 首页](./docs/README.md)。
### 每次产品变更后的检查
1. 用户是否能直接感知变化?如果能,检查 README 和对应 User Guide
2. 第一次使用路径是否变化?如果变化,检查 Getting Started
3. AI 的输入、来源或完整性提示是否变化?如果变化,检查 AI Search、Answer Sources 和 Privacy
4. API、Token、Reader Skill 或 Agent Hub 是否变化?如果变化,同步检查全部 Agent 文档;
5. 兼容性、构建或发布范围是否变化?如果变化,检查平台和开发文档;
6. 文案是否把“可能”“计划”“测试样例”误写成“当前支持”?
推荐的能力陈述格式是:
> 用户可以完成什么 + 当前限制是什么 + 事实来源在哪里。
## 7. 下一任产品经理最应该关注的 5 个产品问题
### 1. 新用户能否在几分钟内完成第一次连接
连接微信数据是所有能力的前置条件。需要建立真实平台和微信版本的成功率、失败原因和耗时数据,而不只依赖开发环境与测试样例。
### 2. 用户能否理解 AI 回答的可信边界
“可回到来源核对”是重要差异,但目前仍需要验证用户是否会打开来源、是否看得懂覆盖提示,以及这些信息能否减少错误决策。
### 3. Knowledge 是否解决了用户可感知的问题
需要验证建立和同步索引的成本、等待时间与搜索收益是否匹配,并明确哪些任务适合普通搜索、哪些任务真正需要 Knowledge。
### 4. Reader Skill 和 Agent Hub 的定位是否足够清楚且安全
两条路径服务不同用户,也有不同的 Token、机器人账号和数据外发边界。需要验证入口命名、配置流程、权限提示和失败恢复是否让用户理解。
### 5. 兼容性和分发是否足以支撑产品承诺
需要维护真实的系统、处理器、微信版本和账号数据兼容矩阵,同时确认安装包、系统授权、连接器和升级路径在发布环境中可用。
## 8. 接手原则
- 先核实能力,再决定怎么宣传;
- 用用户任务描述价值,用正式 docs 承担细节;
- 明确区分当前能力、限制、待验证事项和未来场景;
- 不因文案完整性补充不存在的能力;
- 不把测试通过等同于真实用户环境已经得到验证。
+5 -5
View File
@@ -1,6 +1,6 @@
# WechatExplorer 文档 # TraceMemo 文档
WechatExplorer 的文档按“你想完成什么”组织,而不是按源码模块组织。 TraceMemo 的文档按“你想完成什么”组织,而不是按源码模块组织。
## 从这里开始 ## 从这里开始
@@ -15,7 +15,7 @@ WechatExplorer 的文档按“你想完成什么”组织,而不是按源码
- [语音转文字](./user-guide/voice.md) - [语音转文字](./user-guide/voice.md)
- [导出聊天档案](./user-guide/export.md) - [导出聊天档案](./user-guide/export.md)
- [防撤回](./user-guide/recall-protection.md) - [防撤回](./user-guide/recall-protection.md)
- [在微信里向 WechatExplorer 提问](./agent/agent-hub.md) - [在微信里向 TraceMemo 提问](./agent/agent-hub.md)
- [数据、隐私与安全](./user-guide/privacy.md) - [数据、隐私与安全](./user-guide/privacy.md)
- [常见问题与排查](./user-guide/troubleshooting.md) - [常见问题与排查](./user-guide/troubleshooting.md)
@@ -26,7 +26,7 @@ WechatExplorer 的文档按“你想完成什么”组织,而不是按源码
## 微信机器人和外部 Agent ## 微信机器人和外部 Agent
WechatExplorer 有两种不同的接入方式。微信机器人是普通用户可以直接使用的产品能力;Reader Skill 和 Local HTTP API 面向已经在使用 Codex、Claude Code、OpenClaw 等外部 Agent 的用户。 TraceMemo 有两种不同的接入方式。微信机器人是普通用户可以直接使用的产品能力;Reader Skill 和 Local HTTP API 面向已经在使用 Codex、Claude Code、OpenClaw 等外部 Agent 的用户。
| 你想做什么 | 应该看哪里 | | 你想做什么 | 应该看哪里 |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------- | | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
@@ -57,4 +57,4 @@ WechatExplorer 有两种不同的接入方式。微信机器人是普通用户
- [开发、测试与构建](./development/overview.md) - [开发、测试与构建](./development/overview.md)
- [v2.1.9 API 鉴权迁移说明](./agent/release-notes-v2.1.9.md) - [v2.1.9 API 鉴权迁移说明](./agent/release-notes-v2.1.9.md)
当前工作区版本:**2.1.9**。文档只描述当前代码已经实现的能力;版本兼容性、AI Provider 行为和媒体读取结果可能随系统、微信客户端和服务商变化。 当前工作区版本:**2.2.0**。文档只描述当前代码已经实现的能力;版本兼容性、AI Provider 行为和媒体读取结果可能随系统、微信客户端和服务商变化。
+5 -5
View File
@@ -1,6 +1,6 @@
# 在微信里向 WechatExplorer 提问(Agent Hub # 在微信里向 TraceMemo 提问(Agent Hub
Agent Hub 是 WechatExplorer 内置的微信机器人入口,也是应用一级导航中的“Agent”页面。你先扫码登录一个微信机器人账号,再用微信账号向机器人发送文字;本机 Agent Hub 会接收消息、读取已经连接的微信数据,必要时调用已配置的 AI,再把结果回复给发送者。 Agent Hub 是 TraceMemo 内置的微信机器人入口,也是应用一级导航中的“Agent”页面。你先扫码登录一个微信机器人账号,再用微信账号向机器人发送文字;本机 Agent Hub 会接收消息、读取已经连接的微信数据,必要时调用已配置的 AI,再把结果回复给发送者。
普通用户不需要安装 Reader Skill,也不需要配置 API Token。先连接微信数据库,再扫码登录机器人即可开始;需要总结或自然语言理解的任务还要配置 AI Provider。 普通用户不需要安装 Reader Skill,也不需要配置 API Token。先连接微信数据库,再扫码登录机器人即可开始;需要总结或自然语言理解的任务还要配置 AI Provider。
@@ -11,7 +11,7 @@ Agent Hub 是 WechatExplorer 内置的微信机器人入口,也是应用一级
## 连接器和 Agent Hub 是什么关系 ## 连接器和 Agent Hub 是什么关系
你不需要单独部署这些组件。扫码后,后台的微信连接器负责登录机器人、保持连接、接收微信消息和发送回复;Agent Hub 负责判断消息要做什么、查询 WechatExplorer 本地数据、调用 AI 并组织结果。可以把它理解为:连接器负责“和微信通信”,Hub 负责“处理任务”。 你不需要单独部署这些组件。扫码后,后台的微信连接器负责登录机器人、保持连接、接收微信消息和发送回复;Agent Hub 负责判断消息要做什么、查询 TraceMemo 本地数据、调用 AI 并组织结果。可以把它理解为:连接器负责“和微信通信”,Hub 负责“处理任务”。
## 你能做什么 ## 你能做什么
@@ -50,9 +50,9 @@ Agent Hub 页面会记录系统、Agent Hub 和微信连接器日志。日志支
## 需要满足的条件 ## 需要满足的条件
- WechatExplorer 的微信数据库已经连接,并且数据 API 可以查询; - TraceMemo 的微信数据库已经连接,并且数据 API 可以查询;
- 依赖总结或自然语言理解的任务,需要在“设置 → AI 模型”配置可用的 AI 服务; - 依赖总结或自然语言理解的任务,需要在“设置 → AI 模型”配置可用的 AI 服务;
- WechatExplorer 和 Agent Hub 需要保持运行,机器人才能接收和回复消息。 - TraceMemo 和 Agent Hub 需要保持运行,机器人才能接收和回复消息。
## 安全与边界 ## 安全与边界
+3 -1
View File
@@ -2,10 +2,12 @@
## 当前安全边界 ## 当前安全边界
WechatExplorer 的本地 API 默认监听 `127.0.0.1:6131`。它面向同一台电脑上的 API Center、Reader Skill、CLI 和 Agent,不是公网网关,也不是带用户账户和细粒度权限 Scope 的服务。 TraceMemo 的本地 API 默认监听 `127.0.0.1:6131`。它面向同一台电脑上的 API Center、Reader Skill、CLI 和 Agent,不是公网网关,也不是带用户账户和细粒度权限 Scope 的服务。
## Bearer Token ## Bearer Token
文档中的 `WECHATEXPLORER_API_TOKEN` 是历史兼容环境变量名;TraceMemo v2.2.0 继续支持这一名称,以免已安装的 Reader Skill 和 Agent 配置失效。
- `/api/v1/health` 是公开健康检查; - `/api/v1/health` 是公开健康检查;
- 其他所有端点都要求 `Authorization: Bearer <TOKEN>` - 其他所有端点都要求 `Authorization: Bearer <TOKEN>`
- Token 由应用生成,使用 32 个随机字节编码; - Token 由应用生成,使用 32 个随机字节编码;
+4 -2
View File
@@ -1,4 +1,4 @@
# WechatExplorer Local HTTP API # TraceMemo Local HTTP API
本文面向需要自己写集成的开发者。普通用户请先阅读[Agent 接入概览](./overview.md)。 本文面向需要自己写集成的开发者。普通用户请先阅读[Agent 接入概览](./overview.md)。
@@ -24,6 +24,8 @@ curl -H "Authorization: Bearer $WECHATEXPLORER_API_TOKEN" \
不要把 Token 放入 URL、Skill 文件、仓库或命令历史可被共享的脚本中。 不要把 Token 放入 URL、Skill 文件、仓库或命令历史可被共享的脚本中。
`WECHATEXPLORER_API_TOKEN` 是历史兼容环境变量名,TraceMemo v2.2.0 继续沿用它以保持 Reader Skill 和外部 Agent 兼容。
## 端点 ## 端点
| 方法 | 路径 | 作用 | 参数/请求体 | | 方法 | 路径 | 作用 | 参数/请求体 |
@@ -57,7 +59,7 @@ curl -H "Authorization: Bearer $WECHATEXPLORER_API_TOKEN" \
- `YYYY-MM-DD/HH:mm`:从该分钟开始的 60 秒; - `YYYY-MM-DD/HH:mm`:从该分钟开始的 60 秒;
- 也可以使用 Unix 秒级 `startTime``endTime` - 也可以使用 Unix 秒级 `startTime``endTime`
时间按运行 WechatExplorer 的本机时区解析。用户说“今天”“昨天”时,先调用 `current_time`,再根据返回的 `localDate` 计算日期,避免使用 Agent 自己的时区。 时间按运行 TraceMemo 的本机时区解析。用户说“今天”“昨天”时,先调用 `current_time`,再根据返回的 `localDate` 计算日期,避免使用 Agent 自己的时区。
## 常用工作流 ## 常用工作流
+5 -5
View File
@@ -1,6 +1,6 @@
# 在微信机器人或外部 Agent 中使用 WechatExplorer # 在微信机器人或外部 Agent 中使用 TraceMemo
WechatExplorer 提供两条不同路径。先按你实际想做的事选择,不需要先理解 Agent、Skill 或 API 等术语。 TraceMemo 提供两条不同路径。先按你实际想做的事选择,不需要先理解 Agent、Skill 或 API 等术语。
| 你想做什么 | 使用方式 | 需要什么 | | 你想做什么 | 使用方式 | 需要什么 |
| ---------------------------------------------------- | ----------------------------- | --------------------------------------------------------- | | ---------------------------------------------------- | ----------------------------- | --------------------------------------------------------- |
@@ -9,7 +9,7 @@ WechatExplorer 提供两条不同路径。先按你实际想做的事选择,
## 直接在微信里提问 ## 直接在微信里提问
打开应用一级导航中的“Agent”,进入“Agent Hub”,扫码登录一个微信机器人账号。之后用另一个微信账号向机器人发送文字,它会调用 WechatExplorer 的本机数据,必要时使用已配置的 AI,再把结果回复给发送者。 打开应用一级导航中的“Agent”,进入“Agent Hub”,扫码登录一个微信机器人账号。之后用另一个微信账号向机器人发送文字,它会调用 TraceMemo 的本机数据,必要时使用已配置的 AI,再把结果回复给发送者。
可以先尝试: 可以先尝试:
@@ -23,7 +23,7 @@ WechatExplorer 提供两条不同路径。先按你实际想做的事选择,
## 在外部 Agent 中查询历史微信 ## 在外部 Agent 中查询历史微信
Reader Skill 是给外部 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以通过 WechatExplorer Local HTTP API 按需读取联系人、群聊、最近会话、指定时间范围的聊天和群成员信息。 Reader Skill 是给外部 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以通过 TraceMemo Local HTTP API 按需读取联系人、群聊、最近会话、指定时间范围的聊天和群成员信息。
典型问题包括: 典型问题包括:
@@ -35,7 +35,7 @@ Reader Skill 是给外部 Agent 的操作说明。安装后,Codex、Claude Cod
## 外部 Agent 的安装步骤 ## 外部 Agent 的安装步骤
1. 启动 WechatExplorer 并完成微信数据库连接。 1. 启动 TraceMemo 并完成微信数据库连接。
2. 打开一级导航“API”(页面为“API Center”),确认本地 API、数据库和 Reader Skill 都可用。 2. 打开一级导航“API”(页面为“API Center”),确认本地 API、数据库和 Reader Skill 都可用。
3. 选择目标 Agent,点击“复制安装指令”。 3. 选择目标 Agent,点击“复制安装指令”。
4. 在 Agent 自己的 Skill/配置目录执行或粘贴指令。 4. 在 Agent 自己的 Skill/配置目录执行或粘贴指令。
+6 -4
View File
@@ -2,15 +2,17 @@
## 先理解它能做什么 ## 先理解它能做什么
Reader Skill 是一份给 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以按需调用 WechatExplorer,读取联系人、群聊、最近会话、指定时间的聊天和群成员信息。 Reader Skill 是一份给 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以按需调用 TraceMemo,读取联系人、群聊、最近会话、指定时间的聊天和群成员信息。
它使用的是 WechatExplorer Local HTTP API,不是 MCP Server。 它使用的是 TraceMemo Local HTTP API,不是 MCP Server。
Reader Skill 只负责“外部 Agent 主动查询历史微信数据”。它不负责二维码登录、监听微信实时消息、接收机器人消息或管理 Agent Hub。想让机器人收到微信消息后处理并回复,请阅读[Agent Hub](./agent-hub.md)。 Reader Skill 只负责“外部 Agent 主动查询历史微信数据”。它不负责二维码登录、监听微信实时消息、接收机器人消息或管理 Agent Hub。想让机器人收到微信消息后处理并回复,请阅读[Agent Hub](./agent-hub.md)。
Reader Skill 的展示名称已迁移为 TraceMemo Reader;安装目录和 `WECHATEXPLORER_API_TOKEN` 环境变量仍保留历史兼容标识,以便旧 Agent 配置继续工作。
## 推荐安装流程 ## 推荐安装流程
1. 启动 WechatExplorer 并完成数据库连接。 1. 启动 TraceMemo 并完成数据库连接。
2. 打开“API Center”,确认 API 服务和数据库状态正常。 2. 打开“API Center”,确认 API 服务和数据库状态正常。
3. 在 Reader Skill 区域选择目标 Agent,点击“复制安装指令”。 3. 在 Reader Skill 区域选择目标 Agent,点击“复制安装指令”。
4. 把指令粘贴到对应 Agent 的 Skill/配置目录;应用会根据本机路径生成适合 Codex、Claude Code、OpenClaw 或通用 Agent 的说明。 4. 把指令粘贴到对应 Agent 的 Skill/配置目录;应用会根据本机路径生成适合 Codex、Claude Code、OpenClaw 或通用 Agent 的说明。
@@ -22,7 +24,7 @@ Reader Skill 只负责“外部 Agent 主动查询历史微信数据”。它不
6. 先执行 health 检查,再读取数据端点。 6. 先执行 health 检查,再读取数据端点。
WechatExplorer 不会自动把 Token 写进 Agent 配置。重新生成 Token 后,必须同步更新 Agent 环境。 TraceMemo 不会自动把 Token 写进 Agent 配置。重新生成 Token 后,必须同步更新 Agent 环境。
## Agent 的读取顺序 ## Agent 的读取顺序
+2 -2
View File
@@ -1,4 +1,4 @@
# WechatExplorer 2.1.9Local HTTP API 鉴权迁移 # TraceMemo 2.1.9Local HTTP API 鉴权迁移
2.1.9 为 Local HTTP API 增加 Bearer Token 鉴权。这是一次有意的兼容性变化:除健康检查外,数据接口不再接受裸请求。 2.1.9 为 Local HTTP API 增加 Bearer Token 鉴权。这是一次有意的兼容性变化:除健康检查外,数据接口不再接受裸请求。
@@ -6,7 +6,7 @@
- 2.1.9 中,相同请求必须携带 `Authorization: Bearer <TOKEN>`,否则返回 `401` - 2.1.9 中,相同请求必须携带 `Authorization: Bearer <TOKEN>`,否则返回 `401`
- `GET /api/v1/health` 保持公开; - `GET /api/v1/health` 保持公开;
- 升级后应用会生成并安全保存 Token,原有 API 启用状态、监听地址和端口设置保持不变; - 升级后应用会生成并安全保存 Token,原有 API 启用状态、监听地址和端口设置保持不变;
- Token 可在 WechatExplorer → API Center 中显示、复制和重新生成; - Token 可在 TraceMemo → API Center 中显示、复制和重新生成;
- Reader Skill、Codex、Claude Code、OpenClaw 和其他本地 Agent 需要在自己的环境中设置 `WECHATEXPLORER_API_TOKEN` - Reader Skill、Codex、Claude Code、OpenClaw 和其他本地 Agent 需要在自己的环境中设置 `WECHATEXPLORER_API_TOKEN`
如果旧 Agent 无法访问,请先从 API Center 复制当前 Token,再确认每个非 health 请求都带有 Bearer header。完整规则见[API 安全](./api-security.md)。 如果旧 Agent 无法访问,请先从 API Center 复制当前 Token,再确认每个非 health 请求都带有 Bearer header。完整规则见[API 安全](./api-security.md)。
+1 -1
View File
@@ -4,7 +4,7 @@
AI 回答后,你可以继续查看它参考了哪些聊天内容、这些内容来自哪个会话和时间,并跳回原始消息检查上下文。 AI 回答后,你可以继续查看它参考了哪些聊天内容、这些内容来自哪个会话和时间,并跳回原始消息检查上下文。
这让 WechatExplorer 和只给一段摘要的聊天机器人不同:答案不是终点,来源也应该能被你检查。 这让 TraceMemo 和只给一段摘要的聊天机器人不同:答案不是终点,来源也应该能被你检查。
## 三类来源信息 ## 三类来源信息
+1 -1
View File
@@ -1,4 +1,4 @@
# WechatExplorer 如何把聊天变成可用的信息 # TraceMemo 如何把聊天变成可用的信息
你可以把一次任务想成下面这条路径: 你可以把一次任务想成下面这条路径:
+1 -1
View File
@@ -1,6 +1,6 @@
# 开发、测试与构建 # 开发、测试与构建
本文面向希望参与 WechatExplorer 开发、验证文档或维护集成的贡献者。普通用户请从[第一次使用](../user-guide/getting-started.md)开始。 本文面向希望参与 TraceMemo 开发、验证文档或维护集成的贡献者。普通用户请从[第一次使用](../user-guide/getting-started.md)开始。
## 技术基线 ## 技术基线
+3 -3
View File
@@ -2,11 +2,11 @@
## 你什么时候会看到这些提示 ## 你什么时候会看到这些提示
WechatExplorer 需要读取微信本地数据。macOS 会根据系统版本、微信状态和安全设置,要求应用完成授权;自动获取数据库密钥时,页面可能提示暂时调整系统安全设置。 TraceMemo 需要读取微信本地数据。macOS 会根据系统版本、微信状态和安全设置,要求应用完成授权;自动获取数据库密钥时,页面可能提示暂时调整系统安全设置。
## 推荐步骤 ## 推荐步骤
1. 先启动 WechatExplorer,阅读连接页面显示的当前前置条件。 1. 先启动 TraceMemo,阅读连接页面显示的当前前置条件。
2. 确认微信数据目录指向当前账号。 2. 确认微信数据目录指向当前账号。
3. 只在页面明确要求时处理系统授权或 SIP;按页面提示完成密钥获取后,恢复你平时使用的安全设置。 3. 只在页面明确要求时处理系统授权或 SIP;按页面提示完成密钥获取后,恢复你平时使用的安全设置。
4. 返回应用重新检测账号、数据库和图片资源状态。 4. 返回应用重新检测账号、数据库和图片资源状态。
@@ -15,7 +15,7 @@ WechatExplorer 需要读取微信本地数据。macOS 会根据系统版本、
## SIP 风险 ## SIP 风险
关闭 System Integrity Protection 会降低 macOS 对系统文件和进程的保护。它不是日常使用 WechatExplorer 的功能开关,也不应长期保持关闭。只有在你理解风险、确认页面要求且完成必要操作时才处理;完成后按 Apple 官方方式重新启用。 关闭 System Integrity Protection 会降低 macOS 对系统文件和进程的保护。它不是日常使用 TraceMemo 的功能开关,也不应长期保持关闭。只有在你理解风险、确认页面要求且完成必要操作时才处理;完成后按 Apple 官方方式重新启用。
## 应用无法打开 ## 应用无法打开
+7 -5
View File
@@ -1,21 +1,23 @@
--- ---
name: wechatexplorer-reader name: wechatexplorer-reader
description: 通过 WechatExplorer 本地 HTTP API 按需读取用户有权访问的微信聊天数据。当用户要求查看微信消息、查找联系人或群聊、总结聊天、生成群聊总结时使用。此 Skill 由本机 WechatExplorer 提供数据,不是 MCP Server。 description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的微信聊天数据。当用户要求查看微信消息、查找联系人或群聊、总结聊天、生成群聊总结时使用。此 Skill 由本机 TraceMemo 提供数据,不是 MCP Server。
--- ---
# WechatExplorer Reader # TraceMemo Reader
你是一个通过本机 WechatExplorer 读取微信历史的 Agent。先确认用户已经在 WechatExplorer 中完成数据库连接,再按需调用 API;不要假设数据库已就绪,也不要声称读取了没有调用过的消息。 你是一个通过本机 TraceMemo 读取微信历史的 Agent。先确认用户已经在 TraceMemo 中完成数据库连接,再按需调用 API;不要假设数据库已就绪,也不要声称读取了没有调用过的消息。
## 连接信息 ## 连接信息
- Base URL 默认是 `http://127.0.0.1:6131/api/v1` - Base URL 默认是 `http://127.0.0.1:6131/api/v1`
- `GET /health` 不需要 Token。 - `GET /health` 不需要 Token。
- 其他端点必须带 `Authorization: Bearer $WECHATEXPLORER_API_TOKEN` - 其他端点必须带 `Authorization: Bearer $WECHATEXPLORER_API_TOKEN`
- Token 由用户在 WechatExplorer → API Center 显示/复制,并放在 Agent 自己的本地环境中。 - Token 由用户在 TraceMemo → API Center 显示/复制,并放在 Agent 自己的本地环境中。
- 不要把 Token 放到 URL、回答、日志、Skill 文件或仓库。 - 不要把 Token 放到 URL、回答、日志、Skill 文件或仓库。
- 6131 是普通 Local HTTP API,不是 MCP Server;不要生成 `mcpServers` 配置。 - 6131 是普通 Local HTTP API,不是 MCP Server;不要生成 `mcpServers` 配置。
`wechatexplorer-reader` 是该 Skill 为兼容既有安装而保留的目录和标识名;对用户展示时称为 TraceMemo Reader。
## 每次任务前 ## 每次任务前
1. 调用 `/health`,确认服务和数据库状态。 1. 调用 `/health`,确认服务和数据库状态。
@@ -43,7 +45,7 @@ description: 通过 WechatExplorer 本地 HTTP API 按需读取用户有权访
## 时间与上下文规则 ## 时间与上下文规则
`/chatlog``time` 支持 `YYYY-MM-DD`、日期闭区间和分钟范围;也可以使用 Unix 秒级 `startTime`/`endTime`。时间按 WechatExplorer 所在机器的本机时区解释。 `/chatlog``time` 支持 `YYYY-MM-DD`、日期闭区间和分钟范围;也可以使用 Unix 秒级 `startTime`/`endTime`。时间按 TraceMemo 所在机器的本机时区解释。
当用户问“某个话题是谁说的、后来结论是什么”时,先定位会话和时间,再读取关键消息前后文。回答时区分: 当用户问“某个话题是谁说的、后来结论是什么”时,先定位会话和时间,再读取关键消息前后文。回答时区分:
+11 -11
View File
@@ -1,6 +1,6 @@
# 第一次使用 WechatExplorer # 第一次使用 TraceMemo
如果你刚下载 WechatExplorer,只需要完成一条主线: 如果你刚下载 TraceMemo,只需要完成一条主线:
> 安装应用 → 连接微信数据 → 确认聊天已加载 → 搜索或提问。 > 安装应用 → 连接微信数据 → 确认聊天已加载 → 搜索或提问。
@@ -14,7 +14,7 @@
| Windows | [微信 Windows `4.1.9.57`](https://github.com/iibob/wechat-win-archive/releases#release-v4.1.9.57) | 首次使用时请确认微信数据目录;Windows 不需要关闭 SIP | | Windows | [微信 Windows `4.1.9.57`](https://github.com/iibob/wechat-win-archive/releases#release-v4.1.9.57) | 首次使用时请确认微信数据目录;Windows 不需要关闭 SIP |
- 上表是当前实际测试过的客户端版本,不代表只有这些版本可以使用。其他微信 4.x 版本可能可以连接,但尚未逐一验证。 - 上表是当前实际测试过的客户端版本,不代表只有这些版本可以使用。其他微信 4.x 版本可能可以连接,但尚未逐一验证。
- WechatExplorer 必须取得当前微信账号对应的数据库密钥,才能读取聊天记录。 - TraceMemo 必须取得当前微信账号对应的数据库密钥,才能读取聊天记录。
- 你需要有权访问要读取的微信账号和聊天数据。 - 你需要有权访问要读取的微信账号和聊天数据。
- 如果要使用 AI 问答、群聊日报或图片理解,还需要在应用中配置一个 AI 服务。 - 如果要使用 AI 问答、群聊日报或图片理解,还需要在应用中配置一个 AI 服务。
@@ -26,23 +26,23 @@
### Windows ### Windows
1. 从 Releases 下载 Windows x64 的 `WechatExplorer-<版本号>-setup.exe` 安装包。 1. 从 Releases 下载 Windows x64 的 `TraceMemo-<版本号>-setup.exe` 安装包。
2. 双击安装包,按向导完成安装。 2. 双击安装包,按向导完成安装。
3. 启动 WechatExplorer 3. 启动 TraceMemo
4. 如果安装完成后软件无法启动,请安装 Microsoft Visual C++ x64 运行库:[vc_redist.x64.exe](https://aka.ms/vc14/vc_redist.x64.exe),安装完成后重新启动 WechatExplorer 4. 如果安装完成后软件无法启动,请安装 Microsoft Visual C++ x64 运行库:[vc_redist.x64.exe](https://aka.ms/vc14/vc_redist.x64.exe),安装完成后重新启动 TraceMemo
### macOS ### macOS
1. 根据处理器下载对应的 `.dmg`Apple SiliconM 系列)选择 `arm64`Intel Mac 选择 `x64` 1. 根据处理器下载对应的 `.dmg`Apple SiliconM 系列)选择 `arm64`Intel Mac 选择 `x64`
2. 打开 DMG,将 WechatExplorer 拖入“应用程序”文件夹。 2. 打开 DMG,将 TraceMemo 拖入“应用程序”文件夹。
3. 如果系统提示“无法打开,因为开发者无法验证”,前往“系统设置 → 隐私与安全性”,点击“仍要打开”。 3. 如果系统提示“无法打开,因为开发者无法验证”,前往“系统设置 → 隐私与安全性”,点击“仍要打开”。
4. 如果系统提示应用已损坏,可在终端执行: 4. 如果系统提示应用已损坏,可在终端执行:
```bash ```bash
xattr -cr "/Applications/WechatExplorer.app" xattr -cr "/Applications/TraceMemo.app"
``` ```
5. 启动 WechatExplorer。首次自动获取数据库密钥时,按连接页面显示的授权要求操作;只有页面明确提示时才按[关闭 SIP 教程](../mac-disable-sip.md)处理。关闭 SIP 会降低系统安全性,完成密钥配置后应重新开启。 5. 启动 TraceMemo。首次自动获取数据库密钥时,按连接页面显示的授权要求操作;只有页面明确提示时才按[关闭 SIP 教程](../mac-disable-sip.md)处理。关闭 SIP 会降低系统安全性,完成密钥配置后应重新开启。
更完整的权限和安全边界见 [macOS 数据访问说明](../platform/macos.md)。 更完整的权限和安全边界见 [macOS 数据访问说明](../platform/macos.md)。
@@ -107,12 +107,12 @@
- [转写微信语音](./voice.md) - [转写微信语音](./voice.md)
- [导出聊天档案](./export.md) - [导出聊天档案](./export.md)
- [可选开启防撤回](./recall-protection.md) - [可选开启防撤回](./recall-protection.md)
- [在微信里向 WechatExplorer 提问](../agent/agent-hub.md) - [在微信里向 TraceMemo 提问](../agent/agent-hub.md)
- [让外部 Agent 查询微信历史](../agent/overview.md) - [让外部 Agent 查询微信历史](../agent/overview.md)
## 7. 想直接在微信里提问 ## 7. 想直接在微信里提问
如果你希望直接在微信里向 WechatExplorer 提问,而不是另外配置 Codex 等外部 Agent,请使用 Agent Hub 如果你希望直接在微信里向 TraceMemo 提问,而不是另外配置 Codex 等外部 Agent,请使用 Agent Hub
1. 先完成上面的微信数据库连接,并确认“档案”里能看到聊天。 1. 先完成上面的微信数据库连接,并确认“档案”里能看到聊天。
2. 打开应用主导航中的“Agent”;页面标题为“Agent Hub”。 2. 打开应用主导航中的“Agent”;页面标题为“Agent Hub”。
+2 -2
View File
@@ -4,7 +4,7 @@
如果你经常查同一批工作群、项目讨论或长期联系人,只靠每次临时翻聊天会越来越慢。Knowledge 会在本机建立一份可重复查找的索引,让“以前聊过什么”这类问题更容易跨会话、跨时间找到相关内容。 如果你经常查同一批工作群、项目讨论或长期联系人,只靠每次临时翻聊天会越来越慢。Knowledge 会在本机建立一份可重复查找的索引,让“以前聊过什么”这类问题更容易跨会话、跨时间找到相关内容。
它不是另一个聊天窗口,也不会替你修改微信原始数据库;它是 WechatExplorer 为当前账号维护的本地加速资料。 它不是另一个聊天窗口,也不会替你修改微信原始数据库;它是 TraceMemo 为当前账号维护的本地加速资料。
## 建立和同步 ## 建立和同步
@@ -34,5 +34,5 @@ Knowledge 不会在第一次连接后自动悄悄建立。进入“问问微信
## 产品术语(可选) ## 产品术语(可选)
源码和日志中可能出现 SQLite、FTS、Chunk、索引等词。它们描述的是本地存储和检索实现,不是你开始使用 WechatExplorer 的前置知识。 源码和日志中可能出现 SQLite、FTS、Chunk、索引等词。它们描述的是本地存储和检索实现,不是你开始使用 TraceMemo 的前置知识。
+5 -5
View File
@@ -1,6 +1,6 @@
# 数据、隐私与安全 # 数据、隐私与安全
WechatExplorer 的核心路径是本地优先,但“本地优先”不等于所有功能都完全离线。是否有数据离开电脑,取决于你是否启用了对应的 AI、Agent 或机器人能力。 TraceMemo 的核心路径是本地优先,但“本地优先”不等于所有功能都完全离线。是否有数据离开电脑,取决于你是否启用了对应的 AI、Agent 或机器人能力。
## 默认留在本机的内容 ## 默认留在本机的内容
@@ -12,9 +12,9 @@ WechatExplorer 的核心路径是本地优先,但“本地优先”不等于
- 离线语音转写; - 离线语音转写;
- 导出文件生成和本地日报历史。 - 导出文件生成和本地日报历史。
应用不会因为你打开 WechatExplorer 就自动把整份微信数据库上传。 应用不会因为你打开 TraceMemo 就自动把整份微信数据库上传。
防撤回默认关闭,并且和上面的普通读取路径不同。用户第一次明确开启时,当前实现会在微信消息数据库中安装本地撤回日志/监听结构,同时在 WechatExplorer 用户数据目录保存必要的恢复记录。关闭开关不等于移除已经安装的结构或清空既有记录;当前 UI 没有对应的清理入口。详见[防撤回](./recall-protection.md)。 防撤回默认关闭,并且和上面的普通读取路径不同。用户第一次明确开启时,当前实现会在微信消息数据库中安装本地撤回日志/监听结构,同时在 TraceMemo 用户数据目录保存必要的恢复记录。为兼容旧版本,磁盘上的用户数据目录可能仍沿用历史名称 `WechatExplorer`关闭开关不等于移除已经安装的结构或清空既有记录;当前 UI 没有对应的清理入口。详见[防撤回](./recall-protection.md)。
## 什么时候会请求外部服务 ## 什么时候会请求外部服务
@@ -24,7 +24,7 @@ WechatExplorer 的核心路径是本地优先,但“本地优先”不等于
- 受控检索所需的有限上下文; - 受控检索所需的有限上下文;
- 最终用于总结的 Evidence。 - 最终用于总结的 Evidence。
不会发送完整微信数据库、全量聊天记录、未选中的聊天范围、数据库密钥、内部索引结构或内部会话/消息引用 ID。Provider 的日志、保留、计费和跨境规则不由 WechatExplorer 控制,请查看你所选服务商的政策。 不会发送完整微信数据库、全量聊天记录、未选中的聊天范围、数据库密钥、内部索引结构或内部会话/消息引用 ID。Provider 的日志、保留、计费和跨境规则不由 TraceMemo 控制,请查看你所选服务商的政策。
Ollama 等本机 Provider 可以把模型请求留在本机,但本机服务的日志和配置仍由你负责。 Ollama 等本机 Provider 可以把模型请求留在本机,但本机服务的日志和配置仍由你负责。
@@ -47,7 +47,7 @@ Token 由应用生成,使用 Electron `safeStorage` 加密保存在本机 `loc
外部 Agent 通过 Reader Skill 调用本机 API,按需读取联系人、会话或时间范围内的聊天;它不会因此获得数据库文件路径或任意文件系统权限。Agent 是否把读取结果再次发送给模型,取决于 Agent 本身及其配置。 外部 Agent 通过 Reader Skill 调用本机 API,按需读取联系人、会话或时间范围内的聊天;它不会因此获得数据库文件路径或任意文件系统权限。Agent 是否把读取结果再次发送给模型,取决于 Agent 本身及其配置。
应用内 Agent Hub 是另一条路径:微信机器人通过本机 Hub 调用 WechatExplorer,并且可能使用已配置的 AI 来理解问题。请把机器人账号、发送权限和日志视为独立的安全边界。 应用内 Agent Hub 是另一条路径:微信机器人通过本机 Hub 调用 TraceMemo,并且可能使用已配置的 AI 来理解问题。请把机器人账号、发送权限和日志视为独立的安全边界。
机器人收到的文字会先进入本机 Agent Hub;如果任务需要总结或自然语言理解,受控上下文可能发送给你配置的 AI Provider。机器人账号扫码登录、个人微信数据库连接和外部 Agent/API Token 是不同的边界,使用前请分别确认账号与权限。 机器人收到的文字会先进入本机 Agent Hub;如果任务需要总结或自然语言理解,受控上下文可能发送给你配置的 AI Provider。机器人账号扫码登录、个人微信数据库连接和外部 Agent/API Token 是不同的边界,使用前请分别确认账号与权限。
+5 -5
View File
@@ -1,6 +1,6 @@
# 防撤回 # 防撤回
防撤回是一个默认关闭的可选功能。开启后,WechatExplorer 会尽量保留它能够捕获到的撤回消息,并在聊天气泡旁标记“消息已撤回”。 防撤回是一个默认关闭的可选功能。开启后,TraceMemo 会尽量保留它能够捕获到的撤回消息,并在聊天气泡旁标记“消息已撤回”。
它适合希望在本机档案中保留后续聊天上下文的用户,但不能保证找回每一条撤回消息。 它适合希望在本机档案中保留后续聊天上下文的用户,但不能保证找回每一条撤回消息。
@@ -9,7 +9,7 @@
1. 先连接微信数据库,并确认“档案”可以正常读取聊天。 1. 先连接微信数据库,并确认“档案”可以正常读取聊天。
2. 打开“设置 → 防撤回”。 2. 打开“设置 → 防撤回”。
3. 阅读性能和数据提示后,开启“防撤回”。 3. 阅读性能和数据提示后,开启“防撤回”。
4. 保持 WechatExplorer 与当前微信数据连接;之后捕获到的撤回消息会尽量保留并标记。 4. 保持 TraceMemo 与当前微信数据连接;之后捕获到的撤回消息会尽量保留并标记。
防撤回不是第一次使用的必要步骤。只想浏览、搜索、提问或导出时,可以保持关闭。 防撤回不是第一次使用的必要步骤。只想浏览、搜索、提问或导出时,可以保持关闭。
@@ -18,19 +18,19 @@
- 监听应用能够识别到的后续撤回变化; - 监听应用能够识别到的后续撤回变化;
- 在本地保留必要的消息和撤回关系; - 在本地保留必要的消息和撤回关系;
- 将已识别的原消息与撤回状态一起显示在档案中; - 将已识别的原消息与撤回状态一起显示在档案中;
- 按微信账号隔离 WechatExplorer 保存的恢复记录。 - 按微信账号隔离 TraceMemo 保存的恢复记录。
## 当前限制 ## 当前限制
- 不能恢复开启前已经撤回、且应用从未保存到的消息; - 不能恢复开启前已经撤回、且应用从未保存到的消息;
- WechatExplorer 未运行、数据库未连接或没有捕获到撤回变化时,消息可能无法保留; - TraceMemo 未运行、数据库未连接或没有捕获到撤回变化时,消息可能无法保留;
- 微信版本、消息表结构和数据库事件变化都可能让部分消息无法恢复或正确匹配; - 微信版本、消息表结构和数据库事件变化都可能让部分消息无法恢复或正确匹配;
- 开启后需要为消息表增加监听,聊天很多或磁盘较慢时可能影响加载性能; - 开启后需要为消息表增加监听,聊天很多或磁盘较慢时可能影响加载性能;
- “消息已撤回”只说明应用识别到了撤回关系,不保证恢复内容完整。 - “消息已撤回”只说明应用识别到了撤回关系,不保证恢复内容完整。
## 数据写入与关闭边界 ## 数据写入与关闭边界
普通浏览、搜索和 Knowledge 不会修改微信原始聊天数据库;防撤回是一个例外。用户第一次明确开启时,当前实现会在微信消息数据库中安装用于记录撤回的本地日志/监听结构,并在 WechatExplorer 的用户数据目录保存必要的本地恢复记录。 普通浏览、搜索和 Knowledge 不会修改微信原始聊天数据库;防撤回是一个例外。用户第一次明确开启时,当前实现会在微信消息数据库中安装用于记录撤回的本地日志/监听结构,并在 TraceMemo 的用户数据目录保存必要的本地恢复记录。为兼容旧版本,该目录在磁盘上可能继续显示为历史名称 `WechatExplorer`
关闭设置中的开关,不等同于删除已经安装的日志结构或清空此前保存的恢复记录。当前版本没有在 UI 中提供“移除防撤回日志结构”或“清空防撤回记录”的独立操作。对数据库写入、磁盘占用或完全回滚有要求时,应在开启前先确认这一边界,并使用微信官方方式备份重要数据。 关闭设置中的开关,不等同于删除已经安装的日志结构或清空此前保存的恢复记录。当前版本没有在 UI 中提供“移除防撤回日志结构”或“清空防撤回记录”的独立操作。对数据库写入、磁盘占用或完全回滚有要求时,应在开启前先确认这一边界,并使用微信官方方式备份重要数据。
+1 -1
View File
@@ -1,6 +1,6 @@
# 生成群聊日报和总结 # 生成群聊日报和总结
如果你每天在多个群里聊天,晚上不想重新翻几十个群,可以让 WechatExplorer 根据一个群的聊天内容整理出一份可阅读、可保存的报告。 如果你每天在多个群里聊天,晚上不想重新翻几十个群,可以让 TraceMemo 根据一个群的聊天内容整理出一份可阅读、可保存的报告。
## 报告适合做什么 ## 报告适合做什么
+5 -5
View File
@@ -8,14 +8,14 @@
1. 确认下载的是 GitHub Releases 中的 Windows x64 `-setup.exe`,并已完成安装。 1. 确认下载的是 GitHub Releases 中的 Windows x64 `-setup.exe`,并已完成安装。
2. 安装 [Microsoft Visual C++ x64 运行库](https://aka.ms/vc14/vc_redist.x64.exe)。 2. 安装 [Microsoft Visual C++ x64 运行库](https://aka.ms/vc14/vc_redist.x64.exe)。
3. 安装完成后重新启动 WechatExplorer;如果仍无响应,再重新运行安装包进行覆盖安装。 3. 安装完成后重新启动 TraceMemo;如果仍无响应,再重新运行安装包进行覆盖安装。
### macOS ### macOS
- 提示“无法打开,因为开发者无法验证”时,前往“系统设置 → 隐私与安全性”并点击“仍要打开”。 - 提示“无法打开,因为开发者无法验证”时,前往“系统设置 → 隐私与安全性”并点击“仍要打开”。
- 提示应用已损坏时,确认应用位于“应用程序”目录,再执行 `xattr -cr "/Applications/WechatExplorer.app"` - 提示应用已损坏时,确认应用位于“应用程序”目录,再执行 `xattr -cr "/Applications/TraceMemo.app"`
完整安装步骤见[第一次使用 WechatExplorer](./getting-started.md#2-安装并启动)。 完整安装步骤见[第一次使用 TraceMemo](./getting-started.md#2-安装并启动)。
## 连接微信失败 ## 连接微信失败
@@ -69,7 +69,7 @@ AI Search 失败时可能仍保留部分来源;不要把部分结果当成完
确认: 确认:
1. WechatExplorer 正在运行且 API Center 显示本地服务在线; 1. TraceMemo 正在运行且 API Center 显示本地服务在线;
2. Agent 使用的是当前 Reader Skill,而不是旧的 MCP 配置; 2. Agent 使用的是当前 Reader Skill,而不是旧的 MCP 配置;
3. 请求地址为 `http://127.0.0.1:6131` 3. 请求地址为 `http://127.0.0.1:6131`
4. 非 health 请求带有最新 `Authorization: Bearer <TOKEN>` 4. 非 health 请求带有最新 `Authorization: Bearer <TOKEN>`
@@ -91,4 +91,4 @@ Agent Hub 和外部 Agent 是两条路径。机器人异常时依次确认:
## 防撤回没有保留消息 ## 防撤回没有保留消息
防撤回只能尽量保留开启后且应用成功捕获到的撤回变化。确认开启时数据库已经连接、WechatExplorer 在撤回发生时保持运行,并检查聊天加载是否明显变慢。开启前已经消失、应用未捕获或微信结构无法识别的消息不能保证恢复;详见[防撤回](./recall-protection.md)。 防撤回只能尽量保留开启后且应用成功捕获到的撤回变化。确认开启时数据库已经连接、TraceMemo 在撤回发生时保持运行,并检查聊天加载是否明显变慢。开启前已经消失、应用未捕获或微信结构无法识别的消息不能保证恢复;详见[防撤回](./recall-protection.md)。
+1 -1
View File
@@ -1,6 +1,6 @@
# 语音转文字 # 语音转文字
WechatExplorer 可以把微信语音转换成可搜索的文字,适合你不想逐条播放、希望把语音内容带入后续查找或导出的场景。 TraceMemo 可以把微信语音转换成可搜索的文字,适合你不想逐条播放、希望把语音内容带入后续查找或导出的场景。
## 使用前准备 ## 使用前准备
+7 -5
View File
@@ -1,5 +1,6 @@
# Keep the legacy appId so existing userData directories and upgrades remain readable.
appId: com.wechatexplorer.app appId: com.wechatexplorer.app
productName: WechatExplorer productName: TraceMemo
afterPack: scripts/after-pack.cjs afterPack: scripts/after-pack.cjs
directories: directories:
buildResources: build buildResources: build
@@ -32,8 +33,8 @@ extraResources:
win: win:
icon: icon.ico icon: icon.ico
# WCDB's Windows runtime checks the host executable name. The dev runtime is # WCDB's Windows runtime checks the host executable name. The dev runtime is
# electron.exe, so keep the packaged executable compatible while preserving # electron.exe, so keep the packaged executable compatible while using
# WechatExplorer as the product/shortcut name. # TraceMemo as the product/shortcut name.
executableName: electron executableName: electron
nsis: nsis:
oneClick: false oneClick: false
@@ -47,9 +48,10 @@ mac:
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
extendInfo: extendInfo:
# The bundled WCDB bridge accepts Electron as its internal host name. The # The bundled WCDB bridge accepts Electron as its internal host name. The
# public app name and bundle identifier remain WechatExplorer-specific. # legacy bundle identifier remains unchanged for upgrade compatibility;
# the public display name is migrated to TraceMemo.
CFBundleName: Electron CFBundleName: Electron
CFBundleDisplayName: WechatExplorer CFBundleDisplayName: TraceMemo
NSCameraUsageDescription: Application requests access to the device's camera. NSCameraUsageDescription: Application requests access to the device's camera.
NSMicrophoneUsageDescription: Application requests access to the device's microphone. NSMicrophoneUsageDescription: Application requests access to the device's microphone.
NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
+3 -3
View File
@@ -1,8 +1,8 @@
{ {
"name": "wechatexplorer", "name": "tracememo",
"version": "2.1.9", "version": "2.2.0",
"packageManager": "pnpm@7.33.7", "packageManager": "pnpm@7.33.7",
"description": "macOS / Windows 本地优先的微信聊天记录搜索与 AI 工作台", "description": "macOS / Windows 本地优先、可追溯的 AI 微信知识与分析工作台",
"keywords": [ "keywords": [
"wechat", "wechat",
"wechat chat", "wechat chat",
+1 -1
View File
@@ -594,7 +594,7 @@
</section> </section>
<footer class="footer"> <footer class="footer">
数据来源:WechatExplorer · 微信群聊记录<br /> 数据来源:TraceMemo · 微信群聊记录<br />
生成时间:{{GENERATED_AT}}<br /> 生成时间:{{GENERATED_AT}}<br />
{{FOOTER_NOTE}} {{FOOTER_NOTE}}
</footer> </footer>
+1 -1
View File
@@ -831,7 +831,7 @@
</section> </section>
<footer class="footer"> <footer class="footer">
数据来源:WechatExplorer · 微信群聊记录<br /> 数据来源:TraceMemo · 微信群聊记录<br />
生成时间:{{GENERATED_AT}}<br /> 生成时间:{{GENERATED_AT}}<br />
{{FOOTER_NOTE}} {{FOOTER_NOTE}}
</footer> </footer>
+1 -1
View File
@@ -108,7 +108,7 @@ function setPlistValue(plistPath, key, value) {
function validateReaderSkillRuntime(runtimeResources) { function validateReaderSkillRuntime(runtimeResources) {
const skillPath = path.join(runtimeResources, 'skill', 'wechatexplorer-reader', 'SKILL.md') const skillPath = path.join(runtimeResources, 'skill', 'wechatexplorer-reader', 'SKILL.md')
if (!existsSync(skillPath)) { if (!existsSync(skillPath)) {
throw new Error(`Missing bundled WechatExplorer Reader Skill: ${skillPath}`) throw new Error(`Missing bundled TraceMemo Reader Skill: ${skillPath}`)
} }
return skillPath return skillPath
} }
+3 -3
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# WechatExplorer v2.1.9 Local HTTP API 手动验收脚本 # TraceMemo v2.2.0 Local HTTP API 手动验收脚本
# 仅用于 macOS Terminal;不会写入或输出真实 API Token。 # 仅用于 macOS Terminal;不会写入或输出真实 API Token。
set -u set -u
API_BASE_URL="${API_BASE_URL:-http://127.0.0.1:6131}" API_BASE_URL="${API_BASE_URL:-http://127.0.0.1:6131}"
API_BASE_URL="${API_BASE_URL%/}" API_BASE_URL="${API_BASE_URL%/}"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/wechatexplorer-api-test.XXXXXX")" TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/tracememo-api-test.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT trap 'rm -rf "$TMP_DIR"' EXIT
PASS_COUNT=0 PASS_COUNT=0
@@ -18,7 +18,7 @@ pass() { PASS_COUNT=$((PASS_COUNT + 1)); printf 'PASS %s\n' "$1"; }
fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); printf 'FAIL %s%s\n' "$1" "${2:+ ($2)}"; } fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); printf 'FAIL %s%s\n' "$1" "${2:+ ($2)}"; }
skip() { SKIP_COUNT=$((SKIP_COUNT + 1)); printf 'SKIP %s\n' "$1"; } skip() { SKIP_COUNT=$((SKIP_COUNT + 1)); printf 'SKIP %s\n' "$1"; }
printf 'WechatExplorer Local HTTP API 手动测试\n' printf 'TraceMemo Local HTTP API 手动测试\n'
printf 'API 地址: %s\n\n' "$API_BASE_URL" printf 'API 地址: %s\n\n' "$API_BASE_URL"
read -r -s -p '请输入 API Token(不会显示): ' API_TOKEN read -r -s -p '请输入 API Token(不会显示): ' API_TOKEN
printf '\n' printf '\n'
+1 -1
View File
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..') const root = path.resolve(__dirname, '..')
const templatePath = path.join(root, 'resources', 'mobile_daily_report.html') const templatePath = path.join(root, 'resources', 'mobile_daily_report.html')
const outputDir = path.join(os.tmpdir(), 'wechatexplorer-report-fixtures') const outputDir = path.join(os.tmpdir(), 'tracememo-report-fixtures')
const escapeHtml = (value) => const escapeHtml = (value) =>
String(value ?? '') String(value ?? '')
+1 -1
View File
@@ -34,7 +34,7 @@ const local = {
for (const [target, expected] of [ for (const [target, expected] of [
['codex', 'Codex 项目或用户 Skill 目录'], ['codex', 'Codex 项目或用户 Skill 目录'],
['claude-code', '按照 SKILL\.md 调用本地 HTTP API'], ['claude-code', '按照 SKILL\.md 调用本地 HTTP API'],
['openclaw', '作为 WechatExplorer Reader Skill 安装'], ['openclaw', '作为 TraceMemo Reader Skill 安装'],
['generic', '读取并安装'] ['generic', '读取并安装']
]) { ]) {
const text = buildSkillInstallInstruction({ const text = buildSkillInstallInstruction({
+4 -4
View File
@@ -1,6 +1,6 @@
# WechatExplorer WeChat Connector # TraceMemo WeChat Connector
This repository-local service provides the minimal WeChat bridge required by WechatExplorer: This repository-local service provides the minimal WeChat bridge required by TraceMemo:
- QR-code login with a single persisted credential - QR-code login with a single persisted credential
- account discovery - account discovery
@@ -18,8 +18,8 @@ go run . accounts --json
go run . start --foreground --api-addr 127.0.0.1:18011 --account-id <account-id> go run . start --foreground --api-addr 127.0.0.1:18011 --account-id <account-id>
``` ```
Credential and synchronization state is stored under `~/.wechatexplorer/wechat-connector/accounts`. A successful login is written before the older credential and synchronization state are removed, so an incomplete login cannot destroy the last working credential. Credential and synchronization state is stored under `~/.wechatexplorer/wechat-connector/accounts`. This legacy directory name is intentionally retained so upgrades can reuse existing accounts. A successful login is written before the older credential and synchronization state are removed, so an incomplete login cannot destroy the last working credential.
## Attribution ## Attribution
Low-level protocol and media transport portions are distributed under the MIT license in [LICENSE](LICENSE). WechatExplorer-specific process management, webhook contract, product UI, and Agent Hub behavior live in the surrounding WechatExplorer project. Low-level protocol and media transport portions are distributed under the MIT license in [LICENSE](LICENSE). TraceMemo-specific process management, webhook contract, product UI, and Agent Hub behavior live in the surrounding TraceMemo project.
+2 -2
View File
@@ -73,7 +73,7 @@ func (m *Monitor) Run(ctx context.Context) error {
log.Printf("[monitor] GetUpdates error (%d/%d, backoff=%s): %v", log.Printf("[monitor] GetUpdates error (%d/%d, backoff=%s): %v",
m.failures, maxConsecutiveFailures, backoff, err) m.failures, maxConsecutiveFailures, backoff, err)
if m.failures == maxConsecutiveFailures { if m.failures == maxConsecutiveFailures {
log.Printf("[monitor] WARNING: %d consecutive failures; reconnect from WechatExplorer if this persists.", maxConsecutiveFailures) log.Printf("[monitor] WARNING: %d consecutive failures; reconnect from TraceMemo if this persists.", maxConsecutiveFailures)
} }
select { select {
case <-time.After(backoff): case <-time.After(backoff):
@@ -96,7 +96,7 @@ func (m *Monitor) Run(ctx context.Context) error {
} else { } else {
// Sync buf already empty but still getting session expired: // Sync buf already empty but still getting session expired:
// the bot token itself has expired. The user needs to re-login. // the bot token itself has expired. The user needs to re-login.
log.Printf("[monitor] WARNING: WeChat session expired and cannot be auto-recovered; reconnect from WechatExplorer.") log.Printf("[monitor] WARNING: WeChat session expired and cannot be auto-recovered; reconnect from TraceMemo.")
} }
select { select {
case <-time.After(sessionExpiredBackoff): case <-time.After(sessionExpiredBackoff):
+31
View File
@@ -0,0 +1,31 @@
import { app } from 'electron'
import path from 'path'
import {
chooseUserDataRoot,
getUserDataRoots,
LEGACY_USER_DATA_NAME
} from './app-data-paths'
// This module must remain the first main-process import. Static imports in
// settings/cache services can otherwise resolve Electron paths before the
// legacy runtime identity and selected userData are installed.
app.setName(process.platform === 'win32' ? 'WeFlow' : LEGACY_USER_DATA_NAME)
const isolatedUserData = process.env['WXE_USER_DATA']
const roots = getUserDataRoots(app.getPath('appData'))
const selectedUserData = chooseUserDataRoot({
...roots,
isolated: isolatedUserData
})
app.setPath('userData', selectedUserData)
app.setPath('sessionData', selectedUserData)
// Logs are intentionally independent from userData. New TraceMemo logs go to
// the new visible directory while historical WechatExplorer logs remain in
// place and are never moved or renamed.
if (process.platform === 'darwin') {
app.setPath('logs', path.join(app.getPath('home'), 'Library', 'Logs', 'TraceMemo'))
}
export { roots, selectedUserData }
+104
View File
@@ -0,0 +1,104 @@
import fs from 'fs'
import path from 'path'
export const LEGACY_USER_DATA_NAME = 'WechatExplorer'
export const CURRENT_USER_DATA_NAME = 'tracememo'
export interface UserDataRoots {
legacy: string
current: string
}
export interface UserDataSelectionInput extends UserDataRoots {
isolated?: string
}
function isNonEmptyFile(filePath: string): boolean {
try {
const stat = fs.statSync(filePath)
return stat.isFile() && stat.size > 0
} catch {
return false
}
}
function hasPersistentEntries(directoryPath: string): boolean {
try {
return fs.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
if (entry.name === '.DS_Store') return false
if (entry.name === 'LOCK' || entry.name === 'LOG' || entry.name === 'LOG.old') return false
return entry.isFile() || entry.isDirectory()
})
} catch {
return false
}
}
function hasDatabaseKey(directoryPath: string): boolean {
try {
return fs.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
return entry.isFile() && entry.name.endsWith('.bin') && isNonEmptyFile(path.join(directoryPath, entry.name))
})
} catch {
return false
}
}
function hasKnowledgeDatabase(root: string): boolean {
const knowledgeRoot = path.join(root, 'knowledge')
try {
return fs.readdirSync(knowledgeRoot, { withFileTypes: true }).some((entry) => {
if (!entry.isDirectory()) return false
return isNonEmptyFile(path.join(knowledgeRoot, entry.name, 'knowledge.sqlite'))
})
} catch {
return false
}
}
/**
* Runtime-only Chromium files are deliberately excluded. A directory is a
* valid data root only when it contains at least one user-owned marker.
*/
export function hasValidUserAssets(root: string): boolean {
const markers = [
'settings.json',
'ai-providers.json',
'ai-provider-keys.bin',
'local-api-token.bin',
'wechat-db-key.bin',
'wechat-image-keys.bin',
'image-insights.json',
'wechat-share-service.bin'
]
if (markers.some((marker) => isNonEmptyFile(path.join(root, marker)))) return true
if (hasKnowledgeDatabase(root)) return true
if (hasDatabaseKey(path.join(root, 'database-keys'))) return true
if (hasPersistentEntries(path.join(root, 'reports'))) return true
if (hasPersistentEntries(path.join(root, 'recall-archive'))) return true
if (hasPersistentEntries(path.join(root, 'digital-twin'))) return true
if (hasPersistentEntries(path.join(root, 'group-exit-monitor'))) return true
if (hasPersistentEntries(path.join(root, 'Local Storage', 'leveldb'))) return true
return false
}
export function getUserDataRoots(appDataPath: string): UserDataRoots {
return {
legacy: path.join(appDataPath, LEGACY_USER_DATA_NAME),
current: path.join(appDataPath, CURRENT_USER_DATA_NAME)
}
}
/**
* Select exactly one root. This intentionally does not copy, merge, delete or
* modify either directory. Legacy wins when both roots contain user assets so
* a v2.1.9 upgrade remains deterministic and lossless.
*/
export function chooseUserDataRoot(input: UserDataSelectionInput): string {
const isolated = input.isolated?.trim()
if (isolated) return path.resolve(isolated)
if (hasValidUserAssets(input.legacy)) return input.legacy
if (hasValidUserAssets(input.current)) return input.current
return input.current
}
+1 -1
View File
@@ -34,7 +34,7 @@ export class AppLogger {
} }
get logPath(): string { get logPath(): string {
return path.join(this.logDir, 'wechatexplorer.log') return path.join(this.logDir, 'tracememo.log')
} }
private rotateIfNeeded(): void { private rotateIfNeeded(): void {
+15 -3
View File
@@ -44,6 +44,18 @@ const exportStamp = (): string => {
const pad = (value: number): string => String(value).padStart(2, '0') const pad = (value: number): string => String(value).padStart(2, '0')
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}` return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
} }
const defaultExportRoot = (): string => join(app.getPath('documents'), 'TraceMemo', '导出')
const legacyExportRoot = (): string => join(app.getPath('documents'), 'WechatExplorer', '导出')
const resolveDefaultExportRoot = async (outputFolder?: string): Promise<string> => {
if (!outputFolder) return defaultExportRoot()
try {
await fs.access(join(legacyExportRoot(), outputFolder))
// Continue incremental exports in the legacy folder when it already exists.
return legacyExportRoot()
} catch {
return defaultExportRoot()
}
}
const imageKeys = new ImageKeyConfigService() const imageKeys = new ImageKeyConfigService()
export interface HtmlExportConversation { export interface HtmlExportConversation {
@@ -908,13 +920,13 @@ async function runSingleExport(
total: messages.length, total: messages.length,
percent: request.format === 'html' ? 18 : 20 percent: request.format === 'html' ? 18 : 20
}) })
const root = options.outputRoot || join(app.getPath('documents'), 'WechatExplorer', '导出')
await fs.mkdir(root, { recursive: true })
const ext = request.format === 'markdown' ? 'md' : request.format const ext = request.format === 'markdown' ? 'md' : request.format
const outputFolder = const outputFolder =
request.format === 'html' request.format === 'html'
? options.outputFolderName || safeFilePart(request.outputName) ? options.outputFolderName || safeFilePart(request.outputName)
: `${safeFilePart(request.outputName)}_${exportStamp()}` : `${safeFilePart(request.outputName)}_${exportStamp()}`
const root = options.outputRoot || (await resolveDefaultExportRoot(outputFolder))
await fs.mkdir(root, { recursive: true })
const outputDir = join(root, outputFolder) const outputDir = join(root, outputFolder)
const outputPath = const outputPath =
request.format === 'html' request.format === 'html'
@@ -1506,8 +1518,8 @@ async function runAllExport(
throw new Error('导出聊天不能重复') throw new Error('导出聊天不能重复')
} }
const exportRoot = join(app.getPath('documents'), 'WechatExplorer', '导出')
const outputFolder = safeFilePart(request.outputName) const outputFolder = safeFilePart(request.outputName)
const exportRoot = await resolveDefaultExportRoot(outputFolder)
outputDir = join(exportRoot, outputFolder) outputDir = join(exportRoot, outputFolder)
const folderNames = conversationFolderNames(targets) const folderNames = conversationFolderNames(targets)
let lastProgressAt = 0 let lastProgressAt = 0
+1 -1
View File
@@ -75,7 +75,7 @@ const embedAvatar = async (source: string | undefined, name: string): Promise<st
if (/^https?:\/\//i.test(source)) { if (/^https?:\/\//i.test(source)) {
const response = await fetch(source, { const response = await fetch(source, {
headers: { headers: {
'User-Agent': 'Mozilla/5.0 WechatExplorer', 'User-Agent': 'Mozilla/5.0 TraceMemo',
Referer: 'https://weixin.qq.com/' Referer: 'https://weixin.qq.com/'
}, },
signal: AbortSignal.timeout(8000) signal: AbortSignal.timeout(8000)
+9 -9
View File
@@ -167,7 +167,7 @@ const routes: Record<string, RouteHandler> = {
sendJson(res, 200, { sendJson(res, 200, {
ok: true, ok: true,
ready: isReady(), ready: isReady(),
service: 'WechatExplorer Reader', service: 'TraceMemo Reader',
version: '1.0.0', version: '1.0.0',
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}) })
@@ -186,7 +186,7 @@ const routes: Record<string, RouteHandler> = {
}, },
'/api/v1/contact': ({ res, url }) => { '/api/v1/contact': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const filter = url.searchParams.get('filter') || undefined const filter = url.searchParams.get('filter') || undefined
const type = url.searchParams.get('type') || undefined const type = url.searchParams.get('type') || undefined
let contacts = listContacts(filter) let contacts = listContacts(filter)
@@ -197,7 +197,7 @@ const routes: Record<string, RouteHandler> = {
}, },
'/api/v1/chatroom': ({ res, url }) => { '/api/v1/chatroom': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const keyword = url.searchParams.get('keyword') || '' const keyword = url.searchParams.get('keyword') || ''
let groups = listContacts().filter((c) => c.type === 'group') let groups = listContacts().filter((c) => c.type === 'group')
if (keyword) { if (keyword) {
@@ -212,14 +212,14 @@ const routes: Record<string, RouteHandler> = {
}, },
'/api/v1/recent_chat': ({ res, url }) => { '/api/v1/recent_chat': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const limit = parseNumeric(url.searchParams.get('limit'), 50) const limit = parseNumeric(url.searchParams.get('limit'), 50)
const items = listRecentChat(limit) const items = listRecentChat(limit)
sendJson(res, 200, { count: items.length, items }) sendJson(res, 200, { count: items.length, items })
}, },
'/api/v1/chatlog': ({ res, url }) => { '/api/v1/chatlog': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const talker = url.searchParams.get('talker') const talker = url.searchParams.get('talker')
if (!talker) return sendError(res, 400, '缺少必要参数 talker') if (!talker) return sendError(res, 400, '缺少必要参数 talker')
@@ -257,7 +257,7 @@ const routes: Record<string, RouteHandler> = {
}, },
'/api/v1/group_snapshot': ({ res, url }) => { '/api/v1/group_snapshot': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const md5 = url.searchParams.get('md5') const md5 = url.searchParams.get('md5')
if (!md5) return sendError(res, 400, '缺少必要参数 md5') if (!md5) return sendError(res, 400, '缺少必要参数 md5')
const snapshot = getGroupSnapshot(md5) const snapshot = getGroupSnapshot(md5)
@@ -266,7 +266,7 @@ const routes: Record<string, RouteHandler> = {
}, },
'/api/v1/resolve': ({ res, url }) => { '/api/v1/resolve': ({ res, url }) => {
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
const q = url.searchParams.get('q') const q = url.searchParams.get('q')
if (!q) return sendError(res, 400, '缺少必要参数 q') if (!q) return sendError(res, 400, '缺少必要参数 q')
const contact = resolveMd5(q) const contact = resolveMd5(q)
@@ -276,7 +276,7 @@ const routes: Record<string, RouteHandler> = {
'/api/v1/report': async ({ req, res, body }) => { '/api/v1/report': async ({ req, res, body }) => {
if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求') if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求')
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
if (typeof body !== 'string' || !body.trim()) { if (typeof body !== 'string' || !body.trim()) {
return sendError(res, 400, '请求体为空,需 POST GroupReportExportRequest JSON') return sendError(res, 400, '请求体为空,需 POST GroupReportExportRequest JSON')
} }
@@ -300,7 +300,7 @@ const routes: Record<string, RouteHandler> = {
'/api/v1/agent/group-report': async ({ req, res, body }) => { '/api/v1/agent/group-report': async ({ req, res, body }) => {
if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求') if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求')
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
let request: { group?: string; range?: 'today' | 'yesterday' | '7days' } let request: { group?: string; range?: 'today' | 'yesterday' | '7days' }
try { try {
request = JSON.parse(typeof body === 'string' ? body : '{}') request = JSON.parse(typeof body === 'string' ? body : '{}')
+2 -2
View File
@@ -562,7 +562,7 @@ export class ImageDecryptService {
} }
/** /**
* md5 (WechatExplorer ) * md5
*/ */
findImageFile(md5?: string, imageDatName?: string, options?: ImageFindOptions): string | null { findImageFile(md5?: string, imageDatName?: string, options?: ImageFindOptions): string | null {
const allowThumbnail = options?.allowThumbnail !== false const allowThumbnail = options?.allowThumbnail !== false
@@ -648,7 +648,7 @@ export class ImageDecryptService {
} }
} }
// 尝试 WechatExplorer 的目录结构: msg/attach/{hash}/{YYYY-MM}/Img/ // 尝试 TraceMemo 兼容的微信目录结构: msg/attach/{hash}/{YYYY-MM}/Img/
if (!existsSync(attachDir)) { if (!existsSync(attachDir)) {
imageDecryptLog('[ImageDecrypt] attach dir not found:', attachDir) imageDecryptLog('[ImageDecrypt] attach dir not found:', attachDir)
return rememberPath( return rememberPath(
+6 -16
View File
@@ -1,3 +1,4 @@
import './app-data-bootstrap'
import './preload-env' import './preload-env'
import { import {
app, app,
@@ -272,17 +273,6 @@ protocol.registerSchemesAsPrivileged([
} }
]) ])
// WCDB's Windows runtime checks the host application name during wcdb_init.
// Mirroring WeFlow's name unblocks the -1006 init failure on Windows.
app.setName(
process.platform === 'win32'
? 'WeFlow'
: process.env['WXE_USER_DATA']
? 'WechatExplorer Dev'
: 'WechatExplorer'
)
const isolatedUserData = process.env['WXE_USER_DATA']
if (isolatedUserData) app.setPath('userData', isolatedUserData)
let dbInitInFlight: Promise<{ success: boolean; monitoring?: boolean; error?: string }> | null = let dbInitInFlight: Promise<{ success: boolean; monitoring?: boolean; error?: string }> | null =
null null
let appShutdownRequested = false let appShutdownRequested = false
@@ -435,7 +425,7 @@ function createWindow(): void {
void dialog void dialog
.showMessageBox(mainWindow, { .showMessageBox(mainWindow, {
type: 'question', type: 'question',
title: '关闭 WechatExplorer', title: '关闭 TraceMemo',
message: '请选择关闭方式', message: '请选择关闭方式',
detail: '你可以将窗口隐藏到系统托盘,或退出整个应用进程。', detail: '你可以将窗口隐藏到系统托盘,或退出整个应用进程。',
buttons: ['最小化到系统托盘', '关闭进程', '取消'], buttons: ['最小化到系统托盘', '关闭进程', '取消'],
@@ -516,11 +506,11 @@ app.whenReady().then(async () => {
return new Response('Media unavailable', { status: 500 }) return new Response('Media unavailable', { status: 500 })
} }
}) })
console.log(`WechatExplorer main build: ${BUILD_MARK}`) console.log(`TraceMemo main build: ${BUILD_MARK}`)
appLogger.write({ appLogger.write({
level: 'info', level: 'info',
scope: 'lifecycle', scope: 'lifecycle',
message: 'WechatExplorer 启动', message: 'TraceMemo 启动',
details: { build: BUILD_MARK, platform: process.platform, version: app.getVersion() } details: { build: BUILD_MARK, platform: process.platform, version: app.getVersion() }
}) })
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
@@ -1695,7 +1685,7 @@ function buildTrayMenu(): Menu {
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: '退出 WechatExplorer', label: '退出 TraceMemo',
click: () => { click: () => {
tray?.destroy() tray?.destroy()
tray = null tray = null
@@ -1714,7 +1704,7 @@ function setupTray(): void {
? nativeImage.createEmpty() ? nativeImage.createEmpty()
: image.resize({ width: traySize, height: traySize, quality: 'best' }) : image.resize({ width: traySize, height: traySize, quality: 'best' })
tray = new Tray(trayImage) tray = new Tray(trayImage)
tray.setToolTip('WechatExplorer') tray.setToolTip('TraceMemo')
// macOS may show a Tray context menu on a primary click when it is set // macOS may show a Tray context menu on a primary click when it is set
// directly on the Tray. Keep the menu for an explicit secondary click so // directly on the Tray. Keep the menu for an explicit secondary click so
// the primary click only restores the main window. // the primary click only restores the main window.
+1 -1
View File
@@ -33,5 +33,5 @@ try {
process.env.WEFLOW_PROJECT_NAME = process.env.WEFLOW_PROJECT_NAME || 'WeFlow' process.env.WEFLOW_PROJECT_NAME = process.env.WEFLOW_PROJECT_NAME || 'WeFlow'
prependPath(dllDirs.filter((dir) => fs.existsSync(dir))) prependPath(dllDirs.filter((dir) => fs.existsSync(dir)))
} catch (error) { } catch (error) {
console.error('[WechatExplorer] failed to enforce local DLL priority:', error) console.error('[TraceMemo] failed to enforce local DLL priority:', error)
} }
+5 -5
View File
@@ -455,7 +455,7 @@ class AgentHubService {
private async replyRecentChats(inbound: InboundMessage, limit: number): Promise<void> { private async replyRecentChats(inbound: InboundMessage, limit: number): Promise<void> {
if (!isReady()) { if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。') await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
return return
} }
const items = listRecentChat(limit) const items = listRecentChat(limit)
@@ -474,7 +474,7 @@ class AgentHubService {
const result = await agentAIProvider.chat([ const result = await agentAIProvider.chat([
{ {
role: 'system', role: 'system',
content: `你是 WechatExplorer 微信机器人的意图理解器。只能输出一行 JSON,不要 Markdown。 content: `你是 TraceMemo 微信机器人的意图理解器。只能输出一行 JSON,不要 Markdown。
1. recent limit 1-20 1. recent limit 1-20
2. contact contact limit 2. contact contact limit
@@ -544,7 +544,7 @@ class AgentHubService {
intent: ContactChatIntent intent: ContactChatIntent
): Promise<void> { ): Promise<void> {
if (!isReady()) { if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。') await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
return return
} }
@@ -580,7 +580,7 @@ class AgentHubService {
): Promise<void> { ): Promise<void> {
try { try {
if (!isReady()) { if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。') await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
return return
} }
const contact = resolveMd5(intent.contact) const contact = resolveMd5(intent.contact)
@@ -642,7 +642,7 @@ class AgentHubService {
): Promise<void> { ): Promise<void> {
try { try {
if (!isReady()) { if (!isReady()) {
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。') await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
return return
} }
const group = this.resolveGroup(intent.group) const group = this.resolveGroup(intent.group)
+1 -1
View File
@@ -80,7 +80,7 @@ const agentSystemPrompt = (
question: string, question: string,
scopeLabel: string, scopeLabel: string,
rangeLabel: string rangeLabel: string
): string => `你是 WechatExplorer 的受控本地聊天搜索代理,只负责决定下一步检索,不回答用户问题。 ): string => `你是 TraceMemo 的受控本地聊天搜索代理,只负责决定下一步检索,不回答用户问题。
${question} ${question}
${scopeLabel}${rangeLabel} ${scopeLabel}${rangeLabel}
@@ -28,6 +28,7 @@ export async function inspectImageDecryptionStatus(
const imageDirectoryFound = hasImageDirectory(accountRoot) const imageDirectoryFound = hasImageDirectory(accountRoot)
const stickerCacheFound = const stickerCacheFound =
fs.existsSync(path.join(accountRoot, 'cache')) || fs.existsSync(path.join(accountRoot, 'cache')) ||
fs.existsSync(path.join(os.homedir(), 'Documents', 'TraceMemo', 'Emojis')) ||
fs.existsSync(path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis')) fs.existsSync(path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis'))
const dbConnected = chat.isReady() const dbConnected = chat.isReady()
const [wechatRunning, decoder] = await Promise.all([ const [wechatRunning, decoder] = await Promise.all([
@@ -287,7 +288,7 @@ export function buildImageTestDiagnosticLog(input: {
const rootIsDirectory = rootExists ? safeIsDirectory(root) : false const rootIsDirectory = rootExists ? safeIsDirectory(root) : false
const resultCode = input.result.success ? 'SUCCESS' : input.result.code || 'UNKNOWN' const resultCode = input.result.success ? 'SUCCESS' : input.result.code || 'UNKNOWN'
return [ return [
'WechatExplorer 图片解析测试日志(已脱敏)', 'TraceMemo 图片解析测试日志(已脱敏)',
`时间:${new Date().toISOString()}`, `时间:${new Date().toISOString()}`,
`应用版本:${safeAppVersion()}`, `应用版本:${safeAppVersion()}`,
`运行环境:${process.platform} ${process.arch}`, `运行环境:${process.platform} ${process.arch}`,
+1 -1
View File
@@ -1,5 +1,5 @@
// src/main/services/image-insight-service.ts // src/main/services/image-insight-service.ts
// WechatExplorer AI 图片理解基础设施 // TraceMemo AI 图片理解基础设施
// //
// 设计原则: // 设计原则:
// 1. base64 不走 IPC,只在 main 内部流转(renderer 只看到 ImageInsight 结构化结果) // 1. base64 不走 IPC,只在 main 内部流转(renderer 只看到 ImageInsight 结构化结果)
+1 -1
View File
@@ -90,7 +90,7 @@ export function resolveSkillResourceStatus(
available: false, available: false,
source, source,
githubUrl: GITHUB_URL, githubUrl: GITHUB_URL,
error: `未找到 WechatExplorer Reader Skill 文件(已检查:${candidates.map((item) => item.path).join('')}` error: `未找到 TraceMemo Reader Skill 文件(已检查:${candidates.map((item) => item.path).join('')}`
} }
} }
return { return {
+6 -3
View File
@@ -19,9 +19,12 @@ const downloadCache = new Map<string, Promise<StickerResult>>()
export class StickerService { export class StickerService {
private readonly cacheDir: string private readonly cacheDir: string
private readonly legacyCacheDir: string
constructor(private readonly wcdb4Client?: Wcdb4Client | null) { constructor(private readonly wcdb4Client?: Wcdb4Client | null) {
this.cacheDir = path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis') this.cacheDir = path.join(os.homedir(), 'Documents', 'TraceMemo', 'Emojis')
// Keep reading the former directory so existing sticker caches remain usable.
this.legacyCacheDir = path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis')
} }
async resolveSticker(cdnUrl?: string, md5?: string): Promise<StickerResult> { async resolveSticker(cdnUrl?: string, md5?: string): Promise<StickerResult> {
@@ -69,7 +72,7 @@ export class StickerService {
const extensions = ['.gif', '.png', '.webp', '.jpg', '.jpeg'] const extensions = ['.gif', '.png', '.webp', '.jpg', '.jpeg']
const cacheDirs = [ const cacheDirs = [
this.cacheDir, this.cacheDir,
path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis') this.legacyCacheDir
] ]
for (const cacheDir of cacheDirs) { for (const cacheDir of cacheDirs) {
for (const ext of extensions) { for (const ext of extensions) {
@@ -128,7 +131,7 @@ export class StickerService {
url, url,
{ {
headers: { headers: {
'User-Agent': 'Mozilla/5.0 MicroMessenger WechatExplorer', 'User-Agent': 'Mozilla/5.0 MicroMessenger TraceMemo',
Referer: 'https://weixin.qq.com/' Referer: 'https://weixin.qq.com/'
} }
}, },
+1
View File
@@ -261,6 +261,7 @@ export function resolveWindowsNativeAccountRoot(
.find((candidate) => candidate && isAsciiPath(candidate)) .find((candidate) => candidate && isAsciiPath(candidate))
if (!publicRoot || !isAsciiPath(publicRoot)) return accountRoot if (!publicRoot || !isAsciiPath(publicRoot)) return accountRoot
// Preserve the legacy ASCII bridge path so existing junctions remain reusable.
const bridgeRoot = path.join(publicRoot, 'WechatExplorer', 'path-bridges') const bridgeRoot = path.join(publicRoot, 'WechatExplorer', 'path-bridges')
const bridgePath = path.join( const bridgePath = path.join(
bridgeRoot, bridgeRoot,
+1 -1
View File
@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>WechatExplorer</title> <title>TraceMemo(迹忆)</title>
<link rel="icon" type="image/svg+xml" href="/src/assets/brand-icon.svg" /> <link rel="icon" type="image/svg+xml" href="/src/assets/brand-icon.svg" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta <meta
+2 -2
View File
@@ -805,7 +805,7 @@ function App(): React.ReactElement {
}) })
setStartupProgress({ setStartupProgress({
title: '正在加载账号信息...', title: '正在加载账号信息...',
subtitle: '即将进入 WechatExplorer', subtitle: '即将进入 TraceMemo',
detail: '正在读取联系人和当前账号', detail: '正在读取联系人和当前账号',
percent: 70 percent: 70
}) })
@@ -1813,7 +1813,7 @@ function App(): React.ReactElement {
? autoConnectSource === 'env' ? autoConnectSource === 'env'
? '检测到环境变量中的密钥' ? '检测到环境变量中的密钥'
: '使用上次安全保存的密钥' : '使用上次安全保存的密钥'
: 'WechatExplorer') : 'TraceMemo')
return ( return (
<div className={`boot-splash ${appearanceSettings.showStartupProgress ? '' : 'is-quiet'}`}> <div className={`boot-splash ${appearanceSettings.showStartupProgress ? '' : 'is-quiet'}`}>
<div className="boot-splash-spinner" aria-hidden /> <div className="boot-splash-spinner" aria-hidden />
@@ -141,12 +141,12 @@ export function DatabaseConnectionPage({
return ( return (
<main className="database-login-page"> <main className="database-login-page">
<section className="database-login-brand" aria-label="WechatExplorer 产品说明"> <section className="database-login-brand" aria-label="TraceMemo(迹忆)产品说明">
<div className="database-login-brand-content"> <div className="database-login-brand-content">
<div className="database-login-logo" aria-hidden="true"> <div className="database-login-logo" aria-hidden="true">
<LineIcon name="database" /> <LineIcon name="database" />
</div> </div>
<h1>WechatExplorer</h1> <h1>TraceMemo</h1>
<p className="database-login-tagline"> AI </p> <p className="database-login-tagline"> AI </p>
<p className="database-login-description"> <p className="database-login-description">
使 AI 使 AI
@@ -261,7 +261,7 @@ export function DatabaseConnectionPage({
'确认下方检测结果;没有找到目录时可以手动选择。', '确认下方检测结果;没有找到目录时可以手动选择。',
'请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。', '请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。',
'开始后请按页面提示完成系统授权。', '开始后请按页面提示完成系统授权。',
'正在准备连接组件,请不要关闭微信或 WechatExplorer。', '正在准备连接组件,请不要关闭微信或 TraceMemo。',
'请回到微信完成登录,登录成功后再回来验证。', '请回到微信完成登录,登录成功后再回来验证。',
'正在验证密钥和本地数据库,请稍候。' '正在验证密钥和本地数据库,请稍候。'
][guideStep - 1]} ][guideStep - 1]}
@@ -567,7 +567,7 @@ export function DatabaseConnectionPage({
<button type="button" onClick={onClearKey}> <button type="button" onClick={onClearKey}>
</button> </button>
<span>WechatExplorer</span> <span>TraceMemo</span>
</div> </div>
</div> </div>
</section> </section>
@@ -478,7 +478,7 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({
API Bearer Token Token API Center API Bearer Token Token API Center
Claude / Codex Claude / Codex
<br /> <br />
:<code>docs/skill/wechatexplorer-reader/SKILL.md</code> :<code>docs/skill/wechatexplorer-reader/SKILL.md</code>
</div> </div>
</section> </section>
@@ -45,7 +45,7 @@ export function ExportTaskCenter({
const copyTaskLog = async (task: ExportTaskRecord): Promise<void> => { const copyTaskLog = async (task: ExportTaskRecord): Promise<void> => {
const log = [ const log = [
'WechatExplorer 导出任务日志', 'TraceMemo 导出任务日志',
`时间:${new Date(task.createdAt).toLocaleString('zh-CN')}`, `时间:${new Date(task.createdAt).toLocaleString('zh-CN')}`,
`会话:${task.targetLabel}`, `会话:${task.targetLabel}`,
`格式:${task.format.toUpperCase()}`, `格式:${task.format.toUpperCase()}`,
@@ -431,13 +431,13 @@ export function ExportWorkspace({
const targetPath = exportAll const targetPath = exportAll
? format === 'html' && zip ? format === 'html' && zip
? `文稿/WechatExplorer/导出/${outputName}.zip` ? `文稿/TraceMemo/导出/${outputName}.zip`
: `文稿/WechatExplorer/导出/${outputName}/` : `文稿/TraceMemo/导出/${outputName}/`
: format === 'html' : format === 'html'
? zip ? zip
? `文稿/WechatExplorer/导出/${outputName}.zip` ? `文稿/TraceMemo/导出/${outputName}.zip`
: `文稿/WechatExplorer/导出/${outputName}/` : `文稿/TraceMemo/导出/${outputName}/`
: `文稿/WechatExplorer/导出/${outputName}.${format === 'markdown' ? 'md' : format}` : `文稿/TraceMemo/导出/${outputName}.${format === 'markdown' ? 'md' : format}`
return ( return (
<div className="export-workspace"> <div className="export-workspace">
@@ -26,7 +26,7 @@ interface AppShellProps {
function BrandLogo(): React.ReactElement { function BrandLogo(): React.ReactElement {
return ( return (
<div className="app-brand" title="WechatExplorer" aria-label="WechatExplorer"> <div className="app-brand" title="TraceMemo(迹忆)" aria-label="TraceMemo(迹忆)">
<img src={brandIcon} alt="" aria-hidden="true" /> <img src={brandIcon} alt="" aria-hidden="true" />
</div> </div>
) )
@@ -194,7 +194,7 @@ export function AiReportWorkspace({
<h3></h3> <h3></h3>
<p></p> <p></p>
<p></p> <p></p>
<p>WechatExplorer </p> <p>TraceMemo </p>
</section> </section>
{generatedImage && ( {generatedImage && (
@@ -1094,7 +1094,7 @@ export function AISearchWorkspace({
<div className="ai-search-workspace"> <div className="ai-search-workspace">
<header className="ai-search-header"> <header className="ai-search-header">
<div> <div>
<span className="ai-search-kicker">WechatExplorer · LOCAL INTELLIGENCE</span> <span className="ai-search-kicker">TraceMemo · LOCAL INTELLIGENCE</span>
<h1></h1> <h1></h1>
<p></p> <p></p>
</div> </div>
@@ -95,7 +95,7 @@ export function AgentHubWorkspace(): React.ReactElement {
<div className="agent-hub-workspace"> <div className="agent-hub-workspace">
<header className="agent-hub-header"> <header className="agent-hub-header">
<div> <div>
<div className="agent-hub-eyebrow">WechatExplorer</div> <div className="agent-hub-eyebrow">TraceMemo</div>
<h1>Agent Hub</h1> <h1>Agent Hub</h1>
<p> AI </p> <p> AI </p>
</div> </div>
@@ -193,7 +193,7 @@ export function AgentHubWorkspace(): React.ReactElement {
<aside className="agent-hub-card agent-hub-capability-card"> <aside className="agent-hub-card agent-hub-capability-card">
<span className="agent-hub-card-kicker"></span> <span className="agent-hub-card-kicker"></span>
<h2></h2> <h2></h2>
<p> Agent Hub WechatExplorer</p> <p> Agent Hub TraceMemo</p>
<div className="agent-hub-example"> <div className="agent-hub-example">
<span></span> <span></span>
<strong> 5 </strong> <strong> 5 </strong>
@@ -195,7 +195,7 @@ export function ApiRuntimePanel({
<h3></h3> <h3></h3>
<p> <p>
{localOnly {localOnly
? '本地 API 默认监听 127.0.0.1。WechatExplorer 不会通过该接口自动把聊天内容发送到云端。外部 Agent 是否调用第三方模型,取决于其自身配置。' ? '本地 API 默认监听 127.0.0.1。TraceMemo 不会通过该接口自动把聊天内容发送到云端。外部 Agent 是否调用第三方模型,取决于其自身配置。'
: '当前服务并非仅本机访问。请确认局域网环境可信;API Token 不等同于公网安全防护。'} : '当前服务并非仅本机访问。请确认局域网环境可信;API Token 不等同于公网安全防护。'}
</p> </p>
</section> </section>
@@ -37,13 +37,13 @@ export function ReaderSkillOverview({
<div className="api-workspace-heading"> <div className="api-workspace-heading">
<div> <div>
<div className="api-title-line"> <div className="api-title-line">
<h1>WechatExplorer Reader</h1> <h1>TraceMemo Reader</h1>
<span className={`api-skill-status ${skill?.available ? 'ready' : 'error'}`}> <span className={`api-skill-status ${skill?.available ? 'ready' : 'error'}`}>
{skill?.available ? '已安装' : '文件不可用'} {skill?.available ? '已安装' : '文件不可用'}
</span> </span>
<span className="api-version">{skill?.version || 'v1.0'}</span> <span className="api-version">{skill?.version || 'v1.0'}</span>
</div> </div>
<p> HTTP API WechatExplorer </p> <p> HTTP API TraceMemo </p>
</div> </div>
<div className="api-header-actions"> <div className="api-header-actions">
<button type="button" onClick={onPreview} disabled={!skill?.available}> <button type="button" onClick={onPreview} disabled={!skill?.available}>
@@ -79,9 +79,9 @@ export function ReaderSkillOverview({
<div className="api-introduction"> <div className="api-introduction">
<h2></h2> <h2></h2>
<p> <p>
WechatExplorer Reader AI Agent TraceMemo Reader AI Agent
WechatExplorer API TraceMemo API
</p> </p>
<div className="api-flow"> <div className="api-flow">
<span>AI Agent</span> <span>AI Agent</span>
@@ -18,6 +18,10 @@ export function SkillDetails({
<dl> <dl>
<div> <div>
<dt></dt> <dt></dt>
<dd>TraceMemo Reader</dd>
</div>
<div>
<dt></dt>
<dd>wechatexplorer-reader</dd> <dd>wechatexplorer-reader</dd>
</div> </div>
<div> <div>
@@ -43,7 +43,7 @@ export function SkillInstallFlow({
<section className={service?.running && dbReady ? 'done' : 'active'}> <section className={service?.running && dbReady ? 'done' : 'active'}>
<b>1</b> <b>1</b>
<div> <div>
<h3> WechatExplorer </h3> <h3> TraceMemo </h3>
<p> <p>
API{service?.running ? '运行中' : '已停止'} · {address} API{service?.running ? '运行中' : '已停止'} · {address}
</p> </p>
@@ -16,12 +16,12 @@ export function SkillPreviewDialog({
className="api-markdown-overlay" className="api-markdown-overlay"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="WechatExplorer Reader Skill 预览" aria-label="TraceMemo Reader Skill 预览"
> >
<div> <div>
<header> <header>
<div> <div>
<strong>WechatExplorer Reader</strong> <strong>TraceMemo Reader</strong>
<span>{version || 'v1.0'}</span> <span>{version || 'v1.0'}</span>
</div> </div>
<div> <div>
@@ -18,7 +18,7 @@ export const REPORT_REQUEST_PRESET = JSON.stringify(
activeUsers: 0, activeUsers: 0,
timeSpan: '00:00-23:59', timeSpan: '00:00-23:59',
generatedAt: '2026-07-13 22:00', generatedAt: '2026-07-13 22:00',
recordNote: '本日报由 WechatExplorer 自动生成', recordNote: '本日报由 TraceMemo 自动生成',
footerNote: '', footerNote: '',
heroParticipants: [], heroParticipants: [],
avatars: {}, avatars: {},
@@ -37,7 +37,7 @@ export const AGENT_GROUP_REPORT_PRESET = JSON.stringify(
) )
export const AGENT_SEND_PRESET = JSON.stringify( export const AGENT_SEND_PRESET = JSON.stringify(
{ to: '', text: 'WechatExplorer Agent Hub 发送测试' }, { to: '', text: 'TraceMemo Agent Hub 发送测试' },
null, null,
2 2
) )
@@ -7,13 +7,13 @@ function requestHost(host: string): string {
function opening(target: AgentInstallTarget): string { function opening(target: AgentInstallTarget): string {
switch (target) { switch (target) {
case 'codex': case 'codex':
return '请将本地目录中的 WechatExplorer Reader Skill 安装到当前 Codex 项目或用户 Skill 目录:' return '请将本地目录中的 TraceMemo Reader Skill 安装到当前 Codex 项目或用户 Skill 目录:'
case 'claude-code': case 'claude-code':
return '请安装以下本地 WechatExplorer Reader Skill,并按照 SKILL.md 调用本地 HTTP API' return '请安装以下本地 TraceMemo Reader Skill,并按照 SKILL.md 调用本地 HTTP API'
case 'openclaw': case 'openclaw':
return '请将以下本地目录作为 WechatExplorer Reader Skill 安装,并阅读其中的 SKILL.md' return '请将以下本地目录作为 TraceMemo Reader Skill 安装,并阅读其中的 SKILL.md'
default: default:
return '请读取并安装以下 WechatExplorer Reader Skill' return '请读取并安装以下 TraceMemo Reader Skill'
} }
} }
@@ -32,10 +32,10 @@ export function buildSkillInstallInstruction({
const sourceText = const sourceText =
source.type === 'local' source.type === 'local'
? `${opening(target)}\n\n${source.directoryPath}\n\n请先阅读该目录中的 SKILL.md,然后调用:` ? `${opening(target)}\n\n${source.directoryPath}\n\n请先阅读该目录中的 SKILL.md,然后调用:`
: `请从以下地址安装 WechatExplorer Reader Skill\n\n${source.installUrl}\n\n阅读 SKILL.md 后,调用:` : `请从以下地址安装 TraceMemo Reader Skill\n\n${source.installUrl}\n\n阅读 SKILL.md 后,调用:`
return `${sourceText}\n\n${healthUrl}\n\n先调用公开的 health 接口验证服务。然后请用户在 WechatExplorer → API Center → API Token 中点击“复制 Token”,并把 Token 配置为 Agent 本机环境变量 WECHATEXPLORER_API_TOKEN。读取联系人、会话或聊天记录时,必须发送 Authorization: Bearer $WECHATEXPLORER_API_TOKEN。此服务是 Local HTTP API,不是 MCP Server。安装完成后告诉我验证结果。` return `${sourceText}\n\n${healthUrl}\n\n先调用公开的 health 接口验证服务。然后请用户在 TraceMemo → API Center → API Token 中点击“复制 Token”,并把 Token 配置为 Agent 本机环境变量 WECHATEXPLORER_API_TOKEN。读取联系人、会话或聊天记录时,必须发送 Authorization: Bearer $WECHATEXPLORER_API_TOKEN。此服务是 Local HTTP API,不是 MCP Server。安装完成后告诉我验证结果。`
} }
export function buildSkillVerificationPrompt(): string { export function buildSkillVerificationPrompt(): string {
return '请检查 WechatExplorer Reader 是否已连接,然后列出最近 5 个微信会话。' return '请检查 TraceMemo Reader 是否已连接,然后列出最近 5 个微信会话。'
} }
@@ -75,7 +75,7 @@ export function AIImageUnderstandingTest({
</label> </label>
<p className="ai-vision-privacy"> <p className="ai-vision-privacy">
AI WechatExplorer AI TraceMemo
</p> </p>
{state.error ? <p className="ai-vision-error">{state.error}</p> : null} {state.error ? <p className="ai-vision-error">{state.error}</p> : null}
@@ -30,7 +30,7 @@ export function DatabaseKeyAutoDetect({
<div className="database-key-auto-heading"> <div className="database-key-auto-heading">
<div> <div>
<strong>Windows </strong> <strong>Windows </strong>
<p>WechatExplorer </p> <p>TraceMemo </p>
</div> </div>
<button <button
type="button" type="button"
@@ -63,7 +63,7 @@ export function DatabaseKeyDangerZone({
> >
<h2 id="database-key-confirm-title"></h2> <h2 id="database-key-confirm-title"></h2>
<p> <p>
WechatExplorer TraceMemo
</p> </p>
<div> <div>
@@ -99,7 +99,7 @@ export function DatabaseKeyDangerZone({
> >
<h2 id="database-key-return-confirm-title"></h2> <h2 id="database-key-return-confirm-title"></h2>
<p> <p>
WechatExplorer TraceMemo
</p> </p>
<div> <div>
@@ -26,7 +26,7 @@ export function buildDatabaseKeyDiagnostics(
): string { ): string {
const validation = state.validation const validation = state.validation
return [ return [
'WechatExplorer 数据库密钥诊断', 'TraceMemo 数据库密钥诊断',
`已保存: ${state.saved ? '是' : '否'}`, `已保存: ${state.saved ? '是' : '否'}`,
`已验证: ${validation?.success ? '是' : '否'}`, `已验证: ${validation?.success ? '是' : '否'}`,
`密钥长度合法: ${isDatabaseKeyFormatValid(input) ? '是' : '否'}`, `密钥长度合法: ${isDatabaseKeyFormatValid(input) ? '是' : '否'}`,
@@ -54,13 +54,13 @@ export function AboutPage({ onNotice }: { onNotice: (message: string) => void })
<header className="settings-page-header"> <header className="settings-page-header">
<div> <div>
<h1></h1> <h1></h1>
<p>WechatExplorer </p> <p>TraceMemo AI </p>
</div> </div>
</header> </header>
<div className="settings-page-scroll"> <div className="settings-page-scroll">
<div className="settings-page-content"> <div className="settings-page-content">
<section className="settings-card about-identity-card"> <section className="settings-card about-identity-card">
<div><span className="settings-card-kicker"></span><strong>WechatExplorer</strong><small>v{update.currentVersion}</small></div> <div><span className="settings-card-kicker"></span><strong>TraceMemo</strong><small>v{update.currentVersion}</small></div>
<a href={REPOSITORY_URL} target="_blank" rel="noreferrer">GitHub </a> <a href={REPOSITORY_URL} target="_blank" rel="noreferrer">GitHub </a>
</section> </section>
@@ -76,7 +76,7 @@ export function DatabaseKeyPage({
<div> <div>
<strong></strong> <strong></strong>
<p> <p>
WechatExplorer TraceMemo
使 使
</p> </p>
</div> </div>
@@ -51,7 +51,7 @@ export function ImageDecryptionPage({
</svg> </svg>
<div> <div>
<strong></strong> <strong></strong>
<p>WechatExplorer </p> <p>TraceMemo </p>
</div> </div>
</section> </section>
+1 -1
View File
@@ -658,7 +658,7 @@ export const buildGroupReportFacts = async (
mediaMessageCount: imageCount + voiceCount + stickerCount, mediaMessageCount: imageCount + voiceCount + stickerCount,
timeSpan, timeSpan,
generatedAt: new Date().toLocaleString('zh-CN', { hour12: false }), generatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
recordNote: `基于 WechatExplorer 已加载的 ${transcriptRows.length} 条记录`, recordNote: `基于 TraceMemo 已加载的 ${transcriptRows.length} 条记录`,
footerNote: '基于已读取聊天记录生成;图片、表情等未解析内容默认只按类型与上下文参与日报。', footerNote: '基于已读取聊天记录生成;图片、表情等未解析内容默认只按类型与上下文参与日报。',
heroParticipants: topSpeakers.slice(0, 4).map((speaker) => speaker.name), heroParticipants: topSpeakers.slice(0, 4).map((speaker) => speaker.name),
avatars, avatars,
+1 -1
View File
@@ -4,7 +4,7 @@ export function buildSafeDiagnosticSummary(
environment: Omit<DatabaseKeyEnvironment, 'diagnosticSummary'> environment: Omit<DatabaseKeyEnvironment, 'diagnosticSummary'>
): string { ): string {
return [ return [
`WechatExplorer: ${environment.appVersion}`, `TraceMemo: ${environment.appVersion}`,
`操作系统: ${environment.osVersion}`, `操作系统: ${environment.osVersion}`,
`微信客户端: ${environment.wechatVersion}`, `微信客户端: ${environment.wechatVersion}`,
`数据结构: ${environment.dataStructureVersion}`, `数据结构: ${environment.dataStructureVersion}`,
+1 -1
View File
@@ -1,6 +1,6 @@
// src/shared/image-insight.ts // src/shared/image-insight.ts
// ImageInsight:微信图片的 AI 理解结果持久化数据结构 // ImageInsight:微信图片的 AI 理解结果持久化数据结构
// 与 WechatExplorer 整体 AI 知识平台定位一致 — 图片理解结果可索引、可缓存、可复用。 // 与 TraceMemo 整体 AI 知识平台定位一致 — 图片理解结果可索引、可缓存、可复用。
export type ImageCategory = export type ImageCategory =
| 'screenshot' // 截图 | 'screenshot' // 截图
+1 -1
View File
@@ -1,6 +1,6 @@
export const WINDOWS_VC_RUNTIME_DOWNLOAD_URL = 'https://aka.ms/vc14/vc_redist.x64.exe' export const WINDOWS_VC_RUNTIME_DOWNLOAD_URL = 'https://aka.ms/vc14/vc_redist.x64.exe'
export const WINDOWS_VC_RUNTIME_ERROR_MESSAGE = `当前 Windows 缺少 Microsoft Visual C++ 2015-2022 x64 运行库,无法加载微信数据库组件。请下载安装后重新启动 WechatExplorer${WINDOWS_VC_RUNTIME_DOWNLOAD_URL}` export const WINDOWS_VC_RUNTIME_ERROR_MESSAGE = `当前 Windows 缺少 Microsoft Visual C++ 2015-2022 x64 运行库,无法加载微信数据库组件。请下载安装后重新启动 TraceMemo${WINDOWS_VC_RUNTIME_DOWNLOAD_URL}`
const VC_RUNTIME_LIBRARY_PATTERN = const VC_RUNTIME_LIBRARY_PATTERN =
/(?:vcruntime140(?:_1)?\.dll|msvcp140(?:_[12])?\.dll|concrt140\.dll|ucrtbase\.dll|api-ms-win-crt)/i /(?:vcruntime140(?:_1)?\.dll|msvcp140(?:_[12])?\.dll|concrt140\.dll|ucrtbase\.dll|api-ms-win-crt)/i
+1 -1
View File
@@ -23,7 +23,7 @@ function renderPage(
wechatVersion: '4.1.9.57', wechatVersion: '4.1.9.57',
dataStructureVersion: '微信 4.xWCDB', dataStructureVersion: '微信 4.xWCDB',
dataDirectoryDetected: true, dataDirectoryDetected: true,
diagnosticSummary: 'WechatExplorer: v2.1.6', diagnosticSummary: 'TraceMemo: v2.1.6',
autoDetectSupported: true, autoDetectSupported: true,
wechatRunning: true, wechatRunning: true,
accountIdentified: false, accountIdentified: false,
@@ -40,7 +40,7 @@ describe('ImageTestSection', () => {
fileFound: true, fileFound: true,
decrypted: false, decrypted: false,
readable: false, readable: false,
diagnosticLog: 'WechatExplorer 图片解析测试日志(已脱敏)' diagnosticLog: 'TraceMemo 图片解析测试日志(已脱敏)'
} }
}} }}
batchTest={emptyBatchTest} batchTest={emptyBatchTest}
+1 -1
View File
@@ -9,7 +9,7 @@ test('APP-01 first launch renders a usable connection screen without uncaught er
const pageErrors: Error[] = [] const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error)) fixture.page.on('pageerror', (error) => pageErrors.push(error))
try { try {
await expect(fixture.page.getByRole('heading', { name: 'WechatExplorer' })).toBeVisible() await expect(fixture.page.getByRole('heading', { name: 'TraceMemo(迹忆)' })).toBeVisible()
await expect(fixture.page.getByRole('main')).not.toBeEmpty() await expect(fixture.page.getByRole('main')).not.toBeEmpty()
expect(pageErrors).toEqual([]) expect(pageErrors).toEqual([])
} finally { } finally {
+2 -2
View File
@@ -117,7 +117,7 @@ handle('key:getEnvironment', () => ({
wechatVersion: '4.1.9.57', wechatVersion: '4.1.9.57',
dataStructureVersion: settings.dbRoot === 'fixture-account' ? '微信 4.xWCDB' : '未检测到', dataStructureVersion: settings.dbRoot === 'fixture-account' ? '微信 4.xWCDB' : '未检测到',
dataDirectoryDetected: settings.dbRoot === 'fixture-account', dataDirectoryDetected: settings.dbRoot === 'fixture-account',
diagnosticSummary: 'WechatExplorer: v2.1.6\n数据目录: 已检测到', diagnosticSummary: 'TraceMemo: v2.1.6\n数据目录: 已检测到',
autoDetectSupported: true, autoDetectSupported: true,
wechatRunning: true, wechatRunning: true,
accountIdentified: connected, accountIdentified: connected,
@@ -371,7 +371,7 @@ handle('image:testConfig', () => ({
fileFound: true, fileFound: true,
decrypted: true, decrypted: true,
readable: true, readable: true,
diagnosticLog: 'WechatExplorer 图片解析测试日志(已脱敏)\n测试结果:成功(SUCCESS' diagnosticLog: 'TraceMemo 图片解析测试日志(已脱敏)\n测试结果:成功(SUCCESS'
})) }))
handle('image:clearConfig', () => ({ success: true })) handle('image:clearConfig', () => ({ success: true }))
handle('image:getDecoderStatus', () => ({ handle('image:getDecoderStatus', () => ({
+1 -1
View File
@@ -14,7 +14,7 @@ test('NAV-01 login page visual @visual', async () => {
const fixture = await launchTestApp({ mode: 'disconnected' }) const fixture = await launchTestApp({ mode: 'disconnected' })
try { try {
await fixture.page.setViewportSize(visualViewport) await fixture.page.setViewportSize(visualViewport)
await expect(fixture.page.getByRole('heading', { name: 'WechatExplorer' })).toBeVisible() await expect(fixture.page.getByRole('heading', { name: 'TraceMemo(迹忆)' })).toBeVisible()
await expect(fixture.page).toHaveScreenshot('login-page.png', { await expect(fixture.page).toHaveScreenshot('login-page.png', {
animations: 'disabled', animations: 'disabled',
caret: 'hide' caret: 'hide'
+4 -4
View File
@@ -1138,7 +1138,7 @@ describe('media export flow', () => {
expect(result).toEqual({ success: false, error: '已取消' }) expect(result).toEqual({ success: false, error: '已取消' })
expect(state.exportReads).toEqual(['cancel-1']) expect(state.exportReads).toEqual(['cancel-1'])
const outputDir = join(state.documents, 'WechatExplorer', '导出', 'cancelled-all-conversations') const outputDir = join(state.documents, 'TraceMemo', '导出', 'cancelled-all-conversations')
expect(existsSync(join(outputDir, '联系人', '联系人一', 'index.html'))).toBe(true) expect(existsSync(join(outputDir, '联系人', '联系人一', 'index.html'))).toBe(true)
expect(existsSync(join(outputDir, '联系人', '联系人二', 'index.html'))).toBe(false) expect(existsSync(join(outputDir, '联系人', '联系人二', 'index.html'))).toBe(false)
const partialManifest = JSON.parse(readFileSync(join(outputDir, '导出清单.json'), 'utf8')) as { const partialManifest = JSON.parse(readFileSync(join(outputDir, '导出清单.json'), 'utf8')) as {
@@ -1195,7 +1195,7 @@ describe('media export flow', () => {
expect(firstSize).toBeGreaterThan(0) expect(firstSize).toBeGreaterThan(0)
expect(readFileSync(second.outputPath!).subarray(0, 2).toString()).toBe('PK') expect(readFileSync(second.outputPath!).subarray(0, 2).toString()).toBe('PK')
const entries = execFileSync('unzip', ['-Z1', second.outputPath!], { encoding: 'utf8' }) const entries = execFileSync('unzip', ['-Z1', second.outputPath!], { encoding: 'utf8' })
const htmlPath = join(state.documents, 'WechatExplorer', '导出', 'zip-fixture', 'index.html') const htmlPath = join(state.documents, 'TraceMemo', '导出', 'zip-fixture', 'index.html')
const archive = readArchive(htmlPath) const archive = readArchive(htmlPath)
expect(entries).toContain('zip-fixture/index.html') expect(entries).toContain('zip-fixture/index.html')
expect(entries).toContain('zip-fixture/data/messages.js') expect(entries).toContain('zip-fixture/data/messages.js')
@@ -1209,7 +1209,7 @@ describe('media export flow', () => {
true true
) )
expect( expect(
readdirSync(join(state.documents, 'WechatExplorer', '导出')).some((name) => readdirSync(join(state.documents, 'TraceMemo', '导出')).some((name) =>
name.startsWith('zip-fixture.zip.tmp-') name.startsWith('zip-fixture.zip.tmp-')
) )
).toBe(false) ).toBe(false)
@@ -1250,7 +1250,7 @@ describe('media export flow', () => {
expect(cancelled).toEqual({ success: false, error: '已取消' }) expect(cancelled).toEqual({ success: false, error: '已取消' })
expect(readFileSync(first.outputPath!)).toEqual(completeZip) expect(readFileSync(first.outputPath!)).toEqual(completeZip)
expect( expect(
readdirSync(join(state.documents, 'WechatExplorer', '导出')).some((name) => readdirSync(join(state.documents, 'TraceMemo', '导出')).some((name) =>
name.startsWith('zip-cancel-fixture.zip.tmp-') name.startsWith('zip-cancel-fixture.zip.tmp-')
) )
).toBe(false) ).toBe(false)
+1 -1
View File
@@ -95,7 +95,7 @@ describe('Local API authentication', () => {
const health = await fetch(`${baseUrl(handle)}/api/v1/health`) const health = await fetch(`${baseUrl(handle)}/api/v1/health`)
expect(health.status).toBe(200) expect(health.status).toBe(200)
const healthBody = await health.json() const healthBody = await health.json()
expect(healthBody).toMatchObject({ ok: true, service: 'WechatExplorer Reader' }) expect(healthBody).toMatchObject({ ok: true, service: 'TraceMemo Reader' })
expect(JSON.stringify(healthBody)).not.toMatch( expect(JSON.stringify(healthBody)).not.toMatch(
/token|authorization|wxid|databasePath|provider/i /token|authorization|wxid|databasePath|provider/i
) )
+93
View File
@@ -0,0 +1,93 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
import os from 'os'
import path from 'path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { chooseUserDataRoot, getUserDataRoots, hasValidUserAssets } from '../../src/main/app-data-paths'
let root = ''
beforeEach(() => {
root = mkdtempSync(path.join(os.tmpdir(), 'tracememo-app-data-paths-'))
})
afterEach(() => {
rmSync(root, { recursive: true, force: true })
})
function fixtureRoots(): ReturnType<typeof getUserDataRoots> {
return getUserDataRoots(path.join(root, 'Application Support'))
}
function writeMarker(filePath: string): void {
mkdirSync(path.dirname(filePath), { recursive: true })
writeFileSync(filePath, 'fixture')
}
describe('app data compatibility root selection', () => {
it('chooses the new root for a clean install', () => {
const roots = fixtureRoots()
expect(hasValidUserAssets(roots.legacy)).toBe(false)
expect(hasValidUserAssets(roots.current)).toBe(false)
expect(chooseUserDataRoot(roots)).toBe(roots.current)
})
it('ignores runtime-only Chromium files', () => {
const roots = fixtureRoots()
mkdirSync(roots.current, { recursive: true })
writeMarker(path.join(roots.current, 'Local State'))
mkdirSync(path.join(roots.current, 'Cache'), { recursive: true })
expect(hasValidUserAssets(roots.current)).toBe(false)
expect(chooseUserDataRoot(roots)).toBe(roots.current)
})
it('prefers a legacy root containing user settings', () => {
const roots = fixtureRoots()
writeMarker(path.join(roots.legacy, 'settings.json'))
expect(chooseUserDataRoot(roots)).toBe(roots.legacy)
})
it('uses a current root when only it contains valid user assets', () => {
const roots = fixtureRoots()
writeMarker(path.join(roots.current, 'ai-providers.json'))
expect(chooseUserDataRoot(roots)).toBe(roots.current)
})
it('prefers legacy without modifying a valid current root', () => {
const roots = fixtureRoots()
writeMarker(path.join(roots.legacy, 'reports', '2026', '08', 'report.json'))
writeMarker(path.join(roots.current, 'local-api-token.bin'))
const currentToken = path.join(roots.current, 'local-api-token.bin')
expect(chooseUserDataRoot(roots)).toBe(roots.legacy)
expect(hasValidUserAssets(roots.current)).toBe(true)
expect(readFileSync(currentToken, 'utf8')).toBe('fixture')
})
it('recognizes Knowledge only when a non-empty knowledge.sqlite exists', () => {
const roots = fixtureRoots()
mkdirSync(path.join(roots.legacy, 'knowledge', 'account-hash'), { recursive: true })
expect(hasValidUserAssets(roots.legacy)).toBe(false)
writeMarker(path.join(roots.legacy, 'knowledge', 'account-hash', 'knowledge.sqlite'))
expect(hasValidUserAssets(roots.legacy)).toBe(true)
})
it('honors WXE_USER_DATA isolation before normal root selection', () => {
const roots = fixtureRoots()
writeMarker(path.join(roots.legacy, 'settings.json'))
const isolated = path.join(root, 'isolated-user-data')
expect(chooseUserDataRoot({ ...roots, isolated })).toBe(isolated)
})
it.each([
'ai-provider-keys.bin',
'local-api-token.bin',
'wechat-image-keys.bin',
path.join('database-keys', 'account.bin'),
path.join('recall-archive', 'entry.json'),
path.join('Local Storage', 'leveldb', '000001.ldb')
])('recognizes %s as a persistent user asset', (relativePath) => {
const roots = fixtureRoots()
writeMarker(path.join(roots.legacy, relativePath))
expect(hasValidUserAssets(roots.legacy)).toBe(true)
expect(chooseUserDataRoot(roots)).toBe(roots.legacy)
})
})
+1 -1
View File
@@ -17,7 +17,7 @@ describe('connection diagnostics', () => {
encryptionAvailable: true encryptionAvailable: true
}) })
expect(summary).toContain('WechatExplorer: v2.1.6') expect(summary).toContain('TraceMemo: v2.1.6')
expect(summary).toContain('微信客户端: 4.1.9.57') expect(summary).toContain('微信客户端: 4.1.9.57')
expect(summary).not.toContain('0123456789abcdef') expect(summary).not.toContain('0123456789abcdef')
expect(summary).not.toContain('C:\\Users\\fixture\\xwechat_files\\wxid_secret') expect(summary).not.toContain('C:\\Users\\fixture\\xwechat_files\\wxid_secret')
+2 -2
View File
@@ -45,11 +45,11 @@ describe('production runtime packaging', () => {
expect(config).toContain('docs/skill/wechatexplorer-reader') expect(config).toContain('docs/skill/wechatexplorer-reader')
expect(config).toContain('to: skill/wechatexplorer-reader') expect(config).toContain('to: skill/wechatexplorer-reader')
expect(() => validateReaderSkillRuntime(resources)).toThrow( expect(() => validateReaderSkillRuntime(resources)).toThrow(
/Missing bundled WechatExplorer Reader Skill/ /Missing bundled TraceMemo Reader Skill/
) )
mkdirSync(dirname(skillPath), { recursive: true }) mkdirSync(dirname(skillPath), { recursive: true })
writeFileSync(skillPath, '# WechatExplorer Reader\n') writeFileSync(skillPath, '# TraceMemo Reader\n')
expect(validateReaderSkillRuntime(resources)).toBe(skillPath) expect(validateReaderSkillRuntime(resources)).toBe(skillPath)
}) })
+1 -1
View File
@@ -27,7 +27,7 @@ function environment(root: string, packaged: boolean) {
function writeSkill(filePath: string): void { function writeSkill(filePath: string): void {
mkdirSync(dirname(filePath), { recursive: true }) mkdirSync(dirname(filePath), { recursive: true })
writeFileSync(filePath, '# WechatExplorer Reader\n', 'utf8') writeFileSync(filePath, '# TraceMemo Reader\n', 'utf8')
} }
describe('Reader Skill resource resolution', () => { describe('Reader Skill resource resolution', () => {