@qihoo/tuitui-openclaw-channel 1.0.30 → 1.0.32

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.30",
3
+ "version": "1.0.32",
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/chat_base.ts CHANGED
@@ -33,18 +33,38 @@ export function teamsParseChatId(chatId: string): TuiTuiTeamsTarget {
33
33
  }
34
34
 
35
35
 
36
- export function parseChannelIdBySessionKey(str: string): string {
37
- // 检查是否包含必须的格式 "tuitui:channel:"
36
+ export interface TuiTuiSessionKeyInfo {
37
+ chat_type: ChatType,
38
+ channel_id: string;
39
+ thread_id: string;
40
+ }
41
+
42
+ // 暂时只支持频道(后续可以完善)
43
+ // 格式 agent:coder:tuitui:channel:123:thread:456
44
+ export function parseSessionKey(str: string): TuiTuiSessionKeyInfo {
45
+ const info: TuiTuiSessionKeyInfo = { chat_type: CHAT_TYPE_DIRECT, channel_id: '', thread_id: '' };
46
+
38
47
  if (!str.includes('tuitui:channel:')) {
39
- return "";
48
+ return info;
40
49
  }
41
-
50
+
51
+ info.chat_type = CHAT_TYPE_CHANNEL;
52
+
42
53
  const parts = str.split(':');
43
54
  const channelIndex = parts.findIndex(part => part === 'channel');
44
-
45
55
  if (channelIndex !== -1 && parts[channelIndex + 1]) {
46
- return parts[channelIndex + 1];
56
+ info.channel_id = parts[channelIndex + 1];
47
57
  }
48
-
49
- return "";
58
+
59
+ const threadIndex = parts.findIndex(part => part === 'thread');
60
+ if (threadIndex !== -1 && parts[threadIndex + 1]) {
61
+ info.thread_id = parts[threadIndex + 1];
62
+ }
63
+
64
+ return info;
65
+ }
66
+
67
+ export function parseChannelIdBySessionKey(str: string): string {
68
+ const info = parseSessionKey(str);
69
+ return info.channel_id;
50
70
  }
@@ -1,6 +1,8 @@
1
1
  import { CHANNEL_ID } from "./const";
2
2
  import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,ChatType, teamsParseChatId, guessChatType, teamsBuildChatId} from "./chat_base"
3
3
  import { tuituiRobotApi } from "./robot_api"
4
+ import { parseSessionKey } from "./chat_base"
5
+ import {getChannelInfoByChannelId} from "./robot_helper"
4
6
  import type { TuiTuiMessageData} from './types';
5
7
 
6
8
  export interface TuiTuiChatRecordMessage {
@@ -236,6 +238,7 @@ export async function getChannelInfoById(account: any, channel_id: string): Prom
236
238
  }
237
239
 
