@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.
@@ -43,7 +43,7 @@
43
43
  "enum": ["channel", "thread"]
44
44
  },
45
45
  "emojiReaction": { "type": "boolean", "default": true },
46
- "monitorEnabled": { "type": "boolean", "default": false },
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": false }
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": "是否开启agent事件信息上报(默认 false)。修改后必须重启网关才能生效。",
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": "是否开启agent事件信息上报(默认 false)。修改后必须重启网关才能生效。",
188
+ "help": "是否开启频道agent事件上报,用于频道展示agent执行中间步骤(默认 true)。",
189
189
  "order": 311,
190
190
  "advanced": true
191
191
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihoo/tuitui-openclaw-channel",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "maintainers": [
5
5
  {
6
6
  "name": "huzunjie",
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 = false;
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?.groupPolic || groupPolicyDefault,
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 条(默认 100)
6
- * - 距上次 flush 超过 BATCH_INTERVAL_MS(默认 15s)
5
+ * - 队列积累达 BATCH_MAX_SIZE
6
+ * - 距上次 flush 超过 BATCH_INTERVAL_MS
7
7
  *
8
- * 需在配置中将 monitorEnabled 设为 true 才会上报(默认 false)。
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
- * 从 ctx.sessionKey 中解析出 agentId。
180
- * sessionKey 格式:agent:<agentId>:tuitui:<accountId>:<chatType>:<chatId>
182
+ * 从 sessionKey 字符串中解析出 agentId。
183
+ * sessionKey 格式:agent:<agentId>:...
181
184
  */
182
- function extractAgentIdFromCtx(ctx: unknown): string | null {
183
- const c = ctx as any;
184
- if (typeof c?.sessionKey === 'string') {
185
- const parts = c.sessionKey.split(':');
186
- const agentIdx = parts.indexOf('agent');
187
- if (agentIdx !== -1 && agentIdx + 1 < parts.length && parts[agentIdx + 1]) {
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 = 500;
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 事件后入队(targets 由注册时闭包捕获)
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
- const agentId = extractAgentIdFromCtx(ctx);
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) => { report(targets, eventType, ctx, event); });
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
- console.log(`[${CHANNEL_ID}][monitor] Monitor hooks registered for ${targets.length} account(s): ${targets.map(t => t.accountId).join(', ')} (batch_size=${BATCH_MAX_SIZE} interval=${BATCH_INTERVAL_MS}ms).`);
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
- const channel_info = await getChannelInfoById(account, params?.channel_id);
78
- const team_id = channel_info?.team_id;
79
- const chatId = teamsBuildChatId(team_id, params?.channel_id, params?.parent_id);
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