palz-connector 1.2.5 → 1.2.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "palz-connector",
3
3
  "name": "Palz Connector Channel",
4
- "version": "1.2.5",
4
+ "version": "1.2.7",
5
5
  "description": "Palz IM 接入 OpenClaw",
6
6
  "channels": [
7
7
  "palz-connector"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palz-connector",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
@@ -2,5 +2,6 @@
2
2
  "enabled": true,
3
3
  "streamUrl": "ws://14.103.148.99:9090/ws/bot",
4
4
  "apiBaseUrl": "http://14.103.148.99:9090/api",
5
- "sessionTimeout": 1800000
5
+ "sessionTimeout": 1800000,
6
+ "groupContextCache": false
6
7
  }
@@ -2,5 +2,6 @@
2
2
  "enabled": true,
3
3
  "streamUrl": "wss://claw-server.csaiagent.com/ws/bot",
4
4
  "apiBaseUrl": "https://claw-server.csaiagent.com/api",
5
- "sessionTimeout": 1800000
5
+ "sessionTimeout": 1800000,
6
+ "groupContextCache": false
6
7
  }
@@ -2,5 +2,6 @@
2
2
  "enabled": true,
3
3
  "streamUrl": "wss://claw-server.csagentai.com/ws/bot",
4
4
  "apiBaseUrl": "https://claw-server.csagentai.com/api",
5
- "sessionTimeout": 1800000
5
+ "sessionTimeout": 1800000,
6
+ "groupContextCache": false
6
7
  }
@@ -2,5 +2,6 @@
2
2
  "enabled": true,
3
3
  "streamUrl": "wss://claw-server.csjkagent.com/ws/bot",
4
4
  "apiBaseUrl": "https://claw-server.csjkagent.com/api",
5
- "sessionTimeout": 1800000
5
+ "sessionTimeout": 1800000,
6
+ "groupContextCache": false
6
7
  }
package/src/bot.ts CHANGED
@@ -15,7 +15,19 @@ import { resolvePalzAccount } from "./config.js";
15
15
  import { tryClaimMessage } from "./dedup.js";
16
16
  import { createPalzReplyDispatcher } from "./reply-dispatcher.js";
17
17
  import { resolvePalzMediaList } from "./media.js";
18
- import type { PalzMessageEvent, OpenAIContent, ContentPart, TextContentPart } from "./types.js";
18
+ import type { PalzMessageEvent, OpenAIContent, ContentPart, TextContentPart, PalzMediaInfo } from "./types.js";
19
+
20
+ // ============ group_id 解析 ============
21
+
22
+ /** 从 conversation_id 中解析 group_id,格式: user_{userID}_lobster_{lobsterID}_group_{groupID}_release_{releaseName} */
23
+ const GROUP_ID_RE = /_group_([^_]+)_release_/;
24
+
25
+ function resolveGroupId(msg: PalzMessageEvent): string | undefined {
26
+ if (msg.group_id) return msg.group_id;
27
+ if (msg.conversation_type !== "group") return undefined;
28
+ const m = GROUP_ID_RE.exec(msg.conversation_id);
29
+ return m ? m[1] : undefined;
30
+ }
19
31
 
20
32
  // ============ 文本提取工具 ============
21
33
 
@@ -51,6 +63,7 @@ const enqueue = createChatQueue();
51
63
  interface HistoryEntry {
52
64
  sender: string;
53
65
  body: string;
66
+ mediaList?: PalzMediaInfo[];
54
67
  timestamp: number;
55
68
  messageId: string;
56
69
  }
@@ -193,25 +206,39 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
193
206
 
194
207
  // 群聊 @提及检测
195
208
  const wasMentioned = isGroup ? (msg.mentioned_bot === true) : true;
