diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c05394..d5b5fe1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,6 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.json-language-features" } -} +} \ No newline at end of file diff --git a/README.md b/README.md index 714f7a5..5e957da 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

本地优先的微信聊天记录工作台:查看、搜索、提问、总结和导出
- 查看聊天 · 找回信息 · AI 问答 · 群聊日报 · 语音转写 · 导出 · 实时微信交互 + 查看聊天 · 找回信息 · AI 问答 · 群聊日报总结 · 语音转写 · 导出 · 微信机器人 · Agent 接入

@@ -29,9 +29,15 @@ WechatExplorer 主界面

+

+ WechatExplorer 主界面 +

+ ## WechatExplorer 是什么 -WechatExplorer 帮你在自己的电脑上读取和整理微信聊天记录。 +WechatExplorer 是一个本地优先的微信聊天记录搜索与 AI 工作台。 + +它可以帮你浏览、搜索和整理微信历史,也可以让 AI 帮你找回聊过的内容,并回到原始消息核对答案。 你可以直接浏览聊天,也可以用自然语言提问: @@ -39,11 +45,25 @@ WechatExplorer 帮你在自己的电脑上读取和整理微信聊天记录。 > “张三之前发过的项目地址在哪里?” > “技术交流群今天有哪些结论和待办?” -它和普通聊天记录查看器最大的不同,是 AI 回答可以回到原始聊天核对。你可以看到回答参考了哪些内容、来自哪个会话和时间,再回到消息上下文确认它有没有理解错。 +它和普通聊天记录查看器最大的不同,是 AI 不只是告诉你答案,还会告诉你答案来自哪里。你可以看到答案参考了哪些内容、来自哪个会话和时间,再回到原始消息确认它有没有理解错。 -## 你可以用它做什么 +## 从你的任务开始 -### 查看和搜索自己的微信记录 +| 我现在想做什么 | 在应用里打开 | 需要准备什么 | +| ----------------------------------------- | ------------------------------------------------------- | ------------------------------------ | +| 找一句记得原文或关键词的聊天 | [档案](./docs/user-guide/chat-archive.md) | 连接微信数据,不需要 AI | +| 找一件记得大意、但不知道在哪聊过的事 | [问问微信](./docs/user-guide/ai-search.md) | 配置 AI 服务,并选择会话和时间范围 | +| 让长期、跨群聊查找更稳定 | [问问微信 → 本地知识库](./docs/user-guide/knowledge.md) | 主动建立本地索引;不会自动创建 | +| 快速了解一个群今天、昨天或近 7 天聊了什么 | [日报](./docs/user-guide/report.md) | 选择群聊并配置 AI 服务 | +| 把微信语音变成可搜索的文字 | [设置 → 语音转文字](./docs/user-guide/voice.md) | 准备本地语音模型 | +| 把聊天保存成 HTML、Markdown、CSV 或 JSON | [导出](./docs/user-guide/export.md) | 选择聊天、时间和格式,不需要 AI | +| 尽量保留之后捕获到的撤回消息 | [设置 → 防撤回](./docs/user-guide/recall-protection.md) | 默认关闭;开启前先了解写入和性能边界 | +| 直接在微信里向 WechatExplorer 提问 | [微信机器人](./docs/agent/agent-hub.md) | 扫码连接机器人;总结类任务需要 AI | +| 让 Codex 等外部 Agent 查询微信历史 | [外部 Agent](./docs/agent/overview.md) | 安装 Reader Skill 并配置本机 Token | + +## 最核心的三个能力 + +### 浏览和搜索微信历史 - 浏览联系人、群聊、折叠群聊和公众号消息。 - 查看文本、图片、视频、语音、文件、链接、引用、小程序等内容。 @@ -52,44 +72,42 @@ WechatExplorer 帮你在自己的电脑上读取和整理微信聊天记录。 详细说明:[聊天档案与普通搜索](./docs/user-guide/chat-archive.md) -### 直接向微信历史提问 +### AI 帮你找回聊过的内容 打开“问问微信”,选择搜索范围和时间,然后像提问一样描述你想找的内容。 -WechatExplorer 会先在本机查找候选消息,再把整理后的少量来源交给你配置的 AI 模型生成回答。回答中的来源标记可以定位到对应聊天证据;“查看检索详情”还会展示本次查找经历了哪些阶段。 +WechatExplorer 会先在本机查找候选消息,再把整理后的少量来源交给你配置的 AI 模型生成回答。你可以查看答案参考了哪些聊天、来自哪个人和时间,并从来源标记跳回原始消息核对;“查看检索详情”还会展示本次查找经历了哪些阶段。

- 问问微信与聊天来源 + 问问微信与聊天来源

详细说明:[使用 AI 查找聊天信息](./docs/user-guide/ai-search.md) -### 让重要信息以后更容易找到 +### 直接在微信里问你的历史聊天 + +打开应用中的“Agent”入口(页面标题为“Agent Hub”,对应微信机器人功能),扫码连接一个微信机器人账号。例如,你可以直接给机器人发送“最近 5 个会话”“张三最近和我聊了什么”,或者让它生成指定群聊的总结图片。WechatExplorer 会在本机读取已连接的聊天数据并把结果回复到微信。 + +这个入口不要求另外安装 Codex、Claude Code 等外部 Agent。当前主要处理文字消息,不支持群发、定时任务或通用自主操作微信;总结和自然语言理解需要先配置 AI 服务。 + +详细步骤和能力边界见[在微信里向 WechatExplorer 提问](./docs/agent/agent-hub.md)。 + +## 其他能力 + +### 本地知识库 “问问微信”里的“本地知识库”会为当前微信账号建立一份留在本机的可检索资料。它把聊天文本、附件信息和已有语音转写整理起来,让跨会话、跨时间查找更稳定。 -- 用户主动点击后才会建立。 -- 支持同步最新记录。 -- 显示已索引消息、知识片段和磁盘占用。 -- 可以从设置中清理,清理不会删除微信原始数据库。 +它只在用户主动建立后工作,可以同步、查看占用并清理;清理不会删除微信原始数据库。 详细说明:[本地知识库](./docs/user-guide/knowledge.md) -### 检查 AI 回答依据 - -AI 给出答案后,你可以继续确认: - -- 它参考了哪几条聊天内容; -- 来源属于哪个人、会话和时间; -- 回答中的来源编号对应哪条原始消息; -- 本次搜索经过了哪些步骤、用了多长时间; -- 当前结果是否只覆盖了部分聊天或遗漏了未转写语音。 - -想进一步了解来源标记和查找过程,阅读[如何核对 AI 的回答来源](./docs/concepts/answer-sources.md)。 - ### 生成群聊日报 -选择群聊和时间范围后,可以让 AI 把聊天整理成热点、重要消息、资源、问答、待办、未解决事项、活跃统计和图片精选,并导出 HTML 与 PNG 长图。 +
+查看群聊日报示例、内容和导出方式 + +选择群聊和时间范围后,可以让 AI 把聊天整理成报告,并保存为 HTML 与 PNG 长图。报告可能包含热点、重要消息、资源、问答、待办、未解决事项、活跃统计和图片精选;具体内容取决于消息、媒体是否可读以及模型能力。

群聊日报示例 @@ -97,50 +115,31 @@ AI 给出答案后,你可以继续确认: 详细说明:[生成群聊日报](./docs/user-guide/report.md) +

