mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
Merge pull request #463 from xunchahaha/dev
修复api导出即使选择了优先不生效的问题 新增了可以查看群内成员wxid等信息的接口 https://github.com/hicccc77/WeFlow/issues/461
This commit is contained in:
581
docs/HTTP-API.md
581
docs/HTTP-API.md
@@ -1,33 +1,43 @@
|
|||||||
# WeFlow HTTP API 接口文档
|
# WeFlow HTTP API 文档
|
||||||
|
|
||||||
WeFlow 提供 HTTP API 服务,支持通过 HTTP 接口查询消息数据,支持 [ChatLab](https://github.com/nichuanfang/chatlab-format) 标准化格式输出。
|
WeFlow 提供本地 HTTP API,便于外部脚本或工具读取聊天记录、会话、联系人、群成员和导出的媒体文件。
|
||||||
|
|
||||||
## 启用 API 服务
|
## 启用方式
|
||||||
|
|
||||||
在设置页面 → API 服务 → 点击「启动服务」按钮。
|
在应用设置页启用 `API 服务`。
|
||||||
|
|
||||||
默认端口:`5031`
|
- 默认监听地址:`127.0.0.1`
|
||||||
|
- 默认端口:`5031`
|
||||||
## 基础地址
|
- 基础地址:`http://127.0.0.1:5031`
|
||||||
|
|
||||||
```
|
|
||||||
http://127.0.0.1:5031
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 接口列表
|
## 接口列表
|
||||||
|
|
||||||
### 1. 健康检查
|
- `GET /health`
|
||||||
|
- `GET /api/v1/health`
|
||||||
|
- `GET /api/v1/messages`
|
||||||
|
- `GET /api/v1/sessions`
|
||||||
|
- `GET /api/v1/contacts`
|
||||||
|
- `GET /api/v1/group-members`
|
||||||
|
- `GET /api/v1/media/*`
|
||||||
|
|
||||||
检查 API 服务是否正常运行。
|
---
|
||||||
|
|
||||||
|
## 1. 健康检查
|
||||||
|
|
||||||
**请求**
|
**请求**
|
||||||
```
|
|
||||||
|
```http
|
||||||
GET /health
|
GET /health
|
||||||
```
|
```
|
||||||
|
|
||||||
|
或
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
**响应**
|
**响应**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "ok"
|
"status": "ok"
|
||||||
@@ -36,211 +46,180 @@ GET /health
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. 获取消息列表
|
## 2. 获取消息
|
||||||
|
|
||||||
获取指定会话的消息,支持 ChatLab 格式输出。
|
读取指定会话的消息,支持原始 JSON 和 ChatLab 格式。
|
||||||
|
|
||||||
**请求**
|
**请求**
|
||||||
```
|
|
||||||
|
```http
|
||||||
GET /api/v1/messages
|
GET /api/v1/messages
|
||||||
```
|
```
|
||||||
|
|
||||||
**参数**
|
### 参数
|
||||||
|
|
||||||
| 参数名 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|--------|------|------|------|
|
| --- | --- | --- | --- |
|
||||||
| `talker` | string | ✅ | 会话 ID(wxid 或群 ID) |
|
| `talker` | string | 是 | 会话 ID。私聊通常是对方 `wxid`,群聊是 `xxx@chatroom` |
|
||||||
| `limit` | number | ❌ | 返回数量限制,默认 100,范围 `1~10000` |
|
| `limit` | number | 否 | 返回条数,默认 `100`,范围 `1~10000` |
|
||||||
| `offset` | number | ❌ | 偏移量,用于分页,默认 0 |
|
| `offset` | number | 否 | 分页偏移,默认 `0` |
|
||||||
| `start` | string | ❌ | 开始时间,格式 YYYYMMDD |
|
| `start` | string | 否 | 开始时间,支持 `YYYYMMDD` 或时间戳 |
|
||||||
| `end` | string | ❌ | 结束时间,格式 YYYYMMDD |
|
| `end` | string | 否 | 结束时间,支持 `YYYYMMDD` 或时间戳 |
|
||||||
| `keyword` | string | ❌ | 关键词过滤(基于消息显示文本) |
|
| `keyword` | string | 否 | 基于消息显示文本过滤 |
|
||||||
| `chatlab` | string | ❌ | 设为 `1` 则输出 ChatLab 格式 |
|
| `chatlab` | string | 否 | `1/true` 时输出 ChatLab 格式 |
|
||||||
| `format` | string | ❌ | 输出格式:`json`(默认)或 `chatlab` |
|
| `format` | string | 否 | `json` 或 `chatlab` |
|
||||||
| `media` | string | ❌ | 设为 `1` 时导出媒体并返回媒体路径(兼容别名 `meiti`);`0` 时媒体返回占位符 |
|
| `media` | string | 否 | `1/true` 时导出媒体并返回媒体地址,兼容别名 `meiti` |
|
||||||
| `image` | string | ❌ | 在 `media=1` 时控制图片导出,`1/0`(兼容别名 `tupian`) |
|
| `image` | string | 否 | 在 `media=1` 时控制图片导出,兼容别名 `tupian` |
|
||||||
| `voice` | string | ❌ | 在 `media=1` 时控制语音导出,`1/0`(兼容别名 `vioce`) |
|
| `voice` | string | 否 | 在 `media=1` 时控制语音导出,兼容别名 `vioce` |
|
||||||
| `video` | string | ❌ | 在 `media=1` 时控制视频导出,`1/0` |
|
| `video` | string | 否 | 在 `media=1` 时控制视频导出 |
|
||||||
| `emoji` | string | ❌ | 在 `media=1` 时控制表情导出,`1/0` |
|
| `emoji` | string | 否 | 在 `media=1` 时控制表情导出 |
|
||||||
|
|
||||||
默认媒体导出目录:`%USERPROFILE%\\Documents\\WeFlow\\api-media`
|
### 示例
|
||||||
|
|
||||||
**示例请求**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 获取消息(原始格式)
|
curl "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&limit=20"
|
||||||
GET http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&limit=50
|
curl "http://127.0.0.1:5031/api/v1/messages?talker=xxx@chatroom&chatlab=1"
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&start=20260101&end=20260131"
|
||||||
# 获取消息(ChatLab 格式)
|
curl "http://127.0.0.1:5031/api/v1/messages?talker=xxx@chatroom&media=1&image=1&voice=0&video=0&emoji=0"
|
||||||
GET http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&chatlab=1
|
|
||||||
|
|
||||||
# 带时间范围查询
|
|
||||||
GET http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&start=20260101&end=20260205&limit=100
|
|
||||||
|
|
||||||
# 开启媒体导出(只导出图片和语音)
|
|
||||||
GET http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&media=1&image=1&voice=1&video=0&emoji=0
|
|
||||||
|
|
||||||
# 关键词过滤
|
|
||||||
GET http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&keyword=项目进度&limit=50
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应(原始格式)**
|
### JSON 响应字段
|
||||||
|
|
||||||
|
顶层字段:
|
||||||
|
|
||||||
|
- `success`
|
||||||
|
- `talker`
|
||||||
|
- `count`
|
||||||
|
- `hasMore`
|
||||||
|
- `media.enabled`
|
||||||
|
- `media.exportPath`
|
||||||
|
- `media.count`
|
||||||
|
- `messages`
|
||||||
|
|
||||||
|
单条消息字段:
|
||||||
|
|
||||||
|
- `localId`
|
||||||
|
- `serverId`
|
||||||
|
- `localType`
|
||||||
|
- `createTime`
|
||||||
|
- `isSend`
|
||||||
|
- `senderUsername`
|
||||||
|
- `content`
|
||||||
|
- `rawContent`
|
||||||
|
- `parsedContent`
|
||||||
|
- `mediaType`
|
||||||
|
- `mediaFileName`
|
||||||
|
- `mediaUrl`
|
||||||
|
- `mediaLocalPath`
|
||||||
|
|
||||||
|
**示例响应**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"talker": "wxid_xxx",
|
"talker": "xxx@chatroom",
|
||||||
"count": 50,
|
"count": 2,
|
||||||
"hasMore": true,
|
"hasMore": true,
|
||||||
"media": {
|
"media": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"exportPath": "C:\\Users\\Alice\\Documents\\WeFlow\\api-media",
|
"exportPath": "C:\\Users\\Alice\\Documents\\WeFlow\\api-media",
|
||||||
"count": 12
|
"count": 1
|
||||||
},
|
},
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"localId": 123,
|
"localId": 123,
|
||||||
|
"serverId": "456",
|
||||||
|
"localType": 1,
|
||||||
|
"createTime": 1738713600,
|
||||||
|
"isSend": 0,
|
||||||
|
"senderUsername": "wxid_member",
|
||||||
|
"content": "你好",
|
||||||
|
"rawContent": "你好",
|
||||||
|
"parsedContent": "你好"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"localId": 124,
|
||||||
"localType": 3,
|
"localType": 3,
|
||||||
|
"createTime": 1738713660,
|
||||||
|
"isSend": 0,
|
||||||
|
"senderUsername": "wxid_member",
|
||||||
"content": "[图片]",
|
"content": "[图片]",
|
||||||
"createTime": 1738713600000,
|
|
||||||
"senderUsername": "wxid_sender",
|
|
||||||
"mediaType": "image",
|
"mediaType": "image",
|
||||||
"mediaFileName": "image_123.jpg",
|
"mediaFileName": "abc123.jpg",
|
||||||
"mediaUrl": "http://127.0.0.1:5031/api/v1/media/wxid_xxx/images/image_123.jpg",
|
"mediaUrl": "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/images/abc123.jpg",
|
||||||
"mediaLocalPath": "C:\\Users\\Alice\\Documents\\WeFlow\\api-media\\wxid_xxx\\images\\image_123.jpg"
|
"mediaLocalPath": "C:\\Users\\Alice\\Documents\\WeFlow\\api-media\\xxx@chatroom\\images\\abc123.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应(ChatLab 格式)**
|
### ChatLab 响应
|
||||||
```json
|
|
||||||
{
|
当 `chatlab=1` 或 `format=chatlab` 时,返回 ChatLab 结构:
|
||||||
"chatlab": {
|
|
||||||
"version": "0.0.2",
|
- `chatlab.version`
|
||||||
"exportedAt": 1738713600000,
|
- `chatlab.exportedAt`
|
||||||
"generator": "WeFlow",
|
- `chatlab.generator`
|
||||||
"description": "Exported from WeFlow"
|
- `meta.name`
|
||||||
},
|
- `meta.platform`
|
||||||
"meta": {
|
- `meta.type`
|
||||||
"name": "会话名称",
|
- `meta.groupId`
|
||||||
"platform": "wechat",
|
- `meta.groupAvatar`
|
||||||
"type": "private",
|
- `meta.ownerId`
|
||||||
"ownerId": "wxid_me"
|
- `members[].platformId`
|
||||||
},
|
- `members[].accountName`
|
||||||
"members": [
|
- `members[].groupNickname`
|
||||||
{
|
- `members[].avatar`
|
||||||
"platformId": "wxid_xxx",
|
- `messages[].sender`
|
||||||
"accountName": "用户名",
|
- `messages[].accountName`
|
||||||
"groupNickname": "群昵称"
|
- `messages[].groupNickname`
|
||||||
}
|
- `messages[].timestamp`
|
||||||
],
|
- `messages[].type`
|
||||||
"messages": [
|
- `messages[].content`
|
||||||
{
|
- `messages[].platformMessageId`
|
||||||
"sender": "wxid_xxx",
|
- `messages[].mediaPath`
|
||||||
"accountName": "用户名",
|
|
||||||
"timestamp": 1738713600000,
|
群聊里 `groupNickname` 会优先来自群成员群昵称;若源数据缺失,则回退为空或展示名。
|
||||||
"type": 0,
|
|
||||||
"content": "消息内容",
|
|
||||||
"mediaPath": "http://127.0.0.1:5031/api/v1/media/wxid_xxx/images/image_123.jpg"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"media": {
|
|
||||||
"enabled": true,
|
|
||||||
"exportPath": "C:\\Users\\Alice\\Documents\\WeFlow\\api-media",
|
|
||||||
"count": 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. 访问导出媒体文件
|
## 3. 获取会话列表
|
||||||
|
|
||||||
通过 HTTP 直接访问已导出的媒体文件(图片、语音、视频、表情)。
|
|
||||||
|
|
||||||
**请求**
|
**请求**
|
||||||
```
|
|
||||||
GET /api/v1/media/{relativePath}
|
|
||||||
```
|
|
||||||
|
|
||||||
**路径参数**
|
```http
|
||||||
|
|
||||||
| 参数名 | 类型 | 必填 | 说明 |
|
|
||||||
|--------|------|------|------|
|
|
||||||
| `relativePath` | string | ✅ | 媒体文件的相对路径,如 `wxid_xxx/images/image_123.jpg` |
|
|
||||||
|
|
||||||
**支持的媒体类型**
|
|
||||||
|
|
||||||
| 扩展名 | Content-Type |
|
|
||||||
|--------|-------------|
|
|
||||||
| `.png` | image/png |
|
|
||||||
| `.jpg` / `.jpeg` | image/jpeg |
|
|
||||||
| `.gif` | image/gif |
|
|
||||||
| `.webp` | image/webp |
|
|
||||||
| `.wav` | audio/wav |
|
|
||||||
| `.mp3` | audio/mpeg |
|
|
||||||
| `.mp4` | video/mp4 |
|
|
||||||
|
|
||||||
**示例请求**
|
|
||||||
```bash
|
|
||||||
# 访问导出的图片
|
|
||||||
GET http://127.0.0.1:5031/api/v1/media/wxid_xxx/images/image_123.jpg
|
|
||||||
|
|
||||||
# 访问导出的语音
|
|
||||||
GET http://127.0.0.1:5031/api/v1/media/wxid_xxx/voices/voice_456.wav
|
|
||||||
|
|
||||||
# 访问导出的视频
|
|
||||||
GET http://127.0.0.1:5031/api/v1/media/wxid_xxx/videos/video_789.mp4
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应**
|
|
||||||
|
|
||||||
成功时直接返回文件内容,`Content-Type` 根据文件扩展名自动设置。
|
|
||||||
|
|
||||||
失败时返回:
|
|
||||||
```json
|
|
||||||
{ "error": "Media not found" }
|
|
||||||
```
|
|
||||||
|
|
||||||
> 注意:媒体文件需要先通过消息接口的 `media=1` 参数导出后才能访问。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 获取会话列表
|
|
||||||
|
|
||||||
获取所有会话列表。
|
|
||||||
|
|
||||||
**请求**
|
|
||||||
```
|
|
||||||
GET /api/v1/sessions
|
GET /api/v1/sessions
|
||||||
```
|
```
|
||||||
|
|
||||||
**参数**
|
### 参数
|
||||||
|
|
||||||
| 参数名 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|--------|------|------|------|
|
| --- | --- | --- | --- |
|
||||||
| `keyword` | string | ❌ | 搜索关键词,匹配会话名或 ID |
|
| `keyword` | string | 否 | 匹配 `username` 或 `displayName` |
|
||||||
| `limit` | number | ❌ | 返回数量限制,默认 100 |
|
| `limit` | number | 否 | 默认 `100` |
|
||||||
|
|
||||||
**示例请求**
|
### 响应字段
|
||||||
```bash
|
|
||||||
GET http://127.0.0.1:5031/api/v1/sessions
|
|
||||||
|
|
||||||
GET http://127.0.0.1:5031/api/v1/sessions?keyword=工作群&limit=20
|
- `success`
|
||||||
```
|
- `count`
|
||||||
|
- `sessions[].username`
|
||||||
|
- `sessions[].displayName`
|
||||||
|
- `sessions[].type`
|
||||||
|
- `sessions[].lastTimestamp`
|
||||||
|
- `sessions[].unreadCount`
|
||||||
|
|
||||||
|
**示例响应**
|
||||||
|
|
||||||
**响应**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"count": 50,
|
"count": 1,
|
||||||
"total": 100,
|
|
||||||
"sessions": [
|
"sessions": [
|
||||||
{
|
{
|
||||||
"username": "wxid_xxx",
|
"username": "xxx@chatroom",
|
||||||
"displayName": "用户名",
|
"displayName": "项目群",
|
||||||
"lastMessage": "最后一条消息",
|
"type": 2,
|
||||||
"lastTime": 1738713600000,
|
"lastTimestamp": 1738713600,
|
||||||
"unreadCount": 0
|
"unreadCount": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -249,40 +228,48 @@ GET http://127.0.0.1:5031/api/v1/sessions?keyword=工作群&limit=20
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. 获取联系人列表
|
## 4. 获取联系人列表
|
||||||
|
|
||||||
获取所有联系人信息。
|
|
||||||
|
|
||||||
**请求**
|
**请求**
|
||||||
```
|
|
||||||
|
```http
|
||||||
GET /api/v1/contacts
|
GET /api/v1/contacts
|
||||||
```
|
```
|
||||||
|
|
||||||
**参数**
|
### 参数
|
||||||
|
|
||||||
| 参数名 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|--------|------|------|------|
|
| --- | --- | --- | --- |
|
||||||
| `keyword` | string | ❌ | 搜索关键词 |
|
| `keyword` | string | 否 | 匹配 `username`、`nickname`、`remark`、`displayName` |
|
||||||
| `limit` | number | ❌ | 返回数量限制,默认 100 |
|
| `limit` | number | 否 | 默认 `100` |
|
||||||
|
|
||||||
**示例请求**
|
### 响应字段
|
||||||
```bash
|
|
||||||
GET http://127.0.0.1:5031/api/v1/contacts
|
|
||||||
|
|
||||||
GET http://127.0.0.1:5031/api/v1/contacts?keyword=张三
|
- `success`
|
||||||
```
|
- `count`
|
||||||
|
- `contacts[].username`
|
||||||
|
- `contacts[].displayName`
|
||||||
|
- `contacts[].remark`
|
||||||
|
- `contacts[].nickname`
|
||||||
|
- `contacts[].alias`
|
||||||
|
- `contacts[].avatarUrl`
|
||||||
|
- `contacts[].type`
|
||||||
|
|
||||||
|
**示例响应**
|
||||||
|
|
||||||
**响应**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"count": 50,
|
"count": 1,
|
||||||
"contacts": [
|
"contacts": [
|
||||||
{
|
{
|
||||||
"userName": "wxid_xxx",
|
"username": "wxid_xxx",
|
||||||
"alias": "微信号",
|
"displayName": "张三",
|
||||||
"nickName": "昵称",
|
"remark": "客户张三",
|
||||||
"remark": "备注名"
|
"nickname": "张三",
|
||||||
|
"alias": "zhangsan",
|
||||||
|
"avatarUrl": "https://example.com/avatar.jpg",
|
||||||
|
"type": "friend"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -290,60 +277,157 @@ GET http://127.0.0.1:5031/api/v1/contacts?keyword=张三
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ChatLab 格式说明
|
## 5. 获取群成员列表
|
||||||
|
|
||||||
ChatLab 是一种标准化的聊天记录交换格式,版本 0.0.2。
|
返回群成员的 `wxid`、群昵称、备注、微信号等信息。
|
||||||
|
|
||||||
### 消息类型映射
|
**请求**
|
||||||
|
|
||||||
| ChatLab Type | 值 | 说明 |
|
```http
|
||||||
|--------------|-----|------|
|
GET /api/v1/group-members
|
||||||
| TEXT | 0 | 文本消息 |
|
```
|
||||||
| IMAGE | 1 | 图片 |
|
|
||||||
| VOICE | 2 | 语音 |
|
### 参数
|
||||||
| VIDEO | 3 | 视频 |
|
|
||||||
| FILE | 4 | 文件 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
| EMOJI | 5 | 表情 |
|
| --- | --- | --- | --- |
|
||||||
| LINK | 7 | 链接 |
|
| `chatroomId` | string | 是 | 群 ID,兼容使用 `talker` 传入 |
|
||||||
| LOCATION | 8 | 位置 |
|
| `includeMessageCounts` | string | 否 | `1/true` 时附带成员发言数 |
|
||||||
| RED_PACKET | 20 | 红包 |
|
| `withCounts` | string | 否 | `includeMessageCounts` 的别名 |
|
||||||
| TRANSFER | 21 | 转账 |
|
| `forceRefresh` | string | 否 | `1/true` 时跳过内存缓存强制刷新 |
|
||||||
| CALL | 23 | 通话 |
|
|
||||||
| SYSTEM | 80 | 系统消息 |
|
### 响应字段
|
||||||
| RECALL | 81 | 撤回消息 |
|
|
||||||
| OTHER | 99 | 其他 |
|
- `success`
|
||||||
|
- `chatroomId`
|
||||||
|
- `count`
|
||||||
|
- `fromCache`
|
||||||
|
- `updatedAt`
|
||||||
|
- `members[].wxid`
|
||||||
|
- `members[].displayName`
|
||||||
|
- `members[].nickname`
|
||||||
|
- `members[].remark`
|
||||||
|
- `members[].alias`
|
||||||
|
- `members[].groupNickname`
|
||||||
|
- `members[].avatarUrl`
|
||||||
|
- `members[].isOwner`
|
||||||
|
- `members[].isFriend`
|
||||||
|
- `members[].messageCount`
|
||||||
|
|
||||||
|
**示例请求**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom"
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom&includeMessageCounts=1&forceRefresh=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"chatroomId": "xxx@chatroom",
|
||||||
|
"count": 2,
|
||||||
|
"fromCache": false,
|
||||||
|
"updatedAt": 1760000000000,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"wxid": "wxid_member_a",
|
||||||
|
"displayName": "客户A",
|
||||||
|
"nickname": "阿甲",
|
||||||
|
"remark": "客户A",
|
||||||
|
"alias": "kehua",
|
||||||
|
"groupNickname": "甲方",
|
||||||
|
"avatarUrl": "https://example.com/a.jpg",
|
||||||
|
"isOwner": true,
|
||||||
|
"isFriend": true,
|
||||||
|
"messageCount": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wxid": "wxid_member_b",
|
||||||
|
"displayName": "李四",
|
||||||
|
"nickname": "李四",
|
||||||
|
"remark": "",
|
||||||
|
"alias": "",
|
||||||
|
"groupNickname": "",
|
||||||
|
"avatarUrl": "",
|
||||||
|
"isOwner": false,
|
||||||
|
"isFriend": false,
|
||||||
|
"messageCount": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `displayName` 是当前应用内的主展示名。
|
||||||
|
- `groupNickname` 是成员在该群里的群昵称。
|
||||||
|
- `remark` 是你对该联系人的备注。
|
||||||
|
- `alias` 是微信号。
|
||||||
|
- 当微信源数据里没有群昵称时,`groupNickname` 会为空。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 使用示例
|
## 6. 访问导出媒体
|
||||||
|
|
||||||
|
通过消息接口启用 `media=1` 后,接口会先把图片、语音、视频、表情导出到本地缓存目录,再返回可访问的 HTTP 地址。
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/media/{relativePath}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/images/abc123.jpg"
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/voices/voice_100.wav"
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/videos/video_200.mp4"
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/emojis/emoji_300.gif"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 支持的 Content-Type
|
||||||
|
|
||||||
|
| 扩展名 | Content-Type |
|
||||||
|
| --- | --- |
|
||||||
|
| `.png` | `image/png` |
|
||||||
|
| `.jpg` / `.jpeg` | `image/jpeg` |
|
||||||
|
| `.gif` | `image/gif` |
|
||||||
|
| `.webp` | `image/webp` |
|
||||||
|
| `.wav` | `audio/wav` |
|
||||||
|
| `.mp3` | `audio/mpeg` |
|
||||||
|
| `.mp4` | `video/mp4` |
|
||||||
|
|
||||||
|
常见错误响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Media not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 使用示例
|
||||||
|
|
||||||
### PowerShell
|
### PowerShell
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 健康检查
|
|
||||||
Invoke-RestMethod http://127.0.0.1:5031/health
|
Invoke-RestMethod http://127.0.0.1:5031/health
|
||||||
|
|
||||||
# 获取会话列表
|
|
||||||
Invoke-RestMethod http://127.0.0.1:5031/api/v1/sessions
|
Invoke-RestMethod http://127.0.0.1:5031/api/v1/sessions
|
||||||
|
|
||||||
# 获取消息
|
|
||||||
Invoke-RestMethod "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&limit=10"
|
Invoke-RestMethod "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&limit=10"
|
||||||
|
Invoke-RestMethod "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom&includeMessageCounts=1"
|
||||||
# 获取 ChatLab 格式
|
|
||||||
Invoke-RestMethod "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&chatlab=1" | ConvertTo-Json -Depth 10
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### cURL
|
### cURL
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 健康检查
|
|
||||||
curl http://127.0.0.1:5031/health
|
curl http://127.0.0.1:5031/health
|
||||||
|
|
||||||
# 获取会话列表
|
|
||||||
curl http://127.0.0.1:5031/api/v1/sessions
|
|
||||||
|
|
||||||
# 获取消息(ChatLab 格式)
|
|
||||||
curl "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&chatlab=1"
|
curl "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&chatlab=1"
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/contacts?keyword=张三"
|
||||||
|
curl "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Python
|
### Python
|
||||||
@@ -353,39 +437,26 @@ import requests
|
|||||||
|
|
||||||
BASE_URL = "http://127.0.0.1:5031"
|
BASE_URL = "http://127.0.0.1:5031"
|
||||||
|
|
||||||
# 获取会话列表
|
messages = requests.get(
|
||||||
sessions = requests.get(f"{BASE_URL}/api/v1/sessions").json()
|
f"{BASE_URL}/api/v1/messages",
|
||||||
print(sessions)
|
params={"talker": "xxx@chatroom", "limit": 50}
|
||||||
|
).json()
|
||||||
|
|
||||||
|
members = requests.get(
|
||||||
|
f"{BASE_URL}/api/v1/group-members",
|
||||||
|
params={"chatroomId": "xxx@chatroom", "includeMessageCounts": 1}
|
||||||
|
).json()
|
||||||
|
|
||||||
# 获取消息
|
|
||||||
messages = requests.get(f"{BASE_URL}/api/v1/messages", params={
|
|
||||||
"talker": "wxid_xxx",
|
|
||||||
"limit": 100,
|
|
||||||
"chatlab": 1
|
|
||||||
}).json()
|
|
||||||
print(messages)
|
print(messages)
|
||||||
```
|
print(members)
|
||||||
|
|
||||||
### JavaScript / Node.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const BASE_URL = "http://127.0.0.1:5031";
|
|
||||||
|
|
||||||
// 获取会话列表
|
|
||||||
const sessions = await fetch(`${BASE_URL}/api/v1/sessions`).then(r => r.json());
|
|
||||||
console.log(sessions);
|
|
||||||
|
|
||||||
// 获取消息(ChatLab 格式)
|
|
||||||
const messages = await fetch(`${BASE_URL}/api/v1/messages?talker=wxid_xxx&chatlab=1`)
|
|
||||||
.then(r => r.json());
|
|
||||||
console.log(messages);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 注意事项
|
## 8. 注意事项
|
||||||
|
|
||||||
1. API 仅监听本地地址 `127.0.0.1`,不对外网开放
|
1. API 仅监听本机 `127.0.0.1`,不对外网开放。
|
||||||
2. 需要先连接数据库才能查询数据
|
2. 使用前需要先在 WeFlow 中完成数据库连接。
|
||||||
3. 时间参数格式为 `YYYYMMDD`(如 20260205)
|
3. `start` 和 `end` 支持 `YYYYMMDD` 与时间戳;纯 `YYYYMMDD` 的 `end` 会扩展到当天 `23:59:59`。
|
||||||
4. 支持 CORS,可从浏览器前端直接调用
|
4. 群成员的 `groupNickname` 依赖微信源数据;源数据缺失时不会自动补出。
|
||||||
|
5. 媒体访问链接只有在对应消息已经通过 `media=1` 导出后才可访问。
|
||||||
|
|||||||
@@ -562,23 +562,50 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 contact.chat_room.ext_buffer 解析群昵称(纯 SQL)
|
* 获取群成员群昵称。优先使用 DLL,必要时回退到 `contact.chat_room.ext_buffer` 解析。
|
||||||
*/
|
*/
|
||||||
async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> {
|
async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> {
|
||||||
|
const nicknameMap = new Map<string, string>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dllResult = await wcdbService.getGroupNicknames(chatroomId)
|
||||||
|
if (dllResult.success && dllResult.nicknames) {
|
||||||
|
this.mergeGroupNicknameEntries(nicknameMap, Object.entries(dllResult.nicknames))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('getGroupNicknamesForRoom dll error:', e)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用参数化查询防止SQL注入
|
|
||||||
const sql = 'SELECT ext_buffer FROM chat_room WHERE username = ? LIMIT 1'
|
const sql = 'SELECT ext_buffer FROM chat_room WHERE username = ? LIMIT 1'
|
||||||
const result = await wcdbService.execQuery('contact', null, sql, [chatroomId])
|
const result = await wcdbService.execQuery('contact', null, sql, [chatroomId])
|
||||||
if (!result.success || !result.rows || result.rows.length === 0) {
|
if (!result.success || !result.rows || result.rows.length === 0) {
|
||||||
return new Map<string, string>()
|
return nicknameMap
|
||||||
}
|
}
|
||||||
|
|
||||||
const extBuffer = this.decodeExtBuffer((result.rows[0] as any).ext_buffer)
|
const extBuffer = this.decodeExtBuffer((result.rows[0] as any).ext_buffer)
|
||||||
if (!extBuffer) return new Map<string, string>()
|
if (!extBuffer) return nicknameMap
|
||||||
return this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates)
|
this.mergeGroupNicknameEntries(nicknameMap, this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates).entries())
|
||||||
|
return nicknameMap
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getGroupNicknamesForRoom error:', e)
|
console.error('getGroupNicknamesForRoom error:', e)
|
||||||
return new Map<string, string>()
|
return nicknameMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeGroupNicknameEntries(
|
||||||
|
target: Map<string, string>,
|
||||||
|
entries: Iterable<[string, string]>
|
||||||
|
): void {
|
||||||
|
for (const [memberIdRaw, nicknameRaw] of entries) {
|
||||||
|
const nickname = this.normalizeGroupNickname(nicknameRaw || '')
|
||||||
|
if (!nickname) continue
|
||||||
|
for (const alias of this.buildGroupNicknameIdCandidates([memberIdRaw])) {
|
||||||
|
if (!alias) continue
|
||||||
|
if (!target.has(alias)) target.set(alias, nickname)
|
||||||
|
const lower = alias.toLowerCase()
|
||||||
|
if (!target.has(lower)) target.set(lower, nickname)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,20 +261,47 @@ class GroupAnalyticsService {
|
|||||||
* 从 DLL 获取群成员的群昵称
|
* 从 DLL 获取群成员的群昵称
|
||||||
*/
|
*/
|
||||||
private async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> {
|
private async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> {
|
||||||
|
const nicknameMap = new Map<string, string>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const escapedChatroomId = chatroomId.replace(/'/g, "''")
|
const dllResult = await wcdbService.getGroupNicknames(chatroomId)
|
||||||
const sql = `SELECT ext_buffer FROM chat_room WHERE username='${escapedChatroomId}' LIMIT 1`
|
if (dllResult.success && dllResult.nicknames) {
|
||||||
const result = await wcdbService.execQuery('contact', null, sql)
|
this.mergeGroupNicknameEntries(nicknameMap, Object.entries(dllResult.nicknames))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('getGroupNicknamesForRoom dll error:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sql = 'SELECT ext_buffer FROM chat_room WHERE username = ? LIMIT 1'
|
||||||
|
const result = await wcdbService.execQuery('contact', null, sql, [chatroomId])
|
||||||
if (!result.success || !result.rows || result.rows.length === 0) {
|
if (!result.success || !result.rows || result.rows.length === 0) {
|
||||||
return new Map<string, string>()
|
return nicknameMap
|
||||||
}
|
}
|
||||||
|
|
||||||
const extBuffer = this.decodeExtBuffer((result.rows[0] as any).ext_buffer)
|
const extBuffer = this.decodeExtBuffer((result.rows[0] as any).ext_buffer)
|
||||||
if (!extBuffer) return new Map<string, string>()
|
if (!extBuffer) return nicknameMap
|
||||||
return this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates)
|
this.mergeGroupNicknameEntries(nicknameMap, this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates).entries())
|
||||||
|
return nicknameMap
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getGroupNicknamesForRoom error:', e)
|
console.error('getGroupNicknamesForRoom error:', e)
|
||||||
return new Map<string, string>()
|
return nicknameMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeGroupNicknameEntries(
|
||||||
|
target: Map<string, string>,
|
||||||
|
entries: Iterable<[string, string]>
|
||||||
|
): void {
|
||||||
|
for (const [memberIdRaw, nicknameRaw] of entries) {
|
||||||
|
const nickname = this.normalizeGroupNickname(nicknameRaw || '')
|
||||||
|
if (!nickname) continue
|
||||||
|
for (const alias of this.buildIdCandidates([memberIdRaw])) {
|
||||||
|
if (!alias) continue
|
||||||
|
if (!target.has(alias)) target.set(alias, nickname)
|
||||||
|
const lower = alias.toLowerCase()
|
||||||
|
if (!target.has(lower)) target.set(lower, nickname)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { wcdbService } from './wcdbService'
|
|||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { videoService } from './videoService'
|
import { videoService } from './videoService'
|
||||||
import { imageDecryptService } from './imageDecryptService'
|
import { imageDecryptService } from './imageDecryptService'
|
||||||
|
import { groupAnalyticsService } from './groupAnalyticsService'
|
||||||
|
|
||||||
// ChatLab 格式定义
|
// ChatLab 格式定义
|
||||||
interface ChatLabHeader {
|
interface ChatLabHeader {
|
||||||
@@ -238,6 +239,8 @@ class HttpService {
|
|||||||
await this.handleSessions(url, res)
|
await this.handleSessions(url, res)
|
||||||
} else if (pathname === '/api/v1/contacts') {
|
} else if (pathname === '/api/v1/contacts') {
|
||||||
await this.handleContacts(url, res)
|
await this.handleContacts(url, res)
|
||||||
|
} else if (pathname === '/api/v1/group-members') {
|
||||||
|
await this.handleGroupMembers(url, res)
|
||||||
} else if (pathname.startsWith('/api/v1/media/')) {
|
} else if (pathname.startsWith('/api/v1/media/')) {
|
||||||
this.handleMediaRequest(pathname, res)
|
this.handleMediaRequest(pathname, res)
|
||||||
} else {
|
} else {
|
||||||
@@ -589,6 +592,54 @@ class HttpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理群成员查询
|
||||||
|
* GET /api/v1/group-members?chatroomId=xxx@chatroom&includeMessageCounts=1&forceRefresh=0
|
||||||
|
*/
|
||||||
|
private async handleGroupMembers(url: URL, res: http.ServerResponse): Promise<void> {
|
||||||
|
const chatroomId = (url.searchParams.get('chatroomId') || url.searchParams.get('talker') || '').trim()
|
||||||
|
const includeMessageCounts = this.parseBooleanParam(url, ['includeMessageCounts', 'withCounts'], false)
|
||||||
|
const forceRefresh = this.parseBooleanParam(url, ['forceRefresh'], false)
|
||||||
|
|
||||||
|
if (!chatroomId) {
|
||||||
|
this.sendError(res, 400, 'Missing chatroomId')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await groupAnalyticsService.getGroupMembersPanelData(chatroomId, {
|
||||||
|
forceRefresh,
|
||||||
|
includeMessageCounts
|
||||||
|
})
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
this.sendError(res, 500, result.error || 'Failed to get group members')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendJson(res, {
|
||||||
|
success: true,
|
||||||
|
chatroomId,
|
||||||
|
count: result.data.length,
|
||||||
|
fromCache: result.fromCache,
|
||||||
|
updatedAt: result.updatedAt,
|
||||||
|
members: result.data.map((member) => ({
|
||||||
|
wxid: member.username,
|
||||||
|
displayName: member.displayName,
|
||||||
|
nickname: member.nickname || '',
|
||||||
|
remark: member.remark || '',
|
||||||
|
alias: member.alias || '',
|
||||||
|
groupNickname: member.groupNickname || '',
|
||||||
|
avatarUrl: member.avatarUrl,
|
||||||
|
isOwner: Boolean(member.isOwner),
|
||||||
|
isFriend: Boolean(member.isFriend),
|
||||||
|
messageCount: Number.isFinite(member.messageCount) ? member.messageCount : 0
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.sendError(res, 500, String(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getApiMediaExportPath(): string {
|
private getApiMediaExportPath(): string {
|
||||||
return path.join(this.configService.getCacheBasePath(), 'api-media')
|
return path.join(this.configService.getCacheBasePath(), 'api-media')
|
||||||
}
|
}
|
||||||
@@ -886,7 +937,12 @@ class HttpService {
|
|||||||
|
|
||||||
private lookupGroupNickname(groupNicknamesMap: Map<string, string>, sender: string): string {
|
private lookupGroupNickname(groupNicknamesMap: Map<string, string>, sender: string): string {
|
||||||
if (!sender) return ''
|
if (!sender) return ''
|
||||||
return groupNicknamesMap.get(sender) || groupNicknamesMap.get(sender.toLowerCase()) || ''
|
const cleaned = this.normalizeAccountId(sender)
|
||||||
|
return groupNicknamesMap.get(sender)
|
||||||
|
|| groupNicknamesMap.get(sender.toLowerCase())
|
||||||
|
|| groupNicknamesMap.get(cleaned)
|
||||||
|
|| groupNicknamesMap.get(cleaned.toLowerCase())
|
||||||
|
|| ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveChatLabSenderInfo(
|
private resolveChatLabSenderInfo(
|
||||||
@@ -957,7 +1013,21 @@ class HttpService {
|
|||||||
try {
|
try {
|
||||||
const result = await wcdbService.getGroupNicknames(talkerId)
|
const result = await wcdbService.getGroupNicknames(talkerId)
|
||||||
if (result.success && result.nicknames) {
|
if (result.success && result.nicknames) {
|
||||||
groupNicknamesMap = new Map(Object.entries(result.nicknames))
|
groupNicknamesMap = new Map()
|
||||||
|
for (const [memberIdRaw, nicknameRaw] of Object.entries(result.nicknames)) {
|
||||||
|
const memberId = String(memberIdRaw || '').trim()
|
||||||
|
const nickname = String(nicknameRaw || '').trim()
|
||||||
|
if (!memberId || !nickname) continue
|
||||||
|
|
||||||
|
groupNicknamesMap.set(memberId, nickname)
|
||||||
|
groupNicknamesMap.set(memberId.toLowerCase(), nickname)
|
||||||
|
|
||||||
|
const cleaned = this.normalizeAccountId(memberId)
|
||||||
|
if (cleaned) {
|
||||||
|
groupNicknamesMap.set(cleaned, nickname)
|
||||||
|
groupNicknamesMap.set(cleaned.toLowerCase(), nickname)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[HttpService] Failed to get group nicknames:', e)
|
console.error('[HttpService] Failed to get group nicknames:', e)
|
||||||
|
|||||||
Reference in New Issue
Block a user