palz-connector 1.3.5 → 1.3.6

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.3.5",
4
+ "version": "1.3.6",
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.3.5",
3
+ "version": "1.3.6",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
package/src/bot.ts CHANGED
@@ -18,16 +18,18 @@ import { resolvePalzMediaList, resolveMediaLocalRoots } from "./media.js";
18
18
  import { tracer, trace, context, SpanStatusCode } from "./tracing.js";
19
19
  import type { PalzMessageEvent, OpenAIContent, ContentPart, TextContentPart, PalzMediaInfo } from "./types.js";
20
20
 
21
- // ============ group_id 解析 ============
21
+ // ============ 原始消息透传字段 ============
22
22
 
23
- /** conversation_id 中解析 group_id,格式: user_{userID}_lobster_{lobsterID}_group_{groupID}_release_{releaseName} */
24
- const GROUP_ID_RE = /_group_([^_]+)_release_/;
23
+ const PASSTHROUGH_EXCLUDE = new Set(["event", "content", "timestamp"]);
25
24
 
26
- function resolveGroupId(msg: PalzMessageEvent): string | undefined {
27
- if (msg.group_id) return msg.group_id;
28
- if (msg.conversation_type !== "group") return undefined;
29
- const m = GROUP_ID_RE.exec(msg.conversation_id);
30
- return m ? m[1] : undefined;
25
+ function buildPassthroughFromMsg(msg: PalzMessageEvent): Record<string, unknown> {
26
+ const out: Record<string, unknown> = {};
27
+ for (const [k, v] of Object.entries(msg)) {
28
+ if (PASSTHROUGH_EXCLUDE.has(k)) continue;
29
+ if (v === undefined) continue;
30
+ out[k] = v;
31
+ }
32
+ return out;
31
33
  }
32
34
 
33
35
  // ============ 文本提取工具 ============
@@ -387,9 +389,9 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
387
389
  const plainText = extractPlainText(msg.content).trim();
388
390
  const useStream = msg.stream === true;
389
391
  const senderName = msg.sender_name || msg.sender_id;
390
- const groupId = resolveGroupId(msg);
392
+ const groupId = isGroup ? msg.conversation_id : undefined;
391
393
  if (isGroup) {
392
- log(`${tag}: [group_id] resolved=${groupId ?? "(none)"} from_msg=${msg.group_id ?? "(none)"} conv=${msg.conversation_id}`);
394
+ log(`${tag}: [group_id] resolved=${groupId ?? "(none)"} conv=${msg.conversation_id}`);
393
395
  }
394
396
 
395
397
  // 群聊:peerId = chat:conversation_id(整群共享 session,与 palzTo 格式一致)
@@ -458,9 +460,9 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
458
460
  span?.addEvent(step6aOutput);
459
461
 
460
462
  // STEP 6b: 构建 inbound context
461
- const palzFrom = `palz:${msg.sender_id}`;
462
- // 群聊:To 指向群,DM:To 指向用户会话
463
- const palzTo = isGroup ? `chat:${msg.conversation_id}` : `${msg.sender_id}:${msg.conversation_id}`;
463
+ const palzFrom = `palz:${msg.sender_id}:${msg.conversation_id}`;
464
+ // 新格式: {conversationType}:{senderId}:{lobsterId}:{conversationId}
465
+ const palzTo = `${msg.conversation_type || "direct"}:${msg.sender_id}:${msg.lobster_id || ""}:${msg.conversation_id}`;
464
466
 
465
467
  // 命令授权:DM 默认允许,群聊也默认允许(可后续扩展 allowlist)
466
468
  const wasMentioned = isGroup ? (msg.mentioned_bot === true) : true;
@@ -530,6 +532,7 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
530
532
  `conversation_type: ${msg.conversation_type || "direct"}`,
531
533
  `to: ${palzTo}`,
532
534
  `mentioned_bot: ${wasMentioned}`,
535
+ `lobster_id: ${msg.lobster_id || ""}`,
533
536
  ];