238
240
  interface TeamsPostChainItem {
241
+ from_uid: string;
239
242
  post_id: string;
240
243
  time: string;
241
244
  last_reply_time: string; // 只有主贴有最后回帖时间,所有的回帖更新的都是主贴的属性
@@ -261,6 +264,7 @@ function parsePost(item: any): TeamsPostChainItem[] {
261
264
  const posts: TeamsPostChainItem[] = [];
262
265
 
263
266
  posts.push({
267
+ from_uid: topic.from_uid ?? '',
264
268
  post_id: topic.post_id ?? '',
265
269
  time: topic.create_time ?? '',
266
270
  last_reply_time: topic.last_reply_time ?? '',
@@ -271,6 +275,7 @@ function parsePost(item: any): TeamsPostChainItem[] {
271
275
 
272
276
  for (const post of [...replyList].reverse()) {
273
277
  posts.push({
278
+ from_uid: post.from_uid ?? '',
274
279
  post_id: post.post_id ?? '',
275
280
  time: post.create_time ?? '',
276
281
  last_reply_time : '',
@@ -365,12 +370,26 @@ async function getPostChain(
365
370
 
366
371
  const datas = data.datas ?? {};
367
372
 
368
- const posts = parsePost(datas)
373
+ const posts = parsePost(datas)
369
374
 
370
375
  console.log(`[${CHANNEL_ID}] getPostChain result: ${posts.length} posts`, posts);
371
376
  return posts;
372
377
  }
373
378
 
379
+ export async function getPostChainBySessionKey(
380
+ account: any,
381
+ sessionKey: string,
382
+ ): Promise<TeamsPostChainItem[]> {
383
+ const session_info = parseSessionKey(sessionKey);
384
+
385
+ const channelInfo = await getChannelInfoByChannelId(account, session_info.channel_id);
386
+ if (!channelInfo) {
387
+ throw new Error(`无法获取频道信息`);
388
+ }
389
+
390
+ return await getPostChain(account, channelInfo.team_id, session_info.channel_id, session_info.thread_id);
391
+ }
392
+
374
393
  export async function getPostChainByChatId(
375
394
  account: any,
376
395
  chatId: string,
package/src/filespace.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CHANNEL_ID } from "./const";
2
2
  import { tuituiRobotApi, downloadUrl, tuituiRobotUpload} from "./robot_api"
3
- import {parseChannelIdBySessionKey} from "./chat_base"
3
+ import {CHAT_TYPE_CHANNEL, parseSessionKey} from "./chat_base"
4
+ import {getChannelInfoByChannelId} from "./robot_helper"
4
5
 
5
6
  export const NODE_TYPE_DIR = '1';
6
7
  export const NODE_TYPE_FILE = '2';
@@ -51,29 +52,21 @@ export function flattenFileSpaceList(list: any[]): FileSpaceItem[] {
51
52
  }));
52
53
  }
53
54
 
54
-
55
-
56
- async function getChannelInfoBySessionKey(account: any, sessionKey: string) {
57
- const channel_id = parseChannelIdBySessionKey(sessionKey);
58
- if(!channel_id) {
59
- return null;
60
- }
61
-
62
- const payload = {channel_id: channel_id};
63
- const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
64
- return body?.datas?.info;
65
- }
66
-
67
55
  export async function file_space_list(
68
56
  account: any,
69
57
  sessionKey: string
70
58
  ): Promise<any> {
71
59
 
72
- const channelInfo = await getChannelInfoBySessionKey(account, sessionKey);
73
- if (!channelInfo) {
60
+ const session_info = parseSessionKey(sessionKey);
61
+ if(session_info.chat_type != CHAT_TYPE_CHANNEL) {
74
62
  throw new Error(`私聊、群聊会话不支持共享空间。仅团队频道支持。`);
75
63
  }
76
64
 
65
+ const channelInfo = await getChannelInfoByChannelId(account, session_info.channel_id);
66
+ if (!channelInfo) {
67
+ throw new Error(`无法获取频道信息`);
68
+ }
69
+
77
70
  const payload = {
78
71
  space_id: channelInfo.team_id,
79
72
  space_type: SPACE_TYPE_TEAM,
@@ -81,6 +74,8 @@ export async function file_space_list(
81
74
 
82
75
  const data = await tuituiRobotApi(account, '/file_space/node/list', payload);
83
76
 
77
+ //console.log(JSON.stringify(data));
78
+
84
79
  const list = data?.datas?.list;
85
80
  const flat_list = flattenFileSpaceList(list);
86
81
 
@@ -98,7 +93,8 @@ export async function file_space_list(
98
93
  async function ensureFolderPath(
99
94
  account: any,
100
95
  spaceId: string,
101
- folderPath: string
96
+ folderPath: string,
97
+ source: string
102
98
  ): Promise<string> {
103
99
  // 标准化路径
104
100
  const normalizedPath = folderPath.startsWith('/') ? folderPath : '/' + folderPath;
@@ -151,6 +147,7 @@ async function ensureFolderPath(
151
147
  node_type: NODE_TYPE_DIR,
152
148
  name: folderName,
153
149
  parent_id: currentParentId,
150
+ source: source,
154
151
  };
155
152
 
156
153
  const createResult = await tuituiRobotApi(account, '/file_space/node/add', createFolderPayload);
@@ -184,11 +181,18 @@ export async function file_space_add(
184
181
  cloud_filepath: string,
185
182
  url_or_localpath: string,
186
183
  ): Promise<any> {
187
- const channelInfo = await getChannelInfoBySessionKey(account, sessionKey);
188
- if (!channelInfo) {
184
+ const session_info = parseSessionKey(sessionKey);
185
+ if(session_info.chat_type != CHAT_TYPE_CHANNEL) {
189
186
  throw new Error(`私聊、群聊会话不支持共享空间。仅团队频道支持。`);
190
187
  }
191
188
 
189
+ const channelInfo = await getChannelInfoByChannelId(account, session_info.channel_id);
190
+ if (!channelInfo) {
191
+ throw new Error(`无法获取频道信息`);
192
+ }
193
+
194
+ const source = session_info.thread_id;
195
+
192
196
  // 1. 解析云路径,分离目录和文件名
193
197
  const normalizedPath = cloud_filepath.startsWith('/')
194
198
  ? cloud_filepath
@@ -201,7 +205,7 @@ export async function file_space_add(
201
205
  : '';
202
206
 
203
207
  // 2. 确保目标目录存在,获取 parent_id
204
- const parentId = await ensureFolderPath(account, channelInfo.team_id, folderPath);
208
+ const parentId = await ensureFolderPath(account, channelInfo.team_id, folderPath, source);
205
209
 
206
210
  // 3. 上传文件获取 fid
207
211
  const isImage = /^data:image\//i.test(url_or_localpath) || /\.(jpg|jpeg|png|gif)(?:$|[?#])/i.test(url_or_localpath);
@@ -219,6 +223,7 @@ export async function file_space_add(
219
223
  name: filename,
220
224
  fid: fid,
221
225
  parent_id: parentId,
226
+ source: source,
222
227
  };
223
228
 
224
229
  const body = await tuituiRobotApi(account, '/file_space/node/add', filePayload);
package/src/inbound.ts CHANGED
@@ -19,11 +19,13 @@ import {
19
19
  sendPageMsg,
20
20
  sendMediaMsg,
21
21
  get_announcement,
22
+ get_team_members,
22
23
  } from "./outbound";
23
24
  import { parseChatMessageBody } from './inbound_body_parse';
24
25
  import { parseAllowFroms } from './utils';
25
26
  import { addUnmentionedHistory, popUnmentionedHistories } from "./histories";
26
27
  import { StringDeduplicator } from "./deduplicator"
28
+ import { getPostChainByChatId } from "./chat_record"
27
29
 
28
30
  // 会话上下文注入排重
29
31
  let _session_ctx_injected = new StringDeduplicator(1000, 3600);
@@ -177,19 +179,52 @@ async function sendSingleChatPairingMsg(account: any, payload: any, log: any, ap
177
179
  }
178
180
  }
179
181
 
180
- //目标账号是否允许私聊(私聊白名单、配对、或者open)
181
- async function isAllowAccount(tuituiAccount: any, apiRuntime: any, account: InboundAccount) {
182
- if(!tuituiAccount) return false;
182
+ //tuituiAccounts列表中是否存在至少1个允许私聊的用户(私聊白名单、配对、或者open)
183
+ async function isAllowOneOfAccount(tuituiAccounts: string[], apiRuntime: any, account: InboundAccount) {
184
+ if(!tuituiAccounts) return false;
183
185
  const { accountId, allowFrom, dmPolicy } = account;
184
186
  if (dmPolicy === 'open') return true;
185
187
 
186
- let storeAllowFrom: string[] = [];
188
+ // 是否包含任意一个元素
189
+ if (tuituiAccounts.some(item => allowFrom.includes(item))) return true;
190
+
187
191
  try {
188
- storeAllowFrom = parseAllowFroms(
192
+ let storeAllowFrom = parseAllowFroms(
189
193
  await apiRuntime?.channel?.pairing?.readAllowFromStore?.({ channel: CHANNEL_ID, accountId })
190
194
  );
195
+ if (tuituiAccounts.some(item => storeAllowFrom.includes(item))) return true;
191
196
  } catch {}
192
- return [...allowFrom, ...storeAllowFrom].includes(tuituiAccount);
197
+ return false;
198
+ }
199
+
200
+ async function isAllowAccount(tuituiAccount: string, apiRuntime: any, account: InboundAccount) {
201
+ return await isAllowOneOfAccount([tuituiAccount], apiRuntime, account);
202
+ }
203
+
204
+ async function isChannelCoordAuthorized(tuituiAccount: string, apiRuntime: any, account: InboundAccount, team_id:string, chat_id:string) {
205
+ // 协调员身份校验
206
+ if (tuituiAccount != "bot-FiwwCeDw" && tuituiAccount != "bot-sUwUeknd") return false;
207
+
208
+ // 当前 thread 中有白名单用户发帖,则协调员继承该权限
209
+ const [members, posts] = await Promise.all([
210
+ get_team_members(account, team_id),
211
+ getPostChainByChatId(account, chat_id),
212
+ ]);
213
+ if (!members?.length || !posts?.length) return false;
214
+
215
+ const from_uids = posts.map((p: any) => p.from_uid).filter(Boolean);
216
+ if (!from_uids.length) return false;
217
+
218
+ //console.log("from_uids", from_uids);
219
+
220
+ // 将 uid 映射到 user_account
221
+ const from_accounts: string[] = members
222
+ .filter((m: any) => from_uids.includes(m.uid) && m.account)
223
+ .map((m: any) => String(m.account));
224
+ if (!from_accounts.length) return false;
225
+
226
+ //console.log("from_accounts", from_accounts);
227
+ return await isAllowOneOfAccount(from_accounts, apiRuntime, account);
193
228
  }
194
229
 
195
230
 
@@ -386,6 +421,12 @@ async function parse_teams_post(payload: ChatPayload, msgData: any, account: Inb
386
421
  log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned)`);
387
422
  return false;
388
423
  }
424
+
425
+ // 频道中允许协调员@自己(且帖子里有白名单用户发帖)
426
+ if (await isChannelCoordAuthorized(tuituiAccount, apiRuntime, account, team_id, chatId)) {
427
+ return true;
428
+ }
429
+
389
430
  if (needPairingThrottle(accountId, chatId)) {
390
431
  log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, teams pairing throttled for teamsChatId=${chatId}`);
391
432
  return false;
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
@@ -272,19 +273,27 @@ function report(
272
273
  ctx: unknown,
273
274
  data: unknown,
274
275
  ): void {
275
- let agentId: string | null;
276
+ let sessionKey = "";
276
277
 
277
278
  // subagent hook 的 ctx 有 childSessionKey,普通 hook 有 sessionKey
278
279
  if (eventType === 'subagent_spawned' || eventType === 'subagent_ended') {
279
- agentId = extractAgentId((ctx as any)?.childSessionKey);
280
+ sessionKey = (ctx as any)?.childSessionKey
280
281
  } else {
281
- agentId = extractAgentId((ctx as any)?.sessionKey);
282
+ sessionKey = (ctx as any)?.sessionKey
282
283
  }
283
284
 
285
+ const agentId = extractAgentId(sessionKey);
284
286
  if (!agentId) return;
285
287
 
288
+ // 仅频道支持;私聊群聊不启用
289
+ const channelId = parseChannelIdBySessionKey(sessionKey);
290
+ if(!channelId) return;
291
+
292
+ const reportedAccountIds = new Map<string, boolean>();
286
293
  for (const target of targets) {
287
294
  if (target.agentId !== agentId) continue;
295
+ if (reportedAccountIds.has(target.accountId)) continue;
296
+ reportedAccountIds.set(target.accountId, true);
288
297
 
289
298
  const payload: MonitorPayload = {
290
299
  event: eventType,
package/src/outbound.ts CHANGED
@@ -234,3 +234,12 @@ export async function get_announcement(account: any, id: any, id_is_session: boo
234
234
  const announcement = body?.datas?.info?.announcement;
235
235
  return announcement;
236
236
  }
237
+
238
+
239
+ export async function get_team_members(account: any, team_id: string): Promise<any> {
240
+ const payload = {team_id: team_id};
241
+ const body = await tuituiRobotApi(account, '/teams/member/list', payload);
242
+ const members = body?.datas?.members;
243
+ //console.log("info", members);
244
+ return members;
245
+ }
package/src/robot_api.ts CHANGED
@@ -19,6 +19,8 @@ export function checkAccount(account: any, ctxTips: string = 'send text') {
19
19
  export async function tuituiRobotApi(account: any, api: string, payload: any, log: boolean = true) {
20
20
  checkAccount(account);
21
21
 
22
+ const startTime = Date.now(); // 记录开始时间
23
+
22
24
  if (log) {
23
25
  console.log(`[${CHANNEL_ID}] ${api} request`, payload);
24
26
  }
@@ -51,6 +53,11 @@ export async function tuituiRobotApi(account: any, api: string, payload: any, lo
51
53
  throw err;
52
54
  } finally {
53
55
  await release();
56
+ if (log) {
57
+ const endTime = Date.now(); // 记录结束时间
58
+ const duration = endTime - startTime; // 计算耗时
59
+ console.log(`[${CHANNEL_ID}] ${api} response took ${duration}ms`);
60
+ }
54
61
  }
55
62
  }
56
63
 
@@ -0,0 +1,7 @@
1
+ import { tuituiRobotApi} from "./robot_api"
2
+
3
+ export async function getChannelInfoByChannelId(account: any, channel_id: string) {
4
+ const payload = {channel_id: channel_id};
5
+ const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
6
+ return body?.datas?.info;
7
+ }
package/src/tools.ts CHANGED
@@ -2,9 +2,9 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
3
3
  import { resolveAccount } from "./accounts"
4
4
  import { Type } from "@sinclair/typebox";
5
-
5
+ import { CHANNEL_ID } from './const';
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;
@@ -154,6 +162,14 @@ export function registerTuituiTools(api: OpenClawPluginApi) {
154
162
  api.logger.debug?.("tuitui: Registered tool: No config available");
155
163
  return;
156
164
  }
165
+
166
+ const channels = api.config.channels || {};
167
+ const currChannel = channels[CHANNEL_ID] || {};
168
+ if (!currChannel?.appId && !currChannel?.accounts) {
169
+ // 无推推配置时,不需要注册工具防止污染上下文
170
+ // api.logger.info?.(`tuitui: ignore Registered tools`);
171
+ return
172
+ }
157
173
 
158
174
  api.registerTool(tuitui_im_get_messages_factory);
159
175
  api.registerTool(tuitui_send_channel_post_factory);
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