feat: API服务支持ChatLab新版协议

This commit is contained in:
digua
2026-04-19 14:50:13 +08:00
parent 55885449a3
commit 0ba1067123
3 changed files with 265 additions and 61 deletions

View File

@@ -27,8 +27,8 @@ WeFlow 提供本地 HTTP API已支持GET 和 POST请求便于外部脚
- `GET|POST /api/v1/health`
- `GET|POST /api/v1/push/messages`
- `GET|POST /api/v1/messages`
- `GET|POST /api/v1/messages/new`
- `GET|POST /api/v1/sessions`
- `GET /api/v1/sessions/:id/messages` (ChatLab Pull)
- `GET|POST /api/v1/contacts`
- `GET|POST /api/v1/group-members`
- `GET|POST /api/v1/media/*`
@@ -117,7 +117,7 @@ GET /api/v1/messages
### 参数
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| --------- | ------ | ---- | ----------------------------------------------------- |
| `talker` | string | 是 | 会话 ID。私聊通常是对方 `wxid`,群聊是 `xxx@chatroom` |
| `limit` | number | 否 | 返回条数,默认 `100`,范围 `1~10000` |
| `offset` | number | 否 | 分页偏移,默认 `0` |
@@ -254,7 +254,7 @@ GET /api/v1/sessions
### 参数
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| --------- | ------ | ---- | -------------------------------- |
| `keyword` | string | 否 | 匹配 `username``displayName` |
| `limit` | number | 否 | 默认 `100` |
@@ -288,6 +288,130 @@ GET /api/v1/sessions
---
## 4.1 获取会话列表ChatLab 格式)
`format=chatlab` 时,返回 ChatLab Pull 协议兼容格式,可直接作为 ChatLab 远程数据源。
**请求**
```http
GET /api/v1/sessions?format=chatlab
```
### 参数
| 参数 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | -------------------------------- |
| `format` | string | 是 | 设为 `chatlab` |
| `keyword` | string | 否 | 匹配 `username``displayName` |
| `limit` | number | 否 | 默认 `100` |
### 响应
```json
{
"sessions": [
{
"id": "xxx@chatroom",
"name": "项目群",
"platform": "wechat",
"type": "group",
"messageCount": 58000,
"lastMessageAt": 1738713600
}
]
}
```
| 字段 | 说明 |
| --------------- | ----------------------------------- |
| `id` | 会话 ID微信 username |
| `name` | 会话显示名称 |
| `platform` | 固定 `wechat` |
| `type` | `group`(群聊)或 `private`(私聊) |
| `messageCount` | 消息数量(估算值,可能不精确) |
| `lastMessageAt` | 最后消息的秒级 Unix 时间戳 |
---
## 4.2 拉取会话消息ChatLab Pull
返回 ChatLab 标准格式的聊天数据,支持增量拉取和分页。
**请求**
```http
GET /api/v1/sessions/:id/messages
```
### 参数
| 参数 | 类型 | 必填 | 说明 |
| -------- | ------ | ---- | ---------------------------------------- |
| `:id` | string | 是 | 会话 IDPath 参数) |
| `since` | number | 否 | 秒级 Unix 时间戳,仅返回此时间之后的消息 |
| `end` | number | 否 | 秒级 Unix 时间戳,时间上界 |
| `limit` | number | 否 | 单次返回上限,默认且最大 `5000` |
| `offset` | number | 否 | 分页偏移,默认 `0` |
### 响应
返回 ChatLab 标准 JSON 格式,外加 `sync` 分页块:
```json
{
"chatlab": {
"version": "0.0.2",
"exportedAt": 1738713600,
"generator": "WeFlow"
},
"meta": {
"name": "项目群",
"platform": "wechat",
"type": "group",
"groupId": "xxx@chatroom",
"ownerId": "wxid_xxx"
},
"members": [
{
"platformId": "wxid_a",
"accountName": "张三",
"groupNickname": "产品",
"avatar": "https://example.com/avatar.jpg"
}
],
"messages": [
{
"sender": "wxid_a",
"accountName": "张三",
"timestamp": 1738713600,
"type": 0,
"content": "你好",
"platformMessageId": "123456"
}
],
"sync": {
"hasMore": true,
"nextSince": 1738713600,
"nextOffset": 5000,
"watermark": 1738714000
}
}
```
### sync 块
| 字段 | 说明 |
| ------------ | -------------------------------- |
| `hasMore` | 是否还有更多数据 |
| `nextSince` | 下次请求的 `since` 值 |
| `nextOffset` | 下次请求的 `offset` 值 |
| `watermark` | 本次拉取的时间上界(秒级时间戳) |
**ChatLab 对接方式**:在 ChatLab 设置中添加远程数据源,`baseUrl``http://127.0.0.1:5031/api/v1`Token 填 WeFlow 中配置的 API Token。
---
## 5. 获取联系人列表
> 当使用 POST 时,请将参数放在 JSON Body 中Content-Type: application/json
@@ -301,7 +425,7 @@ GET /api/v1/contacts
### 参数
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| --------- | ------ | ---- | ---------------------------------------------------- |
| `keyword` | string | 否 | 匹配 `username``nickname``remark``displayName` |
| `limit` | number | 否 | 默认 `100` |
@@ -354,7 +478,7 @@ GET /api/v1/group-members
### 参数
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| ---------------------- | ------ | ---- | ------------------------------- |
| `chatroomId` | string | 是 | 群 ID兼容使用 `talker` 传入 |
| `includeMessageCounts` | string | 否 | `1/true` 时附带成员发言数 |
| `withCounts` | string | 否 | `includeMessageCounts` 的别名 |
@@ -444,7 +568,7 @@ GET /api/v1/sns/timeline
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| ----------- | ------ | ---- | ------------------------------------------------------------ |
| `limit` | number | 否 | 返回数量,默认 20范围 `1~200` |
| `offset` | number | 否 | 偏移量,默认 0 |
| `usernames` | string | 否 | 发布者过滤,逗号分隔,如 `wxid_a,wxid_b` |
@@ -491,7 +615,7 @@ GET /api/v1/sns/export/stats
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| ------ | ------ | ---- | ---------------------------- |
| `fast` | number | 否 | `1` 使用快速统计(优先缓存) |
### 7.4 朋友圈媒体代理
@@ -503,7 +627,7 @@ GET /api/v1/sns/media/proxy
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| ----- | ------------- | ---- | ------------------------ |
| `url` | string | 是 | 媒体原始 URL |
| `key` | string/number | 否 | 解密 key部分资源需要 |
@@ -573,7 +697,7 @@ 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` |

View File

@@ -370,6 +370,17 @@ class HttpService {
await this.handleMessages(url, res)
} else if (pathname === '/api/v1/sessions') {
await this.handleSessions(url, res)
} else if (
pathname.startsWith('/api/v1/sessions/') &&
pathname.endsWith('/messages')
) {
const parts = pathname.split('/')
const sessionId = decodeURIComponent(parts[4] || '')
if (!sessionId) {
this.sendError(res, 400, 'Missing session ID')
} else {
await this.handlePullMessages(sessionId, url, res)
}
} else if (pathname === '/api/v1/contacts') {
await this.handleContacts(url, res)
} else if (pathname === '/api/v1/group-members') {
@@ -736,6 +747,7 @@ class HttpService {
private async handleSessions(url: URL, res: http.ServerResponse): Promise<void> {
const keyword = (url.searchParams.get('keyword') || '').trim()
const limit = this.parseIntParam(url.searchParams.get('limit'), 100, 1, 10000)
const format = (url.searchParams.get('format') || '').trim().toLowerCase()
try {
const sessions = await chatService.getSessions()
@@ -753,9 +765,22 @@ class HttpService {
)
}
// 应用 limit
const limitedSessions = filteredSessions.slice(0, limit)
if (format === 'chatlab') {
this.sendJson(res, {
sessions: limitedSessions.map(s => ({
id: s.username,
name: s.displayName || s.username,
platform: 'wechat',
type: s.username.endsWith('@chatroom') ? 'group' : 'private',
messageCount: s.messageCountHint || undefined,
lastMessageAt: s.lastTimestamp
}))
})
return
}
this.sendJson(res, {
success: true,
count: limitedSessions.length,
@@ -772,6 +797,53 @@ class HttpService {
}
}
/**
* ChatLab Pull: GET /api/v1/sessions/:id/messages?since=&limit=&offset=&end=
* 返回 ChatLab 标准格式 + sync 分页块
*/
private async handlePullMessages(sessionId: string, url: URL, res: http.ServerResponse): Promise<void> {
const PULL_MAX_LIMIT = 5000
const limit = this.parseIntParam(url.searchParams.get('limit'), PULL_MAX_LIMIT, 1, PULL_MAX_LIMIT)
const offset = this.parseIntParam(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER)
const sinceParam = url.searchParams.get('since')
const endParam = url.searchParams.get('end')
const startTime = sinceParam ? this.parseTimeParam(sinceParam) : 0
const endTime = endParam ? this.parseTimeParam(endParam, true) : 0
try {
const result = await this.fetchMessagesBatch(sessionId, offset, limit, startTime, endTime, true)
if (!result.success || !result.messages) {
this.sendError(res, 500, result.error || 'Failed to get messages')
return
}
const messages = result.messages
const hasMore = result.hasMore === true
const displayNames = await this.getDisplayNames([sessionId])
const talkerName = displayNames[sessionId] || sessionId
const chatLabData = await this.convertToChatLab(messages, sessionId, talkerName)
const lastTimestamp = messages.length > 0
? messages[messages.length - 1].createTime
: undefined
this.sendJson(res, {
...chatLabData,
sync: {
hasMore,
nextSince: hasMore && lastTimestamp ? lastTimestamp : undefined,
nextOffset: hasMore ? offset + messages.length : undefined,
watermark: Math.floor(Date.now() / 1000)
}
})
} catch (error) {
console.error('[HttpService] handlePullMessages error:', error)
this.sendError(res, 500, String(error))
}
}
/**
* 处理联系人查询
* GET /api/v1/contacts?keyword=xxx&limit=100

View File

@@ -21,6 +21,8 @@ interface MessagePushPayload {
sourceName: string
groupName?: string
content: string | null
eventId: string
timestamp: number
}
const PUSH_CONFIG_KEYS = new Set([
@@ -313,6 +315,8 @@ class MessagePushService {
const sessionType = this.getSessionType(sessionId, session)
const content = this.getMessageDisplayContent(message)
const createTime = Number(message.createTime || 0)
if (isGroup) {
const groupInfo = await chatService.getContactAvatar(sessionId)
const groupName = session.displayName || groupInfo?.displayName || sessionId
@@ -326,7 +330,9 @@ class MessagePushService {
avatarUrl,
groupName,
sourceName,
content
content,
eventId: messageKey,
timestamp: createTime
}
}
@@ -339,7 +345,9 @@ class MessagePushService {
messageKey,
avatarUrl,
sourceName: session.displayName || contactInfo?.displayName || sessionId,
content
content,
eventId: messageKey,
timestamp: createTime
}
}