+ ### 转写微信语音 -WechatExplorer 提供本地离线语音识别: - -- 在聊天气泡中转写单条语音; -- 按联系人或群聊批量转写; -- 转写结果可以参与本地知识库检索和 HTML 导出; -- 本地转写本身不需要把语音文件发送给在线 AI;如果你随后用转写结果进行 AI 问答或日报,文字会按对应功能的规则处理。 +WechatExplorer 支持在本机转写单条或批量微信语音,结果可以参与本地知识库检索和 HTML 导出。转写本身不要求把语音文件发送给在线 AI;随后用于 AI 问答或日报时,文字会按对应功能的规则处理。 详细说明:[语音转文字](./docs/user-guide/voice.md) +### 防撤回 + +可选开启后,WechatExplorer 会尽量保留开启期间捕获到的撤回消息。该能力受微信版本和应用运行状态影响,不保证找回所有内容,也不能恢复开启前已经撤回的消息。 + +详细说明:[防撤回](./docs/user-guide/recall-protection.md) + ### 导出长期可用的聊天档案 -支持 HTML、CSV、JSON 和 Markdown。HTML 可携带媒体、头像和可选语音转写,也可以压缩为 ZIP;再次使用同名档案导出时可以增量合并新消息。 +支持 HTML、CSV、JSON 和 Markdown。HTML 可携带媒体、头像和可选语音转写,支持最多五个会话合并,也可以压缩为 ZIP;增量合并、媒体资源和 ZIP 只适用于 HTML,其他格式主要保留文本内容。 详细说明:[导出聊天](./docs/user-guide/export.md) -### 如果你要让 Agent 读取微信数据 +### 在外部 Agent 中查询微信历史 -WechatExplorer 提供 Local HTTP API 和 Reader Skill。安装后,Codex、Claude Code、OpenClaw 等 Agent 可以在你的电脑上按需查询聊天: +通过 Reader Skill 和本机 Local HTTP API,Codex、Claude Code、OpenClaw 等外部 Agent 可以按需查询联系人、群聊和聊天记录。这和微信机器人是两条不同路径:微信机器人收到消息后在微信中回复;外部 Agent 则主动查询历史。 -> “总结今天技术交流群讨论了什么。” -> “过去一周有没有人提到某个项目?” - -它提供的是随应用安装的 Reader Skill 和本机 Local HTTP API。连接后,Agent 可以按需查找联系人、群聊和聊天记录,再帮你做总结。安装和完整技术说明请看[Agent 接入概览](./docs/agent/overview.md)与[Local HTTP API](./docs/agent/api.md)。 - -### 连接微信机器人,让 AI 参与实时微信工作 - -除了读取过去的聊天,你还可以在应用里的“Agent”页面(页面标题为“Agent Hub”)扫码连接一个微信机器人账号。机器人收到你发来的文字后,会在本机调用 WechatExplorer 已连接的聊天数据,并把结果回复给发消息的人。 - -目前已经支持的实时任务包括: - -- 查看最近会话; -- 查询你和某位联系人的近期聊天; -- 用已配置的 AI 总结你和某位联系人近 7 天的聊天; -- 生成今天、昨天或近 7 天的群聊总结图片; -- 总结指定群成员在群里的近期发言; -- 对不需要读取聊天的普通文字请求给出简短 AI 回复。 - -例如,你可以直接给机器人发“最近 5 个会话”,或“生成产品交流群今天的群聊总结图片”。需要读取历史数据时,WechatExplorer 必须已经连接微信数据库;需要总结或自然语言理解时,还要在“设置 → AI 模型”中配置可用的 AI 服务。 - -这条路径和上面的 Reader Skill / Local HTTP API 不同:Reader Skill/API 是外部 Agent 主动查询历史微信数据;Agent Hub 则是微信机器人收到实时消息后处理并回复。当前实时入口主要处理文字消息,不应把它理解成支持任意媒体理解、群发、定时任务或通用自主操作的机器人。 - -详细步骤和能力边界见[Agent Hub:从微信里向本机助手提问](./docs/agent/agent-hub.md)。 +安装和技术说明请看[Agent 接入概览](./docs/agent/overview.md)与[Local HTTP API](./docs/agent/api.md)。 ## 它如何工作 @@ -152,26 +151,43 @@ flowchart LR D --> E[筛选相关聊天来源] E --> F[用户配置的 AI 模型] F --> G[带来源的回答] - E --> H[日报与导出] - B --> I[按需交给本地 Agent] + B --> H[整理日报输入] + H --> F + B --> I[聊天导出] + B --> J[Local HTTP API] + J --> K[外部 Agent] + L[微信机器人消息] --> M[Agent Hub] + M --> B + M --> F ``` - 微信数据库读取、聊天解析、知识库索引和离线语音识别在本机完成。 - 普通浏览、普通搜索和导出不要求配置 AI 服务。 -- 你主动使用并确认“问问微信”、群聊日报或图片理解时,完成任务所需的内容可能发送到你选择的模型服务。 +- 使用“问问微信”、群聊日报或图片理解等 AI 功能时,完成任务所需的内容可能发送到你选择的模型服务;具体发送范围和确认方式以对应功能页面为准。 - “问问微信”会先在本机缩小范围,不会默认把整个微信数据库作为一次模型请求发送。 + 完整边界见:[数据、隐私与安全](./docs/user-guide/privacy.md) +## 支持平台与安装包 + +| 平台 | 处理器架构 | Releases 安装包 | +| ------- | ------------------------------------ | --------------- | +| Windows | x64 | `-setup.exe` | +| macOS | Intel(x64)、Apple Silicon(arm64) | `.dmg` | + +当前代码面向微信 4.x 数据结构。实际连接结果仍会受到微信客户端版本、账号数据状态和系统权限影响;macOS 首次连接可能需要按页面提示完成额外授权。 + ## 快速开始 1. 从 [GitHub Releases](https://github.com/Wxw-Gu/WechatExplorer/releases) 下载安装包。 2. 启动 WechatExplorer,按照“第一次使用”页面选择微信数据目录。 3. 第一次使用请先点击“开始连接”,按页面提示准备连接组件并获取数据库密钥;只有已经有密钥的高级用户才需要“手动连接”。 4. 连接成功后打开“档案”,确认联系人和聊天消息已经出现。 -5. 需要 AI 时,在“设置 → AI 模型”添加并测试 AI 服务。 -6. 打开“问问微信”,按需要建立本地知识库;如果正在同步,等待状态变为“已同步”后再开始第一个问题。 +5. 先在“档案”里搜索一句你记得的原话;这一步不需要 AI。 +6. 需要 AI 问答或日报时,在“设置 → AI 模型”添加并测试 AI 服务,再打开“问问微信”或“日报”。 +7. 想直接在微信里提问时,打开“Agent”扫码连接微信机器人;想让 Codex 等外部 Agent 查询时,再进入“API”。 -当前代码面向微信 4.x 数据结构。Windows 发布构建为 x64;macOS 发布构建提供 Intel 和 Apple Silicon 版本。macOS 首次连接可能需要按页面提示完成系统授权;如果页面提示处理 SIP,请先阅读对应说明。具体步骤和限制见[第一次使用](./docs/user-guide/getting-started.md)。 +如果 macOS 页面提示处理 SIP,请先阅读对应说明。具体步骤和限制见[第一次使用](./docs/user-guide/getting-started.md)。 完整步骤:[第一次使用 WechatExplorer](./docs/user-guide/getting-started.md) @@ -193,6 +209,7 @@ flowchart LR - [群聊日报](./docs/user-guide/report.md) - [语音转文字](./docs/user-guide/voice.md) - [导出聊天](./docs/user-guide/export.md) +- [防撤回](./docs/user-guide/recall-protection.md) - [数据、隐私与安全](./docs/user-guide/privacy.md) - [Agent 接入](./docs/agent/overview.md) - [微信机器人与 Agent Hub](./docs/agent/agent-hub.md) diff --git a/WechatExplorer-Product-Manager-Handoff.md b/WechatExplorer-Product-Manager-Handoff.md new file mode 100644 index 0000000..b719164 --- /dev/null +++ b/WechatExplorer-Product-Manager-Handoff.md @@ -0,0 +1,241 @@ +# 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 在 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 承担细节; +- 明确区分当前能力、限制、待验证事项和未来场景; +- 不因文案完整性补充不存在的能力; +- 不把测试通过等同于真实用户环境已经得到验证。 diff --git a/docs/README.md b/docs/README.md index 98d090e..fc8747c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,9 @@ WechatExplorer 的文档按“你想完成什么”组织,而不是按源码 - [建立本地知识库](./user-guide/knowledge.md) - [生成群聊日报和总结](./user-guide/report.md) - [语音转文字](./user-guide/voice.md) -- [导出聊天和报告](./user-guide/export.md) +- [导出聊天档案](./user-guide/export.md) +- [防撤回](./user-guide/recall-protection.md) +- [在微信里向 WechatExplorer 提问](./agent/agent-hub.md) - [数据、隐私与安全](./user-guide/privacy.md) - [常见问题与排查](./user-guide/troubleshooting.md) @@ -22,14 +24,20 @@ WechatExplorer 的文档按“你想完成什么”组织,而不是按源码 - [如何核对 AI 的回答来源](./concepts/answer-sources.md):用用户语言解释依据、来源标记和查找过程。 - [从微信数据到回答、日报和导出](./concepts/how-it-works.md):了解哪些步骤在本机完成,哪些步骤可能调用 Provider。 -## 如果你想连接 Agent +## 微信机器人和外部 Agent -WechatExplorer 有两种不同的 Agent 使用方式,请先按你的目标选择: +WechatExplorer 有两种不同的接入方式。微信机器人是普通用户可以直接使用的产品能力;Reader Skill 和 Local HTTP API 面向已经在使用 Codex、Claude Code、OpenClaw 等外部 Agent 的用户。 -| 你想做什么 | 应该看哪里 | -| --- | --- | +| 你想做什么 | 应该看哪里 | +| --------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| 在微信里给机器人发消息,让本机读取数据、生成总结并回复 | [Agent Hub](./agent/agent-hub.md) | | 在 Codex、Claude Code、OpenClaw 等外部 Agent 中主动查询过去的微信数据 | [Reader Skill](./agent/reader-skill.md) + [Local HTTP API](./agent/api.md) | -| 在微信里给机器人发消息,让本机读取数据、生成总结并回复 | [Agent Hub](./agent/agent-hub.md) | + +### 在微信里提问 + +打开应用一级导航中的“Agent”,进入“Agent Hub”后扫码登录微信机器人。机器人收到文字消息后,可以查询最近会话、读取联系人聊天、生成群聊总结图片或总结群成员发言,并把结果回复给发消息的人。它需要本地微信数据库已经连接;依赖 AI 的任务还需要配置 AI 服务。 + +- [Agent Hub](./agent/agent-hub.md):连接机器人、查看运行状态和了解实时交互边界。 ### 让外部 Agent 查询历史微信 @@ -43,12 +51,6 @@ WechatExplorer 有两种不同的 Agent 使用方式,请先按你的目标选 - [Local HTTP API](./agent/api.md):完整端点和请求示例。 - [API 安全](./agent/api-security.md):Bearer Token、CORS、轮换和边界。 -### 让微信机器人参与实时工作 - -打开应用主导航中的“Agent”,进入“Agent Hub”后扫码登录微信机器人。机器人收到文字消息后,可以查询最近会话、读取联系人聊天、生成群聊总结图片或总结群成员发言,并把结果回复给发消息的人。它需要本地微信数据库已经连接;依赖 AI 的任务还需要配置 AI 服务。 - -- [Agent Hub](./agent/agent-hub.md):连接机器人、查看运行状态和了解实时交互边界。 - ## 开发与平台 - [macOS 数据访问说明](./platform/macos.md) diff --git a/docs/agent/agent-hub.md b/docs/agent/agent-hub.md index 1c86a04..c1dceae 100644 --- a/docs/agent/agent-hub.md +++ b/docs/agent/agent-hub.md @@ -1,6 +1,8 @@ -# Agent Hub:让微信机器人参与实时工作 +# 在微信里向 WechatExplorer 提问(Agent Hub) -Agent Hub 是 WechatExplorer 内置的实时微信交互入口。你先在应用中扫码登录一个微信机器人账号,之后向这个机器人发送文字;本机 Agent Hub 会接收消息、读取已经连接的微信数据,必要时调用已配置的 AI,再把结果回复给发消息的人。 +Agent Hub 是 WechatExplorer 内置的微信机器人入口,也是应用一级导航中的“Agent”页面。你先扫码登录一个微信机器人账号,再用微信账号向机器人发送文字;本机 Agent Hub 会接收消息、读取已经连接的微信数据,必要时调用已配置的 AI,再把结果回复给发送者。 + +普通用户不需要安装 Reader Skill,也不需要配置 API Token。先连接微信数据库,再扫码登录机器人即可开始;需要总结或自然语言理解的任务还要配置 AI Provider。 它和 Reader Skill 是两条不同的路径: @@ -15,7 +17,7 @@ Agent Hub 是 WechatExplorer 内置的实时微信交互入口。你先在应用 连接 Agent Hub 后,可以在微信中询问: -- “最近 5 条消息是谁?” +- “最近 5 个会话”; - “帮我看看最近跟某人聊了些什么。” - “生成产品交流群今天的群聊总结图片。” @@ -38,7 +40,7 @@ Agent Hub 是 WechatExplorer 内置的实时微信交互入口。你先在应用 2. 确认 Hub 显示“运行中”,数据库状态为“可查询”。 3. 点击“扫码登录微信机器人”。 4. 用微信扫描二维码;如果页面显示“已扫码,等待手机确认”,在手机上确认。 -5. 状态变为“在线”后,从该机器人账号发送测试问题。 +5. 状态变为“在线”后,用另一个微信账号向机器人发送测试问题。 可以重新扫码登录或断开连接。登录凭证失效时,需要重新扫码。 diff --git a/docs/agent/overview.md b/docs/agent/overview.md index 915b267..2eeeb58 100644 --- a/docs/agent/overview.md +++ b/docs/agent/overview.md @@ -1,47 +1,52 @@ -# 连接 Agent:你能用它做什么 +# 在微信机器人或外部 Agent 中使用 WechatExplorer -WechatExplorer 的 Agent 能力分成“查询过去的数据”和“处理实时微信消息”两条路径。先按你想完成的任务选择,不需要先学习内部模块名称。 +WechatExplorer 提供两条不同路径。先按你实际想做的事选择,不需要先理解 Agent、Skill 或 API 等术语。 -## 两种不同的使用方式 +| 你想做什么 | 使用方式 | 需要什么 | +| ---------------------------------------------------- | ----------------------------- | --------------------------------------------------------- | +| 直接在微信里发文字,让本机查询聊天并回复 | 微信机器人(Agent Hub) | 在应用“Agent”页面扫码登录机器人;部分任务需要 AI Provider | +| 在 Codex、Claude Code、OpenClaw 等工具里查询微信历史 | Reader Skill + Local HTTP API | 安装 Skill,并配置本机 API Token | -### 让外部 Agent 查询历史微信 +## 直接在微信里提问 -连接 Reader Skill 后,你可以在本机的 Codex、Claude Code、OpenClaw 或其他 Agent 中询问自己的微信历史,例如: +打开应用一级导航中的“Agent”,进入“Agent Hub”,扫码登录一个微信机器人账号。之后用另一个微信账号向机器人发送文字,它会调用 WechatExplorer 的本机数据,必要时使用已配置的 AI,再把结果回复给发送者。 + +可以先尝试: + +- “最近 5 个会话”; +- “帮我看看最近跟张三聊了些什么”; +- “生成产品交流群今天的群聊总结图片”。 + +这条路径不要求安装 Reader Skill,也不要求用户配置 API Token。它主要处理文字请求,不支持群发、定时任务或与文字同等的任意媒体理解。 + +连接步骤、当前任务清单和安全边界见[Agent Hub](./agent-hub.md)。 + +## 在外部 Agent 中查询历史微信 + +Reader Skill 是给外部 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以通过 WechatExplorer Local HTTP API 按需读取联系人、群聊、最近会话、指定时间范围的聊天和群成员信息。 + +典型问题包括: - “总结今天技术交流群讨论的内容。” - “帮我找上个月讨论过的项目地址。” - “过去一周有没有人提到退款?” -Agent 会按需读取 WechatExplorer 提供的联系人、群聊和聊天记录;它不会直接打开微信数据库文件。 +外部 Agent 不会直接打开微信数据库文件,但它能取得本机 API 返回的聊天内容。Agent 是否继续把结果发送给云端模型,取决于 Agent 自己的模型和工具配置。 -| 方式 | 适合谁 | 作用 | 是否需要外部 Agent 配置 | -| --- | --- | --- | --- | -| Reader Skill + Local HTTP API | 想在 Codex/Claude Code/OpenClaw 中查微信的人 | Agent 通过本机 HTTP 请求读取聊天 | 是,需要安装 Skill 和 Token | -| Agent Hub | 想从微信机器人账号发消息、让本机处理并回复的人 | 微信连接器把消息送到本机 Hub,Hub 调用数据和 AI | 不使用外部 Reader Skill,但需要扫码连接机器人 | - -这两条路径不要混写:Reader Skill/API 是外部 Agent 主动读取历史;Agent Hub 是机器人收到实时消息后处理并回复。`127.0.0.1:6131` 是 Local HTTP API,不是 MCP Server。 - -## 外部 Agent 的安装路径 +## 外部 Agent 的安装步骤 1. 启动 WechatExplorer 并完成微信数据库连接。 -2. 打开“API Center”,确认本地 API、数据库和 Reader Skill 都显示可用。 -3. 在 API Center 选择目标 Agent(Codex、Claude Code、OpenClaw 或其他 Agent),点击“复制安装指令”。 -4. 在 Agent 自己的本地 Skill/配置目录执行或粘贴指令。 +2. 打开一级导航“API”(页面为“API Center”),确认本地 API、数据库和 Reader Skill 都可用。 +3. 选择目标 Agent,点击“复制安装指令”。 +4. 在 Agent 自己的 Skill/配置目录执行或粘贴指令。 5. 在 API Center 复制当前 Token,并在 Agent 运行环境中设置 `WECHATEXPLORER_API_TOKEN`。 -6. 先让 Agent 调用 health,再尝试查询联系人或最近会话。 +6. 先让 Agent 调用 health,再尝试查询最近会话。 详细说明:[Reader Skill](./reader-skill.md)、[Local HTTP API](./api.md)、[API 安全](./api-security.md)。 -## Agent 能看到什么 +## 不要混淆两条路径 -外部 Agent 通过 API 按需读取联系人、群聊、最近会话、指定时间范围聊天和群成员快照,也可以请求生成群聊总结图片。API 本身不提供任意文件系统浏览,也不会把完整数据库自动上传到网络。 - -Agent 是否把读取结果再次交给云端模型,取决于 Agent 的模型配置和它如何处理工具结果。使用前请检查 Agent 自己的隐私设置。 - -## 什么时候用 Agent Hub - -如果你希望直接在微信里问“最近 5 条消息是谁?”或“生成产品交流群今天的群聊总结图片”,打开应用主导航中的“Agent”,进入“Agent Hub”,扫码登录一个微信机器人账号。机器人收到文字后,会调用本地数据和已配置的 AI,再把结果回复给发消息的人。 - -当前实时入口支持最近会话、联系人近期聊天、联系人近 7 天总结、群聊总结图片、群成员发言总结和有限的普通自然语言回复。它不等于通用聊天机器人,也不提供群发、定时或任意媒体理解。 - -详细流程见[Agent Hub](./agent-hub.md)。 +- Agent Hub:微信机器人收到实时文字后处理并回复; +- Reader Skill/API:外部 Agent 主动查询历史数据; +- `127.0.0.1:6131` 是 Local HTTP API,不是 MCP Server; +- Local HTTP API 当前没有对外提供实时入站消息订阅。 diff --git a/docs/concepts/how-it-works.md b/docs/concepts/how-it-works.md index 1f6d63e..c098733 100644 --- a/docs/concepts/how-it-works.md +++ b/docs/concepts/how-it-works.md @@ -10,9 +10,15 @@ flowchart LR D --> E[筛选相关消息] E --> F[用户配置的 AI Provider] F --> G[回答与可核对来源] - B --> H[日报与导出] - B --> I[Local HTTP API] - I --> J[外部 Agent] + B --> H[聊天导出] + B --> I[整理日报输入] + I --> F + F --> J[本地保存 HTML 与 PNG] + B --> K[Local HTTP API] + K --> L[外部 Agent] + M[微信机器人消息] --> N[Agent Hub] + N --> B + N --> F ``` ## 哪些步骤在本机 @@ -21,24 +27,25 @@ flowchart LR - 聊天档案浏览和普通搜索; - Knowledge 索引与增量同步; - 离线语音转写; -- 报告、导出文件和本地历史记录。 +- 聊天导出文件、日报 HTML/PNG 和本地历史记录的保存。 ## 哪些步骤可能调用外部服务 当你主动使用 AI Search、群聊日报或图片理解时,应用会把完成任务所需的受控问题和上下文发送给你配置的 Provider。它不会因为打开软件就自动上传完整数据库。 +Agent Hub 收到微信机器人的文字后,也可能为了理解请求或生成总结调用已配置的 Provider。Reader Skill 调用的是本机 API;外部 Agent 是否把读取结果继续交给云端模型,取决于外部 Agent 自己的配置。 + 如果 Provider 是 Ollama 等本机服务,请把它视为本机的另一个进程;如果是云服务,数据处理和留存规则由该服务商决定。 ## 产品名词和用户任务的对应关系 -| 用户想做什么 | 产品中可能看到的名称 | -| --- | --- | -| 让 AI 找相关聊天 | AI Search、Retrieval | -| 让答案能回到原消息 | Evidence、Citation | -| 查看 AI 查找过程 | Search Trace | -| 让跨会话查找更稳定 | Knowledge、FTS 索引 | -| 让外部 Agent 读取聊天 | Reader Skill、Local HTTP API | -| 让微信机器人调用本机能力 | Agent Hub | +| 用户想做什么 | 产品中可能看到的名称 | +| ------------------------ | ---------------------------- | +| 让 AI 找相关聊天 | AI Search、Retrieval | +| 让答案能回到原消息 | Evidence、Citation | +| 查看 AI 查找过程 | Search Trace | +| 让跨会话查找更稳定 | Knowledge、FTS 索引 | +| 让外部 Agent 读取聊天 | Reader Skill、Local HTTP API | +| 让微信机器人调用本机能力 | Agent Hub | 先按任务使用,再在需要排查或开发集成时阅读术语。 - diff --git a/docs/development/overview.md b/docs/development/overview.md index bb30742..343595c 100644 --- a/docs/development/overview.md +++ b/docs/development/overview.md @@ -32,16 +32,17 @@ pnpm test:e2e:build ## 代码变更对应文档 -| 代码区域 | 需要同步检查的文档 | -| --- | --- | -| `src/shared/ai-search.ts`、AI Search pipeline | `user-guide/ai-search.md`、`concepts/answer-sources.md` | -| `src/shared/knowledge.ts`、`src/main/knowledge/` | `user-guide/knowledge.md`、`concepts/how-it-works.md` | -| `src/shared/voice-recognition.ts` | `user-guide/voice.md` | -| `src/shared/group-report.ts`、报告 UI | `user-guide/report.md`、API/Agent 文档 | -| `src/shared/export.ts`、导出服务/UI | `user-guide/export.md` | -| `src/shared/local-api-test.ts`、`src/main/http-server.ts` | `agent/api.md`、`api-security.md`、打包 Skill | -| Agent Hub service/UI | `agent/agent-hub.md`、`user-guide/privacy.md` | -| 设置导航、连接页面 | `user-guide/getting-started.md`、`docs/README.md` | +| 代码区域 | 需要同步检查的文档 | +| --------------------------------------------------------- | ---------------------------------------------------------- | +| `src/shared/ai-search.ts`、AI Search pipeline | `user-guide/ai-search.md`、`concepts/answer-sources.md` | +| `src/shared/knowledge.ts`、`src/main/knowledge/` | `user-guide/knowledge.md`、`concepts/how-it-works.md` | +| `src/shared/voice-recognition.ts` | `user-guide/voice.md` | +| `src/shared/group-report.ts`、报告 UI | `user-guide/report.md`、API/Agent 文档 | +| `src/shared/export.ts`、导出服务/UI | `user-guide/export.md` | +| `src/main/services/recall-archive-service.ts`、防撤回设置 | `user-guide/recall-protection.md`、`user-guide/privacy.md` | +| `src/shared/local-api-test.ts`、`src/main/http-server.ts` | `agent/api.md`、`api-security.md`、打包 Skill | +| Agent Hub service/UI | `agent/agent-hub.md`、`user-guide/privacy.md` | +| 设置导航、连接页面 | `user-guide/getting-started.md`、`docs/README.md` | ## 文档检查 diff --git a/docs/user-guide/ai-search.md b/docs/user-guide/ai-search.md index fec4b98..50c3e93 100644 --- a/docs/user-guide/ai-search.md +++ b/docs/user-guide/ai-search.md @@ -16,7 +16,7 @@ 1. 进入“设置 → AI 模型”,添加一个 Provider,填写服务地址、模型和认证信息,然后测试连接。 2. 打开“问问微信”。 -3. 根据问题选择时间范围和会话范围;范围越明确,答案越容易核对。 +3. 选择所有聊天、群聊、单聊或当前会话,并选择今天、近 7 天、近 30 天或不限时间;范围越明确,答案越容易核对。 4. 输入问题并开始分析。 如果知识库尚未建立,页面会提示你建立或同步;你也可以先直接使用当前可用的搜索路径。 @@ -58,3 +58,4 @@ 本地解析、索引和候选消息查找在本机完成。只有完成 AI 任务所需的用户问题、受控检索上下文和最终用于总结的来源内容,才可能发送到你配置的 Provider;具体边界见[数据、隐私与安全](./privacy.md)。 +使用远程 Provider 时,当前界面会在本次请求发出前显示接收方和发送范围,等待你确认。当前实现最多发送 8 条最终来源,不会发送完整微信数据库、数据库密钥、绝对文件路径或内部会话/消息引用 ID;这次确认不会自动授权之后的其他请求。 diff --git a/docs/user-guide/chat-archive.md b/docs/user-guide/chat-archive.md index d3334e8..3ccd022 100644 --- a/docs/user-guide/chat-archive.md +++ b/docs/user-guide/chat-archive.md @@ -25,6 +25,14 @@ 不要把“消息类型已读取”理解成“所有媒体都一定能解码”。遇到图片或视频空白时,请先检查[媒体与导出排查](./troubleshooting.md#媒体显示或导出异常)。 +如果文字正常但图片无法打开,进入“设置 → 图片解密”查看当前状态。可以尝试自动获取,也可以在已经知道正确密钥时手动配置;原文件已经被微信清理时,仅配置密钥也无法恢复图片。 + +## 可选保留撤回消息 + +“设置 → 防撤回”提供一个默认关闭的可选功能。开启后,应用会尽量保留之后捕获到的撤回消息,并在气泡旁标记“消息已撤回”。它不能找回开启前已经消失或应用未捕获到的内容,也可能增加加载开销。 + +该功能与普通只读浏览的数据边界不同。开启前请阅读[防撤回](./recall-protection.md)。 + ## 保护自己不被误导 档案中的原始消息是核对 AI 结果的最终依据。看到 AI 的总结、日报或来源时,建议: @@ -47,4 +55,3 @@ ### 想跨多个会话查找 使用“问问微信”,并在问题中写清时间范围、人物或群聊范围。需要更稳定的跨会话查找时,先建立[本地知识库](./knowledge.md)。 - diff --git a/docs/user-guide/export.md b/docs/user-guide/export.md index 61952d6..96dd694 100644 --- a/docs/user-guide/export.md +++ b/docs/user-guide/export.md @@ -1,34 +1,39 @@ -# 导出聊天和报告 +# 导出聊天档案 导出适合把微信里的重要讨论保存成可阅读、可分享或可继续处理的文件。 ## 支持的格式 -- **HTML**:适合完整阅读,可包含媒体和头像; -- **Markdown**:适合笔记、版本管理和再次编辑; -- **CSV**:适合表格分析; -- **JSON**:适合程序处理和数据归档。 +| 格式 | 适合什么任务 | 当前边界 | +| -------- | ------------------------ | ---------------------------------------------------------- | +| HTML | 完整阅读和长期归档 | 可包含媒体、头像和可选语音转写;支持多会话、增量合并和 ZIP | +| Markdown | 笔记、版本管理和再次编辑 | 主要保留文本内容,不复制 HTML 资源文件 | +| CSV | 表格分析 | 主要保留文本内容,不复制 HTML 资源文件 | +| JSON | 程序处理和数据归档 | 主要保留文本内容,不复制 HTML 资源文件 | + +ZIP 是 HTML 资源包的压缩选项,不是第五种内容格式。 ## 导出步骤 -1. 打开“导出”。 -2. 选择一个或多个联系人/群聊。 -3. 选择时间范围和消息类型。 -4. 按需要打开媒体、头像、原图/缩略图、语音转写和保留缺失资源等选项。 -5. 设置文件名,必要时选择 ZIP,然后开始导出。 +可以打开一级导航“导出”,也可以在“档案”的聊天顶部点击“导出”并选择时间范围。 + +1. 选择一个或多个联系人/群聊。 +2. 选择时间范围和消息类型。 +3. 选择格式;只有 HTML 可以配置媒体资源、语音转写和 ZIP。 +4. 按需要设置头像、原图/缩略图和缺失资源处理。 +5. 设置文件名并开始导出。 6. 在导出任务中心查看读取、解析、媒体处理、转写、写入和压缩进度;完成后打开文件位置。 ## 多会话和增量导出 -HTML 支持把最多五个会话合并到一个档案中。再次使用相同名称导出时,可以把新消息增量合并到已有档案;这不会删除之前已导出的消息。 +HTML 支持把最多五个会话合并到一个档案中;选择多个会话后,其他格式会不可用。再次使用相同名称导出 HTML 时,可以把新消息增量合并到已有档案;这不会删除之前已导出的消息。 ## 媒体怎么处理 原图、缩略图、缺失资源和头像都可能影响导出大小与可读性。想要小文件时关闭媒体或选择缩略图;想要长期保存时,确认原始媒体目录仍可访问,并考虑 ZIP 归档。 -语音转写是可选步骤。只有已经成功转写的语音才会写入导出内容,导出不会替你自动补齐失败的识别。 +HTML 导出可以选择在任务中执行本地语音转写,并把成功结果显示在语音气泡下方。语音模型不可用或识别失败时,导出不会把失败内容当成已转写文本。 ## 导出和原始数据的关系 导出是复制/整理结果,不会修改微信原始数据库。删除导出文件也不会影响应用内聊天记录或本地知识库。 - diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index 1e02b6e..33869b5 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -44,6 +44,8 @@ macOS 如果提示无法验证开发者,请按系统提示允许打开。自 如果联系人列表为空,先检查是否连到了正确账号和数据目录,再重新加载会话。 +文字消息正常但图片打不开时,不代表数据库连接失败。打开“设置 → 图片解密”查看状态并尝试自动获取;图片原文件缺失、权限不足或密钥不匹配时,部分图片仍可能无法显示。 + ## 5. 完成你的第一个任务 ### 只是想找一句话 @@ -52,7 +54,7 @@ macOS 如果提示无法验证开发者,请按系统提示允许打开。自 ### 想找一个模糊的结论 -进入“问问微信”,直接描述问题,例如: +先在“设置 → AI 模型”添加并测试一个 Provider,再进入“问问微信”描述问题,例如: - “上个月技术群讨论过哪些发布问题?” - “张三之前发过的项目地址在哪里?” @@ -74,11 +76,13 @@ macOS 如果提示无法验证开发者,请按系统提示允许打开。自 - [生成群聊日报或总结](./report.md) - [转写微信语音](./voice.md) - [导出聊天档案](./export.md) -- [连接外部 Agent 或微信机器人](../agent/overview.md) +- [可选开启防撤回](./recall-protection.md) +- [在微信里向 WechatExplorer 提问](../agent/agent-hub.md) +- [让外部 Agent 查询微信历史](../agent/overview.md) -## 7. 想让微信机器人参与实时对话 +## 7. 想直接在微信里提问 -如果你希望直接在微信里向本机助手提问,而不是在外部 Agent 中查询,请使用 Agent Hub: +如果你希望直接在微信里向 WechatExplorer 提问,而不是另外配置 Codex 等外部 Agent,请使用 Agent Hub: 1. 先完成上面的微信数据库连接,并确认“档案”里能看到聊天。 2. 打开应用主导航中的“Agent”;页面标题为“Agent Hub”。 @@ -94,6 +98,8 @@ macOS 如果提示无法验证开发者,请按系统提示允许打开。自 机器人会把处理结果回复给发消息的人。联系人聊天总结、群聊总结和需要理解自然语言的请求依赖“设置 → AI 模型”中已经配置好的 AI 服务。当前实时入口主要处理文字消息;它不是支持任意图片、语音、文件理解、群发或定时任务的通用机器人。机器人账号扫码登录与读取你微信数据库是两条独立流程,都需要分别确认账号和权限。 +Agent Hub 是普通用户可以直接使用的入口,不需要安装 Reader Skill 或配置 API Token。Reader Skill 和 API 只用于让外部 Agent 主动查询历史微信。 + ## 8. 需要配置 AI 吗? 不一定。浏览聊天、普通关键词搜索、建立本地知识库和导出不要求在线 AI 服务。 @@ -106,6 +112,7 @@ macOS 如果提示无法验证开发者,请按系统提示允许打开。自 - 离线语音转写使用本地模型;它与在线 AI 请求是两条不同的数据路径。 - 你主动开始并确认 AI 问答或日报后,完成任务所需的受控上下文才可能发送给你选择的 AI 服务;打开应用不会自动上传全部聊天。 - 应用内 Local HTTP API 默认只监听 `127.0.0.1:6131`,受保护接口需要 Token。 +- 防撤回默认关闭;首次开启会为微信消息数据库增加本地撤回日志/监听结构,详细边界见[防撤回](./recall-protection.md)。 完整边界见[数据、隐私与安全](./privacy.md)。 diff --git a/docs/user-guide/privacy.md b/docs/user-guide/privacy.md index 6831380..de4185f 100644 --- a/docs/user-guide/privacy.md +++ b/docs/user-guide/privacy.md @@ -14,6 +14,8 @@ WechatExplorer 的核心路径是本地优先,但“本地优先”不等于 应用不会因为你打开 WechatExplorer 就自动把整份微信数据库上传。 +防撤回默认关闭,并且和上面的普通读取路径不同。用户第一次明确开启时,当前实现会在微信消息数据库中安装本地撤回日志/监听结构,同时在 WechatExplorer 用户数据目录保存必要的恢复记录。关闭开关不等于移除已经安装的结构或清空既有记录;当前 UI 没有对应的清理入口。详见[防撤回](./recall-protection.md)。 + ## 什么时候会请求外部服务 当你主动使用 AI Search、群聊日报或图片理解,并配置了远程 Provider 时,完成任务所需的内容可能发送给该 Provider。当前设置页给出的边界是: @@ -56,3 +58,4 @@ Token 由应用生成,使用 Electron `safeStorage` 加密保存在本机 `loc - 对需要外发的 AI 功能逐项确认 Provider; - 定期在“设置 → 缓存与清理”清理不再需要的检索、导出和索引缓存; - 在共享电脑上退出应用并保护系统账户。 +- 在开启防撤回前确认你接受其数据库写入、性能和清理边界,并先用微信官方方式备份重要数据。 diff --git a/docs/user-guide/recall-protection.md b/docs/user-guide/recall-protection.md new file mode 100644 index 0000000..a9ad01e --- /dev/null +++ b/docs/user-guide/recall-protection.md @@ -0,0 +1,37 @@ +# 防撤回 + +防撤回是一个默认关闭的可选功能。开启后,WechatExplorer 会尽量保留它能够捕获到的撤回消息,并在聊天气泡旁标记“消息已撤回”。 + +它适合希望在本机档案中保留后续聊天上下文的用户,但不能保证找回每一条撤回消息。 + +## 如何开启 + +1. 先连接微信数据库,并确认“档案”可以正常读取聊天。 +2. 打开“设置 → 防撤回”。 +3. 阅读性能和数据提示后,开启“防撤回”。 +4. 保持 WechatExplorer 与当前微信数据连接;之后捕获到的撤回消息会尽量保留并标记。 + +防撤回不是第一次使用的必要步骤。只想浏览、搜索、提问或导出时,可以保持关闭。 + +## 当前能做什么 + +- 监听应用能够识别到的后续撤回变化; +- 在本地保留必要的消息和撤回关系; +- 将已识别的原消息与撤回状态一起显示在档案中; +- 按微信账号隔离 WechatExplorer 保存的恢复记录。 + +## 当前限制 + +- 不能恢复开启前已经撤回、且应用从未保存到的消息; +- WechatExplorer 未运行、数据库未连接或没有捕获到撤回变化时,消息可能无法保留; +- 微信版本、消息表结构和数据库事件变化都可能让部分消息无法恢复或正确匹配; +- 开启后需要为消息表增加监听,聊天很多或磁盘较慢时可能影响加载性能; +- “消息已撤回”只说明应用识别到了撤回关系,不保证恢复内容完整。 + +## 数据写入与关闭边界 + +普通浏览、搜索和 Knowledge 不会修改微信原始聊天数据库;防撤回是一个例外。用户第一次明确开启时,当前实现会在微信消息数据库中安装用于记录撤回的本地日志/监听结构,并在 WechatExplorer 的用户数据目录保存必要的本地恢复记录。 + +关闭设置中的开关,不等同于删除已经安装的日志结构或清空此前保存的恢复记录。当前版本没有在 UI 中提供“移除防撤回日志结构”或“清空防撤回记录”的独立操作。对数据库写入、磁盘占用或完全回滚有要求时,应在开启前先确认这一边界,并使用微信官方方式备份重要数据。 + +完整的数据边界见[数据、隐私与安全](./privacy.md)。 diff --git a/docs/user-guide/report.md b/docs/user-guide/report.md index 5f1a759..70a85f6 100644 --- a/docs/user-guide/report.md +++ b/docs/user-guide/report.md @@ -13,12 +13,13 @@ ## 生成步骤 -1. 打开“日报”。 -2. 选择一个群聊。当前日报入口只支持群聊,不支持单聊。 -3. 选择时间范围:今天、昨天或近 7 天。 -4. 按需要选择参与总结的消息类型,先从文字开始最容易核对。 -5. 选择报告模板/内容模式并开始生成。 -6. 等待“整理输入 → AI 生成 → HTML/PNG 导出”完成。 +你可以从两个入口开始:打开一级导航“日报”后新建报告,或者在“档案”中选中一个群聊并点击“生成 AI 日报”。 + +1. 选择一个群聊。当前日报入口只支持群聊,不支持单聊。 +2. 选择时间范围:今天、昨天或近 7 天。 +3. 按需要选择参与总结的消息类型,先从文字开始最容易核对。 +4. 选择报告模板/内容模式并开始生成。 +5. 等待“整理输入 → AI 生成 → HTML/PNG 导出”完成。 报告可能包含主题、重要消息、问答、资源、待办、未解决事项、关键词、活跃统计,以及可用媒体的精选内容。具体展示内容会随消息类型、资源可用性和模型能力变化。 @@ -39,4 +40,3 @@ - 群太活跃时分成“今天”和“近 7 天”两次生成; - 看到待办和结论后回到原消息核对上下文; - AI Provider 不可用时先检查模型配置和网络/本地服务状态。 - diff --git a/docs/user-guide/troubleshooting.md b/docs/user-guide/troubleshooting.md index 5c74d41..a766b02 100644 --- a/docs/user-guide/troubleshooting.md +++ b/docs/user-guide/troubleshooting.md @@ -44,6 +44,8 @@ AI Search 失败时可能仍保留部分来源;不要把部分结果当成完 原图/缩略图目录缺失、权限不足或微信资源已被清理都会导致图片、视频或语音不可用。导出时可以切换缩略图、关闭媒体或保留缺失项,先确认文本档案是否正常。 +文字正常但图片打不开时,进入“设置 → 图片解密”查看状态并尝试自动获取。密钥正确也不能恢复已经被微信清理的原图文件。 + ## 日报生成失败 日报只支持群聊。确认已选择群聊、时间范围内确实有消息、Provider 可用,并尝试先只选择文字消息。图片理解失败不会自动变成图片内容;报告可能跳过图片精选但仍生成文字日报。 @@ -60,3 +62,18 @@ AI Search 失败时可能仍保留部分来源;不要把部分结果当成完 详细步骤见[Agent 接入概览](../agent/overview.md)和[API 安全](../agent/api-security.md)。 +## 微信机器人无法连接或不回复 + +Agent Hub 和外部 Agent 是两条路径。机器人异常时依次确认: + +1. “Agent”页面中的 Agent Hub、微信连接器和数据库状态是否正常; +2. 二维码是否过期,手机是否已经确认登录; +3. 是否由另一个微信账号向已登录的机器人账号发送文字; +4. 请求是否属于当前支持的最近会话、联系人聊天、近 7 天联系人总结、群聊总结或群成员发言总结; +5. 需要总结或自然语言理解时,AI Provider 是否可用。 + +当前机器人不支持群发、定时任务或与文字同等的图片、语音、文件和视频理解。详细边界见[Agent Hub](../agent/agent-hub.md)。 + +## 防撤回没有保留消息 + +防撤回只能尽量保留开启后且应用成功捕获到的撤回变化。确认开启时数据库已经连接、WechatExplorer 在撤回发生时保持运行,并检查聊天加载是否明显变慢。开启前已经消失、应用未捕获或微信结构无法识别的消息不能保证恢复;详见[防撤回](./recall-protection.md)。 diff --git a/package.json b/package.json index 7219207..ba2f571 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,20 @@ "name": "wechatexplorer", "version": "2.1.9", "packageManager": "pnpm@7.33.7", - "description": "macOS / Windows 微信聊天记录查看与 AI 群聊总结助手", + "description": "macOS / Windows 本地优先的微信聊天记录搜索与 AI 工作台", "keywords": [ "wechat", - "chat", + "wechat chat", + "wechat history", "mac微信", "windows微信", "微信聊天记录", - "AI群聊总结助手" + "微信聊天记录搜索", + "微信AI", + "微信机器人", + "AI聊天搜索", + "AI群聊总结", + "本地AI" ], "author": "Qingmao", "repository": { @@ -127,4 +133,4 @@ "ffmpeg-static" ] } -} +} \ No newline at end of file diff --git a/public/机器人.png b/public/机器人.png new file mode 100644 index 0000000..3e7647d Binary files /dev/null and b/public/机器人.png differ diff --git a/public/问一问.png b/public/问一问.png new file mode 100644 index 0000000..c567199 Binary files /dev/null and b/public/问一问.png differ diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index 3390cd7..a804e97 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -103,9 +103,18 @@ function setPlistValue(plistPath, key, value) { execFileSync('/usr/libexec/PlistBuddy', ['-c', `Set :${key} ${value}`, plistPath]) } +function validateReaderSkillRuntime(runtimeResources) { + const skillPath = path.join(runtimeResources, 'skill', 'wechatexplorer-reader', 'SKILL.md') + if (!existsSync(skillPath)) { + throw new Error(`Missing bundled WechatExplorer Reader Skill: ${skillPath}`) + } + return skillPath +} + exports.default = async function afterPack(context) { const runtimeResources = getRuntimeResources(context) validateAsarRuntimeDependencies(runtimeResources) + validateReaderSkillRuntime(runtimeResources) validateSilkWasmRuntime(runtimeResources) const ffmpegPath = validateFfmpegRuntime(runtimeResources, context.electronPlatformName) validateSherpaRuntime( @@ -182,6 +191,7 @@ exports.default = async function afterPack(context) { exports.getRuntimeResources = getRuntimeResources exports.validateAsarRuntimeDependencies = validateAsarRuntimeDependencies +exports.validateReaderSkillRuntime = validateReaderSkillRuntime exports.validateFfmpegRuntime = validateFfmpegRuntime exports.validateSilkWasmRuntime = validateSilkWasmRuntime exports.validateSherpaRuntime = validateSherpaRuntime diff --git a/src/main/services/skill-resource-service.ts b/src/main/services/skill-resource-service.ts index dc3cf4a..3e15e43 100644 --- a/src/main/services/skill-resource-service.ts +++ b/src/main/services/skill-resource-service.ts @@ -6,34 +6,81 @@ import { isPackagedRuntime } from '../runtime-mode' const SKILL_RELATIVE_PATH = join('skill', 'wechatexplorer-reader', 'SKILL.md') const GITHUB_URL = 'https://github.com/Wxw-Gu/WechatExplorer/tree/main/docs/skill/wechatexplorer-reader' +const SKILL_VERSION = 'v1.1' + +type SkillResourceSource = 'development' | 'bundled' + +interface SkillPathEnvironment { + appPath: string + cwd: string + resourcesPath: string + execPath: string + packaged: boolean +} + +interface SkillCandidate { + path: string + source: SkillResourceSource +} export interface SkillResourceStatus { available: boolean version?: string filePath?: string directoryPath?: string - source: 'development' | 'bundled' + source: SkillResourceSource githubUrl: string error?: string } -function getSkillCandidates(): { path: string; source: 'development' | 'bundled' }[] { - const developmentPath = join(app.getAppPath(), 'docs', SKILL_RELATIVE_PATH) - const bundledPaths = [ - join(process.resourcesPath, SKILL_RELATIVE_PATH), - join(dirname(app.getAppPath()), SKILL_RELATIVE_PATH), - join(dirname(process.execPath), 'resources', SKILL_RELATIVE_PATH) - ] - return isPackagedRuntime() - ? bundledPaths.map((path) => ({ path, source: 'bundled' as const })) - : [ - { path: developmentPath, source: 'development' as const }, - ...bundledPaths.map((path) => ({ path, source: 'bundled' as const })) - ] +function currentEnvironment(): SkillPathEnvironment { + return { + appPath: app.getAppPath(), + cwd: process.cwd(), + resourcesPath: process.resourcesPath || '', + execPath: process.execPath, + packaged: isPackagedRuntime() + } } -function getStatus(): SkillResourceStatus { - const candidates = getSkillCandidates() +function uniqueCandidates(candidates: SkillCandidate[]): SkillCandidate[] { + const seen = new Set() + return candidates.filter((candidate) => { + if (!candidate.path || seen.has(candidate.path)) return false + seen.add(candidate.path) + return true + }) +} + +export function getSkillCandidates(environment?: SkillPathEnvironment): SkillCandidate[] { + const runtime = environment || currentEnvironment() + const developmentPaths = [ + join(runtime.appPath, 'docs', SKILL_RELATIVE_PATH), + join(runtime.cwd, 'docs', SKILL_RELATIVE_PATH), + join(dirname(runtime.appPath), 'docs', SKILL_RELATIVE_PATH) + ] + const execDirectory = dirname(runtime.execPath) + const bundledPaths = [ + join(runtime.resourcesPath, SKILL_RELATIVE_PATH), + join(runtime.resourcesPath, 'resources', SKILL_RELATIVE_PATH), + join(dirname(runtime.appPath), SKILL_RELATIVE_PATH), + join(execDirectory, 'resources', SKILL_RELATIVE_PATH), + join(dirname(execDirectory), 'Resources', SKILL_RELATIVE_PATH) + ] + return uniqueCandidates( + runtime.packaged + ? bundledPaths.map((path) => ({ path, source: 'bundled' })) + : [ + ...developmentPaths.map((path) => ({ path, source: 'development' as const })), + ...bundledPaths.map((path) => ({ path, source: 'bundled' as const })) + ] + ) +} + +export function resolveSkillResourceStatus( + environment?: SkillPathEnvironment +): SkillResourceStatus { + const candidates = getSkillCandidates(environment) const resolved = candidates.find((candidate) => existsSync(candidate.path)) const filePath = resolved?.path || candidates[0].path const source = resolved?.source || candidates[0].source @@ -48,7 +95,7 @@ function getStatus(): SkillResourceStatus { } return { available: true, - version: 'v1.1', + version: SKILL_VERSION, filePath, directoryPath, source, @@ -56,6 +103,10 @@ function getStatus(): SkillResourceStatus { } } +function getStatus(): SkillResourceStatus { + return resolveSkillResourceStatus() +} + export const skillResourceService = { getStatus, diff --git a/tests/unit/runtime-packaging.test.ts b/tests/unit/runtime-packaging.test.ts index c03b8b1..43de4c5 100644 --- a/tests/unit/runtime-packaging.test.ts +++ b/tests/unit/runtime-packaging.test.ts @@ -11,11 +11,13 @@ const asar = nodeRequire('@electron/asar') as { const { validateAsarRuntimeDependencies, validateFfmpegRuntime, + validateReaderSkillRuntime, validateSherpaRuntime, validateSilkWasmRuntime } = nodeRequire('../../scripts/after-pack.cjs') as { validateAsarRuntimeDependencies: (runtimeResources: string) => void validateFfmpegRuntime: (runtimeResources: string, platform?: NodeJS.Platform) => void + validateReaderSkillRuntime: (runtimeResources: string) => string validateSherpaRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void validateSilkWasmRuntime: (runtimeResources: string) => void } @@ -35,6 +37,22 @@ describe('production runtime packaging', () => { expect(() => validateSilkWasmRuntime(join(root, 'resources'))).not.toThrow() }) + it('requires the bundled Reader Skill declared by extraResources', () => { + const resources = join(root, 'reader-skill-resources') + const skillPath = join(resources, 'skill', 'wechatexplorer-reader', 'SKILL.md') + const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8') + + expect(config).toContain('docs/skill/wechatexplorer-reader') + expect(config).toContain('to: skill/wechatexplorer-reader') + expect(() => validateReaderSkillRuntime(resources)).toThrow( + /Missing bundled WechatExplorer Reader Skill/ + ) + + mkdirSync(dirname(skillPath), { recursive: true }) + writeFileSync(skillPath, '# WechatExplorer Reader\n') + expect(validateReaderSkillRuntime(resources)).toBe(skillPath) + }) + it('keeps silk-wasm in electron-builder asarUnpack', () => { const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8') expect(config).toContain('node_modules/silk-wasm/**') diff --git a/tests/unit/skill-resource-service.test.ts b/tests/unit/skill-resource-service.test.ts new file mode 100644 index 0000000..3e0acff --- /dev/null +++ b/tests/unit/skill-resource-service.test.ts @@ -0,0 +1,94 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { dirname, join, resolve } from 'path' +import { afterEach, describe, expect, it } from 'vitest' +import { + getSkillCandidates, + resolveSkillResourceStatus +} from '../../src/main/services/skill-resource-service' + +const roots: string[] = [] + +function fixtureRoot(): string { + const root = mkdtempSync(join(tmpdir(), 'wxe-reader-skill-')) + roots.push(root) + return root +} + +function environment(root: string, packaged: boolean) { + return { + appPath: join(root, 'application'), + cwd: join(root, 'workspace'), + resourcesPath: join(root, 'runtime', 'resources'), + execPath: join(root, 'runtime', 'WechatExplorer.exe'), + packaged + } +} + +function writeSkill(filePath: string): void { + mkdirSync(dirname(filePath), { recursive: true }) + writeFileSync(filePath, '# WechatExplorer Reader\n', 'utf8') +} + +describe('Reader Skill resource resolution', () => { + afterEach(() => { + while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true }) + }) + + it('finds the repository Skill from the current working directory in development', () => { + const root = fixtureRoot() + const runtime = environment(root, false) + const skillPath = join(runtime.cwd, 'docs', 'skill', 'wechatexplorer-reader', 'SKILL.md') + writeSkill(skillPath) + + expect(resolveSkillResourceStatus(runtime)).toMatchObject({ + available: true, + source: 'development', + version: 'v1.1', + filePath: skillPath, + directoryPath: dirname(skillPath) + }) + }) + + it('resolves the Reader Skill from this checkout', () => { + const workspace = resolve(__dirname, '../..') + const status = resolveSkillResourceStatus({ + appPath: workspace, + cwd: workspace, + resourcesPath: join(workspace, 'node_modules', 'electron', 'resources'), + execPath: join(workspace, 'node_modules', 'electron', 'electron.exe'), + packaged: false + }) + + expect(status).toMatchObject({ + available: true, + source: 'development', + version: 'v1.1', + filePath: join(workspace, 'docs', 'skill', 'wechatexplorer-reader', 'SKILL.md') + }) + }) + + it('uses the extraResources Skill directory in a packaged runtime', () => { + const root = fixtureRoot() + const runtime = environment(root, true) + const skillPath = join(runtime.resourcesPath, 'skill', 'wechatexplorer-reader', 'SKILL.md') + writeSkill(skillPath) + + expect(resolveSkillResourceStatus(runtime)).toMatchObject({ + available: true, + source: 'bundled', + filePath: skillPath + }) + }) + + it('reports every checked path without duplicating candidates', () => { + const root = fixtureRoot() + const runtime = environment(root, false) + const candidates = getSkillCandidates(runtime) + const status = resolveSkillResourceStatus(runtime) + + expect(new Set(candidates.map((candidate) => candidate.path)).size).toBe(candidates.length) + expect(status).toMatchObject({ available: false, source: 'development' }) + for (const candidate of candidates) expect(status.error).toContain(candidate.path) + }) +})