@qihoo/tuitui-openclaw-channel 1.0.29 → 1.0.31
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/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/accounts.ts +2 -2
- package/src/channel.ts +2 -1
- package/src/monitor.ts +44 -26
- package/src/tools.ts +12 -4
- package/src/websocket.ts +8 -0
package/openclaw.plugin.json
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"enum": ["channel", "thread"]
|
|
44
44
|
},
|
|
45
45
|
"emojiReaction": { "type": "boolean", "default": true },
|
|
46
|
-
"monitorEnabled": { "type": "boolean", "default":
|
|
46
|
+
"monitorEnabled": { "type": "boolean", "default": true },
|
|
47
47
|
"accounts": {
|
|
48
48
|
"type": "object",
|
|
49
49
|
"additionalProperties": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"enum": ["channel", "thread"]
|
|
80
80
|
},
|
|
81
81
|
"emojiReaction": { "type": "boolean", "default": true },
|
|
82
|
-
"monitorEnabled": { "type": "boolean", "default":
|
|
82
|
+
"monitorEnabled": { "type": "boolean", "default": true }
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"advanced": true
|
|
132
132
|
},
|
|
133
133
|
"monitorEnabled": {
|
|
134
|
-
"help": "
|
|
134
|
+
"help": "是否开启频道agent事件上报,用于频道展示agent执行中间步骤(默认 true)",
|
|
135
135
|
"order": 17,
|
|
136
136
|
"advanced": true
|
|
137
137
|
},
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"advanced": true
|
|
186
186
|
},
|
|
187
187
|
"accounts.*.monitorEnabled": {
|
|
188
|
-
"help": "
|
|
188
|
+
"help": "是否开启频道agent事件上报,用于频道展示agent执行中间步骤(默认 true)。",
|
|
189
189
|
"order": 311,
|
|
190
190
|
"advanced": true
|
|
191
191
|
}
|
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const dmPolicyDefault = 'pairing';
|
|
|
7
7
|
export const groupPolicyDefault = 'allowlist';
|
|
8
8
|
export const requireMentionDefault = true;
|
|
9
9
|
export const emojiReactionDefault = true;
|
|
10
|
-
export const monitorEnabledDefault =
|
|
10
|
+
export const monitorEnabledDefault = true;
|
|
11
11
|
|
|
12
12
|
const mergeArrs = (arr1: any, arr2: any) => {
|
|
13
13
|
return [...new Set([...(arr1 || []), ...arr2 || []])];
|
|
@@ -20,7 +20,7 @@ export const getAccountInfo = (acct?: any) => ({
|
|
|
20
20
|
dmPolicy: acct?.dmPolicy || dmPolicyDefault,
|
|
21
21
|
allowFrom: parseAllowFroms(acct?.allowFrom || []),
|
|
22
22
|
// 群组策略与白名单、群组级覆盖
|
|
23
|
-
groupPolicy: acct?.
|
|
23
|
+
groupPolicy: acct?.groupPolicy || groupPolicyDefault,
|
|
24
24
|
groupAllowFrom: parseAllowFroms(acct?.groupAllowFrom || []),
|
|
25
25
|
requireMention: isEnabled(acct?.requireMention ?? requireMentionDefault),
|
|
26
26
|
emojiReaction: isEnabled(acct?.emojiReaction ?? emojiReactionDefault),
|
package/src/channel.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
|
|
7
7
|
import { setAccountEnabledInConfigSection, deleteAccountFromConfigSection, OpenClawPluginApi } from 'openclaw/plugin-sdk/core';
|
|
8
8
|
import { CHANNEL_ID, CHANNEL_NAME } from "./const";
|
|
9
|
+
import { version } from "../package.json"
|
|
9
10
|
import { handleInboundMessage } from './inbound';
|
|
10
11
|
import { guessChatTypeV2 } from "./chat_record"
|
|
11
12
|
import {
|
|
@@ -55,7 +56,7 @@ function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
55
56
|
id: CHANNEL_ID,
|
|
56
57
|
label: CHANNEL_NAME,
|
|
57
58
|
selectionLabel: CHANNEL_NAME,
|
|
58
|
-
detailLabel: CHANNEL_NAME,
|
|
59
|
+
detailLabel: CHANNEL_NAME + " " + version,
|
|
59
60
|
docsPath: `/channels/${CHANNEL_ID}`,
|
|
60
61
|
blurb: `Connect to ${CHANNEL_NAME} bot via WebSocket`,
|
|
61
62
|
order: 100,
|
package/src/monitor.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* TuiTui Monitor — 将 OpenClaw Hook 事件批量上报到监控接口。
|
|
3
3
|
*
|
|
4
4
|
* 每个账户(appId/appSecret)维护独立队列,满足以下任一条件时批量上报:
|
|
5
|
-
* - 队列积累达 BATCH_MAX_SIZE
|
|
6
|
-
* - 距上次 flush 超过 BATCH_INTERVAL_MS
|
|
5
|
+
* - 队列积累达 BATCH_MAX_SIZE 条
|
|
6
|
+
* - 距上次 flush 超过 BATCH_INTERVAL_MS
|
|
7
7
|
*
|
|
8
|
-
* 需在配置中将 monitorEnabled 设为 true
|
|
8
|
+
* 需在配置中将 monitorEnabled 设为 true 才会上报
|
|
9
9
|
*
|
|
10
10
|
* 配置示例(openclaw.json):
|
|
11
11
|
* channels.tuitui.monitorEnabled: true
|
|
@@ -14,6 +14,7 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
|
14
14
|
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
|
|
15
15
|
import { CHANNEL_ID } from './const';
|
|
16
16
|
import { tuituiRobotApi } from './robot_api';
|
|
17
|
+
import {parseChannelIdBySessionKey} from "./chat_base"
|
|
17
18
|
|
|
18
19
|
// 批量上报参数
|
|
19
20
|
const BATCH_MAX_SIZE = 100; // 达到此条数立即 flush
|
|
@@ -34,7 +35,9 @@ export type MonitorEventType =
|
|
|
34
35
|
| 'after_tool_call'
|
|
35
36
|
| 'session_start'
|
|
36
37
|
| 'session_end'
|
|
37
|
-
| 'agent_end'
|
|
38
|
+
| 'agent_end'
|
|
39
|
+
| 'subagent_spawned'
|
|
40
|
+
| 'subagent_ended';
|
|
38
41
|
|
|
39
42
|
export interface MonitorPayload {
|
|
40
43
|
event: MonitorEventType;
|
|
@@ -176,26 +179,22 @@ function resolveMonitorTargets(cfg: any): MonitorTarget[] {
|
|
|
176
179
|
// ─────────────────────────────────────────────────────────────
|
|
177
180
|
|
|
178
181
|
/**
|
|
179
|
-
* 从
|
|
180
|
-
* sessionKey 格式:agent:<agentId
|
|
182
|
+
* 从 sessionKey 字符串中解析出 agentId。
|
|
183
|
+
* sessionKey 格式:agent:<agentId>:...
|
|
181
184
|
*/
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return parts[agentIdx + 1];
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
185
|
+
function extractAgentId(sessionKey: unknown): string | null {
|
|
186
|
+
if (typeof sessionKey !== 'string') return null;
|
|
187
|
+
const parts = sessionKey.split(':');
|
|
188
|
+
const idx = parts.indexOf('agent');
|
|
189
|
+
if (idx === -1 || !parts[idx + 1]) return null;
|
|
190
|
+
return parts[idx + 1];
|
|
192
191
|
}
|
|
193
192
|
|
|
194
193
|
// ─────────────────────────────────────────────────────────────
|
|
195
194
|
// 大字段裁剪
|
|
196
195
|
// ─────────────────────────────────────────────────────────────
|
|
197
196
|
|
|
198
|
-
const TRUNCATE_LIMIT =
|
|
197
|
+
const TRUNCATE_LIMIT = 64 * 1024; // 64 KB
|
|
199
198
|
|
|
200
199
|
function truncate(s: unknown): unknown {
|
|
201
200
|
if (typeof s !== 'string' || s.length <= TRUNCATE_LIMIT) return s;
|
|
@@ -265,7 +264,7 @@ function trimData(eventType: MonitorEventType, data: unknown): unknown {
|
|
|
265
264
|
}
|
|
266
265
|
|
|
267
266
|
// ─────────────────────────────────────────────────────────────
|
|
268
|
-
// 主入口:收到 hook
|
|
267
|
+
// 主入口:收到 hook 事件后入队
|
|
269
268
|
// ─────────────────────────────────────────────────────────────
|
|
270
269
|
|
|
271
270
|
function report(
|
|
@@ -274,11 +273,27 @@ function report(
|
|
|
274
273
|
ctx: unknown,
|
|
275
274
|
data: unknown,
|
|
276
275
|
): void {
|
|
277
|
-
|
|
276
|
+
let sessionKey = "";
|
|
277
|
+
|
|
278
|
+
// subagent hook 的 ctx 有 childSessionKey,普通 hook 有 sessionKey
|
|
279
|
+
if (eventType === 'subagent_spawned' || eventType === 'subagent_ended') {
|
|
280
|
+
sessionKey = (ctx as any)?.childSessionKey
|
|
281
|
+
} else {
|
|
282
|
+
sessionKey = (ctx as any)?.sessionKey
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const agentId = extractAgentId(sessionKey);
|
|
278
286
|
if (!agentId) return;
|
|
279
287
|
|
|
288
|
+
// 仅频道支持;私聊群聊不启用
|
|
289
|
+
const channelId = parseChannelIdBySessionKey(sessionKey);
|
|
290
|
+
if(!channelId) return;
|
|
291
|
+
|
|
292
|
+
const reportedAccountIds = new Map<string, boolean>();
|
|
280
293
|
for (const target of targets) {
|
|
281
294
|
if (target.agentId !== agentId) continue;
|
|
295
|
+
if (reportedAccountIds.has(target.accountId)) continue;
|
|
296
|
+
reportedAccountIds.set(target.accountId, true);
|
|
282
297
|
|
|
283
298
|
const payload: MonitorPayload = {
|
|
284
299
|
event: eventType,
|
|
@@ -295,16 +310,17 @@ function report(
|
|
|
295
310
|
}
|
|
296
311
|
|
|
297
312
|
// ─────────────────────────────────────────────────────────────
|
|
298
|
-
// 注册全量 Hook
|
|
313
|
+
// 注册全量 Hook
|
|
299
314
|
// ─────────────────────────────────────────────────────────────
|
|
300
315
|
|
|
301
316
|
export function registerMonitorHooks(api: OpenClawPluginApi) {
|
|
302
|
-
const targets = resolveMonitorTargets(api.config);
|
|
303
|
-
if (targets.length === 0) return;
|
|
304
|
-
|
|
305
|
-
// targets 通过闭包捕获,所有 hook 共享,零运行时开销
|
|
306
317
|
const on = (eventType: MonitorEventType) =>
|
|
307
|
-
api.on(eventType, (event, ctx) => {
|
|
318
|
+
api.on(eventType as any, (event: unknown, ctx: unknown) => {
|
|
319
|
+
const targets = resolveMonitorTargets(api.runtime.config.loadConfig());
|
|
320
|
+
if (targets.length > 0) {
|
|
321
|
+
report(targets, eventType, ctx, event);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
308
324
|
|
|
309
325
|
// ── Agent run 阶段 ────────────────────────────────────────
|
|
310
326
|
on('llm_input');
|
|
@@ -327,5 +343,7 @@ export function registerMonitorHooks(api: OpenClawPluginApi) {
|
|
|
327
343
|
on('session_start');
|
|
328
344
|
on('session_end');
|
|
329
345
|
|
|
330
|
-
|
|
346
|
+
// ── Subagent 阶段 ─────────────────────────────────────────
|
|
347
|
+
on('subagent_spawned');
|
|
348
|
+
on('subagent_ended');
|
|
331
349
|
}
|
package/src/tools.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveAccount } from "./accounts"
|
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
|
|
6
6
|
import {sendTextMsg, get_announcement} from "./outbound"
|
|
7
|
-
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL, teamsBuildChatId} from "./chat_base"
|
|
7
|
+
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,guessChatType, teamsBuildChatId} from "./chat_base"
|
|
8
8
|
import {getChatRecord, getChannelInfoById} from "./chat_record"
|
|
9
9
|
import {file_space_list, file_space_add} from "./filespace"
|
|
10
10
|
|
|
@@ -74,9 +74,17 @@ const tuitui_send_channel_post_factory = (ctx: OpenClawPluginToolContext) => {
|
|
|
74
74
|
return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
77
|
+
let chatId = "";
|
|
78
|
+
const guessType = guessChatType(params?.channel_id);
|
|
79
|
+
if(guessType == CHAT_TYPE_CHANNEL) {
|
|
80
|
+
// 解决badcase: 发个帖子,内容是个笑话
|
|
81
|
+
// 如果用户没明确说频道ID,大模型在频道调用发帖tool会传sessionKey过来
|
|
82
|
+
chatId = params?.channel_id;
|
|
83
|
+
} else {
|
|
84
|
+
const channel_info = await getChannelInfoById(account, params?.channel_id);
|
|
85
|
+
const team_id = channel_info?.team_id;
|
|
86
|
+
chatId = teamsBuildChatId(team_id, params?.channel_id, params?.parent_id);
|
|
87
|
+
}
|
|
80
88
|
|
|
81
89
|
const result: Promise<any> = sendTextMsg(account, chatId, CHAT_TYPE_CHANNEL, params?.markdown);
|
|
82
90
|
return result;
|
package/src/websocket.ts
CHANGED
|
@@ -73,6 +73,14 @@ export default function createWebSocket({ account, log, abortSignal, onConnected
|
|
|
73
73
|
if (firsEvtId) wsEvtIds.delete(firsEvtId);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// 收到包含心跳在内的任意消息,则重置心跳超时计时器,如果 300 秒内没有收到任何消息(包括心跳),则认为连接已失效
|
|
77
|
+
_clearTimeoutTimer();
|
|
78
|
+
_timeoutId = setTimeout(() => {
|
|
79
|
+
log?.warn?.(`[${CHANNEL_ID}] AccountId: ${accountId}, WebSocket[${wsId}] Heartbeat Timeout`);
|
|
80
|
+
_closeWS();
|
|
81
|
+
_restartWS(1e3);
|
|
82
|
+
}, 3e5); // 300秒心跳超时
|
|
83
|
+
|
|
76
84
|
const wsEvent = json?.body?.event;
|
|
77
85
|
if (wsEvent === 'keepalive') return;
|
|
78
86
|
|