@qihoo/tuitui-openclaw-channel 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +4 -2
- package/openclaw.plugin.json +1 -0
- package/package.json +5 -1
- package/skills/tuitui-im-read/SKILL.md +143 -0
- package/src/accounts.ts +24 -0
- package/src/channel.ts +6 -28
- package/src/confs.ts +15 -8
- package/src/inbound.ts +271 -303
- package/src/outbound.ts +178 -69
- package/src/tools.ts +87 -0
- package/src/types.ts +5 -5
- package/src/utils.ts +7 -0
package/index.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
2
|
-
import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk';
|
|
2
|
+
import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk/core';
|
|
3
3
|
import { createTuiTuiChannelPlugin } from './src/channel';
|
|
4
|
+
import { registerTuituiTools } from './src/tools';
|
|
4
5
|
import { CHANNEL_ID, CHANNEL_NAME } from './src/const';
|
|
5
6
|
import { id } from './openclaw.plugin.json';
|
|
6
7
|
|
|
7
8
|
const plugin = {
|
|
8
9
|
id, // plugin id not is CHANNEL_ID
|
|
9
10
|
name: CHANNEL_NAME,
|
|
10
|
-
description: `${CHANNEL_NAME} chat integration for OpenClaw via
|
|
11
|
+
description: `${CHANNEL_NAME} chat integration for OpenClaw via WebSocket`,
|
|
11
12
|
configSchema: emptyPluginConfigSchema(),
|
|
12
13
|
register(api: OpenClawPluginApi) {
|
|
13
14
|
console.log(`[${CHANNEL_ID}] Plugin.register Before.`);
|
|
14
15
|
api.registerChannel({ plugin: createTuiTuiChannelPlugin(api.runtime) });
|
|
16
|
+
registerTuituiTools(api);
|
|
15
17
|
console.log(`[${CHANNEL_ID}] Plugin.register After.`);
|
|
16
18
|
},
|
|
17
19
|
};
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qihoo/tuitui-openclaw-channel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"maintainers": [
|
|
5
5
|
{
|
|
6
6
|
"name": "huzunjie",
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
],
|
|
18
18
|
"description": "TuiTui channel plugin for OpenClaw",
|
|
19
19
|
"type": "module",
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"openclaw": ">=2026.3.13"
|
|
22
|
+
},
|
|
20
23
|
"dependencies": {
|
|
24
|
+
"@sinclair/typebox": "^0.34.48",
|
|
21
25
|
"ws": "^8.13.0"
|
|
22
26
|
},
|
|
23
27
|
"openclaw": {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tuitui-im-read
|
|
3
|
+
description: |
|
|
4
|
+
推推(tuitui) IM 消息读取工具使用指南
|
|
5
|
+
|
|
6
|
+
**当以下情况时使用此 Skill**:
|
|
7
|
+
(1) 需要获取群聊或单聊的历史消息
|
|
8
|
+
(2) 用户提到"聊天记录"、"消息"、"群里说了什么"
|
|
9
|
+
(3) 需要按时间范围过滤消息、分页获取更多消息
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# 推推 IM 消息读取
|
|
13
|
+
|
|
14
|
+
## 执行前必读
|
|
15
|
+
|
|
16
|
+
- 该 Skill 中的所有消息读取工具均以机器人身份调用,只能读取机器人有权限的会话。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 工具:tuitui_im_get_messages
|
|
21
|
+
|
|
22
|
+
### 参数说明
|
|
23
|
+
|
|
24
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
25
|
+
|------|------|------|------|
|
|
26
|
+
| `chatId` | string | ✅ | 聊天 ID。单聊填对方的 tuitui account,群聊填群 ID |
|
|
27
|
+
| `chatType` | string | ✅ | 聊天类型:单聊填 `direct`,群聊填 `group` |
|
|
28
|
+
| `relativeTime` | string | ❌ | 相对时间范围:today / yesterday / day_before_yesterday / this_week / last_week / this_month / last_month / last_{N}_{unit}(unit: minutes/hours/days)。与 `startTime`/`endTime` 互斥 |
|
|
29
|
+
| `startTime` | string | ❌ | 起始时间,ISO 8601 格式,如 `2026-02-27T00:00:00+08:00`。不填默认从最早开始。与 `relativeTime` 互斥 |
|
|
30
|
+
| `endTime` | string | ❌ | 结束时间,ISO 8601 格式,如 `2026-02-27T23:59:59+08:00`。不填默认到当前时间。与 `relativeTime` 互斥 |
|
|
31
|
+
| `limit` | number | ❌ | 每页条数,范围 1~100,默认 100 |
|
|
32
|
+
| `cursor` | string | ❌ | 分页游标,首次调用填 `"0"`,翻页时传上次返回的 `cursor` |
|
|
33
|
+
| `orderAsc` | boolean | ❌ | 排序方向,`true` 正序(从旧到新),`false` 逆序(从新到旧,默认) |
|
|
34
|
+
|
|
35
|
+
### 返回结构
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"errcode": 0,
|
|
40
|
+
"errmsg": "ok",
|
|
41
|
+
"cursor": "xxx",
|
|
42
|
+
"has_more": true,
|
|
43
|
+
"current_time": "2026-03-21 10:00:00",
|
|
44
|
+
"msgs": [
|
|
45
|
+
{
|
|
46
|
+
"user_account": "zhangsan",
|
|
47
|
+
"user_name": "张三",
|
|
48
|
+
"msg_time": "2026-01-03 15:42:44",
|
|
49
|
+
"msg_type": "text",
|
|
50
|
+
"text": "消息内容",
|
|
51
|
+
"images": [],
|
|
52
|
+
"group_id": "group_abc",
|
|
53
|
+
"group_name": "某群",
|
|
54
|
+
"at_me": false,
|
|
55
|
+
"ref": null
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
字段说明:
|
|
62
|
+
|
|
63
|
+
| 字段 | 说明 |
|
|
64
|
+
|------|------|
|
|
65
|
+
| `current_time` | 当前服务器时间,格式 `YYYY-MM-DD HH:MM:SS`,辅助理解消息的相对时间 |
|
|
66
|
+
| `has_more` | `true` 时说明还有更多数据,可用返回的 `cursor` 继续翻页 |
|
|
67
|
+
| `msgs` | 消息列表,空数组表示该时间段内没有消息 |
|
|
68
|
+
| `msgs[].user_account` | 发送者账号 |
|
|
69
|
+
| `msgs[].user_name` | 发送者显示名称 |
|
|
70
|
+
| `msgs[].msg_time` | 消息时间,格式 `YYYY-MM-DD HH:MM:SS` |
|
|
71
|
+
| `msgs[].msg_type` | 消息类型:`text` / `image` / `mixed` / `voice` / `file` |
|
|
72
|
+
| `msgs[].text` | 文本内容(`msg_type` 为 `text` / `mixed` 时有值) |
|
|
73
|
+
| `msgs[].images` | 图片 URL 列表(`msg_type` 为 `image` / `mixed` 时有值) |
|
|
74
|
+
| `msgs[].file` | 文件信息 `{ name, url, file_id }`(`msg_type` 为 `file` 时有值) |
|
|
75
|
+
| `msgs[].at_me` | 是否 @ 了机器人(群聊) |
|
|
76
|
+
| `msgs[].ref` | 引用/回复的原消息,无引用时为 `null` |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 核心约束
|
|
81
|
+
|
|
82
|
+
### 1. 时间范围:确保消息覆盖完整
|
|
83
|
+
|
|
84
|
+
- 用户说"今天"、"最近"等相对时间时,自主推算对应的 `startTime` / `endTime`(ISO 8601,+08:00 时区)
|
|
85
|
+
- 不确定范围时适当放宽,宁可多拉再过滤
|
|
86
|
+
|
|
87
|
+
### 2. 分页:根据需要翻页获取更多结果
|
|
88
|
+
|
|
89
|
+
- 返回结果中 `has_more=true` 时,将 `cursor` 传入下次调用继续获取下一页
|
|
90
|
+
- 根据用户需求判断是否需要翻页:需要完整结果时继续翻页,浏览概览时第一页通常够用
|
|
91
|
+
|
|
92
|
+
### 3. 排序方向
|
|
93
|
+
|
|
94
|
+
- 默认 `orderAsc=false`(逆序),拉取最新消息
|
|
95
|
+
- 需要按时间顺序阅读时,传 `orderAsc=true`
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 使用场景示例
|
|
100
|
+
|
|
101
|
+
### 场景 1:获取群聊最新消息
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"chatId": "4511334567",
|
|
106
|
+
"chatType": "group"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 场景 2:获取某时间段内的单聊消息
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"chatId": "zhangsan",
|
|
115
|
+
"chatType": "direct",
|
|
116
|
+
"startTime": "2026-03-01T00:00:00+08:00",
|
|
117
|
+
"endTime": "2026-03-01T23:59:59+08:00",
|
|
118
|
+
"orderAsc": true
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 场景 3:分页获取更多消息
|
|
123
|
+
|
|
124
|
+
第一次调用返回 `has_more: true` 和 `cursor: "xxx"`,继续获取:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"chatId": "4511334567",
|
|
129
|
+
"chatType": "group",
|
|
130
|
+
"cursor": "xxx"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 常见错误与排查
|
|
137
|
+
|
|
138
|
+
| 错误现象 | 根本原因 | 解决方案 |
|
|
139
|
+
|---------|---------|---------|
|
|
140
|
+
| 消息结果为空 | 时间范围不对或 chatId 有误 | 检查 `chatId`、`chatType`,适当放宽时间范围 |
|
|
141
|
+
| 消息不完整 | 没有检查 `has_more` 并翻页 | `has_more=true` 时用返回的 `cursor` 继续翻页 |
|
|
142
|
+
| 报错 invalid tuitui account | 账号未配置或未启用 | 确认 tuitui 账号已正确配置 |
|
|
143
|
+
| 消息顺序不对 | `orderAsc` 方向有误 | 按需传 `orderAsc: true/false` |
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseAllowFroms, isEnabled } from './utils';
|
|
2
|
+
import { baseFildsDefault } from './confs';
|
|
3
|
+
import { CHANNEL_ID } from './const';
|
|
4
|
+
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
|
|
5
|
+
|
|
6
|
+
export const resolveAccount = (cfg: any, accountId?: string | null) => {
|
|
7
|
+
const channelConfig = cfg?.channels?.[CHANNEL_ID] || {};
|
|
8
|
+
const targetId = accountId || DEFAULT_ACCOUNT_ID;
|
|
9
|
+
const acct = targetId === DEFAULT_ACCOUNT_ID ? channelConfig : channelConfig.accounts?.[targetId];
|
|
10
|
+
return {
|
|
11
|
+
accountId: targetId,
|
|
12
|
+
enabled: isEnabled(acct?.enabled),
|
|
13
|
+
appId: acct?.appId || '',
|
|
14
|
+
appSecret: acct?.appSecret || '',
|
|
15
|
+
dmPolicy: acct?.dmPolicy || baseFildsDefault.dmPolicy,
|
|
16
|
+
allowFrom: parseAllowFroms(acct?.allowFrom || baseFildsDefault.allowFrom),
|
|
17
|
+
// 群组策略与白名单、群组级覆盖
|
|
18
|
+
groupPolicy: acct?.groupPolic || baseFildsDefault.groupPolicy,
|
|
19
|
+
groupAllowFrom: parseAllowFroms(acct?.groupAllowFrom || baseFildsDefault.groupAllowFrom),
|
|
20
|
+
requireMention: isEnabled(acct?.requireMention ?? baseFildsDefault.requireMention),
|
|
21
|
+
channelContext: acct?.channelContext || baseFildsDefault.channelContext,
|
|
22
|
+
emojiReaction: isEnabled(acct?.emojiReaction ?? baseFildsDefault.emojiReaction),
|
|
23
|
+
};
|
|
24
|
+
};
|
package/src/channel.ts
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
* Supports single chat (text, image, voice, file) and group chat with @mentions.
|
|
6
6
|
*/
|
|
7
7
|
import WebSocket from 'ws';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
setAccountEnabledInConfigSection,
|
|
11
|
-
deleteAccountFromConfigSection,
|
|
12
|
-
} from 'openclaw/plugin-sdk';
|
|
8
|
+
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
|
|
9
|
+
import { setAccountEnabledInConfigSection, deleteAccountFromConfigSection } from 'openclaw/plugin-sdk/core';
|
|
13
10
|
import { CHANNEL_ID, CHANNEL_NAME } from "./const";
|
|
14
|
-
|
|
15
11
|
import {
|
|
16
12
|
checkAccount,
|
|
17
13
|
sendTextMsg,
|
|
@@ -19,31 +15,13 @@ import {
|
|
|
19
15
|
sendMediaMsg,
|
|
20
16
|
guessChatType,
|
|
21
17
|
} from "./outbound";
|
|
22
|
-
|
|
23
18
|
import { handleInboundMessage } from './inbound';
|
|
24
|
-
|
|
25
19
|
import { capabilities, configSchema, baseFildsDefault } from './confs';
|
|
20
|
+
import { resolveAccount } from "./accounts"
|
|
21
|
+
import { isEnabled } from './utils';
|
|
26
22
|
|
|
27
|
-
const isEnabled = (val: any) => val === undefined || !!val;
|
|
28
23
|
const isConfigured = (account: any)=> !!(account?.appId && account?.appSecret);
|
|
29
|
-
|
|
30
|
-
const channelConfig = cfg?.channels?.[CHANNEL_ID] || {};
|
|
31
|
-
const targetId = accountId || DEFAULT_ACCOUNT_ID;
|
|
32
|
-
const acct = targetId === DEFAULT_ACCOUNT_ID ? channelConfig : channelConfig.accounts?.[targetId];
|
|
33
|
-
return {
|
|
34
|
-
accountId: targetId,
|
|
35
|
-
enabled: isEnabled(acct?.enabled),
|
|
36
|
-
appId: acct?.appId as string | undefined,
|
|
37
|
-
appSecret: acct?.appSecret as string | undefined,
|
|
38
|
-
dmPolicy: acct?.dmPolicy || baseFildsDefault.dmPolicy,
|
|
39
|
-
allowFrom: acct?.allowFrom || baseFildsDefault.allowFrom,
|
|
40
|
-
// 群组策略与白名单、群组级覆盖
|
|
41
|
-
groupPolicy: (acct?.groupPolicy as string | undefined) || baseFildsDefault.groupPolicy,
|
|
42
|
-
groupAllowFrom: acct?.groupAllowFrom || baseFildsDefault.groupAllowFrom,
|
|
43
|
-
requireMention: isEnabled(acct?.requireMention),
|
|
44
|
-
channelContext: acct?.channelContext || baseFildsDefault.channelContext,
|
|
45
|
-
};
|
|
46
|
-
};
|
|
24
|
+
|
|
47
25
|
const wsReadyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'] as const;
|
|
48
26
|
let wsNumber = 0;
|
|
49
27
|
|
|
@@ -288,7 +266,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
288
266
|
const wsEvent = json?.body?.event;
|
|
289
267
|
if (wsEvent === 'keepalive') return;
|
|
290
268
|
|
|
291
|
-
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, WebSocket[${wsId}] Received
|
|
269
|
+
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, WebSocket[${wsId}], Received event ${wsEvent}, body ${JSON.stringify(json?.body, null, 2)}`);
|
|
292
270
|
|
|
293
271
|
if (!json?.header || !wsEvent || !json?.body?.data) {
|
|
294
272
|
return log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, WebSocket[${wsId}] invalid message`);
|
package/src/confs.ts
CHANGED
|
@@ -42,7 +42,10 @@ const baseFields = {
|
|
|
42
42
|
default: 'channel',
|
|
43
43
|
enum: ['channel', 'thread']
|
|
44
44
|
},
|
|
45
|
-
|
|
45
|
+
emojiReaction: {
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
default: true,
|
|
48
|
+
},
|
|
46
49
|
};
|
|
47
50
|
const baseFieldsKeys = Object.keys(baseFields);
|
|
48
51
|
export const baseFildsDefault = {} as Record<string, any>;
|
|
@@ -86,7 +89,7 @@ const fieldsUiHints = {
|
|
|
86
89
|
advanced: true,
|
|
87
90
|
},
|
|
88
91
|
groupAllowFrom: {
|
|
89
|
-
help: '
|
|
92
|
+
help: '群组/团队白名单-包含群ID、团队ID或频道ID(仅在 groupPolicy=allowlist 生效)',
|
|
90
93
|
order: 14,
|
|
91
94
|
advanced: true,
|
|
92
95
|
},
|
|
@@ -95,9 +98,14 @@ const fieldsUiHints = {
|
|
|
95
98
|
order: 15,
|
|
96
99
|
advanced: true,
|
|
97
100
|
},
|
|
101
|
+
emojiReaction: {
|
|
102
|
+
help: '在收到消息后,大模型给出反应结果前,先对原消息发送一个”收到“的表情回复。',
|
|
103
|
+
order: 16,
|
|
104
|
+
advanced: true,
|
|
105
|
+
},
|
|
98
106
|
};
|
|
99
107
|
|
|
100
|
-
/* 多账户管理
|
|
108
|
+
/* 多账户管理 */
|
|
101
109
|
const accountsFieldsUiHints = {} as Record<string, any>;
|
|
102
110
|
function isValidObjKey(key: string | number | symbol , object: object): key is keyof typeof object {
|
|
103
111
|
return key in object;
|
|
@@ -105,7 +113,6 @@ function isValidObjKey(key: string | number | symbol , object: object): key is k
|
|
|
105
113
|
for (const k in fieldsUiHints) {
|
|
106
114
|
if (isValidObjKey(k, fieldsUiHints)) accountsFieldsUiHints[`accounts.*.${k}` as string] = fieldsUiHints[k];
|
|
107
115
|
}
|
|
108
|
-
*/
|
|
109
116
|
|
|
110
117
|
export const configSchema = {
|
|
111
118
|
schema: {
|
|
@@ -113,7 +120,7 @@ export const configSchema = {
|
|
|
113
120
|
additionalProperties: false,
|
|
114
121
|
properties: {
|
|
115
122
|
...fields,
|
|
116
|
-
/* 基础配置信息中,准备 accounts
|
|
123
|
+
/* 基础配置信息中,准备 accounts 配置支持多账户管理 */
|
|
117
124
|
accounts: {
|
|
118
125
|
type: 'object',
|
|
119
126
|
additionalProperties: {
|
|
@@ -121,17 +128,17 @@ export const configSchema = {
|
|
|
121
128
|
additionalProperties: false,
|
|
122
129
|
properties: { ...fields },
|
|
123
130
|
},
|
|
124
|
-
},
|
|
131
|
+
},
|
|
125
132
|
},
|
|
126
133
|
},
|
|
127
134
|
uiHints: {
|
|
128
135
|
...fieldsUiHints,
|
|
129
|
-
/* 基础配置信息中,准备 accounts
|
|
136
|
+
/* 基础配置信息中,准备 accounts 配置支持多账户管理 */
|
|
130
137
|
accounts: {
|
|
131
138
|
help: 'Accounts(多账户配置)',
|
|
132
139
|
order: 30,
|
|
133
140
|
advanced: true
|
|
134
141
|
},
|
|
135
|
-
...accountsFieldsUiHints,
|
|
142
|
+
...accountsFieldsUiHints,
|
|
136
143
|
},
|
|
137
144
|
};
|