196
- if (isGroup && !wasMentioned) {
197
- // 未@机器人:记录到群聊历史,下次被@时作为上下文
209
+ // 群聊上下文缓存:groupContextCache=true(默认)时,未@消息仅缓存不发送;
210
+ // groupContextCache=false 时,所有群聊消息都发送给AI
211
+ const account = resolvePalzAccount({ cfg, accountId });
212
+ const groupContextCacheEnabled = account.config.groupContextCache !== false;
213
+ if (isGroup && !wasMentioned && groupContextCacheEnabled) {
214
+ // 未@机器人:记录到群聊历史(含媒体),下次被@时作为上下文
198
215
  const historyKey = `${effectiveAgentId}:${msg.conversation_id}`;
199
216
  const senderName = msg.sender_name || msg.sender_id;
200
217
  log(`${tag}: [STEP 1 群聊历史] 未@机器人, 准备记录历史 historyKey=${historyKey} mentioned_bot=${msg.mentioned_bot} conversation_type=${msg.conversation_type}`);
218
+ // 解析媒体文件(图片/文档等),缓存到本地
219
+ const historyMediaList = await resolvePalzMediaList(msg.content, log);
220
+ log(`${tag}: [STEP 1 群聊历史] 媒体解析完成: mediaCount=${historyMediaList.length}`);
221
+ const bodyWithPlaceholder = historyMediaList.length > 0
222
+ ? `${senderName}: ${plainText} ${historyMediaList.map(m => m.placeholder).join(' ')}`
223
+ : `${senderName}: ${plainText}`;
201
224
  recordGroupHistoryEntry({
202
225
  historyKey,
203
226
  entry: {
204
227
  sender: msg.sender_id,
205
- body: `${senderName}: ${plainText}`,
228
+ body: bodyWithPlaceholder,
229
+ mediaList: historyMediaList.length > 0 ? historyMediaList : undefined,
206
230
  timestamp: Date.now(),
207
231
  messageId: msg.msg_id,
208
232
  },
209
233
  limit: DEFAULT_GROUP_HISTORY_LIMIT,
210
234
  log,
211
235
  });
212
- log(`${tag}: [STEP 1 跳过] 原因=群聊中未@机器人, 已记录到历史 historyKey=${historyKey}`);
236
+ log(`${tag}: [STEP 1 跳过] 原因=群聊中未@机器人, 已记录到历史 historyKey=${historyKey} mediaCount=${historyMediaList.length}`);
213
237
  return;
214
238
  }
239
+ if (isGroup && !wasMentioned && !groupContextCacheEnabled) {
240
+ log(`${tag}: [STEP 1 群聊直通] groupContextCache=false, 未@消息也将发送给AI mentioned_bot=${msg.mentioned_bot}`);
241
+ }
215
242
 
216
243
  // 去重(按 agentId + conversationId 隔离,同群多 bot 场景)
217
244
  const claimed = tryClaimMessage(msg.msg_id, effectiveAgentId, msg.conversation_id);
@@ -247,6 +274,10 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
247
274
  const plainText = extractPlainText(msg.content).trim();
248
275
  const useStream = msg.stream === true;
249
276
  const senderName = msg.sender_name || msg.sender_id;
277
+ const groupId = resolveGroupId(msg);
278
+ if (isGroup) {
279
+ log(`${tag}: [group_id] resolved=${groupId ?? "(none)"} from_msg=${msg.group_id ?? "(none)"} conv=${msg.conversation_id}`);
280
+ }
250
281
 
251
282
  // 群聊:peerId = chat:conversation_id(整群共享 session,与 palzTo 格式一致)
252
283
  // DM:peerId = sender_id:conversation_id(每用户每会话独立 session)
@@ -258,7 +289,7 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
258
289
  : 0;
259
290
  log(`${tag}: [STEP 4/6 媒体解析] 输入: contentType=${typeof msg.content === "string" ? "string" : "array"} mediaCount=${mediaCount}`);
260
291
  const mediaList = await resolvePalzMediaList(msg.content, log);
261
- const mediaPayload = buildMediaPayload(mediaList);
292
+ let mediaPayload = buildMediaPayload(mediaList);
262
293
  log(`${tag}: [STEP 4 输出] mediaList=${JSON.stringify(mediaList.map((m) => ({ path: m.path, contentType: m.contentType })))} mediaPayload=${JSON.stringify(mediaPayload)}`);
263
294
 
264
295
  // STEP 5: 解析路由
@@ -316,10 +347,11 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
316
347
 
317
348
  const chatType = isGroup ? "group" : "direct";
318
349
 
319
- // 群聊历史:将积攒的未@消息拼入 Body 上下文
320
- const historyKey = isGroup ? `${effectiveAgentId}:${msg.conversation_id}` : undefined;
350
+ // 群聊历史:将积攒的未@消息拼入 Body 上下文(仅 groupContextCache=true 时生效)
351
+ const groupContextCacheEnabled = account.config.groupContextCache !== false;
352
+ const historyKey = isGroup && groupContextCacheEnabled ? `${effectiveAgentId}:${msg.conversation_id}` : undefined;
321
353
  let combinedBody = body;
322
- if (isGroup && historyKey) {
354
+ if (isGroup && historyKey && groupContextCacheEnabled) {
323
355
  log(`${tag}: [STEP 6b 群聊历史] 开始构建, historyKey=${historyKey} bodyLen=${body.length}`);
324
356
  combinedBody = buildGroupHistoryContext({
325
357
  historyKey,
@@ -336,11 +368,20 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
336
368
  });
337
369
  log(`${tag}: [STEP 6b 群聊历史] 构建完成, historyKey=${historyKey} bodyLen=${body.length} combinedBodyLen=${combinedBody.length} hasHistory=${combinedBody.length !== body.length}`);
338
370
  // log(`${tag}: [STEP 6b 群聊历史] combinedBody=\n${combinedBody}`);
371
+
372
+ // 将历史中缓存的媒体合并到当前 mediaPayload
373
+ const historyEntries = chatHistories.get(historyKey) ?? [];
374
+ const historyMediaList = historyEntries.flatMap(e => e.mediaList ?? []);
375
+ if (historyMediaList.length > 0) {
376
+ const allMedia = [...historyMediaList, ...mediaList];
377
+ mediaPayload = buildMediaPayload(allMedia);
378
+ log(`${tag}: [STEP 6b 历史媒体合并] historyMedia=${historyMediaList.length} currentMedia=${mediaList.length} totalMedia=${allMedia.length}`);
379
+ }
339
380
  }
340
381
 
341
382
  // 构建 InboundHistory(结构化历史数据,Runtime 会注入到系统提示中)
342
383
  const inboundHistory =
343
- isGroup && historyKey
384
+ isGroup && historyKey && groupContextCacheEnabled
344
385
  ? (chatHistories.get(historyKey) ?? []).map((entry) => ({
345
386
  sender: entry.sender,
346
387
  body: entry.body,
@@ -349,6 +390,25 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
349
390
  : undefined;
350
391
  log(`${tag}: [STEP 6b InboundHistory] count=${inboundHistory?.length ?? 0}`);
351
392
 
393
+ // 构建 UntrustedContext:将 IM 消息的关键字段注入到 AI agent 的上下文中
394
+ const untrustedContext: string[] = [
395
+ `sender_id: ${msg.sender_id}`,
396
+ `sender_name: ${senderName}`,
397
+ `conversation_id: ${msg.conversation_id}`,
398
+ `conversation_type: ${msg.conversation_type || "direct"}`,
399
+ `mentioned_bot: ${wasMentioned}`,
400
+ ];
401
+ if (groupId) {
402
+ untrustedContext.push(`group_id: ${groupId}`);
403
+ }
404
+ if (msg.owner_id) {
405
+ untrustedContext.push(`owner_id: ${msg.owner_id}`);
406
+ }
407
+ if (msg.owner_name) {
408
+ untrustedContext.push(`owner_name: ${msg.owner_name}`);
409
+ }
410
+ log(`${tag}: [STEP 6b UntrustedContext] entries=${untrustedContext.length} values=${JSON.stringify(untrustedContext)}`);
411
+
352
412
  const ctx = core.channel.reply.finalizeInboundContext({
353
413
  Body: combinedBody,
354
414
  BodyForAgent: messageBody,
@@ -363,6 +423,7 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
363
423
  GroupSubject: isGroup ? msg.conversation_id : undefined,
364
424
  SenderId: msg.sender_id,
365
425
  SenderName: senderName,
426
+ UntrustedContext: untrustedContext,
366
427
  Provider: "palz-connector",
367
428
  Surface: "palz-connector",
368
429
  MessageSid: msg.msg_id,
@@ -401,6 +462,7 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
401
462
  enableStreaming: useStream,
402
463
  msgId: msg.msg_id,
403
464
  msgType: msg.msg_type,
465
+ groupId,
404
466
  });
405
467
 
406
468
  // STEP 6d: 分发消息给 AI
@@ -422,7 +484,7 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
422
484
  });
423
485
 
424
486
  // AI 回复完成后清空群聊历史(已拼入上下文,避免下次重复)
425
- if (isGroup && historyKey) {
487
+ if (isGroup && historyKey && groupContextCacheEnabled) {
426
488
  clearGroupHistory(historyKey, log);
427
489
  }
428
490
 
package/src/config.ts CHANGED
@@ -72,6 +72,7 @@ export function resolvePalzConfig(_cfg?: any): PalzConfig {
72
72
  streamUrl: file.streamUrl || "",
73
73
  apiBaseUrl: file.apiBaseUrl || "",
74
74
  sessionTimeout: file.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT,
75
+ groupContextCache: file.groupContextCache !== false,
75
76
  };
76
77
  if (!_configLoggedOnce) {
77
78
  _configLoggedOnce = true;
@@ -42,6 +42,7 @@ export interface CreatePalzReplyDispatcherParams {
42
42
  enableStreaming: boolean;
43
43
  msgId: string;
44
44
  msgType?: string;
45
+ groupId?: string;
45
46
  }
46
47
 
47
48
  export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParams) {
@@ -57,6 +58,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
57
58
  enableStreaming,
58
59
  msgId,
59
60
  msgType,
61
+ groupId,
60
62
  } = params;
61
63
 
62
64
  const log = typeof runtime?.log === "function" ? runtime.log : console.log;
@@ -87,6 +89,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
87
89
  senderId,
88
90
  stream: streamOpts,
89
91
  msgType,
92
+ groupId,
90
93
  });
91
94
  log(`${tag}: [DISPATCHER←sendToIM] 输出: ${JSON.stringify(result)}`);
92
95
  return result;
package/src/send.ts CHANGED
@@ -14,7 +14,7 @@ function nextMsgId(): string {
14
14
  }
15
15
 
16
16
  export async function sendToPalzIM(params: SendToIMParams): Promise<any> {
17
- const { config, conversationId, content, conversationType, msgId, senderId, stream, msgType } = params;
17
+ const { config, conversationId, content, conversationType, msgId, senderId, stream, msgType, groupId } = params;
18
18
  const url = `${config.apiBaseUrl}/bot/send`;
19
19
  const resolvedMsgId = msgId || nextMsgId();
20
20
 
@@ -34,6 +34,10 @@ export async function sendToPalzIM(params: SendToIMParams): Promise<any> {
34
34
  reqBody.msg_type = msgType;
35
35
  }
36
36
 
37
+ if (groupId) {
38
+ reqBody.group_id = groupId;
39
+ }
40
+
37
41
  if (stream) {
38
42
  reqBody.stream_id = stream.streamId;
39
43
  reqBody.seq = stream.seq;
@@ -41,15 +45,16 @@ export async function sendToPalzIM(params: SendToIMParams): Promise<any> {
41
45
  reqBody.delta = stream.delta;
42
46
  }
43
47
 
48
+ const reqBodyStr = JSON.stringify(reqBody);
44
49
  console.log(
45
- `palz-send: [HTTP_REQ] POST ${url}\n request_body=${JSON.stringify(reqBody).slice(0, 800)}`,
50
+ `palz-send: [HTTP_REQ] POST ${url} body_length=${reqBodyStr.length}\n request_body=${reqBodyStr}`,
46
51
  );
47
52
 
48
53
  const startMs = Date.now();
49
54
  const response = await fetch(url, {
50
55
  method: "POST",
51
56
  headers: { "Content-Type": "application/json" },
52
- body: JSON.stringify(reqBody),
57
+ body: reqBodyStr,
53
58
  });
54
59
  const elapsedMs = Date.now() - startMs;
55
60
 
package/src/types.ts CHANGED
@@ -31,6 +31,12 @@ export interface PalzMessageEvent {
31
31
  mentioned_bot?: boolean;
32
32
  /** 可选,IM 下发的消息类型,回复时原样透传 */
33
33
  msg_type?: string;
34
+ /** Bot 所属用户的 ID */
35
+ owner_id?: string;
36
+ /** Bot 所属用户的用户名 */
37
+ owner_name?: string;
38
+ /** 群组 ID,群聊时 IM 可直接下发;若未提供则从 conversation_id 中解析 */
39
+ group_id?: string;
34
40
  }
35
41
 
36
42
  // ============ 配置 ============
@@ -41,6 +47,8 @@ export interface PalzConfig {
41
47
  streamUrl?: string;
42
48
  apiBaseUrl?: string;
43
49
  sessionTimeout?: number;
50
+ /** 群聊上下文缓存开关:true=未@消息缓存为上下文(默认),false=所有群聊消息直接发送给AI */
51
+ groupContextCache?: boolean;
44
52
  }
45
53
 
46
54
  export interface ResolvedPalzAccount {
@@ -78,4 +86,6 @@ export interface SendToIMParams {
78
86
  stream?: StreamChunkOpts;
79
87
  /** IM 下发的消息类型,回复时原样透传 */
80
88
  msgType?: string;
89
+ /** 群组 ID,群聊时透传 */
90
+ groupId?: string;
81
91
  }