@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 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 webhook`,
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
  };
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "tuitui-openclaw-channel",
3
3
  "channels": ["tuitui"],
4
+ "skills": ["./skills"],
4
5
  "configSchema": {
5
6
  "type": "object",
6
7
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihoo/tuitui-openclaw-channel",
3
- "version": "1.0.12",
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` |
@@ -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
- DEFAULT_ACCOUNT_ID,
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
- const resolveAccount = (cfg: any, accountId?: string | null) => {
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 message: ${wsData}`);
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: '群组/团队白名单-群ID和团队ID(仅在 groupPolicy=allowlist 生效)',
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
  };