palz-connector 1.3.4 → 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.4",
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.4",
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
  // ============ 文本提取工具 ============
@@ -45,8 +47,10 @@ function extractPlainText(content: OpenAIContent): string {
45
47
 
46
48
  // ============ reasoning 激活状态缓存(内存) ============
47
49
 
48
- /** 已激活 reasoning stream session key 集合,避免每次请求重复发送 /reasoning stream */
49
- const reasoningActivated = new Set<string>();
50
+ const REASONING_ACTIVATE_TTL_MS = 15 * 60 * 1000; // 15 分钟
51
+
52
+ /** 已激活 reasoning stream 的 session key → 激活时间戳(ms),超过 TTL 后重新发送激活指令 */
53
+ const reasoningActivated = new Map<string, number>();
50
54
 
51
55
  /** 已扫描过 session store 的 agent 集合,每个 agent 只扫描一次 */
52
56
  const reasoningScannedAgents = new Set<string>();
@@ -64,7 +68,7 @@ function prefillReasoningActivated(core: any, agentId: string, log: (...args: an
64
68
  let count = 0;
65
69
  for (const [key, entry] of Object.entries(store)) {
66
70
  if ((entry as any)?.reasoningLevel === "stream") {
67
- reasoningActivated.add(key);
71
+ reasoningActivated.set(key, Date.now());
68
72
  count++;
69
73
  }
70
74
  }
@@ -385,9 +389,9 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
385
389
  const plainText = extractPlainText(msg.content).trim();
386
390
  const useStream = msg.stream === true;
387
391
  const senderName = msg.sender_name || msg.sender_id;
388
- const groupId = resolveGroupId(msg);
392
+ const groupId = isGroup ? msg.conversation_id : undefined;
389
393
  if (isGroup) {
390
- 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}`);
391
395
  }
392
396
 
393
397
  // 群聊:peerId = chat:conversation_id(整群共享 session,与 palzTo 格式一致)
@@ -456,9 +460,9 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
456
460
  span?.addEvent(step6aOutput);
457
461
 
458
462
  // STEP 6b: 构建 inbound context
459
- const palzFrom = `palz:${msg.sender_id}`;
460
- // 群聊:To 指向群,DM:To 指向用户会话
461
- 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}`;
462
466
 
463
467
  // 命令授权:DM 默认允许,群聊也默认允许(可后续扩展 allowlist)
464
468
  const wasMentioned = isGroup ? (msg.mentioned_bot === true) : true;
@@ -528,6 +532,7 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
528
532
  `conversation_type: ${msg.conversation_type || "direct"}`,
529
533
  `to: ${palzTo}`,
530
534
  `mentioned_bot: ${wasMentioned}`,
535
+ `lobster_id: ${msg.lobster_id || ""}`,
531
536
  ];
532
537
  if (groupId) {
533
538
  untrustedContext.push(`group_id: ${groupId}`);
@@ -600,7 +605,9 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
600
605
  prefillReasoningActivated(core, route.agentId, log);
601
606
  }
602
607
 
603
- if (showProcess && !reasoningActivated.has(route.sessionKey)) {
608
+ const reasoningExpired = !reasoningActivated.has(route.sessionKey)
609
+ || (Date.now() - reasoningActivated.get(route.sessionKey)! > REASONING_ACTIVATE_TTL_MS);
610
+ if (showProcess && reasoningExpired) {
604
611
  try {
605
612
  log(`${tag}: [REASONING ACTIVATE] 开始激活 reasoning stream, sessionKey=${route.sessionKey}`);
606
613
  // 构造 directive-only 的 context,Body 仅包含 /reasoning stream
@@ -659,7 +666,7 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
659
666
  });
660
667
 
661
668
  if (activateSuccess) {
662
- reasoningActivated.add(route.sessionKey);
669
+ reasoningActivated.set(route.sessionKey, Date.now());
663
670
  log(`${tag}: [REASONING ACTIVATE] 激活成功, sessionKey=${route.sessionKey}`);
664
671
  } else {
665
672
  log(`${tag}: [REASONING ACTIVATE] 未收到确认 ack, 下次请求将重试, sessionKey=${route.sessionKey}`);
@@ -697,6 +704,7 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
697
704
  mediaLocalRoots: resolveMediaLocalRoots(effectiveAgentId),
698
705
  showProcess,
699
706
  sessionKey: route.sessionKey,
707
+ passthrough: buildPassthroughFromMsg(msg),
700
708
  });
701
709
 
702
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
  }