534
537
  if (groupId) {
535
538
  untrustedContext.push(`group_id: ${groupId}`);
@@ -701,6 +704,7 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
701
704
  mediaLocalRoots: resolveMediaLocalRoots(effectiveAgentId),
702
705
  showProcess,
703
706
  sessionKey: route.sessionKey,
707
+ passthrough: buildPassthroughFromMsg(msg),
704
708
  });
705
709
 
706
710
  // STEP 6d: 分发消息给 AI
package/src/channel.ts CHANGED
@@ -66,13 +66,13 @@ export const palzPlugin = {
66
66
  normalizeTarget: (raw: string) => normalizePalzTarget(raw),
67
67
  targetResolver: {
68
68
  looksLikeId: looksLikePalzId,
69
- hint: "<senderId>:<conversationId>",
69
+ hint: "<conversationType>:<senderId>:<lobsterId>:<conversationId>",
70
70
  },
71
71
  },
72
72
 
73
73
  agentPrompt: {
74
74
  messageToolHints: () => [
75
- "- Palz targeting: DO NOT set `target` — always omit it so the system auto-infers the correct conversation. Never use sender_id or conversation_id alone as target. If you must specify an explicit target, the only valid format is `<senderId>:<conversationId>`.",
75
+ "- Palz targeting: DO NOT set `target` — always omit it so the system auto-infers the correct conversation. Never use sender_id or conversation_id alone as target. If you must specify an explicit target, the only valid format is `<conversationType>:<senderId>:<lobsterId>:<conversationId>`.",
76
76
  ],
77
77
  },
78
78
 
