4 Commits

Author SHA1 Message Date
Shen Junzheng
27fdb9eac3 fix db file dependencies 2025-04-18 00:28:26 +08:00
Sarv
b866d6eddd fix server cmd & http index page (#54) 2025-04-17 18:50:09 +08:00
Sarv
871ad50b3b server command (#49) 2025-04-17 01:04:50 +08:00
Sarv
b64a3a1caa docs (#48) 2025-04-16 18:19:41 +08:00
16 changed files with 1500 additions and 307 deletions

121
DISCLAIMER.md Normal file
View File

@@ -0,0 +1,121 @@
# Chatlog 免责声明
## 1. 定义
在本免责声明中,除非上下文另有说明,下列术语应具有以下含义:
- **"本项目"或"Chatlog"**:指本开源软件项目,包括其源代码、可执行程序、文档及相关资源。
- **"开发者"**:指本项目的创建者、维护者及代码贡献者。
- **"用户"**:指下载、安装、使用或以任何方式接触本项目的个人或实体。
- **"聊天数据"**:指通过各类即时通讯软件生成的对话内容及相关元数据。
- **"合法授权"**:指根据适用法律法规,由数据所有者或数据主体明确授予的处理其聊天数据的权限。
- **"第三方服务"**:指由非本项目开发者提供的外部服务,如大型语言模型(LLM) API 服务。
## 2. 使用目的与法律遵守
本项目仅供学习、研究和个人合法使用。用户须严格遵守所在国家/地区的法律法规使用本工具。任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,相关法律责任由用户自行承担。
⚠️ **用户应自行了解并遵守当地有关数据访问、隐私保护、计算机安全和网络安全的法律法规。不同司法管辖区对数据处理有不同的法律要求,用户有责任确保其使用行为符合所有适用法规。**
## 3. 授权范围与隐私保护
- 本工具仅限于处理用户自己合法拥有的聊天数据,或已获得数据所有者明确授权的数据。
- 严禁将本工具用于未经授权获取、查看或分析他人聊天记录,或侵犯他人隐私权。
- 用户应采取适当措施保护通过本工具获取和处理的聊天数据安全,包括但不限于加密存储、限制访问权限、定期删除不必要数据等。
- 用户应确保其处理的聊天数据符合相关数据保护法规,包括但不限于获得必要的同意、保障数据主体权利、遵守数据最小化原则等。
## 4. 使用限制
- 本项目仅允许在合法授权情况下对聊天数据库进行备份与查看。
- 未经明确授权,严禁将本项目用于访问、查看、分析或处理任何第三方聊天数据。
- 使用第三方 LLM 服务时,用户应遵守相关服务提供商的服务条款和使用政策。
- 用户不得规避本项目中的任何技术限制,或尝试反向工程、反编译或反汇编本项目,除非适用法律明确允许此类活动。
## 5. 技术风险声明
⚠️ **使用本项目存在以下技术风险,用户应充分了解并自行承担:**
- 本工具需要访问聊天软件的数据库文件,可能因聊天软件版本更新导致功能失效或数据不兼容。
- 在 macOS 系统上使用时,需要临时关闭 SIP 安全机制,这可能降低系统安全性,用户应了解相关风险并自行决定是否使用。
- 本项目可能存在未知的技术缺陷或安全漏洞,可能导致数据损坏、丢失或泄露。
- 使用本项目处理大量数据可能导致系统性能下降或资源占用过高。
- 第三方依赖库或 API 的变更可能影响本项目的功能或安全性。
## 6. 禁止非法用途
严禁将本项目用于以下用途:
- 从事任何形式的非法活动,包括但不限于未授权系统测试、网络渗透或其他违反法律法规的行为。
- 监控、窃取或未经授权获取他人聊天记录或个人信息。
- 将获取的数据用于骚扰、诈骗、敲诈、威胁或其他侵害他人合法权益的行为。
- 规避任何安全措施或访问控制机制。
- 传播虚假信息、仇恨言论或违反公序良俗的内容。
- 侵犯任何第三方的知识产权、隐私权或其他合法权益。
**违反上述规定的,用户应自行承担全部法律责任,并赔偿因此给开发者或第三方造成的全部损失。**
## 7. 第三方服务集成
- 用户将聊天数据与第三方 LLM 服务(如 OpenAI、Claude 等)结合使用时,应仔细阅读并遵守这些服务的使用条款、隐私政策和数据处理协议。
- 用户应了解,向第三方服务传输数据可能导致数据离开用户控制范围,并受第三方服务条款约束。
- 本项目开发者不对第三方服务的可用性、安全性、准确性或数据处理行为负责,用户应自行评估相关风险。
- 用户应确保其向第三方服务传输数据的行为符合适用的数据保护法规和第三方服务条款。
## 8. 责任限制
**在法律允许的最大范围内:**
- 本项目按"原样"和"可用"状态提供,不对功能的适用性、可靠性、准确性、完整性或及时性做任何明示或暗示的保证。
- 开发者明确否认对适销性、特定用途适用性、不侵权以及任何其他明示或暗示的保证。
- 本项目开发者和贡献者不对用户使用本工具的行为及后果承担任何法律责任。
- 对于因使用本工具而可能导致的任何直接、间接、附带、特殊、惩罚性或后果性损失,包括但不限于数据丢失、业务中断、隐私泄露、声誉损害、利润损失、法律纠纷等,本项目开发者概不负责,即使开发者已被告知此类损失的可能性。
- 在任何情况下,开发者对用户的全部责任累计不超过用户为获取本软件实际支付的金额(如为免费获取则为零)。
## 9. 知识产权声明
- 本项目基于 Apache-2.0 许可证开源,用户在使用、修改和分发时应严格遵守该许可证的所有条款。
- 本项目的名称"Chatlog"、相关标识及商标权(如有)归开发者所有,未经明确授权,用户不得以任何方式使用这些标识进行商业活动。
- 根据 Apache-2.0 许可证,用户可自由使用、修改和分发本项目代码,但须遵守许可证规定的归属声明等要求。
- 用户对其修改版本自行承担全部责任,且不得以原项目名义发布,必须明确标明其为修改版本并与原项目区分。
- 用户不得移除或更改本项目中的版权声明、商标或其他所有权声明。
## 10. 数据处理合规性
- 用户在使用本项目处理个人数据时,应遵守适用的数据保护法规,包括但不限于《中华人民共和国个人信息保护法》、《通用数据保护条例》(GDPR)等。
- 用户应确保其具有处理相关数据的合法依据,如获得数据主体的明确同意。
- 用户应实施适当的技术和组织措施,确保数据安全,防止未授权访问、意外丢失或泄露。
- 在跨境传输数据时,用户应确保符合相关法律对数据出境的要求。
- 用户应尊重数据主体权利,包括访问权、更正权、删除权等。
## 11. 免责声明接受
下载、安装、使用本项目,表示用户已阅读、理解并同意遵守本免责声明的所有条款。如不同意,请立即停止使用本工具并删除相关代码和程序。
**用户确认:**
- 已完整阅读并理解本免责声明的全部内容
- 自愿接受本免责声明的全部条款
- 具有完全民事行为能力,能够理解并承担使用本项目的风险和责任
- 将遵守本免责声明中规定的所有义务和限制
## 12. 免责声明修改与通知
- 本免责声明可能根据项目发展和法律法规变化进行修改和调整,修改后的声明将在项目官方仓库页面公布。
- 开发者没有义务个别通知用户免责声明的变更,用户应定期查阅最新版本。
- 重大变更将通过项目仓库的 Release Notes 或 README 文件更新进行通知。
- 在免责声明更新后继续使用本项目,即视为接受修改后的条款。
## 13. 法律适用与管辖
- 本免责声明受中华人民共和国法律管辖,并按其解释。
- 任何与本免责声明有关的争议,应首先通过友好协商解决;协商不成的,提交至本项目开发者所在地有管辖权的人民法院诉讼解决。
- 对于中国境外用户,如本免责声明与用户所在地强制性法律规定冲突,应以不违反该强制性规定的方式解释和适用本声明,但本声明的其余部分仍然有效。
## 14. 可分割性
如本免责声明中的任何条款被有管辖权的法院或其他权威机构认定为无效、不合法或不可执行,不影响其余条款的有效性和可执行性。无效条款应被视为从本声明中分割,并在法律允许的最大范围内由最接近原条款意图的有效条款替代。
## 15. 完整协议
本免责声明构成用户与开发者之间关于本项目使用的完整协议,取代先前或同时期关于本项目的所有口头或书面协议、提议和陈述。本声明的任何豁免、修改或补充均应以书面形式作出并经开发者签署方为有效。

233
README.md
View File

@@ -2,7 +2,7 @@
# Chatlog # Chatlog
![chatlog](https://socialify.git.ci/sjzar/chatlog/image?font=Rokkitt&name=1&pattern=Diagonal+Stripes&theme=Auto) ![chatlog](https://socialify.git.ci/sjzar/chatlog/image?font=Rokkitt&forks=1&issues=1&name=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto)
_聊天记录工具,帮助大家轻松使用自己的聊天数据_ _聊天记录工具,帮助大家轻松使用自己的聊天数据_
@@ -13,7 +13,7 @@ _聊天记录工具帮助大家轻松使用自己的聊天数据_
</div> </div>
![chatlog](https://github.com/user-attachments/assets/746717b8-9b39-4a45-97f3-f0ae8fc5a344) ![chatlog](https://github.com/user-attachments/assets/e085d3a2-e009-4463-b2fd-8bd7df2b50c3)
## Feature ## Feature
@@ -23,16 +23,35 @@ _聊天记录工具帮助大家轻松使用自己的聊天数据_
- 提供 Terminal UI 界面 & 命令行工具 - 提供 Terminal UI 界面 & 命令行工具
- 提供 HTTP API 服务,支持查询聊天记录、联系人、群聊、最近会话等信息 - 提供 HTTP API 服务,支持查询聊天记录、联系人、群聊、最近会话等信息
- 支持 MCP SSE 协议,可与支持 MCP 的 AI 助手无缝集成 - 支持 MCP SSE 协议,可与支持 MCP 的 AI 助手无缝集成
- 支持多媒体消息,支持解密图片、语音
- 支持自动解密数据,简化使用流程
- 支持多账号管理,可在不同账号间切换
## TODO ## TODO
- 支持多媒体数据
- 聊天数据全文索引 - 聊天数据全文索引
- 聊天数据统计 & Dashboard - 聊天数据统计 & Dashboard
## Quick Start
## Install ### 基本步骤
1. **安装 Chatlog**[下载预编译版本](#下载预编译版本) 或 [使用 Go 安装](#从源码安装)
2. **运行程序**:执行 `chatlog` 启动 Terminal UI 界面
3. **解密数据**:选择 `解密数据` 菜单项
4. **开启 HTTP 服务**:选择 `开启 HTTP 服务` 菜单项
5. **访问数据**:通过 [HTTP API](#http-api) 或 [MCP 集成](#mcp-集成) 访问聊天记录
> 💡 **提示**:如果电脑端微信聊天记录不全,可以[从手机端迁移数据](#从手机迁移聊天记录)
### 常见问题快速解决
- **macOS 用户**:获取密钥前需[临时关闭 SIP](#macos-版本说明)
- **Windows 用户**:遇到界面显示问题请[使用 Windows Terminal](#windows-版本说明)
- **集成 AI 助手**:查看 [MCP 集成指南](#mcp-集成)
## 安装指南
### 从源码安装 ### 从源码安装
@@ -44,150 +63,164 @@ go install github.com/sjzar/chatlog@latest
访问 [Releases](https://github.com/sjzar/chatlog/releases) 页面下载适合您系统的预编译版本。 访问 [Releases](https://github.com/sjzar/chatlog/releases) 页面下载适合您系统的预编译版本。
## Quick Start ## 使用指南
### 操作流程
1. 下载并安装微信客户端
2. 手机微信上操作 `我 - 设置 - 通用 - 聊天记录迁移与备份 - 迁移 - 迁移到电脑`,这一步的目的是将手机中的聊天记录传输到电脑上。可以放心操作,不会影响到手机上的聊天记录。
3. 下载 `chatlog` 预编译版本或从源码安装,推荐使用 go 进行安装。
4. 运行 `chatlog`,按照提示进行操作,解密数据并开启 HTTP 服务后,即可通过浏览器或 AI 助手访问聊天记录。
### macOS 版本提示
1. macOS 用户在获取密钥前,需要确认已经关闭 SIP 并安装 Xcode Command Line Tools。由于 macOS 的安全机制,在正常情况在无法读取微信进程的内存数据,所以需要临时关闭 SIP。关闭 SIP 的方法:
```shell
# 1. 进入恢复模式
Apple Intel Mac: 关机后,按住 Command + R 键开机,直到出现苹果标志和进度条。
Apple Silicon Mac: 关机后,按住开机键不松开,直到出现苹果标志和进度条。
# 2. 打开终端
选项 - 实用工具 - 终端
# 3. 关闭 SIP
输入以下命令关闭 SIP
csrutil disable
# 4. 重启系统
```
2. 目前的 macOS 版本方案依赖 `lldb` 工具,所以需要安装 Xcode Command Line Tools。
```shell
# 在 terminal 执行以下命令安装 Xcode Command Line Tools
xcode-select --install
```
3. 仅获取数据密钥步骤需要关闭 SIP获取数据密钥后即可重新打开 SIP不影响解密数据和 HTTP 服务的运行。
4. 如果是 Apple Silicon 芯片的 mac 用户,请检查 微信、chatlog、terminal 均不要运行在 Rosetta 模式下运行,否则可能无法获取密钥。
### Terminal UI 模式 ### Terminal UI 模式
1. 启动程序 最简单的使用方式是通过 Terminal UI 界面操作
```bash ```bash
./chatlog chatlog
``` ```
2. 使用界面操作: 操作方法
- 使用方向键导航菜单 - 使用 `↑` `↓` 键选择菜单
-`Enter` 选择菜单项 -`Enter` 确认选择
-`Esc` 返回上级菜单 -`Esc` 返回上级菜单
-`Ctrl+C` 退出程序 -`Ctrl+C` 退出程序
### 命令行模式 ### 命令行模式
获取微信数据密钥 对于熟悉命令行的用户,可以直接使用以下命令
```bash ```bash
./chatlog key # 获取微信数据密钥
chatlog key
# 解密数据库文件
chatlog decrypt
# 启动 HTTP 服务
chatlog server
``` ```
解密数据库文件: ### 从手机迁移聊天记录
```bash 如果电脑端微信聊天记录不全,可以从手机端迁移数据:
./chatlog decrypt
``` 1. 打开手机微信,进入 `我 - 设置 - 通用 - 聊天记录迁移与备份`
2. 选择 `迁移 - 迁移到电脑`,按照提示操作
3. 完成迁移后,重新运行 `chatlog` 获取密钥并解密数据
> 此操作不会影响手机上的聊天记录,只是将数据复制到电脑端
## 平台特定说明
### Windows 版本说明
如遇到界面显示异常(如花屏、乱码等),请使用 [Windows Terminal](https://github.com/microsoft/terminal) 运行程序
### macOS 版本说明
macOS 用户在获取密钥前需要临时关闭 SIP系统完整性保护
1. **关闭 SIP**
```shell
# 进入恢复模式
# Intel Mac: 重启时按住 Command + R
# Apple Silicon: 重启时长按电源键
# 在恢复模式中打开终端并执行
csrutil disable
# 重启系统
```
2. **安装必要工具**
```shell
# 安装 Xcode Command Line Tools
xcode-select --install
```
3. **获取密钥后**:可以重新启用 SIP`csrutil enable`),不影响后续使用
> Apple Silicon 用户注意确保微信、chatlog 和终端都不在 Rosetta 模式下运行
## HTTP API ## HTTP API
启动 HTTP 服务后,可通过以下 API 访问数据: 启动 HTTP 服务后(默认地址 `http://127.0.0.1:5030`,可通过以下 API 访问数据:
### 聊天记录 ### 聊天记录查询
``` ```
GET /api/v1/chatlog?time=2023-01-01&talker=wxid_xxx&limit=100&offset=0&format=json GET /api/v1/chatlog?time=2023-01-01&talker=wxid_xxx
``` ```
参数说明: 参数说明:
- `time`: 时间范围,格式为 `YYYY-MM-DD` 或 `YYYY-MM-DD~YYYY-MM-DD` - `time`: 时间范围,格式为 `YYYY-MM-DD` 或 `YYYY-MM-DD~YYYY-MM-DD`
- `talker`: 聊天对象的 ID不知道 ID 的话也可以尝试备注名、昵称、群聊 ID等 - `talker`: 聊天对象标识(支持 wxid、群聊 ID、备注名、昵称等
- `limit`: 返回记录数量限制 - `limit`: 返回记录数量
- `offset`: 分页偏移量 - `offset`: 分页偏移量
- `format`: 输出格式,支持 `json`、`csv` 或纯文本 - `format`: 输出格式,支持 `json`、`csv` 或纯文本
### 联系人列表 ### 其他 API 接口
``` - **联系人列表**`GET /api/v1/contact`
GET /api/v1/contact - **群聊列表**`GET /api/v1/chatroom`
``` - **会话列表**`GET /api/v1/session`
- **多媒体内容**`GET /api/v1/media?msgid=xxx`
### 群聊列表
``` ## MCP 集成
GET /api/v1/chatroom
```
### 会话列表 Chatlog 支持 MCP (Model Context Protocol) SSE 协议,可与支持 MCP 的 AI 助手无缝集成。
启动 HTTP 服务后,通过 SSE Endpoint 访问服务:
```
GET /api/v1/session
```
## MCP
支持 MCP SSE 协议,启动 HTTP 服务后,通过 SSE Endpoint 访问服务:
``` ```
GET /sse GET /sse
``` ```
提供了 4 个 tool 用于与 AI 助手集成: ### 快速集成
- `chatlog`: 查询聊天记录
- `query_contact`: 查询联系人
- `query_chat_room`: 查询群聊
- `query_recent_chat`: 查询最近会话
### 示例 Chatlog 可以与多种支持 MCP 的 AI 助手集成,包括:
以 [ChatWise](https://chatwise.app/) 工具为例,在 `设置 - 工具` 下新建工具,类型为 `sse`ID 为 `chatlog`URL 为 `http://127.0.0.1:5030/sse`,勾选自动执行工具,即可使用。 - **ChatWise**: 直接支持 SSE在工具设置中添加 `http://127.0.0.1:5030/sse`
- **Cherry Studio**: 直接支持 SSE在 MCP 服务器设置中添加 `http://127.0.0.1:5030/sse`
部分 AI 聊天工具暂时不支持 MCP SSE 协议,可以通过 [`mcp-proxy`](https://github.com/sparfenyuk/mcp-proxy) 工具转发请求,以 [Claude Desktop](https://claude.ai/download) 为例,在安装好 `mcp-proxy` 后,将 `mcp-proxy` 配置到 `Claude Desktop``config.json` 文件中,即可使用 对于不直接支持 SSE 的客户端,可以使用 [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy) 工具转发请求:
```json - **Claude Desktop**: 通过 mcp-proxy 支持,需要配置 `claude_desktop_config.json`
{ - **Monica Code**: 通过 mcp-proxy 支持,需要配置 VSCode 插件设置
"mcpServers": {
"mcp-proxy": { ### 详细集成指南
"command": "/Users/sarv/.local/bin/mcp-proxy",
"args": [ 查看 [MCP 集成指南](docs/mcp.md) 获取各平台的详细配置步骤和注意事项。
"http://localhost:5030/sse"
], ## Prompt 示例
"env": {}
} 为了帮助大家更好地利用 Chatlog 与 AI 助手,我们整理了一些 prompt 示例。希望这些 prompt 可以启发大家更有效地查询和分析聊天记录,获取更精准的信息。
},
"globalShortcut": "" 查看 [Prompt 指南](docs/prompt.md) 获取详细示例。
}
``` 同时欢迎大家分享使用经验和 prompt如果您有好的 prompt 示例或使用技巧,请通过 [Discussions](https://github.com/sjzar/chatlog/discussions) 进行分享,共同进步。
## 免责声明
⚠️ **重要提示:使用本项目前,请务必阅读并理解完整的 [免责声明](./DISCLAIMER.md)。**
本项目仅供学习、研究和个人合法使用,禁止用于任何非法目的或未授权访问他人数据。下载、安装或使用本工具即表示您同意遵守免责声明中的所有条款,并自行承担使用过程中的全部风险和法律责任。
### 摘要(请阅读完整免责声明)
- 仅限处理您自己合法拥有的聊天数据或已获授权的数据
- 严禁用于未经授权获取、查看或分析他人聊天记录
- 开发者不对使用本工具可能导致的任何损失承担责任
- 使用第三方 LLM 服务时,您应遵守这些服务的使用条款和隐私政策
**本项目完全免费开源,任何以本项目名义收费的行为均与本项目无关。**
## License ## License
`chatlog` 是在 Apache-2.0 许可下的开源软件 本项目基于 [Apache-2.0 许可证](./LICENSE) 开源
## 隐私政策
本项目不收集任何用户数据。所有数据处理均在用户本地设备上进行。使用第三方服务时,请参阅相应服务的隐私政策。
## Thanks ## Thanks
- [@0xlane](https://github.com/0xlane) 的 [wechat-dump-rs](https://github.com/0xlane/wechat-dump-rs) 项目 - [@0xlane](https://github.com/0xlane) 的 [wechat-dump-rs](https://github.com/0xlane/wechat-dump-rs) 项目
- [@xaoyaoo](https://github.com/xaoyaoo) 的 [PyWxDump](https://github.com/xaoyaoo/PyWxDump) 项目 - [@xaoyaoo](https://github.com/xaoyaoo) 的 [PyWxDump](https://github.com/xaoyaoo/PyWxDump) 项目
- [@git-jiadong](https://github.com/git-jiadong) 的 [go-lame](https://github.com/git-jiadong/go-lame) 和 [go-silk](https://github.com/git-jiadong/go-silk) 项目
- [Anthropic](https://www.anthropic.com/) 的 [MCP]((https://github.com/modelcontextprotocol) ) 协议 - [Anthropic](https://www.anthropic.com/) 的 [MCP]((https://github.com/modelcontextprotocol) ) 协议
- 各个 Go 开源库的贡献者们 - 各个 Go 开源库的贡献者们

43
cmd/chatlog/cmd_server.go Normal file
View File

@@ -0,0 +1,43 @@
package chatlog
import (
"runtime"
"github.com/sjzar/chatlog/internal/chatlog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(serverCmd)
serverCmd.Flags().StringVarP(&serverAddr, "addr", "a", "127.0.0.1:5030", "server address")
serverCmd.Flags().StringVarP(&serverDataDir, "data-dir", "d", "", "data dir")
serverCmd.Flags().StringVarP(&serverWorkDir, "work-dir", "w", "", "work dir")
serverCmd.Flags().StringVarP(&serverPlatform, "platform", "p", runtime.GOOS, "platform")
serverCmd.Flags().IntVarP(&serverVer, "version", "v", 3, "version")
}
var (
serverAddr string
serverDataDir string
serverWorkDir string
serverPlatform string
serverVer int
)
var serverCmd = &cobra.Command{
Use: "server",
Short: "Start HTTP server",
Run: func(cmd *cobra.Command, args []string) {
m, err := chatlog.New("")
if err != nil {
log.Err(err).Msg("failed to create chatlog instance")
return
}
if err := m.CommandHTTPServer(serverAddr, serverDataDir, serverWorkDir, serverPlatform, serverVer); err != nil {
log.Err(err).Msg("failed to start server")
return
}
},
}

151
docs/mcp.md Normal file
View File

@@ -0,0 +1,151 @@
# MCP 集成指南
## 目录
- [MCP 集成指南](#mcp-集成指南)
- [目录](#目录)
- [前期准备](#前期准备)
- [mcp-proxy](#mcp-proxy)
- [ChatWise](#chatwise)
- [Cherry Studio](#cherry-studio)
- [Claude Desktop](#claude-desktop)
- [Monica Code](#monica-code)
## 前期准备
运行 `chatlog`,完成数据解密并开启 HTTP 服务
### mcp-proxy
如果遇到不支持 `SSE` 的客户端,可以尝试使用 `mcp-proxy``stdio` 的请求转换为 `SSE`
项目地址https://github.com/sparfenyuk/mcp-proxy
安装方式:
```shell
# 使用 uv 工具安装,也可参考项目文档的其他安装方式
uv tool install mcp-proxy
# 查询 mcp-proxy 的路径,后续可直接使用该路径
which mcp-proxy
/Users/sarv/.local/bin/mcp-proxy
```
## ChatWise
- 官网https://chatwise.app/
- 使用方式MCP SSE
- 注意事项:使用 ChatWise 的 MCP 功能需要 Pro 权限
1.`设置 - 工具` 下新建 `SSE 请求` 工具
![chatwise-1](https://github.com/user-attachments/assets/87e40f39-9fbc-4ff1-954a-d95548cde4c2)
1. 在 URL 中填写 `http://127.0.0.1:5030/sse`,并勾选 `自动执行工具`,点击 `查看工具` 即可检查连接 `chatlog` 是否正常
![chatwise-2](https://github.com/user-attachments/assets/8f98ef18-8e6c-40e6-ae78-8cd13e411c36)
3. 返回主页,选择支持 MCP 调用的模型,打开 `chatlog` 工具选项
![chatwise-3](https://github.com/user-attachments/assets/ea2aa178-5439-492b-a92f-4f4fc08828e7)
4. 测试功能是否正常
![chatwise-4](https://github.com/user-attachments/assets/8f82cb53-8372-40ee-a299-c02d3399403a)
## Cherry Studio
- 官网https://cherry-ai.com/
- 使用方式MCP SSE
1.`设置 - MCP 服务器` 下点击 `添加服务器`,输入名称为 `chatlog`,选择类型为 `服务器发送事件(sse)`,填写 URL 为 `http://127.0.0.1:5030/sse`,点击 `保存`。(注意:点击保存前不要先点击左侧的开启按钮)
![cherry-1](https://github.com/user-attachments/assets/93fc8b0a-9d95-499e-ab6c-e22b0c96fd6a)
2. 选择支持 MCP 调用的模型,打开 `chatlog` 工具选项
![cherry-2](https://github.com/user-attachments/assets/4e5bf752-2eab-4e7c-b73b-1b759d4a5f29)
3. 测试功能是否正常
![cherry-3](https://github.com/user-attachments/assets/c58a019f-fd5f-4fa3-830a-e81a60f2aa6f)
## Claude Desktop
- 官网https://claude.ai/download
- 使用方式mcp-proxy
- 参考资料https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server
1. 请先参考 [mcp-proxy](#mcp-proxy) 安装 `mcp-proxy`
2. 进入 Claude Desktop `Settings - Developer`,点击 `Edit Config` 按钮,这样会创建一个 `claude_desktop_config.json` 配置文件,并引导你编辑该文件
3. 编辑 `claude_desktop_config.json` 文件,配置名称为 `chatlog`command 为 `mcp-proxy` 的路径args 为 `http://127.0.0.1:5030/sse`,如下所示:
```json
{
"mcpServers": {
"chatlog": {
"command": "/Users/sarv/.local/bin/mcp-proxy",
"args": [
"http://localhost:5030/sse"
]
}
},
"globalShortcut": ""
}
```
4. 保存 `claude_desktop_config.json` 文件,重启 Claude Desktop可以看到 `chatlog` 已经添加成功
![claude-1](https://github.com/user-attachments/assets/f4e872cc-e6c1-4e24-97da-266466949cdf)
5. 测试功能是否正常
![claude-2](https://github.com/user-attachments/assets/832bb4d2-3639-4cbc-8b17-f4b812ea3637)
## Monica Code
- 官网https://monica.im/en/code
- 使用方式mcp-proxy
- 参考资料https://github.com/Monica-IM/Monica-Code/blob/main/Reference/config.md#modelcontextprotocolserver
1. 请先参考 [mcp-proxy](#mcp-proxy) 安装 `mcp-proxy`
2. 在 vscode 插件文件夹(`~/.vscode/extensions`)下找到 Monica Code 的目录,编辑 `config_schema.json` 文件。将 `experimental - modelContextProtocolServer``transport` 设置为如下内容:
```json
{
"experimental": {
"type": "object",
"title": "Experimental",
"description": "Experimental properties are subject to change.",
"properties": {
"modelContextProtocolServer": {
"type": "object",
"properties": {
"transport": {
"type": "stdio",
"command": "/Users/sarv/.local/bin/mcp-proxy",
"args": [
"http://localhost:5030/sse"
]
}
},
"required": [
"transport"
]
}
}
}
}
```
3. 重启 vscode可以看到 `chatlog` 已经添加成功
![monica-1](https://github.com/user-attachments/assets/8d0a96f2-ed05-48aa-a99a-06648ae1c500)
4. 测试功能是否正常
![monica-2](https://github.com/user-attachments/assets/054e0a30-428a-48a6-9f31-d2596fb8f743)

70
docs/prompt.md Normal file
View File

@@ -0,0 +1,70 @@
# Prompt 指南
## 概述
优秀的 `prompt` 可以极大的提高 `chatlog` 使用体验,收集了部分群友分享的 `prompt`,供大家参考。
在处理聊天记录时,尽量选择上下文长度足够的 LLM例如 `Gemini 2.5 Pro``Claude 3.5 Sonnet` 等。
欢迎大家在 [Discussions](https://github.com/sjzar/chatlog/discussions/47) 中分享自己的使用方式,共同进步。
## 群聊总结
作者:@eyaeya
```md
你是一个中文的群聊总结的助手,你可以为一个微信的群聊记录,提取并总结每个时间段大家在重点讨论的话题内容。
请帮我将 "<talker>" 在 <Time> 的群聊内容总结成一个群聊报告包含不多于5个的话题的总结如果还有更多话题可以在后面简单补充。每个话题包含以下内容
- 话题名(50字以内带序号1⃣2⃣3同时附带热度以🔥数量表示
- 参与者(不超过5个人将重复的人名去重)
- 时间段(从几点到几点)
- 过程(50到200字左右
- 评价(50字以下)
- 分割线: ------------
另外有以下要求:
1. 每个话题结束使用 ------------ 分割
2. 使用中文冒号
3. 无需大标题
4. 开始给出本群讨论风格的整体评价,例如活跃、太水、太黄、太暴力、话题不集中、无聊诸如此类
最后总结下最活跃的前五个发言者。
```
## 微信聊天记录可视化
作者:@数字声明卡兹克
原文地址https://mp.weixin.qq.com/s/Z66YRjY1EnC_hMgXE9_nnw
Prompt[微信聊天记录可视化prompt.txt](https://github.com/user-attachments/files/19773263/prompt.txt)
这份 prompt 可以使用聊天记录生成 HTML 网页,再使用 [YOURWARE](https://www.yourware.so/) 部署为可分享的静态网页。
### 技术讨论分析
作者:@eyaeya
```md
你作为一个专业的技术讨论分析者,请对以下聊天记录进行分析和结构化总结:
1. 基础信息提取:
- 将每个主题分成独立的问答对
- 保持原始对话的时间顺序
1. 问题分析要点:
- 提取问题的具体场景和背景
- 识别问题的核心技术难点
- 突出问题的实际影响
1. 解决方案总结:
- 列出具体的解决步骤
- 提取关键工具和资源
- 包含实践经验和注意事项
- 保留重要的链接和参考资料
1. 输出格式:
- 不要输出"日期:YYYY-MM-DD"这一行直接从问题1开始
- 问题1<简明扼要的问题描述>
- 回答1<完整的解决方案>
- 补充:<额外的讨论要点或注意事项>
1. 额外要求(严格执行)
- 如果有多个相关问题,保持逻辑顺序
- 标记重要的警告和建议、突出经验性的分享内容、保留有价值的专业术语解释、移除"我来分析"等过渡语确保链接的完整性
- 直接以日期开始,不要添加任何开场白
```

View File

@@ -77,6 +77,21 @@ func (s *Service) Start() error {
return nil return nil
} }
func (s *Service) ListenAndServe() error {
if s.ctx.HTTPAddr == "" {
s.ctx.HTTPAddr = DefalutHTTPAddr
}
s.server = &http.Server{
Addr: s.ctx.HTTPAddr,
Handler: s.router,
}
log.Info().Msg("Starting HTTP server on " + s.ctx.HTTPAddr)
return s.server.ListenAndServe()
}
func (s *Service) Stop() error { func (s *Service) Stop() error {
if s.server == nil { if s.server == nil {

View File

@@ -1,156 +1,733 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chatlog</title> <title>Chatlog</title>
<style> <style>
.random-paragraph { :root {
display: none; --primary-color: #3498db;
--primary-dark: #2980b9;
--success-color: #2ecc71;
--success-dark: #27ae60;
--error-color: #e74c3c;
--bg-light: #f5f5f5;
--bg-white: #ffffff;
--text-color: #333333;
--border-color: #dddddd;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
line-height: 1.6;
color: var(--text-color);
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #fafafa;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.welcome-text {
text-align: center;
margin-bottom: 30px;
}
.api-section {
background-color: var(--bg-light);
border-radius: 10px;
padding: 25px;
width: 100%;
max-width: 850px;
margin-bottom: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
h1 {
color: #2c3e50;
margin-bottom: 15px;
}
h2 {
color: var(--primary-color);
margin-top: 20px;
border-bottom: 2px solid var(--primary-color);
padding-bottom: 8px;
display: inline-block;
}
h3 {
margin-top: 20px;
color: #34495e;
}
.docs-link {
color: var(--primary-color);
text-decoration: none;
font-weight: bold;
transition: all 0.2s ease;
}
.docs-link:hover {
text-decoration: underline;
color: var(--primary-dark);
}
.api-tester {
background-color: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 25px;
margin-top: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
}
.form-group {
margin-bottom: 18px;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 600;
color: #34495e;
}
input,
select,
textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
box-sizing: border-box;
font-size: 14px;
transition: all 0.3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
input::placeholder,
textarea::placeholder {
color: #aaa;
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 12px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
display: inline-flex;
align-items: center;
justify-content: center;
}
button:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
button:active {
transform: translateY(0);
}
.result-container {
margin-top: 20px;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
background-color: #f9f9f9;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo,
monospace;
font-size: 14px;
line-height: 1.5;
position: relative;
}
.request-url {
background-color: #f0f0f0;
padding: 10px;
border-radius: 6px;
margin-bottom: 10px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo,
monospace;
font-size: 14px;
word-break: break-all;
border: 1px dashed #ccc;
display: flex;
justify-content: space-between;
align-items: center;
}
.url-text {
flex-grow: 1;
margin-right: 10px;
}
.copy-url-button {
background-color: #9b59b6;
padding: 6px 12px;
font-size: 12px;
white-space: nowrap;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.loading::after {
content: "...";
animation: dots 1.5s steps(5, end) infinite;
}
@keyframes dots {
0%,
20% {
content: ".";
} }
40% {
content: "..";
}
60% {
content: "...";
}
80%,
100% {
content: "";
}
}
.tab-container {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #e0e0e0;
}
.tab {
padding: 12px 20px;
cursor: pointer;
margin-right: 5px;
border-radius: 6px 6px 0 0;
font-weight: 500;
transition: all 0.2s;
border: 1px solid transparent;
border-bottom: none;
position: relative;
bottom: -1px;
}
.tab:hover {
background-color: #f0f8ff;
}
.tab.active {
background-color: var(--bg-white);
border-color: #e0e0e0;
color: var(--primary-color);
border-bottom: 1px solid white;
}
.tab-content {
display: none;
padding: 20px 0;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.button-group {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.copy-button {
background-color: var(--success-color);
padding: 8px 15px;
font-size: 14px;
margin-left: 10px;
}
.copy-button:hover {
background-color: var(--success-dark);
}
.error-message {
color: var(--error-color);
font-weight: bold;
margin-top: 10px;
padding: 10px;
border-radius: 4px;
background-color: rgba(231, 76, 60, 0.1);
border-left: 4px solid var(--error-color);
display: none;
}
.api-description {
margin-bottom: 15px;
color: #555;
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin-left: 8px;
background-color: rgba(52, 152, 219, 0.1);
color: var(--primary-color);
}
.optional-param {
font-size: 12px;
color: #777;
margin-left: 5px;
font-style: italic;
}
.required-field {
color: var(--error-color);
font-weight: bold;
}
</style> </style>
</head> </head>
<body> <body>
<div id="paragraphContainer"> <div class="container">
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <div class="welcome-text">
('-. .-. ('-. .-') _ <h1>🎉 恭喜Chatlog 服务已成功启动</h1>
( OO ) / ( OO ).-. ( OO) ) <p>
.-----. ,--. ,--. / . --. / / '._ ,--. .-'),-----. ,----. Chatlog 是一个帮助你轻松使用自己聊天数据的工具,现在你可以通过 HTTP
' .--./ | | | | | \-. \ |'--...__) | |.-') ( OO' .-. ' ' .-./-') API 访问你的聊天记录、联系人和群聊信息。
| |('-. | .| | .-'-' | | '--. .--' | | OO ) / | | | | | |_( O- ) </p>
/_) |OO ) | | \| |_.' | | | | |`-' | \_) | |\| | | | .--, \ </div>
|| |`-'| | .-. | | .-. | | | (| '---.' \ | | | |(| | '. (_/
(_' '--'\ | | | | | | | | | | | | `' '-' ' | '--' | <div class="api-section">
`-----' `--' `--' `--' `--' `--' `------' `-----' `------' <h2>🔍 API 接口与调试</h2>
</pre>
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <div class="api-tester">
_____ _ _ _ <div class="tab-container">
/ __ \| | | | | | <div class="tab active" data-tab="session">最近会话</div>
| / \/| |__ __ _ | |_ | | ___ __ _ <div class="tab" data-tab="chatroom">群聊</div>
| | | '_ \ / _` || __|| | / _ \ / _` | <div class="tab" data-tab="contact">联系人</div>
| \__/\| | | || (_| || |_ | || (_) || (_| | <div class="tab" data-tab="chatlog">聊天记录</div>
\____/|_| |_| \__,_| \__||_| \___/ \__, | </div>
__/ |
|___/ <!-- 会话查询表单 -->
</pre> <div class="tab-content active" id="session-tab">
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <div class="api-description">
<p>
,-----. ,--. ,--. ,--. 查询最近会话列表。<span class="badge">GET /api/v1/session</span>
' .--./ | ,---. ,--,--. ,-' '-. | | ,---. ,---. </p>
| | | .-. | ' ,-. | '-. .-' | | | .-. | | .-. | </div>
' '--'\ | | | | \ '-' | | | | | ' '-' ' ' '-' ' <div class="form-group">
`-----' `--' `--' `--`--' `--' `--' `---' .`- / <label for="session-format"
`---' >输出格式:<span class="optional-param">可选</span></label
</pre> >
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <select id="session-format">
____ _ _ _ <option value="">默认</option>
/ ___| | |__ __ _ | |_ | | ___ __ _ <option value="json">JSON</option>
| | | '_ \ / _` | | __| | | / _ \ / _` | <option value="text">纯文本</option>
| |___ | | | | | (_| | | |_ | | | (_) | | (_| | </select>
\____| |_| |_| \__,_| \__| |_| \___/ \__, | </div>
|___/ </div>
</pre>
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <!-- 群聊查询表单 -->
_____ _____ _____ _____ _____ _______ _____ <div class="tab-content" id="chatroom-tab">
/\ \ /\ \ /\ \ /\ \ /\ \ /::\ \ /\ \ <div class="api-description">
/::\ \ /::\____\ /::\ \ /::\ \ /::\____\ /::::\ \ /::\ \ <p>
/::::\ \ /:::/ / /::::\ \ \:::\ \ /:::/ / /::::::\ \ /::::\ \ 查询群聊列表,可选择性地按关键词搜索。<span class="badge"
/::::::\ \ /:::/ / /::::::\ \ \:::\ \ /:::/ / /::::::::\ \ /::::::\ \ >GET /api/v1/chatroom</span
/:::/\:::\ \ /:::/ / /:::/\:::\ \ \:::\ \ /:::/ / /:::/~~\:::\ \ /:::/\:::\ \ >
/:::/ \:::\ \ /:::/____/ /:::/__\:::\ \ \:::\ \ /:::/ / /:::/ \:::\ \ /:::/ \:::\ \ </p>
/:::/ \:::\ \ /::::\ \ /::::\ \:::\ \ /::::\ \ /:::/ / /:::/ / \:::\ \ /:::/ \:::\ \ </div>
/:::/ / \:::\ \ /::::::\ \ _____ /::::::\ \:::\ \ /::::::\ \ /:::/ / /:::/____/ \:::\____\ /:::/ / \:::\ \ <div class="form-group">
/:::/ / \:::\ \ /:::/\:::\ \ /\ \ /:::/\:::\ \:::\ \ /:::/\:::\ \ /:::/ / |:::| | |:::| | /:::/ / \:::\ ___\ <label for="chatroom-query"
/:::/____/ \:::\____\/:::/ \:::\ /::\____\/:::/ \:::\ \:::\____\ /:::/ \:::\____\/:::/____/ |:::|____| |:::| |/:::/____/ ___\:::| | >搜索群聊:<span class="optional-param">可选</span></label
\:::\ \ \::/ /\::/ \:::\ /:::/ /\::/ \:::\ /:::/ / /:::/ \::/ /\:::\ \ \:::\ \ /:::/ / \:::\ \ /\ /:::|____| >
\:::\ \ \/____/ \/____/ \:::\/:::/ / \/____/ \:::\/:::/ / /:::/ / \/____/ \:::\ \ \:::\ \ /:::/ / \:::\ /::\ \::/ / <input
\:::\ \ \::::::/ / \::::::/ / /:::/ / \:::\ \ \:::\ /:::/ / \:::\ \:::\ \/____/ type="text"
\:::\ \ \::::/ / \::::/ / /:::/ / \:::\ \ \:::\__/:::/ / \:::\ \:::\____\ id="chatroom-query"
\:::\ \ /:::/ / /:::/ / \::/ / \:::\ \ \::::::::/ / \:::\ /:::/ / placeholder="输入关键词搜索群聊"
\:::\ \ /:::/ / /:::/ / \/____/ \:::\ \ \::::::/ / \:::\/:::/ / />
\:::\ \ /:::/ / /:::/ / \:::\ \ \::::/ / \::::::/ / </div>
\:::\____\ /:::/ / /:::/ / \:::\____\ \::/____/ \::::/ / <div class="form-group">
\::/ / \::/ / \::/ / \::/ / ~~ \::/____/ <label for="chatroom-format"
\/____/ \/____/ \/____/ \/____/ >输出格式:<span class="optional-param">可选</span></label
>
</pre> <select id="chatroom-format">
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <option value="">默认</option>
___ _ _ __ ____ __ _____ ___ <option value="json">JSON</option>
/ __)( )_( ) /__\ (_ _)( ) ( _ ) / __) <option value="text">纯文本</option>
( (__ ) _ ( /(__)\ )( )(__ )(_)( ( (_-. </select>
\___)(_) (_)(__)(__) (__) (____)(_____) \___/ </div>
</pre> </div>
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
________ ___ ___ ________ _________ ___ ________ ________ <!-- 联系人查询表单 -->
|\ ____\ |\ \|\ \ |\ __ \ |\___ ___\ |\ \ |\ __ \ |\ ____\ <div class="tab-content" id="contact-tab">
\ \ \___| \ \ \\\ \ \ \ \|\ \ \|___ \ \_| \ \ \ \ \ \|\ \ \ \ \___| <div class="api-description">
\ \ \ \ \ __ \ \ \ __ \ \ \ \ \ \ \ \ \ \\\ \ \ \ \ ___ <p>
\ \ \____ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \____ \ \ \\\ \ \ \ \|\ \ 查询联系人列表,可选择性地按关键词搜索。<span class="badge"
\ \_______\ \ \__\ \__\ \ \__\ \__\ \ \__\ \ \_______\ \ \_______\ \ \_______\ >GET /api/v1/contact</span
\|_______| \|__|\|__| \|__|\|__| \|__| \|_______| \|_______| \|_______| >
</pre> </p>
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> </div>
╔═╗┬ ┬┌─┐┌┬┐┬ ┌─┐┌─┐ <div class="form-group">
├─┤├─┤ │ │ │ ││ ┬ <label for="contact-query"
╚═╝┴ ┴┴ ┴ ┴ ┴─┘└─┘└─┘ >搜索联系人:<span class="optional-param">可选</span></label
</pre> >
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <input
▄▄· ▄ .▄ ▄▄▄· ▄▄▄▄▄▄▄▌ ▄▄ • type="text"
▐█ ▌▪██▪▐█▐█ ▀█ •██ ██• ▪ ▐█ ▀ ▪ id="contact-query"
██ ▄▄██▀▐█▄█▀▀█ ▐█.▪██▪ ▄█▀▄ ▄█ ▀█▄ placeholder="输入关键词搜索联系人"
▐███▌██▌▐▀▐█ ▪▐▌ ▐█▌·▐█▌▐▌▐█▌.▐▌▐█▄▪▐█ />
·▀▀▀ ▀▀▀ · ▀ ▀ ▀▀▀ .▀▀▀ ▀█▄▀▪·▀▀▀▀ </div>
</pre> <div class="form-group">
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <label for="contact-format"
,. - ., .·¨'`; ,.·´¨;\ ,., ' , . ., ° ,. ' , ·. ,.-·~·., ,.-·^*ª'` ·, >输出格式:<span class="optional-param">可选</span></label
,·'´ ,. - , ';\ '; ;'\ '; ;::\ ;´ '· ., ;'´ , ., _';\' / ';\ / ·'´,.-·-., `,' .·´ ,·'´:¯'`·, '\ >
,·´ .'´\:::::;' ;:'\ ' ; ;::'\ ,' ;::'; .´ .-, ';\ \:´¨¯:;' `;::'\:'\ ,' ,'::'\ / .'´\:::::::'\ '\ ° ,´ ,'\:::::::::\,.·\' <select id="contact-format">
/ ,'´::::'\;:-/ ,' ::; ' ; ;::_';,. ,.' ;:::';° / /:\:'; ;:'\' \::::; ,'::_'\;' ,' ;:::';' ,·' ,'::::\:;:-·-:'; ';\ / /:::\;·'´¯'`·;\:::\° <option value="">默认</option>
,' ;':::::;'´ '; /\::;' ' .' ,. -·~-·, ;:::'; ' ,' ,'::::'\'; ;::'; ,' ,'::;' '; ,':::;' ;. ';:::;´ ,' ,':'\ ; ;:::;' '\;:·´ <option value="json">JSON</option>
; ;:::::; '\*'´\::\' ° '; ;'\::::::::; '/::::; ,.-·' '·~^*'´¨, ';::; ; ;:::; ° ; ,':::;' ' '; ;::; ,'´ .'´\::'; '; ;::/ ,·´¯'; ° <option value="text">纯文本</option>
'; ';::::'; '\::'\/.' ; ';:;\;::-··; ;::::; ':, ,·:²*´¨¯'`; ;::'; ; ;::;' ,' ,'::;' '; ':;: ,.·´,.·´::::\;'° '; '·;' ,.·´, ;'\ </select>
\ '·:;:'_ ,. -·'´.·´\ ':,.·´\;' ;' ,' :::/ ' ,' / \::::::::'; ;::'; ; ;::;' ; ';_:,.-·´';\ \·, `*´,.·'´::::::;·´ \'·. `'´,.·:´'; ;::\' </div>
'\:` · .,. -·:´::::::\' \:::::\ \·.'::::; ,' ,'::::\·²*'´¨¯':,'\:; ',.'\::;' ', _,.-·'´:\:\ \\:¯::\:::::::;:·´ '\::\¯::::::::'; ;::'; </div>
\:::::::\:::::::;:·'´' \;:·´ \:\::'; \`¨\:::/ \::\' \::\:;' \¨:::::::::::\'; `\:::::\;::·'´ ° `·:\:::;:·´';.·´\::;'
`· :;::\;::-·´ `·\;' '\::\;' '\;' ' \;:' '\;::_;:-·'´ ¯ ¯ \::::\;' <!-- 聊天记录查询表单 -->
' `¨' ° '¨ '\:·´' <div class="tab-content" id="chatlog-tab">
</pre> <div class="api-description">
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> <p>
▄████▄ ██░ ██ ▄▄▄ ▄▄▄█████▓ ██▓ ▒█████ ▄████ 查询指定时间范围内与特定联系人或群聊的聊天记录。<span
▒██▀ ▀█ ▓██░ ██▒▒████▄ ▓ ██▒ ▓▒▓██▒ ▒██▒ ██▒ ██▒ ▀█▒ class="badge"
▒▓█ ▄ ▒██▀▀██░▒██ ▀█▄ ▒ ▓██░ ▒░▒██░ ▒██░ ██▒▒██░▄▄▄░ >GET /api/v1/chatlog</span
▒▓▓▄ ▄██▒░▓█ ░██ ░██▄▄▄▄██ ░ ▓██▓ ░ ▒██░ ▒██ ██░░▓█ ██▓ >
▒ ▓███▀ ░░▓█▒░██▓ ▓█ ▓██▒ ▒██▒ ░ ░██████▒░ ████▓▒░░▒▓███▀▒ </p>
░ ░▒ ▒ ░ ▒ ░░▒░▒ ▒▒ ▓▒█░ ▒ ░░ ░ ▒░▓ ░░ ▒░▒░▒░ ░▒ ▒ </div>
░ ▒ ▒ ░▒░ ░ ▒ ▒▒ ░ ░ ░ ░ ▒ ░ ░ ▒ ▒░ ░ ░ <div class="form-group">
░ ░ ░░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ <label for="time"
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ >时间范围:<span class="required-field">*</span></label
>
</pre> <input
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph"> type="text"
▄█▄ ▄ █ ██ ▄▄▄▄▀ █ ████▄ ▄▀ id="time"
█▀ ▀▄ █ █ █ ▀▀▀ █ █ █ █ ▄▀ placeholder="例如2023-01-01 或 2023-01-01~2023-01-31"
▀ ██▀▀█ █▄▄█ █ █ █ █ █ ▀▄ />
█▄ ▄▀ █ █ █ █ █ ███▄ ▀████ █ █ </div>
▀███▀ █ █ ▀ ▀ ███ <div class="form-group">
▀ █ <label for="talker"
>聊天对象:<span class="required-field">*</span></label
</pre> >
<input
type="text"
id="talker"
placeholder="wxid、群ID、备注名或昵称"
/>
</div>
<div class="form-group">
<label for="limit"
>返回数量:<span class="optional-param">可选</span></label
>
<input type="number" id="limit" placeholder="默认不做限制" />
</div>
<div class="form-group">
<label for="offset"
>偏移量:<span class="optional-param">可选</span></label
>
<input type="number" id="offset" placeholder="默认 0" />
</div>
<div class="form-group">
<label for="format"
>输出格式:<span class="optional-param">可选</span></label
>
<select id="format">
<option value="">默认</option>
<option value="text">纯文本</option>
<option value="json">JSON</option>
<option value="csv">CSV</option>
</select>
</div>
</div>
<button id="test-api">执行查询</button>
<div id="result-wrapper" style="display: none; margin-top: 20px">
<div class="request-url" id="request-url-container">
<span class="url-text" id="request-url"></span>
<button class="copy-button copy-url-button" id="copy-url">
复制请求URL
</button>
</div>
<div class="result-container" id="api-result">
<p>查询结果将显示在这里...</p>
</div>
<div class="button-group">
<button class="copy-button" id="copy-result">复制结果</button>
</div>
</div>
<div class="error-message" id="error-message"></div>
</div>
</div>
<div class="api-section">
<h2>🤖 MCP 集成</h2>
<p>
Chatlog 支持 MCP (Model Context Protocol) SSE 协议,可与支持 MCP 的 AI
助手无缝集成。
</p>
<p>SSE 端点:<strong>/sse</strong></p>
<p>
详细集成指南请参考
<a
href="https://github.com/sjzar/chatlog/blob/main/docs/mcp.md"
class="docs-link"
target="_blank"
>MCP 集成指南</a
>
</p>
</div>
<div class="api-section">
<h2>📚 更多资源</h2>
<p>
查看
<a
href="https://github.com/sjzar/chatlog"
class="docs-link"
target="_blank"
>GitHub 项目</a
>
获取完整文档和使用指南。
</p>
<p>
如果你有任何问题或建议,欢迎通过
<a
href="https://github.com/sjzar/chatlog/discussions"
class="docs-link"
target="_blank"
>Discussions</a
>
进行交流。
</p>
</div>
</div> </div>
<script> <script>
window.onload = function() { // 标签切换功能
showRandomParagraph(); document.querySelectorAll(".tab").forEach((tab) => {
}; tab.addEventListener("click", function () {
function showRandomParagraph() { // 移除所有标签的活动状态
const paragraphs = document.getElementsByClassName("random-paragraph"); document
for (let i = 0; i < paragraphs.length; i++) { .querySelectorAll(".tab")
paragraphs[i].style.display = "none"; .forEach((t) => t.classList.remove("active"));
// 设置当前标签为活动状态
this.classList.add("active");
// 隐藏所有内容区域
document.querySelectorAll(".tab-content").forEach((content) => {
content.classList.remove("active");
});
// 显示当前标签对应的内容
const tabId = this.getAttribute("data-tab") + "-tab";
document.getElementById(tabId).classList.add("active");
// 清空结果区域
document.getElementById("result-wrapper").style.display = "none";
document.getElementById("api-result").innerHTML =
"<p>查询结果将显示在这里...</p>";
document.getElementById("request-url").textContent = "";
document.getElementById("error-message").style.display = "none";
document.getElementById("error-message").textContent = "";
});
});
// API 测试功能
document
.getElementById("test-api")
.addEventListener("click", async function () {
const resultContainer = document.getElementById("api-result");
const requestUrlContainer = document.getElementById("request-url");
const errorMessage = document.getElementById("error-message");
const resultWrapper = document.getElementById("result-wrapper");
errorMessage.style.display = "none";
errorMessage.textContent = "";
try {
// 获取当前活动的标签
const activeTab = document
.querySelector(".tab.active")
.getAttribute("data-tab");
let url = "/api/v1/";
let params = new URLSearchParams();
// 根据不同的标签构建不同的请求
switch (activeTab) {
case "chatlog":
url += "chatlog";
const time = document.getElementById("time").value;
const talker = document.getElementById("talker").value;
const limit = document.getElementById("limit").value;
const offset = document.getElementById("offset").value;
const format = document.getElementById("format").value;
// 验证必填项
if (!time || !talker) {
errorMessage.textContent =
"错误: 时间范围和聊天对象为必填项!";
errorMessage.style.display = "block";
return;
}
if (time) params.append("time", time);
if (talker) params.append("talker", talker);
if (limit) params.append("limit", limit);
if (offset) params.append("offset", offset);
if (format) params.append("format", format);
break;
case "contact":
url += "contact";
const contactQuery =
document.getElementById("contact-query").value;
const contactFormat =
document.getElementById("contact-format").value;
if (contactQuery) params.append("query", contactQuery);
if (contactFormat) params.append("format", contactFormat);
break;
case "chatroom":
url += "chatroom";
const chatroomQuery =
document.getElementById("chatroom-query").value;
const chatroomFormat =
document.getElementById("chatroom-format").value;
if (chatroomQuery) params.append("query", chatroomQuery);
if (chatroomFormat) params.append("format", chatroomFormat);
break;
case "session":
url += "session";
const sessionFormat =
document.getElementById("session-format").value;
if (sessionFormat) params.append("format", sessionFormat);
break;
} }
const randomIndex = Math.floor(Math.random() * paragraphs.length);
paragraphs[randomIndex].style.display = "block"; // 添加参数到URL
} const apiUrl = params.toString()
? `${url}?${params.toString()}`
: url;
// 获取完整URL包含域名部分
const fullUrl = window.location.origin + apiUrl;
// 显示完整请求URL
requestUrlContainer.textContent = fullUrl;
resultWrapper.style.display = "block";
// 显示加载中
resultContainer.innerHTML = '<div class="loading">加载中</div>';
// 发送请求
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// 获取响应内容类型
const contentType = response.headers.get("content-type");
let result;
if (contentType && contentType.includes("application/json")) {
// 如果是JSON格式化显示
result = await response.json();
resultContainer.innerHTML = JSON.stringify(result, null, 2);
} else {
// 其他格式直接显示文本
result = await response.text();
resultContainer.innerHTML = result;
}
} catch (error) {
resultContainer.innerHTML = "";
errorMessage.textContent = `查询出错: ${error.message}`;
errorMessage.style.display = "block";
console.error("API查询出错:", error);
}
});
// 复制结果功能
document
.getElementById("copy-result")
.addEventListener("click", function () {
const resultText = document.getElementById("api-result").innerText;
copyToClipboard(resultText, this, "已复制结果!");
});
// 复制URL功能
document
.getElementById("copy-url")
.addEventListener("click", function () {
// 获取完整URL包含域名部分
const urlText = document.getElementById("request-url").innerText;
copyToClipboard(urlText, this, "已复制URL!");
});
// 通用复制功能
function copyToClipboard(text, button, successMessage) {
navigator.clipboard
.writeText(text)
.then(() => {
const originalText = button.textContent;
button.textContent = successMessage;
setTimeout(() => {
button.textContent = originalText;
}, 2000);
})
.catch((err) => {
console.error("复制失败:", err);
});
}
</script> </script>
</body> </body>
</html> </html>

View File

@@ -306,3 +306,44 @@ func (m *Manager) CommandDecrypt(dataDir string, workDir string, key string, pla
return nil return nil
} }
func (m *Manager) CommandHTTPServer(addr string, dataDir string, workDir string, platform string, version int) error {
if addr == "" {
addr = "127.0.0.1:5030"
}
if workDir == "" {
return fmt.Errorf("workDir is required")
}
if platform == "" {
return fmt.Errorf("platform is required")
}
if version == 0 {
return fmt.Errorf("version is required")
}
m.ctx.HTTPAddr = addr
m.ctx.DataDir = dataDir
m.ctx.WorkDir = workDir
m.ctx.Platform = platform
m.ctx.Version = version
// 如果是 4.0 版本,更新下 xorkey
if m.ctx.Version == 4 && m.ctx.DataDir != "" {
go dat2img.ScanAndSetXorKey(m.ctx.DataDir)
}
// 按依赖顺序启动服务
if err := m.db.Start(); err != nil {
return err
}
if err := m.mcp.Start(); err != nil {
return err
}
return m.http.ListenAndServe()
}

View File

@@ -22,7 +22,7 @@ const (
var V3KeyPatterns = []KeyPatternInfo{ var V3KeyPatterns = []KeyPatternInfo{
{ {
Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32}, Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32},
Offset: 24, Offsets: []int{24},
}, },
} }
@@ -122,16 +122,73 @@ func (e *V3Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
return err return err
} }
log.Debug().Msgf("Read memory region, size: %d bytes", len(memory)) totalSize := len(memory)
log.Debug().Msgf("Read memory region, size: %d bytes", totalSize)
// Send memory data to channel for processing // If memory is small enough, process it as a single chunk
select { if totalSize <= MinChunkSize {
case memoryChannel <- memory: select {
log.Debug().Msg("Memory region sent for analysis") case memoryChannel <- memory:
case <-ctx.Done(): log.Debug().Msg("Memory sent as a single chunk for analysis")
return ctx.Err() case <-ctx.Done():
return ctx.Err()
}
return nil
} }
chunkCount := MaxWorkers * ChunkMultiplier
// Calculate chunk size based on fixed chunk count
chunkSize := totalSize / chunkCount
if chunkSize < MinChunkSize {
// Reduce number of chunks if each would be too small
chunkCount = totalSize / MinChunkSize
if chunkCount == 0 {
chunkCount = 1
}
chunkSize = totalSize / chunkCount
}
// Process memory in chunks from end to beginning
for i := chunkCount - 1; i >= 0; i-- {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Calculate start and end positions for this chunk
start := i * chunkSize
end := (i + 1) * chunkSize
// Ensure the last chunk includes all remaining memory
if i == chunkCount-1 {
end = totalSize
}
// Add overlap area to catch patterns at chunk boundaries
if i > 0 {
start -= ChunkOverlapBytes
if start < 0 {
start = 0
}
}
chunk := memory[start:end]
log.Debug().
Int("chunk_index", i+1).
Int("total_chunks", chunkCount).
Int("chunk_size", len(chunk)).
Int("start_offset", start).
Int("end_offset", end).
Msg("Processing memory chunk")
select {
case memoryChannel <- chunk:
case <-ctx.Done():
return ctx.Err()
}
}
}
return nil return nil
} }
@@ -173,24 +230,26 @@ func (e *V3Extractor) SearchKey(ctx context.Context, memory []byte) (string, boo
break // No more matches found break // No more matches found
} }
// Check if we have enough space for the key // Try each offset for this pattern
keyOffset := index + keyPattern.Offset for _, offset := range keyPattern.Offsets {
if keyOffset < 0 || keyOffset+32 > len(memory) { // Check if we have enough space for the key
index -= 1 keyOffset := index + offset
continue if keyOffset < 0 || keyOffset+32 > len(memory) {
} continue
}
// Extract the key data, which is 32 bytes long // Extract the key data, which is at the offset position and 32 bytes long
keyData := memory[keyOffset : keyOffset+32] keyData := memory[keyOffset : keyOffset+32]
// Validate key against database header // Validate key against database header
if e.validator.Validate(keyData) { if e.validator.Validate(keyData) {
log.Debug(). log.Debug().
Str("pattern", hex.EncodeToString(keyPattern.Pattern)). Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
Int("offset", keyPattern.Offset). Int("offset", offset).
Str("key", hex.EncodeToString(keyData)). Str("key", hex.EncodeToString(keyData)).
Msg("Key found") Msg("Key found")
return hex.EncodeToString(keyData), true return hex.EncodeToString(keyData), true
}
} }
index -= 1 index -= 1

View File

@@ -16,17 +16,16 @@ import (
) )
const ( const (
MaxWorkers = 8 MaxWorkers = 8
MinChunkSize = 1 * 1024 * 1024 // 1MB
ChunkOverlapBytes = 1024 // Greater than all offsets
ChunkMultiplier = 2 // Number of chunks = MaxWorkers * ChunkMultiplier
) )
var V4KeyPatterns = []KeyPatternInfo{ var V4KeyPatterns = []KeyPatternInfo{
{ {
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00}, Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
Offset: 16, Offsets: []int{16, -80, 64},
},
{
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
Offset: -80,
}, },
} }
@@ -126,14 +125,72 @@ func (e *V4Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
return err return err
} }
log.Debug().Msgf("Read memory region, size: %d bytes", len(memory)) totalSize := len(memory)
log.Debug().Msgf("Read memory region, size: %d bytes", totalSize)
// Send memory data to channel for processing // If memory is small enough, process it as a single chunk
select { if totalSize <= MinChunkSize {
case memoryChannel <- memory: select {
log.Debug().Msg("Memory region sent for analysis") case memoryChannel <- memory:
case <-ctx.Done(): log.Debug().Msg("Memory sent as a single chunk for analysis")
return ctx.Err() case <-ctx.Done():
return ctx.Err()
}
return nil
}
chunkCount := MaxWorkers * ChunkMultiplier
// Calculate chunk size based on fixed chunk count
chunkSize := totalSize / chunkCount
if chunkSize < MinChunkSize {
// Reduce number of chunks if each would be too small
chunkCount = totalSize / MinChunkSize
if chunkCount == 0 {
chunkCount = 1
}
chunkSize = totalSize / chunkCount
}
// Process memory in chunks from end to beginning
for i := chunkCount - 1; i >= 0; i-- {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Calculate start and end positions for this chunk
start := i * chunkSize
end := (i + 1) * chunkSize
// Ensure the last chunk includes all remaining memory
if i == chunkCount-1 {
end = totalSize
}
// Add overlap area to catch patterns at chunk boundaries
if i > 0 {
start -= ChunkOverlapBytes
if start < 0 {
start = 0
}
}
chunk := memory[start:end]
log.Debug().
Int("chunk_index", i+1).
Int("total_chunks", chunkCount).
Int("chunk_size", len(chunk)).
Int("start_offset", start).
Int("end_offset", end).
Msg("Processing memory chunk")
select {
case memoryChannel <- chunk:
case <-ctx.Done():
return ctx.Err()
}
}
} }
return nil return nil
@@ -177,24 +234,26 @@ func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, boo
break // No more matches found break // No more matches found
} }
// Check if we have enough space for the key // Try each offset for this pattern
keyOffset := index + keyPattern.Offset for _, offset := range keyPattern.Offsets {
if keyOffset < 0 || keyOffset+32 > len(memory) { // Check if we have enough space for the key
index -= 1 keyOffset := index + offset
continue if keyOffset < 0 || keyOffset+32 > len(memory) {
} continue
}
// Extract the key data, which is 16 bytes after the pattern and 32 bytes long // Extract the key data, which is at the offset position and 32 bytes long
keyData := memory[keyOffset : keyOffset+32] keyData := memory[keyOffset : keyOffset+32]
// Validate key against database header // Validate key against database header
if e.validator.Validate(keyData) { if keyData, ok := e.validate(ctx, keyData); ok {
log.Debug(). log.Debug().
Str("pattern", hex.EncodeToString(keyPattern.Pattern)). Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
Int("offset", keyPattern.Offset). Int("offset", offset).
Str("key", hex.EncodeToString(keyData)). Str("key", hex.EncodeToString(keyData)).
Msg("Key found") Msg("Key found")
return hex.EncodeToString(keyData), true return hex.EncodeToString(keyData), true
}
} }
index -= 1 index -= 1
@@ -204,11 +263,19 @@ func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, boo
return "", false return "", false
} }
func (e *V4Extractor) validate(ctx context.Context, keyDate []byte) ([]byte, bool) {
if e.validator.Validate(keyDate) {
return keyDate, true
}
// Try to find a valid key by ***
return nil, false
}
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) { func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator e.validator = validator
} }
type KeyPatternInfo struct { type KeyPatternInfo struct {
Pattern []byte Pattern []byte
Offset int Offsets []int
} }

View File

@@ -13,8 +13,8 @@ import (
const ( const (
V3ProcessName = "WeChat" V3ProcessName = "WeChat"
V4ProcessName = "Weixin" V4ProcessName = "Weixin"
V3DBFile = "Msg\\Misc.db" V3DBFile = `Msg\Misc.db`
V4DBFile = "db_storage\\message\\message_0.db" V4DBFile = `db_storage\session\session.db`
) )
// Detector 实现 Windows 平台的进程检测器 // Detector 实现 Windows 平台的进程检测器

View File

@@ -25,7 +25,7 @@ const (
Media = "media" Media = "media"
) )
var Groups = []dbm.Group{ var Groups = []*dbm.Group{
{ {
Name: Message, Name: Message,
Pattern: `^msg_([0-9]?[0-9])?\.db$`, Pattern: `^msg_([0-9]?[0-9])?\.db$`,
@@ -114,6 +114,10 @@ func (ds *DataSource) initMessageDbs() error {
dbPaths, err := ds.dbm.GetDBPath(Message) dbPaths, err := ds.dbm.GetDBPath(Message)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "db file not found") {
ds.talkerDBMap = make(map[string]string)
return nil
}
return err return err
} }
// 处理每个数据库文件 // 处理每个数据库文件
@@ -155,6 +159,10 @@ func (ds *DataSource) initMessageDbs() error {
func (ds *DataSource) initChatRoomDb() error { func (ds *DataSource) initChatRoomDb() error {
db, err := ds.dbm.GetDB(ChatRoom) db, err := ds.dbm.GetDB(ChatRoom)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "db file not found") {
ds.user2DisplayName = make(map[string]string)
return nil
}
return err return err
} }

View File

@@ -34,7 +34,7 @@ func NewDBManager(path string) *DBManager {
} }
} }
func (d *DBManager) AddGroup(g Group) error { func (d *DBManager) AddGroup(g *Group) error {
fg, err := filemonitor.NewFileGroup(g.Name, d.path, g.Pattern, g.BlackList) fg, err := filemonitor.NewFileGroup(g.Name, d.path, g.Pattern, g.BlackList)
if err != nil { if err != nil {
return err return err

View File

@@ -9,7 +9,7 @@ import (
func TestXxx(t *testing.T) { func TestXxx(t *testing.T) {
path := "/Users/sarv/Documents/chatlog/bigjun_9e7a" path := "/Users/sarv/Documents/chatlog/bigjun_9e7a"
g := Group{ g := &Group{
Name: "session", Name: "session",
Pattern: `session\.db$`, Pattern: `session\.db$`,
BlackList: []string{}, BlackList: []string{},

View File

@@ -27,7 +27,7 @@ const (
Voice = "voice" Voice = "voice"
) )
var Groups = []dbm.Group{ var Groups = []*dbm.Group{
{ {
Name: Message, Name: Message,
Pattern: `^message_([0-9]?[0-9])?\.db$`, Pattern: `^message_([0-9]?[0-9])?\.db$`,
@@ -113,6 +113,10 @@ func (ds *DataSource) SetCallback(name string, callback func(event fsnotify.Even
func (ds *DataSource) initMessageDbs() error { func (ds *DataSource) initMessageDbs() error {
dbPaths, err := ds.dbm.GetDBPath(Message) dbPaths, err := ds.dbm.GetDBPath(Message)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "db file not found") {
ds.messageInfos = make([]MessageDBInfo, 0)
return nil
}
return err return err
} }

View File

@@ -27,7 +27,7 @@ const (
Voice = "voice" Voice = "voice"
) )
var Groups = []dbm.Group{ var Groups = []*dbm.Group{
{ {
Name: Message, Name: Message,
Pattern: `^MSG([0-9]?[0-9])?\.db$`, Pattern: `^MSG([0-9]?[0-9])?\.db$`,
@@ -35,7 +35,7 @@ var Groups = []dbm.Group{
}, },
{ {
Name: Contact, Name: Contact,
Pattern: `^MicroMsg.db$`, Pattern: `^MicroMsg\.db$`,
BlackList: []string{}, BlackList: []string{},
}, },
{ {
@@ -122,6 +122,10 @@ func (ds *DataSource) initMessageDbs() error {
// 获取所有消息数据库文件路径 // 获取所有消息数据库文件路径
dbPaths, err := ds.dbm.GetDBPath(Message) dbPaths, err := ds.dbm.GetDBPath(Message)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "db file not found") {
ds.messageInfos = make([]MessageDBInfo, 0)
return nil
}
return err return err
} }