@zeyiy/openclaw-channel 0.3.6 → 0.3.7

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/dist/config.js CHANGED
@@ -62,6 +62,7 @@ function envDefaultAccount() {
62
62
  platformID,
63
63
  enabled: true,
64
64
  requireMention: true,
65
+ historyLimit: 20,
65
66
  botId,
66
67
  portalWsAddr,
67
68
  };
@@ -89,6 +90,7 @@ function normalizeAccount(accountId, raw) {
89
90
  const enabled = raw.enabled !== false;
90
91
  const requireMention = raw.requireMention !== false;
91
92
  const inboundWhitelist = normalizeInboundWhitelist(raw.inboundWhitelist);
93
+ const historyLimit = Math.max(1, Math.min(100, toFiniteNumber(raw.historyLimit, 20)));
92
94
  const botId = String(raw.botId ?? "").trim() || undefined;
93
95
  const portalWsAddr = String(raw.portalWsAddr ?? "").trim() || undefined;
94
96
  if (!userID)
@@ -103,6 +105,7 @@ function normalizeAccount(accountId, raw) {
103
105
  platformID,
104
106
  requireMention,
105
107
  inboundWhitelist,
108
+ historyLimit,
106
109
  botId,
107
110
  portalWsAddr,
108
111
  };
package/dist/inbound.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  import { type MessageItem } from "@openim/client-sdk";
2
- import type { OpenIMClientState } from "./types";
2
+ import type { GroupInfoCacheEntry, GroupMemberCacheEntry, OpenIMClientState } from "./types";
3
+ export declare function fetchGroupInfo(client: OpenIMClientState, groupID: string, logger?: any, forceRefresh?: boolean): Promise<GroupInfoCacheEntry | null>;
4
+ export declare function fetchGroupMembers(client: OpenIMClientState, groupID: string, logger?: any, forceRefresh?: boolean): Promise<GroupMemberCacheEntry | null>;
3
5
  export declare function processInboundMessage(api: any, client: OpenIMClientState, msg: MessageItem): Promise<void>;
package/dist/inbound.js CHANGED
@@ -1,4 +1,4 @@
1
- import { SessionType } from "@openim/client-sdk";
1
+ import { GroupMemberFilter, SessionType } from "@openim/client-sdk";
2
2
  import { appendFileSync } from "node:fs";
3
3
  import { sendTextToTarget } from "./media";
4
4
  import { formatSdkError } from "./utils";
@@ -14,6 +14,106 @@ const inboundDedup = new Map();
14
14
  const INBOUND_DEDUP_TTL_MS = 5 * 60 * 1000;
15
15
  const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
16
16
  const IMAGE_FETCH_TIMEOUT_MS = 15000;
17
+ // ── Group context caches ──────────────────────────────────────────────
18
+ const groupInfoCache = new Map();
19
+ const groupMemberCache = new Map();
20
+ const groupHistories = new Map();
21
+ const GROUP_CACHE_TTL = 60 * 60 * 1000; // 1 hour
22
+ const DEFAULT_HISTORY_LIMIT = 20;
23
+ const DEFAULT_HISTORY_PROMPT_TEMPLATE = `[Group Chat History] Below are messages from others since your last reply (sender is user ID, body is message content):\n\`\`\`json\n{messages}\n\`\`\`\nPlease respond to the current @mention based on this context.\n\n`;
24
+ export async function fetchGroupInfo(client, groupID, logger, forceRefresh = false) {
25
+ const cached = groupInfoCache.get(groupID);
26
+ if (!forceRefresh && cached && Date.now() - cached.fetchedAt < GROUP_CACHE_TTL)
27
+ return cached;
28
+ try {
29
+ const res = await client.sdk.getSpecifiedGroupsInfo([groupID]);
30
+ const groups = res?.data;
31
+ if (!Array.isArray(groups) || groups.length === 0)
32
+ return cached ?? null;
33
+ const g = groups[0];
34
+ const entry = {
35
+ groupName: g.groupName ?? "",
36
+ notification: g.notification ?? "",
37
+ introduction: g.introduction ?? "",
38
+ memberCount: g.memberCount ?? 0,
39
+ fetchedAt: Date.now(),
40
+ };
41
+ groupInfoCache.set(groupID, entry);
42
+ logger?.info?.(`[openim] group info cached: ${groupID} name=${entry.groupName} members=${entry.memberCount}`);
43
+ return entry;
44
+ }
45
+ catch (e) {
46
+ logger?.warn?.(`[openim] fetchGroupInfo failed for ${groupID}: ${formatSdkError(e)}`);
47
+ return cached ?? null;
48
+ }
49
+ }
50
+ export async function fetchGroupMembers(client, groupID, logger, forceRefresh = false) {
51
+ const cached = groupMemberCache.get(groupID);
52
+ if (!forceRefresh && cached && Date.now() - cached.fetchedAt < GROUP_CACHE_TTL)
53
+ return cached;
54
+ try {
55
+ const uidToName = new Map();
56
+ let offset = 0;
57
+ const pageSize = 200;
58
+ // Paginate until we have all members
59
+ for (;;) {
60
+ const res = await client.sdk.getGroupMemberList({ groupID, filter: GroupMemberFilter.All, offset, count: pageSize });
61
+ const members = res?.data;
62
+ if (!Array.isArray(members) || members.length === 0)
63
+ break;
64
+ for (const m of members) {
65
+ if (m.userID) {
66
+ uidToName.set(m.userID, m.nickname || m.userID);
67
+ }
68
+ }
69
+ if (members.length < pageSize)
70
+ break;
71
+ offset += pageSize;
72
+ }
73
+ if (uidToName.size === 0)
74
+ return cached ?? null;
75
+ const entry = { uidToName, fetchedAt: Date.now() };
76
+ groupMemberCache.set(groupID, entry);
77
+ logger?.info?.(`[openim] group members cached: ${groupID} count=${uidToName.size}`);
78
+ return entry;
79
+ }
80
+ catch (e) {
81
+ logger?.warn?.(`[openim] fetchGroupMembers failed for ${groupID}: ${formatSdkError(e)}`);
82
+ return cached ?? null;
83
+ }
84
+ }
85
+ function buildGroupSystemPrompt(info) {
86
+ const parts = [];
87
+ parts.push(`## 群聊上下文(Group Chat Context)\n`);
88
+ parts.push(`你当前在 OpenIM 群:\`${info.groupName}\`。`);
89
+ parts.push(`普通回复会自动发到该群,不需要额外操作。`);
90
+ parts.push(`只在需要对方回复你时才使用 @,仅仅提及某人时直接说名字即可。\n`);
91
+ if (info.notification) {
92
+ parts.push(`群公告:${info.notification}`);
93
+ }
94
+ if (info.introduction) {
95
+ parts.push(`群简介:${info.introduction}`);
96
+ }
97
+ return parts.join("\n");
98
+ }
99
+ function buildMemberListPrefix(uidToName) {
100
+ if (uidToName.size === 0)
101
+ return "";
102
+ const members = Array.from(uidToName.entries());
103
+ const memberLines = members
104
+ .map(([uid, name]) => ` ${name} (${uid})`)
105
+ .join("\n");
106
+ return `[Group Members]\n${memberLines}\n\nWhen mentioning a group member, use @userID (e.g. @${members[0][0]}). Do NOT use @displayName — only the numeric/alphanumeric userID works.\n\n`;
107
+ }
108
+ function buildHistoryPrefix(entries, uidToName) {
109
+ if (entries.length === 0)
110
+ return "";
111
+ const messagesJson = JSON.stringify(entries.map((e) => ({
112
+ sender: e.senderName ? `${e.senderName}(${e.sender})` : e.sender,
113
+ body: e.body,
114
+ })), null, 2);
115
+ return DEFAULT_HISTORY_PROMPT_TEMPLATE.replace("{messages}", messagesJson);
116
+ }
17
117
  function normalizeImageMimeType(value) {
18
118
  const mime = String(value ?? "").trim().toLowerCase();
19
119
  return mime.startsWith("image/") ? mime : undefined;
@@ -340,9 +440,95 @@ export async function processInboundMessage(api, client, msg) {
340
440
  return;
341
441
  }
342
442
  }
343
- else if (group && client.config.requireMention && !mentioned) {
344
- api.logger?.debug?.(`[openim] ignore group message: requireMention=true but bot not mentioned, groupID=${msg.groupID}`);
345
- return;
443
+ // ── Group context: fetch group info & members (cached) ────────────
444
+ let groupSystemPrompt;
445
+ let memberListPrefix = "";
446
+ let historyPrefix = "";
447
+ const historyLimit = client.config.historyLimit ?? DEFAULT_HISTORY_LIMIT;
448
+ if (group && msg.groupID) {
449
+ // Fetch group info → SystemPrompt (fire-and-forget-safe, uses cache)
450
+ const groupInfo = await fetchGroupInfo(client, msg.groupID, api.logger);
451
+ if (groupInfo) {
452
+ groupSystemPrompt = buildGroupSystemPrompt(groupInfo);
453
+ }
454
+ // Fetch group members → member list prefix (cached)
455
+ const memberData = await fetchGroupMembers(client, msg.groupID, api.logger);
456
+ if (memberData) {
457
+ memberListPrefix = buildMemberListPrefix(memberData.uidToName);
458
+ }
459
+ }
460
+ // ── requireMention history gating ─────────────────────────────────
461
+ if (group && client.config.requireMention) {
462
+ if (!mentioned) {
463
+ // Not @mentioned → cache message for later history injection, don't trigger LLM
464
+ const groupID = String(msg.groupID);
465
+ if (!groupHistories.has(groupID)) {
466
+ groupHistories.set(groupID, []);
467
+ }
468
+ const entries = groupHistories.get(groupID);
469
+ entries.push({
470
+ sender: String(msg.sendID),
471
+ senderName: String(msg.senderNickname || ""),
472
+ body: inbound.body,
473
+ timestamp: msg.sendTime || Date.now(),
474
+ });
475
+ // Sliding window
476
+ while (entries.length > historyLimit) {
477
+ entries.shift();
478
+ }
479
+ api.logger?.info?.(`[openim] [HISTORY] cached non-mention msg | group=${groupID} | sender=${msg.sendID} | cached=${entries.length}`);
480
+ return;
481
+ }
482
+ // @mentioned → build history prefix from cached + API messages
483
+ const groupID = String(msg.groupID);
484
+ let entries = groupHistories.get(groupID) ?? [];
485
+ // If cache is insufficient, supplement from API
486
+ const cacheInsufficient = entries.length < Math.ceil(historyLimit / 2);
487
+ if (cacheInsufficient) {
488
+ api.logger?.info?.(`[openim] [MENTION] cache insufficient (${entries.length}/${historyLimit}), fetching from API...`);
489
+ try {
490
+ const conversationID = `sg_${msg.groupID}`;
491
+ const res = await client.sdk.getAdvancedHistoryMessageList({
492
+ conversationID,
493
+ startClientMsgID: "",
494
+ count: historyLimit,
495
+ });
496
+ const apiMessages = res?.data?.messageList;
497
+ if (Array.isArray(apiMessages) && apiMessages.length > 0) {
498
+ // Filter out empty/meaningless messages
499
+ const filtered = apiMessages
500
+ .filter((m) => {
501
+ const body = String(m.textElem?.content ?? m.atTextElem?.text ?? m.content ?? "").trim();
502
+ return !!body;
503
+ })
504
+ .slice(-historyLimit);
505
+ entries = filtered.map((m) => ({
506
+ sender: String(m.sendID),
507
+ senderName: String(m.senderNickname || ""),
508
+ body: String(m.textElem?.content ?? m.atTextElem?.text ?? m.content ?? "[消息]").trim(),
509
+ timestamp: m.sendTime || Date.now(),
510
+ }));
511
+ api.logger?.info?.(`[openim] [MENTION] fetched ${entries.length} history messages from API`);
512
+ }
513
+ }
514
+ catch (e) {
515
+ api.logger?.warn?.(`[openim] [MENTION] API history fetch failed: ${formatSdkError(e)}`);
516
+ }
517
+ }
518
+ // Take last N entries
519
+ if (entries.length > historyLimit) {
520
+ entries = entries.slice(-historyLimit);
521
+ groupHistories.set(groupID, entries);
522
+ }
523
+ const memberUidToName = groupMemberCache.get(groupID)?.uidToName ?? new Map();
524
+ historyPrefix = buildHistoryPrefix(entries, memberUidToName);
525
+ if (historyPrefix) {
526
+ api.logger?.info?.(`[openim] [MENTION] injected history context | ${entries.length} messages | ${historyPrefix.length} chars`);
527
+ }
528
+ }
529
+ else if (group && !client.config.requireMention && !mentioned) {
530
+ // requireMention=false: all messages trigger LLM, no history gating needed
531
+ // (no return, fall through to dispatch)
346
532
  }
347
533
  // 会话隔离:群聊按 groupID 分 session,私聊按发送者 ID 分 session
348
534
  const baseSessionKey = group ? `openim:group:${msg.groupID}`.toLowerCase() : `openim:dm:${msg.sendID}`.toLowerCase();
@@ -367,7 +553,12 @@ export async function processInboundMessage(api, client, msg) {
367
553
  const mediaResult = await materializeInboundMedia(inbound.media);
368
554
  const warningText = mediaResult.warnings.map((warning) => `[Media fetch failed] ${warning}`).join("\n");
369
555
  const rawBody = warningText ? `${inbound.body}\n${warningText}` : inbound.body;
370
- const body = buildTextEnvelope(runtime, cfg, fromLabel, senderId, timestamp, rawBody, chatType);
556
+ // Envelope wraps only the raw message text (with sender prefix)
557
+ const envelopedBody = buildTextEnvelope(runtime, cfg, fromLabel, senderId, timestamp, rawBody, chatType);
558
+ // Prepend member list + history BEFORE the envelope so they appear cleanly above the sender line
559
+ const body = (memberListPrefix || historyPrefix)
560
+ ? (memberListPrefix + historyPrefix + envelopedBody)
561
+ : envelopedBody;
371
562
  if (mediaResult.warnings.length > 0) {
372
563
  for (const warning of mediaResult.warnings) {
373
564
  api.logger?.warn?.(`[openim] inbound media fetch failed: ${warning}`);
@@ -375,13 +566,14 @@ export async function processInboundMessage(api, client, msg) {
375
566
  }
376
567
  const ctxPayload = {
377
568
  Body: body,
569
+ BodyForAgent: body,
378
570
  RawBody: rawBody,
379
571
  From: group ? `openim:group:${msg.groupID}` : `openim:${msg.sendID}`,
380
572
  To: `openim:${client.config.userID}`,
381
573
  SessionKey: sessionKey,
382
574
  AccountId: client.config.accountId,
383
575
  ChatType: chatType,
384
- ConversationLabel: group ? `openim:g-${msg.groupID}` : `openim:${senderId}`, // 会话标签:群聊用群ID,私聊用用户ID
576
+ ConversationLabel: group ? `openim:g-${msg.groupID}` : `openim:${senderId}`,
385
577
  SenderName: fromLabel,
386
578
  SenderId: senderId,
387
579
  Provider: "openim",
@@ -391,6 +583,7 @@ export async function processInboundMessage(api, client, msg) {
391
583
  OriginatingChannel: "openim",
392
584
  OriginatingTo: `openim:${client.config.userID}`,
393
585
  CommandAuthorized: true,
586
+ GroupSystemPrompt: groupSystemPrompt,
394
587
  _openim: {
395
588
  accountId: client.config.accountId,
396
589
  isGroup: group,
package/dist/media.js CHANGED
@@ -78,8 +78,8 @@ export async function sendTextToTarget(client, target, text) {
78
78
  if (target.kind === "group") {
79
79
  // 统一 <@ID> 和 @ID 两种格式,收集去重后的被 @ 用户 ID
80
80
  const atIDs = new Set();
81
- const normalizedText = text.replace(/<@(\d{6,})>/g, (_m, id) => { atIDs.add(id); return `@${id}`; });
82
- for (const m of normalizedText.matchAll(/@(\d{6,})/g))
81
+ const normalizedText = text.replace(/<@([a-zA-Z0-9_]{4,})>/g, (_m, id) => { atIDs.add(id); return `@${id}`; });
82
+ for (const m of normalizedText.matchAll(/@([a-zA-Z0-9_]{4,})/g))
83
83
  atIDs.add(m[1]);
84
84
  if (atIDs.size > 0) {
85
85
  const atUserIDList = [...atIDs];