@qihoo/tuitui-openclaw-channel 1.0.14 → 1.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihoo/tuitui-openclaw-channel",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "maintainers": [
5
5
  {
6
6
  "name": "huzunjie",
package/src/confs.ts CHANGED
@@ -39,7 +39,7 @@ const baseFields = {
39
39
  },
40
40
  channelContext: {
41
41
  type: 'string',
42
- default: 'channel',
42
+ default: 'thread',
43
43
  enum: ['channel', 'thread']
44
44
  },
45
45
  emojiReaction: {
@@ -0,0 +1,69 @@
1
+ import {
2
+ DEFAULT_GROUP_HISTORY_LIMIT,
3
+ recordPendingHistoryEntryIfEnabled,
4
+ type HistoryEntry,
5
+ } from "openclaw/plugin-sdk/reply-history";
6
+
7
+ // 每个 accountId 独立的 group history map,防止多账户冲突
8
+ const accountGroupHistories = new Map<string, Map<string, HistoryEntry[]>>();
9
+
10
+ /**
11
+ * 记录没有被@,被忽略的群消息到对应 account 的 pending history 中
12
+ */
13
+ export async function addUnmentionedHistory(apiRuntime, accountId: string, chatId: string, tuituiUserName: any, tuituiAccount: string, text: any, timestamp :any) {
14
+ const cfg = await apiRuntime.config.loadConfig();
15
+ const senderDesc = tuituiUserName ? `${tuituiUserName} (${tuituiAccount})` : tuituiAccount;
16
+
17
+ const entry: HistoryEntry = {
18
+ sender: senderDesc,
19
+ body: text,
20
+ timestamp: timestamp ? Number(timestamp) : Date.now(),
21
+ };
22
+
23
+ // 为每个 accountId 维护独立的 group history map
24
+ let groupHistories = accountGroupHistories.get(accountId);
25
+ if (!groupHistories) {
26
+ groupHistories = new Map<string, HistoryEntry[]>();
27
+ accountGroupHistories.set(accountId, groupHistories);
28
+ }
29
+
30
+ const historyLimit = resolveHistoryLimit(cfg);
31
+
32
+ // 这个API内部没啥神秘的,只是帮你维护map变量。openclaw本身不持有这些数据。
33
+ recordPendingHistoryEntryIfEnabled({
34
+ historyMap: groupHistories,
35
+ historyKey: chatId,
36
+ limit: historyLimit,
37
+ entry: entry,
38
+ });
39
+ }
40
+
41
+ /**
42
+ * 获取指定 account 和群组的 pending history,格式化为 ctx.InboundHistory 所需的格式
43
+ */
44
+ export function popUnmentionedHistories(accountId: string, chatId: string): {sender: string, body: string, timestamp?: number}[] {
45
+ const groupHistories = accountGroupHistories.get(accountId);
46
+ if (!groupHistories) {
47
+ return [];
48
+ }
49
+ const his = groupHistories.get(chatId);
50
+ if (!his) {
51
+ return [];
52
+ }
53
+
54
+ groupHistories.delete(chatId);
55
+
56
+ return his.map((entry) => ({
57
+ sender: entry.sender,
58
+ body: entry.body,
59
+ timestamp: entry.timestamp,
60
+ }));
61
+ }
62
+
63
+ /**
64
+ * 从配置中解析 history limit
65
+ * dashboard->messages->groupChat->historyLimit
66
+ */
67
+ export function resolveHistoryLimit(cfg: any): number {
68
+ return cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
69
+ }
package/src/inbound.ts CHANGED
@@ -23,6 +23,10 @@ import {
23
23
  teamsParseChatId,
24
24
  } from "./outbound";
25
25
  import { parseAllowFroms } from './utils';
26
+ import {
27
+ addUnmentionedHistory,
28
+ popUnmentionedHistories,
29
+ } from "./histories";
26
30
 
27
31
  /** 子函数共享的可变上下文,子函数直接修改字段,外层读取结果 */
28
32
  interface ChatPayload {
@@ -30,12 +34,14 @@ interface ChatPayload {
30
34
  chatId: string | undefined;
31
35
  text: string | undefined;
32
36
  groupName: string | undefined;
37
+ channelName: string | undefined;
33
38
  mediaUrls: string[] | undefined;
34
39
  replyToId: string | undefined; // 引用ID
35
40
  msgId?: string; // 消息ID
36
41
  tuituiAccount: string;
37
42
  tuituiUid: string | undefined;
38
43
  tuituiUserName: string | undefined;
44
+ timestamp: Number | undefined;
39
45
  }
40
46
 
41
47
  export interface InboundAccount {
@@ -227,6 +233,7 @@ const parseAndVerifyPayload: Record<string, Function> = {
227
233
  payload.chatId = chatId;
228
234
  payload.groupName = msgData.group_name;
229
235
  payload.msgId = msgData.msgid;
236
+ payload.text = buildMessageBody(msgData);
230
237
  log?.debug?.(
231
238
  `[${CHANNEL_ID}] AccountId: ${accountId}, inbound group_chat:
232
239
  tuituiAccount=${payload.tuituiAccount},
@@ -249,7 +256,8 @@ const parseAndVerifyPayload: Record<string, Function> = {
249
256
  }
250
257
 
251
258
  if (!msgData.at_me && account.requireMention) {
252
- log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore group message (not mentioned)`);
259
+ log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore group message (not mentioned), add to history key: ${chatId}`);
260
+ await addUnmentionedHistory(apiRuntime, accountId, chatId, payload.tuituiUserName, payload.tuituiAccount, payload.text, payload.timestamp);
253
261
  return false;
254
262
  }
255
263
 
@@ -268,19 +276,20 @@ const parseAndVerifyPayload: Record<string, Function> = {
268
276
  return false;
269
277
  }
270
278
 
271
- payload.text = buildMessageBody(msgData);
279
+
272
280
 
273
281
  return true;
274
282
  },
275
283
  teams_post_create: async (payload: ChatPayload, msgData: any, account: InboundAccount, apiRuntime: any, log: any): Promise<boolean> => {
276
284
  const { accountId, groupPolicy, groupAllowFrom } = account;
277
285
  payload.chatType = CHAT_TYPE_CHANNEL;
278
- const { team_id, channel_id, parent_id, post_id, content } = msgData;
286
+ const { team_id, channel_id, parent_id, post_id, content, team_name, channel_name } = msgData;
279
287
  const thread_id = (parent_id && parent_id != "0")?parent_id: post_id;
280
288
  const chatId = teamsBuildChatId(team_id, channel_id, thread_id);
281
289
  payload.chatId = chatId;
282
290
  payload.msgId = post_id;
283
291
  payload.text = content;
292
+ payload.channelName = channel_name;
284
293
  payload.replyToId = "";
285
294
  log?.debug?.(
286
295
  `[${CHANNEL_ID}] AccountId: ${accountId}, inbound teams:
@@ -305,7 +314,8 @@ const parseAndVerifyPayload: Record<string, Function> = {
305
314
  }
306
315
 
307
316
  if (!msgData.at_me && account.requireMention) {
308
- log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned)`);
317
+ log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned), add to history key: ${chatId}`);
318
+ await addUnmentionedHistory(apiRuntime, accountId, chatId, payload.tuituiUserName, payload.tuituiAccount, payload.text, payload.timestamp);
309
319
  return false;
310
320
  }
311
321
 
@@ -356,11 +366,13 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
356
366
  chatId: undefined,
357
367
  text: undefined,
358
368
  groupName: undefined,
369
+ channelName: undefined,
359
370
  mediaUrls: getMediaUrls(msgData),
360
371
  replyToId: ref?.is_me && ref?.msgid ? ref.msgid : undefined,
361
372
  tuituiAccount: msg.user_account || "",
362
373
  tuituiUid: msg.uid || "",
363
374
  tuituiUserName: msg.user_name || "",
375
+ timestamp: msg.timestamp ? parseInt(msg.timestamp, 10)*1000 : undefined
364
376
  };
365
377
  const wsEvent = json.body.event;
366
378
  // 按event类型,标准化并校验基础数据
@@ -388,6 +400,14 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
388
400
  const sessionKey = getSessionKey(cfg, payload, account, apiRuntime);
389
401
  log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, dispatching to agent session=${sessionKey}, chatType: ${payload.chatType}, chatId ${payload.chatId}, routeSenderFrom: ${routeSenderFrom}`);
390
402
 
403
+ if(payload.text?.startsWith("/new") || payload.text?.endsWith("/new")) {
404
+ // 清理agent上下文时,同时清空历史
405
+ popUnmentionedHistories(accountId, payload.chatId);
406
+ }
407
+
408
+ // 字段意义 参考 openclaw 代码
409
+ // src\auto-reply\reply\inbound-meta.ts
410
+ // buildInboundUserContextPrefix()
391
411
  const ctx: any = {
392
412
  Body: payload.text! || ' ',
393
413
  From: String(routeSenderFrom),
@@ -401,13 +421,20 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
401
421
  Surface: CHANNEL_ID,
402
422
  Provider: CHANNEL_ID,
403
423
  SenderName: payload.tuituiUserName,
404
- MsgUname: payload.tuituiAccount,
424
+ SenderId: payload.tuituiAccount,
425
+ SenderUsername: payload.tuituiAccount,
405
426
  UserAccount: payload.tuituiAccount,
427
+ Timestamp: payload.timestamp,
406
428
  CommandAuthorized: true, // 允许 /new 等内置命令
407
429
  };
408
430
  if (payload.chatType == CHAT_TYPE_GROUP && payload.chatId) {
409
431
  ctx.GroupSubject = payload.groupName;
410
432
  ctx.GroupId = payload.chatId;
433
+ ctx.InboundHistory = popUnmentionedHistories(accountId, payload.chatId);
434
+ }
435
+ if (payload.chatType == CHAT_TYPE_CHANNEL) {
436
+ ctx.GroupChannel = payload.channelName;
437
+ ctx.InboundHistory = popUnmentionedHistories(accountId, payload.chatId);
411
438
  }
412
439
  if (payload.mediaUrls?.length) ctx.MediaUrls = payload.mediaUrls;
413
440
  if (payload.replyToId) ctx.ReplyToId = payload.replyToId;
package/src/outbound.ts CHANGED
@@ -491,6 +491,7 @@ export async function sendMediaMsg(
491
491
  atList?: string[],
492
492
  ): Promise<void> {
493
493
  if (!chatId) return console.error(`[${CHANNEL_ID}] sendMediaMsg Error ${auditCtx}: Missing "target"`);
494
+ console.log(`[${CHANNEL_ID}] sendMediaMsg ${chatType} ${chatId} ${auditCtx} uploading`);
494
495
  // Check if mediaUrl looks like an image
495
496
  const isImage = /^data:image\//i.test(mediaUrl) || /\.(jpg|jpeg|png|gif)(?:$|[?#])/i.test(mediaUrl);
496
497
  const mediaType = isImage ? 'image' : 'file';