palz-connector 1.2.1 → 1.2.2

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.1",
4
+ "version": "1.2.2",
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.1",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "enabled": true,
3
- "streamUrl": "wss://claw-server.csjkagent.com/ws/bot",
4
- "apiBaseUrl": "https://claw-server.csjkagent.com/api",
3
+ "streamUrl": "wss://claw-server.csagentai.com/ws/bot",
4
+ "apiBaseUrl": "https://claw-server.csagentai.com/api",
5
5
  "sessionTimeout": 1800000
6
6
  }
package/src/bot.ts CHANGED
@@ -46,6 +46,91 @@ function createChatQueue() {
46
46
 
47
47
  const enqueue = createChatQueue();
48
48
 
49
+ // ============ 群聊历史记录 ============
50
+
51
+ interface HistoryEntry {
52
+ sender: string;
53
+ body: string;
54
+ timestamp: number;
55
+ messageId: string;
56
+ }
57
+
58
+ const DEFAULT_GROUP_HISTORY_LIMIT = 50;
59
+ const MAX_HISTORY_KEYS = 3000;
60
+
61
+ /** 群聊历史缓存,key = historyKey(agentId:conversationId) */
62
+ const chatHistories = new Map<string, HistoryEntry[]>();
63
+
64
+ function recordGroupHistoryEntry(params: {
65
+ historyKey: string;
66
+ entry: HistoryEntry;
67
+ limit: number;
68
+ log?: (...args: any[]) => void;
69
+ }): void {
70
+ const log = params.log ?? console.log;
71
+ if (params.limit <= 0) {
72
+ log(`[HISTORY record] 跳过: limit=${params.limit} historyKey=${params.historyKey}`);
73
+ return;
74
+ }
75
+ const history = chatHistories.get(params.historyKey) ?? [];
76
+ const beforeLen = history.length;
77
+ history.push(params.entry);
78
+ while (history.length > params.limit) {
79
+ history.shift();
80
+ }
81
+ // 刷新插入顺序(LRU)
82
+ if (chatHistories.has(params.historyKey)) {
83
+ chatHistories.delete(params.historyKey);
84
+ }
85
+ chatHistories.set(params.historyKey, history);
86
+ log(`[HISTORY record] historyKey=${params.historyKey} before=${beforeLen} after=${history.length} limit=${params.limit} sender=${params.entry.sender} msgId=${params.entry.messageId} body="${params.entry.body.slice(0, 80)}" totalKeys=${chatHistories.size}`);
87
+ // 超过最大 key 数量时淘汰最老的
88
+ if (chatHistories.size > MAX_HISTORY_KEYS) {
89
+ const keysToDelete = chatHistories.size - MAX_HISTORY_KEYS;
90
+ const iterator = chatHistories.keys();
91
+ for (let i = 0; i < keysToDelete; i++) {
92
+ const key = iterator.next().value;
93
+ if (key !== undefined) chatHistories.delete(key);
94
+ }
95
+ log(`[HISTORY evict] 淘汰了 ${keysToDelete} 个 key, 剩余 ${chatHistories.size}`);
96
+ }
97
+ }
98
+
99
+ function buildGroupHistoryContext(params: {
100
+ historyKey: string;
101
+ currentMessage: string;
102
+ formatEntry: (entry: HistoryEntry) => string;
103
+ log?: (...args: any[]) => void;
104
+ }): string {
105
+ const log = params.log ?? console.log;
106
+ const entries = chatHistories.get(params.historyKey) ?? [];
107
+ log(`[HISTORY build] historyKey=${params.historyKey} entriesCount=${entries.length} currentMsgLen=${params.currentMessage.length}`);
108
+ if (entries.length === 0) {
109
+ log(`[HISTORY build] 无历史记录, 返回原始消息`);
110
+ return params.currentMessage;
111
+ }
112
+ for (let i = 0; i < entries.length; i++) {
113
+ log(`[HISTORY build] entry[${i}] sender=${entries[i].sender} msgId=${entries[i].messageId} body="${entries[i].body.slice(0, 80)}" ts=${entries[i].timestamp}`);
114
+ }
115
+ const historyText = entries.map(params.formatEntry).join("\n");
116
+ const combined = [
117
+ "[Chat messages since your last reply - for context]",
118
+ historyText,
119
+ "",
120
+ "[Current message - respond to this]",
121
+ params.currentMessage,
122
+ ].join("\n");
123
+ log(`[HISTORY build] 拼接完成: historyTextLen=${historyText.length} combinedLen=${combined.length}`);
124
+ return combined;
125
+ }
126
+
127
+ function clearGroupHistory(historyKey: string, log?: (...args: any[]) => void): void {
128
+ const _log = log ?? console.log;
129
+ const entries = chatHistories.get(historyKey) ?? [];
130
+ _log(`[HISTORY clear] historyKey=${historyKey} clearedEntries=${entries.length}`);
131
+ chatHistories.set(historyKey, []);
132
+ }
133
+
49
134
  // ============ 媒体 payload 构建 ============
50
135
 
51
136
  function buildMediaPayload(
@@ -80,7 +165,10 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
80
165
  const error = typeof runtime?.error === "function" ? runtime.error : console.error;
81
166
  const tag = `palz[${accountId}]`;
82
167
 
83
- log(`${tag}: [STEP 1/6 入站过滤] msg_id=${msg.msg_id} sender=${msg.sender_id} conv=${msg.conversation_id}`);
168
+ const isGroup = msg.conversation_type === "group";
169
+ const effectiveAgentId = msg.agent_id || "main";
170
+
171
+ log(`${tag}: [STEP 1/6 入站过滤] msg_id=${msg.msg_id} sender=${msg.sender_id} conv=${msg.conversation_id} type=${msg.conversation_type} agent=${effectiveAgentId}`);
84
172
 
85
173
  const content = msg.content;
86
174
  if (!content) {
@@ -102,14 +190,38 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
102
190
  return;
103
191
  }
104
192
 
105
- // 去重
106
- const claimed = tryClaimMessage(msg.msg_id);
107
- log(`${tag}: [STEP 2/6 去重] msg_id=${msg.msg_id} claimed=${claimed}`);
193
+ // 群聊 @提及检测
194
+ const wasMentioned = isGroup ? (msg.mentioned_bot === true) : true;
195
+ if (isGroup && !wasMentioned) {
196
+ // 未@机器人:记录到群聊历史,下次被@时作为上下文
197
+ const historyKey = `${effectiveAgentId}:${msg.conversation_id}`;
198
+ const senderName = msg.sender_name || msg.sender_id;
199
+ log(`${tag}: [STEP 1 群聊历史] 未@机器人, 准备记录历史 historyKey=${historyKey} mentioned_bot=${msg.mentioned_bot} conversation_type=${msg.conversation_type}`);
200
+ recordGroupHistoryEntry({
201
+ historyKey,
202
+ entry: {
203
+ sender: msg.sender_id,
204
+ body: `${senderName}: ${plainText}`,
205
+ timestamp: Date.now(),
206
+ messageId: msg.msg_id,
207
+ },
208
+ limit: DEFAULT_GROUP_HISTORY_LIMIT,
209
+ log,
210
+ });
211
+ log(`${tag}: [STEP 1 跳过] 原因=群聊中未@机器人, 已记录到历史 historyKey=${historyKey}`);
212
+ return;
213
+ }
214
+
215
+ // 去重(按 agentId + conversationId 隔离,同群多 bot 场景)
216
+ const claimed = tryClaimMessage(msg.msg_id, effectiveAgentId, msg.conversation_id);
217
+ log(`${tag}: [STEP 2/6 去重] msg_id=${msg.msg_id} agent=${effectiveAgentId} conv=${msg.conversation_id} claimed=${claimed}`);
108
218
  if (!claimed) return;
109
219
 
110
- // 入队
111
- const queueKey = `${msg.sender_id}:${msg.conversation_id}`;
112
- log(`${tag}: [STEP 3/6 入队] queueKey="${queueKey}"`);
220
+ // 入队(按 agentId 隔离,不同 agent 并行处理)
221
+ const queueKey = isGroup
222
+ ? `${effectiveAgentId}:${msg.conversation_id}`
223
+ : `${effectiveAgentId}:${msg.sender_id}:${msg.conversation_id}`;
224
+ log(`${tag}: [STEP 3/6 入队] queueKey="${queueKey}" isGroup=${isGroup}`);
113
225
 
114
226
  enqueue(queueKey, async () => {
115
227
  const startMs = Date.now();
@@ -130,9 +242,14 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
130
242
  const account = resolvePalzAccount({ cfg, accountId });
131
243
  const config = account.config;
132
244
 
245
+ const isGroup = msg.conversation_type === "group";
133
246
  const plainText = extractPlainText(msg.content).trim();
134
247
  const useStream = msg.stream === true;
135
- const peerId = `${msg.sender_id}:${msg.conversation_id}`;
248
+ const senderName = msg.sender_name || msg.sender_id;
249
+
250
+ // 群聊:peerId = chat:conversation_id(整群共享 session,与 palzTo 格式一致)
251
+ // DM:peerId = sender_id:conversation_id(每用户每会话独立 session)
252
+ const peerId = isGroup ? `chat:${msg.conversation_id}` : `${msg.sender_id}:${msg.conversation_id}`;
136
253
 
137
254
  // STEP 4: 解析媒体
138
255
  log(`${tag}: [STEP 4/6 媒体解析] 输入: contentType=${typeof msg.content === "string" ? "string" : "array"} imageCount=${Array.isArray(msg.content) ? msg.content.filter((p: ContentPart) => p.type === "image_url").length : 0}`);
@@ -141,13 +258,14 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
141
258
  log(`${tag}: [STEP 4 输出] mediaList=${JSON.stringify(mediaList.map((m) => ({ path: m.path, contentType: m.contentType })))} mediaPayload=${JSON.stringify(mediaPayload)}`);
142
259
 
143
260
  // STEP 5: 解析路由
144
- const routeInput = { cfg: "(cfg)", channel: "palz-connector", accountId, peer: { kind: "direct", id: peerId } };
261
+ const peerKind = isGroup ? "group" : "direct";
262
+ const routeInput = { cfg: "(cfg)", channel: "palz-connector", accountId, peer: { kind: peerKind, id: peerId } };
145
263
  log(`${tag}: [STEP 5/6 路由解析] 输入: ${JSON.stringify(routeInput)} agent_id=${msg.agent_id ?? "(auto)"}`);
146
264
  const route = core.channel.routing.resolveAgentRoute({
147
265
  cfg,
148
266
  channel: "palz-connector",
149
267
  accountId,
150
- peer: { kind: "direct", id: peerId },
268
+ peer: { kind: peerKind, id: peerId },
151
269
  });
152
270
 
153
271
  // IM 指定 agent_id 时走指定 agent,否则强制走 main
@@ -163,11 +281,13 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
163
281
 
164
282
  // STEP 6a: 构建 envelope body
165
283
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
166
- const messageBody = `${msg.sender_id}: ${plainText}`;
167
- log(`${tag}: [STEP 6a/6 envelope构建] 输入: channel=Palz from=${msg.sender_id} messageBody="${messageBody.slice(0, 120)}" envelopeOptions=${JSON.stringify(envelopeOptions)}`);
284
+ const messageBody = `${senderName}: ${plainText}`;
285
+ // 群聊 from 带上 conversation_id 以区分不同用户
286
+ const envelopeFrom = isGroup ? `${msg.conversation_id}:${msg.sender_id}` : msg.sender_id;
287
+ log(`${tag}: [STEP 6a/6 envelope构建] 输入: channel=Palz from=${envelopeFrom} messageBody="${messageBody.slice(0, 120)}" envelopeOptions=${JSON.stringify(envelopeOptions)}`);
168
288
  const body = core.channel.reply.formatAgentEnvelope({
169
289
  channel: "Palz",
170
- from: msg.sender_id,
290
+ from: envelopeFrom,
171
291
  timestamp: new Date(msg.timestamp || Date.now()),
172
292
  body: messageBody,
173
293
  envelope: envelopeOptions,
@@ -176,10 +296,11 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
176
296
 
177
297
  // STEP 6b: 构建 inbound context
178
298
  const palzFrom = `palz:${msg.sender_id}`;
179
- const palzTo = `${msg.sender_id}:${msg.conversation_id}`;
299
+ // 群聊:To 指向群,DM:To 指向用户会话
300
+ const palzTo = isGroup ? `chat:${msg.conversation_id}` : `${msg.sender_id}:${msg.conversation_id}`;
180
301
 
181
- // Palz DM 场景:检测是否包含斜杠命令,并计算命令授权。
182
- // DM 用户默认视为已授权(与飞书插件 dmPolicy=open 时的行为一致)。
302
+ // 命令授权:DM 默认允许,群聊也默认允许(可后续扩展 allowlist)
303
+ const wasMentioned = isGroup ? (msg.mentioned_bot === true) : true;
183
304
  const needsCommandAuth = core.channel.commands.shouldComputeCommandAuthorized(plainText, cfg);
184
305
  const commandAuthorized = needsCommandAuth
185
306
  ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
@@ -189,23 +310,60 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
189
310
  : undefined;
190
311
  log(`${tag}: [STEP 6b 命令授权] needsCommandAuth=${needsCommandAuth} commandAuthorized=${commandAuthorized}`);
191
312
 
313
+ const chatType = isGroup ? "group" : "direct";
314
+
315
+ // 群聊历史:将积攒的未@消息拼入 Body 上下文
316
+ const historyKey = isGroup ? `${effectiveAgentId}:${msg.conversation_id}` : undefined;
317
+ let combinedBody = body;
318
+ if (isGroup && historyKey) {
319
+ log(`${tag}: [STEP 6b 群聊历史] 开始构建, historyKey=${historyKey} bodyLen=${body.length}`);
320
+ combinedBody = buildGroupHistoryContext({
321
+ historyKey,
322
+ currentMessage: body,
323
+ formatEntry: (entry) =>
324
+ core.channel.reply.formatAgentEnvelope({
325
+ channel: "Palz",
326
+ from: entry.sender,
327
+ timestamp: new Date(entry.timestamp),
328
+ body: entry.body,
329
+ envelope: envelopeOptions,
330
+ }),
331
+ log,
332
+ });
333
+ log(`${tag}: [STEP 6b 群聊历史] 构建完成, historyKey=${historyKey} bodyLen=${body.length} combinedBodyLen=${combinedBody.length} hasHistory=${combinedBody.length !== body.length}`);
334
+ // log(`${tag}: [STEP 6b 群聊历史] combinedBody=\n${combinedBody}`);
335
+ }
336
+
337
+ // 构建 InboundHistory(结构化历史数据,Runtime 会注入到系统提示中)
338
+ const inboundHistory =
339
+ isGroup && historyKey
340
+ ? (chatHistories.get(historyKey) ?? []).map((entry) => ({
341
+ sender: entry.sender,
342
+ body: entry.body,
343
+ timestamp: entry.timestamp,
344
+ }))
345
+ : undefined;
346
+ log(`${tag}: [STEP 6b InboundHistory] count=${inboundHistory?.length ?? 0}`);
347
+
192
348
  const rawCtx = {
193
- Body: `(envelope, len=${body.length})`,
349
+ Body: `(envelope, len=${combinedBody.length})`,
194
350
  BodyForAgent: messageBody.slice(0, 100),
351
+ InboundHistory: inboundHistory ? `(${inboundHistory.length} entries)` : undefined,
195
352
  RawBody: plainText.slice(0, 100),
196
353
  CommandBody: plainText.slice(0, 100),
197
354
  From: palzFrom,
198
355
  To: palzTo,
199
356
  SessionKey: route.sessionKey,
200
357
  AccountId: route.accountId,
201
- ChatType: "direct",
358
+ ChatType: chatType,
359
+ GroupSubject: isGroup ? msg.conversation_id : undefined,
202
360
  SenderId: msg.sender_id,
203
- SenderName: msg.sender_id,
361
+ SenderName: senderName,
204
362
  Provider: "palz-connector",
205
363
  Surface: "palz-connector",
206
364
  MessageSid: msg.msg_id,
207
365
  Timestamp: Date.now(),
208
- WasMentioned: true,
366
+ WasMentioned: wasMentioned,
209
367
  CommandAuthorized: commandAuthorized,
210
368
  OriginatingChannel: "palz-connector",
211
369
  OriginatingTo: palzTo,
@@ -214,22 +372,24 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
214
372
  log(`${tag}: [STEP 6b inbound context] 输入: ${JSON.stringify(rawCtx)}`);
215
373
 
216
374
  const ctx = core.channel.reply.finalizeInboundContext({
217
- Body: body,
375
+ Body: combinedBody,
218
376
  BodyForAgent: messageBody,
377
+ InboundHistory: inboundHistory,
219
378
  RawBody: plainText,
220
379
  CommandBody: plainText,
221
380
  From: palzFrom,
222
381
  To: palzTo,
223
382
  SessionKey: route.sessionKey,
224
383
  AccountId: route.accountId,
225
- ChatType: "direct",
384
+ ChatType: chatType,
385
+ GroupSubject: isGroup ? msg.conversation_id : undefined,
226
386
  SenderId: msg.sender_id,
227
- SenderName: msg.sender_id,
387
+ SenderName: senderName,
228
388
  Provider: "palz-connector",
229
389
  Surface: "palz-connector",
230
390
  MessageSid: msg.msg_id,
231
391
  Timestamp: Date.now(),
232
- WasMentioned: true,
392
+ WasMentioned: wasMentioned,
233
393
  CommandAuthorized: commandAuthorized,
234
394
  OriginatingChannel: "palz-connector",
235
395
  OriginatingTo: palzTo,
@@ -276,6 +436,11 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
276
436
  }),
277
437
  });
278
438
 
439
+ // AI 回复完成后清空群聊历史(已拼入上下文,避免下次重复)
440
+ if (isGroup && historyKey) {
441
+ clearGroupHistory(historyKey, log);
442
+ }
443
+
279
444
  const dispatchElapsedMs = Date.now() - dispatchStartMs;
280
445
  log(
281
446
  `${tag}: [STEP 6d 输出] queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)} 耗时=${dispatchElapsedMs}ms`,
package/src/channel.ts CHANGED
@@ -25,7 +25,7 @@ export const palzPlugin = {
25
25
  },
26
26
 
27
27
  capabilities: {
28
- chatTypes: ["direct"],
28
+ chatTypes: ["direct", "channel"],
29
29
  polls: false,
30
30
  threads: false,
31
31
  media: true,
package/src/dedup.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * 消息去重
3
3
  *
4
- * 基于 msg_id 防止重复处理,TTL 5 分钟。
4
+ * 基于 agentId + conversationId + msg_id 防止重复处理,TTL 5 分钟。
5
+ * 同一条消息可以被不同 agent 各自处理一次(同群多 bot 场景)。
5
6
  */
6
7
 
7
8
  const processedMessages = new Map<string, number>();
@@ -10,8 +11,8 @@ const MESSAGE_DEDUP_TTL = 5 * 60 * 1000;
10
11
  function cleanup(): void {
11
12
  const before = processedMessages.size;
12
13
  const now = Date.now();
13
- for (const [msgId, ts] of processedMessages) {
14
- if (now - ts > MESSAGE_DEDUP_TTL) processedMessages.delete(msgId);
14
+ for (const [key, ts] of processedMessages) {
15
+ if (now - ts > MESSAGE_DEDUP_TTL) processedMessages.delete(key);
15
16
  }
16
17
  console.log(`palz-dedup: cleanup ${before} → ${processedMessages.size} entries`);
17
18
  }
@@ -20,11 +21,15 @@ function cleanup(): void {
20
21
  * 尝试标记消息为已处理。
21
22
  * 返回 true 表示此消息之前未处理过(可以继续处理),
22
23
  * 返回 false 表示重复消息(应跳过)。
24
+ *
25
+ * agentId 用于同群多 bot 场景:同一条 msg_id 可被不同 agent 各自处理。
26
+ * conversationId 防止不同会话间 msg_id 碰撞导致误去重。
23
27
  */
24
- export function tryClaimMessage(msgId: string): boolean {
28
+ export function tryClaimMessage(msgId: string, agentId: string = "main", conversationId: string = ""): boolean {
25
29
  if (!msgId) return true;
26
- if (processedMessages.has(msgId)) return false;
27
- processedMessages.set(msgId, Date.now());
30
+ const key = `${agentId}:${conversationId}:${msgId}`;
31
+ if (processedMessages.has(key)) return false;
32
+ processedMessages.set(key, Date.now());
28
33
  if (processedMessages.size >= 200) cleanup();
29
34
  return true;
30
35
  }
package/src/outbound.ts CHANGED
@@ -20,14 +20,15 @@ export const palzOutbound = {
20
20
  log(`palz-outbound: [sendText] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} text="${(text || "").slice(0, 120)}"`);
21
21
 
22
22
  const account = resolvePalzAccount({ cfg, accountId });
23
- const { senderId, conversationId } = parsePalzTarget(to);
24
- log(`palz-outbound: [sendText] 解析: senderId="${senderId}" conversationId="${conversationId}" botId=${account.config.botId}`);
23
+ const { senderId, conversationId, conversationType } = parsePalzTarget(to);
24
+ log(`palz-outbound: [sendText] 解析: senderId="${senderId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
25
25
 
26
26
  const result = await sendToPalzIM({
27
27
  config: account.config,
28
28
  conversationId,
29
29
  content: text,
30
30
  senderId,
31
+ conversationType,
31
32
  });
32
33
 
33
34
  const output = { channel: "palz-connector", messageId: Date.now().toString() };
@@ -41,8 +42,8 @@ export const palzOutbound = {
41
42
  log(`palz-outbound: [sendMedia] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} mediaUrl="${(mediaUrl || "").slice(0, 200)}"`);
42
43
 
43
44
  const account = resolvePalzAccount({ cfg, accountId });
44
- const { senderId, conversationId } = parsePalzTarget(to);
45
- log(`palz-outbound: [sendMedia] 解析: senderId="${senderId}" conversationId="${conversationId}" botId=${account.config.botId}`);
45
+ const { senderId, conversationId, conversationType } = parsePalzTarget(to);
46
+ log(`palz-outbound: [sendMedia] 解析: senderId="${senderId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
46
47
 
47
48
  const contentParts: ContentPart[] = [];
48
49
 
@@ -73,6 +74,7 @@ export const palzOutbound = {
73
74
  conversationId,
74
75
  content,
75
76
  senderId,
77
+ conversationType,
76
78
  });
77
79
 
78
80
  const output = { channel: "palz-connector", messageId: Date.now().toString() };
package/src/targets.ts CHANGED
@@ -16,18 +16,26 @@ export function looksLikePalzId(raw: string): boolean {
16
16
 
17
17
  /**
18
18
  * 从 "to" 地址中解析出 senderId 和 conversationId。
19
- * 格式: "{senderId}:{conversationId}"
19
+ * 格式:
20
+ * DM: "{senderId}:{conversationId}"
21
+ * 群聊: "chat:{conversationId}"(senderId 为空,群消息不需要指定 senderId)
20
22
  */
21
23
  export function parsePalzTarget(to: string): {
22
24
  senderId?: string;
23
25
  conversationId: string;
26
+ conversationType: string;
24
27
  } {
28
+ // 群聊目标:chat:xxx → senderId 留空
29
+ if (to.startsWith("chat:")) {
30
+ return { conversationId: to.slice(5), conversationType: "group" };
31
+ }
25
32
  const parts = to.split(":");
26
33
  if (parts.length >= 2) {
27
34
  return {
28
35
  senderId: parts[0],
29
36
  conversationId: parts.slice(1).join(":"),
37
+ conversationType: "direct",
30
38
  };
31
39
  }
32
- return { conversationId: to };
40
+ return { conversationId: to, conversationType: "direct" };
33
41
  }
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@ export type OpenAIContent = string | ContentPart[];
17
17
  export interface PalzMessageEvent {
18
18
  event: string;
19
19
  sender_id: string;
20
+ sender_name?: string;
20
21
  receiver_id: string;
21
22
  conversation_id: string;
22
23
  conversation_type: string;
@@ -26,6 +27,8 @@ export interface PalzMessageEvent {
26
27
  timestamp: number;
27
28
  /** 可选,指定 OpenClaw agent ID,覆盖默认路由 */
28
29
  agent_id?: string;
30
+ /** 群聊中是否 @了机器人 */
31
+ mentioned_bot?: boolean;
29
32
  }
30
33
 
31
34
  // ============ 配置 ============