package/src/outbound.ts CHANGED
@@ -17,11 +17,11 @@ export const palzOutbound = {
17
17
  resolveTarget: (params: { to?: string; mode?: string }) => {
18
18
  const to = params.to?.trim();
19
19
  if (!to) {
20
- return { ok: false as const, error: new Error("Palz target is required. Format: <senderId>:<conversationId> or chat:<conversationId>") };
20
+ return { ok: false as const, error: new Error("Palz target is required. Format: <conversationType>:<senderId>:<lobsterId>:<conversationId>") };
21
21
  }
22
22
  // Must contain ":" — bare sender_id or bare conversation_id is invalid
23
23
  if (!to.includes(":")) {
24
- return { ok: false as const, error: new Error(`Invalid Palz target "${to}": must be <senderId>:<conversationId> or chat:<conversationId>. A bare ID without ":" is not a valid target.`) };
24
+ return { ok: false as const, error: new Error(`Invalid Palz target "${to}": must be <conversationType>:<senderId>:<lobsterId>:<conversationId>. A bare ID without ":" is not a valid target.`) };
25
25
  }
26
26
  return { ok: true as const, to };
27
27
  },
@@ -32,8 +32,8 @@ export const palzOutbound = {
32
32
  log(`palz-outbound: [sendText] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} text="${(text || "").slice(0, 120)}"`);
33
33
 
34
34
  const account = resolvePalzAccount({ cfg, accountId });
35
- const { senderId, conversationId, conversationType } = parsePalzTarget(to);
36
- log(`palz-outbound: [sendText] 解析: senderId="${senderId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
35
+ const { senderId, lobsterId, conversationId, conversationType } = parsePalzTarget(to);
36
+ log(`palz-outbound: [sendText] 解析: senderId="${senderId}" lobsterId="${lobsterId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
37
37
 
38
38
  const result = await sendToPalzIM({
39
39
  config: account.config,
@@ -41,6 +41,7 @@ export const palzOutbound = {
41
41
  content: text,
42
42
  senderId,
43
43
  conversationType,
44
+ lobsterId,
44
45
  });
45
46
 
46
47
  const output = { channel: "palz-connector", messageId: Date.now().toString() };
@@ -53,9 +54,9 @@ export const palzOutbound = {
53
54
  const log = typeof ctx.log === "function" ? ctx.log : console.log;
54
55
 
55
56
  const account = resolvePalzAccount({ cfg, accountId });
56
- const { senderId, conversationId, conversationType } = parsePalzTarget(to);
57
+ const { senderId, lobsterId, conversationId, conversationType } = parsePalzTarget(to);
57
58
  log(`palz-outbound: [sendMedia] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} mediaUrl="${(mediaUrl || "").slice(0, 200)}"`);
58
- log(`palz-outbound: [sendMedia] 解析: senderId="${senderId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
59
+ log(`palz-outbound: [sendMedia] 解析: senderId="${senderId}" lobsterId="${lobsterId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
59
60
 
60
61
  const contentParts: ContentPart[] = [];
61
62
 
@@ -87,6 +88,7 @@ export const palzOutbound = {
87
88
  content,
88
89
  senderId,
89
90
  conversationType,
91
+ lobsterId,
90
92
  });
91
93
 
92
94
  const output = { channel: "palz-connector", messageId: Date.now().toString() };
@@ -47,6 +47,8 @@ export interface CreatePalzReplyDispatcherParams {
47
47
  mediaLocalRoots?: readonly string[];
48
48
  showProcess?: boolean;
49
49
  sessionKey?: string;
50
+ /** IM 原始消息透传字段(除 event/content/timestamp) */
51
+ passthrough?: Record<string, unknown>;
50
52
  }
51
53
 
52
54
  export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParams) {
@@ -65,6 +67,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
65
67
  groupId,
66
68
  mediaLocalRoots,
67
69
  showProcess,
70
+ passthrough,
68
71
  } = params;
69
72
 
70
73
  const log = typeof runtime?.log === "function" ? runtime.log : console.log;
@@ -115,6 +118,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
115
118
  groupId,
116
119
  palzMsgType,
117
120
  toolContent,
121
+ passthrough,
118
122
  });
119
123
  log(`${tag}: [DISPATCHER←sendToIM] 输出: ${JSON.stringify(result)}`);
120
124
  return result;
package/src/send.ts CHANGED
@@ -29,12 +29,13 @@ export async function sendToPalzIM(params: SendToIMParams): Promise<any> {
29
29
  }
30
30
 
31
31
  async function _sendToPalzIMInner(params: SendToIMParams): Promise<any> {
32
- const { config, conversationId, content, conversationType, msgId, senderId, stream, msgType, groupId, palzMsgType, toolContent } = params;
32
+ const { config, conversationId, content, conversationType, msgId, senderId, stream, msgType, groupId, lobsterId, palzMsgType, toolContent, passthrough } = params;
33
33
  const url = `${config.apiBaseUrl}/bot/send`;
34
34
  const resolvedMsgId = msgId || nextMsgId();
35
35
  const span = trace.getActiveSpan();
36
36
 
37
37
  const reqBody: Record<string, unknown> = {
38
+ ...(passthrough ?? {}),
38
39
  bot_id: config.botId,
39
40
  conversation_id: conversationId,
40
41
  conversation_type: conversationType || "direct",
@@ -54,6 +55,10 @@ async function _sendToPalzIMInner(params: SendToIMParams): Promise<any> {
54
55
  reqBody.group_id = groupId;
55
56
  }
56
57
 
58
+ if (lobsterId) {
59
+ reqBody.lobster_id = lobsterId;
60
+ }
61
+
57
62
  if (stream) {
58
63
  reqBody.stream_id = stream.streamId;
59
64
  reqBody.seq = stream.seq;
package/src/targets.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Palz Connector 目标地址解析
3
3
  *
4
- * 目标格式: "{senderId}:{conversationId}"
4
+ * 新格式: "{conversationType}:{senderId}:{lobsterId}:{conversationId}"
5
+ * 旧格式(兼容):
6
+ * DM: "{senderId}:{conversationId}"
7
+ * 群聊: "chat:{conversationId}"
5
8
  */
6
9
 
7
10
  export function normalizePalzTarget(raw: string): string | undefined {
@@ -14,22 +17,44 @@ export function looksLikePalzId(raw: string): boolean {
14
17
  return /^[\p{L}\p{N}\w:._-]+$/u.test(trimmed);
15
18
  }
16
19
 
17
- /**
18
- * 从 "to" 地址中解析出 senderId 和 conversationId。
19
- * 格式:
20
- * DM: "{senderId}:{conversationId}"
21
- * 群聊: "chat:{conversationId}"(senderId 为空,群消息不需要指定 senderId)
22
- */
23
- export function parsePalzTarget(to: string): {
20
+ export interface ParsedPalzTarget {
24
21
  senderId?: string;
22
+ lobsterId?: string;
25
23
  conversationId: string;
26
24
  conversationType: string;
27
- } {
28
- // 群聊目标:chat:xxx → senderId 留空
25
+ }
26
+
27
+ /**
28
+ * 从 "to" 地址中解析出 senderId、lobsterId 和 conversationId。
29
+ *
30
+ * 新格式(4段): "{conversationType}:{senderId}:{lobsterId}:{conversationId}"
31
+ * 示例: "group:6a9e1cfd...:lobster123:group_grp_3b14486f"
32
+ * 示例: "direct:6a9e1cfd...:lobster123:conv_xxx"
33
+ *
34
+ * 旧格式(兼容):
35
+ * 群聊: "chat:{conversationId}"(senderId 为空)
36
+ * DM: "{senderId}:{conversationId}"
37
+ */
38
+ export function parsePalzTarget(to: string): ParsedPalzTarget {
39
+ const parts = to.split(":");
40
+
41
+ // 新4段格式: conversationType:senderId:lobsterId:conversationId
42
+ // conversationType 为 "group" 或 "direct"
43
+ if (parts.length >= 4 && (parts[0] === "group" || parts[0] === "direct")) {
44
+ return {
45
+ conversationType: parts[0],
46
+ senderId: parts[1] || undefined,
47
+ lobsterId: parts[2] || undefined,
48
+ conversationId: parts.slice(3).join(":"),
49
+ };
50
+ }
51
+
52
+ // 旧格式兼容:chat:xxx → 群聊,senderId 留空
29
53
  if (to.startsWith("chat:")) {
30
54
  return { conversationId: to.slice(5), conversationType: "group" };
31
55
  }
32
- const parts = to.split(":");
56
+
57
+ // 旧格式兼容:senderId:conversationId
33
58
  if (parts.length >= 2) {
34
59
  return {
35
60
  senderId: parts[0],
@@ -37,5 +62,6 @@ export function parsePalzTarget(to: string): {
37
62
  conversationType: "direct",
38
63
  };
39
64
  }
65
+
40
66
  return { conversationId: to, conversationType: "direct" };
41
67
  }
package/src/types.ts CHANGED
@@ -37,6 +37,8 @@ export interface PalzMessageEvent {
37
37
  owner_name?: string;
38
38
  /** 群组 ID,群聊时 IM 可直接下发;若未提供则从 conversation_id 中解析 */
39
39
  group_id?: string;
40
+ /** Lobster ID,标识 agent 身份(IM 通过此字段区分不同 agent) */
41
+ lobster_id?: string;
40
42
  /** W3C Trace Context traceparent,由 IM 上游传递 */
41
43
  traceparent?: string;
42
44
  }
@@ -92,8 +94,12 @@ export interface SendToIMParams {
92
94
  msgType?: string;
93
95
  /** 群组 ID,群聊时透传 */
94
96
  groupId?: string;
97
+ /** Lobster ID,标识 agent 身份 */
98
+ lobsterId?: string;
95
99
  /** Palz 自定义消息类型(tool_start / tool_result) */
96
100
  palzMsgType?: string;
97
101
  /** 工具调用结构化内容 */
98
102
  toolContent?: Record<string, unknown>;
103
+ /** IM 原始消息中除 event/content/timestamp 外的字段,原样透传到发送请求体 */
104
+ passthrough?: Record<string, unknown>;
99
105
  }