palz-connector 1.4.7 → 1.4.9

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.4.7",
4
+ "version": "1.4.9",
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.4.7",
3
+ "version": "1.4.9",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
@@ -2,6 +2,8 @@
2
2
  "enabled": true,
3
3
  "streamUrl": "ws://14.103.148.99:8090/ws/bot",
4
4
  "apiBaseUrl": "http://14.103.148.99:8090/api",
5
+ "clawGatewayUrl": "http://claw-gateway:8080",
6
+ "activityReportEnabled": false,
5
7
  "sessionTimeout": 1800000,
6
8
  "groupContextCache": false,
7
9
  "showProcess": true
@@ -2,6 +2,8 @@
2
2
  "enabled": true,
3
3
  "streamUrl": "wss://claw-server.csaiagent.com/ws/bot",
4
4
  "apiBaseUrl": "https://claw-server.csaiagent.com/api",
5
+ "clawGatewayUrl": "http://claw-gateway:8080",
6
+ "activityReportEnabled": true,
5
7
  "sessionTimeout": 1800000,
6
8
  "groupContextCache": false,
7
9
  "showProcess": true
@@ -2,6 +2,8 @@
2
2
  "enabled": true,
3
3
  "streamUrl": "wss://claw-server.csagentai.com/ws/bot",
4
4
  "apiBaseUrl": "https://claw-server.csagentai.com/api",
5
+ "clawGatewayUrl": "http://claw-gateway:8080",
6
+ "activityReportEnabled": true,
5
7
  "sessionTimeout": 1800000,
6
8
  "groupContextCache": false,
7
9
  "showProcess": true
@@ -2,6 +2,8 @@
2
2
  "enabled": true,
3
3
  "streamUrl": "wss://claw-server.csjkagent.com/ws/bot",
4
4
  "apiBaseUrl": "https://claw-server.csjkagent.com/api",
5
+ "clawGatewayUrl": "https://claw-server.csjkagent.com/api",
6
+ "activityReportEnabled": true,
5
7
  "sessionTimeout": 1800000,
6
8
  "groupContextCache": false,
7
9
  "showProcess": true
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Palz IM 收到消息后的 claw-gateway 活动上报。
3
+ */
4
+
5
+ import type { PalzConfig, PalzMessageEvent } from "./types.js";
6
+
7
+ const ACTIVITY_REPORT_TIMEOUT_MS = 2000;
8
+ const ACTIVITY_REPORT_PATH_PREFIX = "/openclaw-gateway/be/deployments";
9
+
10
+ const warnedMissingConfig = new Set<string>();
11
+
12
+ export interface ReportPalzActivityParams {
13
+ config: PalzConfig;
14
+ msg: PalzMessageEvent;
15
+ receivedAt: Date;
16
+ runtime?: any;
17
+ accountId?: string;
18
+ }
19
+
20
+ function formatDateTimeWithOffset(date: Date): string {
21
+ const pad = (n: number, width = 2) => String(n).padStart(width, "0");
22
+ const offsetMinutes = -date.getTimezoneOffset();
23
+ const sign = offsetMinutes >= 0 ? "+" : "-";
24
+ const absOffset = Math.abs(offsetMinutes);
25
+ const offsetHours = Math.floor(absOffset / 60);
26
+ const offsetRestMinutes = absOffset % 60;
27
+
28
+ return [
29
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
30
+ "T",
31
+ `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`,
32
+ `${sign}${pad(offsetHours)}:${pad(offsetRestMinutes)}`,
33
+ ].join("");
34
+ }
35
+
36
+ function resolveActivityUrl(baseUrl: string, releaseName: string): string {
37
+ const normalizedBase = baseUrl.replace(/\/+$/, "");
38
+ return `${normalizedBase}${ACTIVITY_REPORT_PATH_PREFIX}/${encodeURIComponent(releaseName)}/activity`;
39
+ }
40
+
41
+ export async function reportPalzActivity(params: ReportPalzActivityParams): Promise<void> {
42
+ const { config, msg, receivedAt, runtime, accountId } = params;
43
+ const log = typeof runtime?.log === "function" ? runtime.log : console.log;
44
+ const error = typeof runtime?.error === "function" ? runtime.error : console.error;
45
+ const tag = `palz[${accountId ?? config.botId ?? "default"}]`;
46
+
47
+ if (config.activityReportEnabled !== true) return;
48
+
49
+ const baseUrl = config.clawGatewayUrl?.trim();
50
+ const releaseName = (process.env.botID || config.botId || "").trim();
51
+ if (!baseUrl || !releaseName) {
52
+ const warnKey = `${accountId ?? ""}:${baseUrl ? "hasBaseUrl" : "missingBaseUrl"}:${releaseName ? "hasRelease" : "missingRelease"}`;
53
+ if (!warnedMissingConfig.has(warnKey)) {
54
+ warnedMissingConfig.add(warnKey);
55
+ error(`${tag}: [ACTIVITY_REPORT] enabled but missing ${!baseUrl ? "clawGatewayUrl" : "botID/releaseName"}, skip`);
56
+ }
57
+ return;
58
+ }
59
+
60
+ const payload: Record<string, unknown> = {
61
+ source: "palz-connector",
62
+ eventType: "im_message_received",
63
+ messageId: msg.msg_id,
64
+ conversationId: msg.conversation_id,
65
+ conversationType: msg.conversation_type || "direct",
66
+ userId: msg.owner_id ?? "",
67
+ senderId: msg.sender_id,
68
+ receivedAt: formatDateTimeWithOffset(receivedAt),
69
+ };
70
+
71
+ if (msg.traceparent) {
72
+ payload.traceId = msg.traceparent;
73
+ }
74
+
75
+ const url = resolveActivityUrl(baseUrl, releaseName);
76
+ const body = JSON.stringify(payload);
77
+ const controller = new AbortController();
78
+ const timeout = setTimeout(() => controller.abort(), ACTIVITY_REPORT_TIMEOUT_MS);
79
+ const startMs = Date.now();
80
+
81
+ try {
82
+ const response = await fetch(url, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json" },
85
+ body,
86
+ signal: controller.signal,
87
+ });
88
+ const elapsedMs = Date.now() - startMs;
89
+ const responseText = await response.text().catch(() => "");
90
+ if (!response.ok) {
91
+ error(`${tag}: [ACTIVITY_REPORT] failed status=${response.status} elapsed=${elapsedMs}ms msg_id=${msg.msg_id} response=${responseText.slice(0, 300)}`);
92
+ return;
93
+ }
94
+ log(`${tag}: [ACTIVITY_REPORT] ok status=${response.status} elapsed=${elapsedMs}ms msg_id=${msg.msg_id}`);
95
+ } catch (err: any) {
96
+ const elapsedMs = Date.now() - startMs;
97
+ const reason = err?.name === "AbortError" ? `timeout ${ACTIVITY_REPORT_TIMEOUT_MS}ms` : (err?.message ?? String(err));
98
+ error(`${tag}: [ACTIVITY_REPORT] error elapsed=${elapsedMs}ms msg_id=${msg.msg_id} reason=${reason}`);
99
+ } finally {
100
+ clearTimeout(timeout);
101
+ }
102
+ }
package/src/bot.ts CHANGED
@@ -558,6 +558,9 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
558
558
  if (groupId) {
559
559
  untrustedContext.push(`group_id: ${groupId}`);
560
560
  }
561
+ if (msg.group_name) {
562
+ untrustedContext.push(`group_name: ${msg.group_name}`);
563
+ }
561
564
  if (msg.group_owner_id) {
562
565
  untrustedContext.push(`group_owner_id: ${msg.group_owner_id}`);
563
566
  }
@@ -584,7 +587,7 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
584
587
  SessionKey: route.sessionKey,
585
588
  AccountId: route.accountId,
586
589
  ChatType: chatType,
587
- GroupSubject: isGroup ? msg.conversation_id : undefined,
590
+ GroupSubject: isGroup ? (msg.group_name || msg.conversation_id) : undefined,
588
591
  SenderId: msg.sender_id,
589
592
  SenderName: senderName,
590
593
  UntrustedContext: untrustedContext,
package/src/channel.ts CHANGED
@@ -44,6 +44,8 @@ export const palzPlugin = {
44
44
  enabled: { type: "boolean" },
45
45
  streamUrl: { type: "string" },
46
46
  apiBaseUrl: { type: "string" },
47
+ clawGatewayUrl: { type: "string" },
48
+ activityReportEnabled: { type: "boolean" },
47
49
  sessionTimeout: { type: "integer", minimum: 0 },
48
50
  },
49
51
  },
package/src/config.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 配置来源:
5
5
  * - botId: 仅从环境变量 botID 读取
6
- * - streamUrl / apiBaseUrl / sessionTimeout: 从配置文件读取
6
+ * - streamUrl / apiBaseUrl / clawGatewayUrl / sessionTimeout: 从配置文件读取
7
7
  * - 若环境变量 HELM_ENV 存在且非空,读取 palz-connector.{HELM_ENV}.config.json
8
8
  * - 否则读取默认配置文件 palz-connector.config.json
9
9
  */
@@ -71,6 +71,8 @@ export function resolvePalzConfig(_cfg?: any): PalzConfig {
71
71
  botId: process.env.botID || "",
72
72
  streamUrl: file.streamUrl || "",
73
73
  apiBaseUrl: file.apiBaseUrl || "",
74
+ clawGatewayUrl: file.clawGatewayUrl || "",
75
+ activityReportEnabled: file.activityReportEnabled === true,
74
76
  sessionTimeout: file.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT,
75
77
  groupContextCache: file.groupContextCache !== false,
76
78
  showProcess: file.showProcess === true,
package/src/monitor.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import WebSocket from "ws";
9
9
  import { handlePalzMessage } from "./bot.js";
10
+ import { reportPalzActivity } from "./activity.js";
10
11
  import { tracer, extractTraceparentContext, SpanStatusCode } from "./tracing.js";
11
12
  import type { PalzConfig, PalzMessageEvent } from "./types.js";
12
13
 
@@ -40,6 +41,7 @@ export async function monitorPalzProvider(params: MonitorPalzParams): Promise<vo
40
41
  let currentWs: WebSocket | null = null;
41
42
  let consecutive4002 = 0;
42
43
  const MAX_CONSECUTIVE_4002 = 10;
44
+ const MAX_RECONNECT_DELAY_MS = 16_000;
43
45
 
44
46
  const cleanup = () => {
45
47
  closed = true;
@@ -72,15 +74,28 @@ export async function monitorPalzProvider(params: MonitorPalzParams): Promise<vo
72
74
  }
73
75
  abortSignal?.addEventListener("abort", handleAbort, { once: true });
74
76
 
75
- function scheduleReconnect() {
77
+ function scheduleReconnect(delay = reconnectDelay, advanceBackoff = true) {
76
78
  if (closed) return;
77
- log(`palz[${accountId}]: scheduling reconnect in ${reconnectDelay}ms`);
78
- reconnectTimer = setTimeout(connect, reconnectDelay);
79
- reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
79
+ if (reconnectTimer) {
80
+ clearTimeout(reconnectTimer);
81
+ reconnectTimer = null;
82
+ }
83
+ log(`palz[${accountId}]: scheduling reconnect in ${delay}ms`);
84
+ reconnectTimer = setTimeout(() => {
85
+ reconnectTimer = null;
86
+ connect();
87
+ }, delay);
88
+ if (advanceBackoff) {
89
+ reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
90
+ }
80
91
  }
81
92
 
82
93
  function connect() {
83
94
  if (closed) return;
95
+ if (reconnectTimer) {
96
+ clearTimeout(reconnectTimer);
97
+ reconnectTimer = null;
98
+ }
84
99
 
85
100
  let ws: WebSocket;
86
101
  try {
@@ -94,7 +109,55 @@ export async function monitorPalzProvider(params: MonitorPalzParams): Promise<vo
94
109
  let connectedAt = 0;
95
110
  let pingInterval: ReturnType<typeof setInterval> | null = null;
96
111
  let messageCount = 0;
97
- let closeFired = false;
112
+ let ended = false;
113
+
114
+ const handleSocketEnd = (code: number, reasonStr: string) => {
115
+ if (ended) return;
116
+ ended = true;
117
+ if (pingInterval) {
118
+ clearInterval(pingInterval);
119
+ pingInterval = null;
120
+ }
121
+ if (closed) return;
122
+
123
+ const wasConnected = connectedAt > 0;
124
+ const stableMs = wasConnected ? Date.now() - connectedAt : 0;
125
+
126
+ // 4002: 被新连接替代,带退避重试
127
+ if (code === 4002) {
128
+ consecutive4002++;
129
+ if (consecutive4002 >= MAX_CONSECUTIVE_4002) {
130
+ log(
131
+ `palz[${accountId}]: replaced ${consecutive4002} times (code=4002), giving up`,
132
+ );
133
+ cleanup();
134
+ resolve();
135
+ return;
136
+ }
137
+ const jitter = Math.floor(Math.random() * 2000);
138
+ const delay = 3000 * consecutive4002 + jitter;
139
+ log(
140
+ `palz[${accountId}]: replaced (code=4002, attempt=${consecutive4002}), retry in ${delay}ms`,
141
+ );
142
+ scheduleReconnect(delay, false);
143
+ return;
144
+ }
145
+
146
+ // 4001: bot_id 缺失,不重连
147
+ if (code === 4001) {
148
+ error(`palz[${accountId}]: bot_id missing (code=4001), not reconnecting`);
149
+ cleanup();
150
+ resolve();
151
+ return;
152
+ }
153
+
154
+ // 其他断开:指数退避重连
155
+ if (wasConnected && stableMs > 10_000) reconnectDelay = 1000;
156
+ log(
157
+ `palz[${accountId}]: disconnected (code=${code}, reason=${reasonStr}, uptime=${Math.round(stableMs / 1000)}s, msgs=${messageCount}), reconnecting in ${reconnectDelay}ms`,
158
+ );
159
+ scheduleReconnect();
160
+ };
98
161
 
99
162
  ws.on("open", () => {
100
163
  connectedAt = Date.now();
@@ -111,6 +174,7 @@ export async function monitorPalzProvider(params: MonitorPalzParams): Promise<vo
111
174
  });
112
175
 
113
176
  ws.on("message", (data: Buffer) => {
177
+ const receivedAt = new Date();
114
178
  const raw = data.toString();
115
179
  try {
116
180
  const msg = JSON.parse(raw) as PalzMessageEvent;
@@ -129,7 +193,22 @@ export async function monitorPalzProvider(params: MonitorPalzParams): Promise<vo
129
193
 
130
194
  log(`palz[${accountId}]: [WS_RECV] #${messageCount} full_message=${raw.slice(0, 1000)} traceId=${span.spanContext().traceId}`);
131
195
 
132
- handlePalzMessage({ cfg, msg, runtime, accountId }).catch((err: any) => {
196
+ reportPalzActivity({
197
+ config,
198
+ msg,
199
+ receivedAt,
200
+ runtime,
201
+ accountId,
202
+ }).catch((err: any) => {
203
+ error(`palz[${accountId}]: [ACTIVITY_REPORT] unhandled error: ${err.message ?? err}`);
204
+ });
205
+
206
+ handlePalzMessage({
207
+ cfg,
208
+ msg,
209
+ runtime,
210
+ accountId,
211
+ }).catch((err: any) => {
133
212
  error(`palz[${accountId}]: [WS_RECV] handlePalzMessage unhandled error: ${err.message ?? err}`);
134
213
  });
135
214
  } catch (e) {
@@ -145,62 +224,12 @@ export async function monitorPalzProvider(params: MonitorPalzParams): Promise<vo
145
224
  });
146
225
 
147
226
  ws.on("close", (code: number, reason: Buffer) => {
148
- closeFired = true;
149
- if (pingInterval) {
150
- clearInterval(pingInterval);
151
- pingInterval = null;
152
- }
153
- if (closed) return;
154
-
155
- const stableMs = Date.now() - connectedAt;
156
- const reasonStr = reason?.toString() || "";
157
-
158
- // 4002: 被新连接替代,带退避重试
159
- if (code === 4002) {
160
- consecutive4002++;
161
- if (consecutive4002 >= MAX_CONSECUTIVE_4002) {
162
- log(
163
- `palz[${accountId}]: replaced ${consecutive4002} times (code=4002), giving up`,
164
- );
165
- cleanup();
166
- resolve();
167
- return;
168
- }
169
- const jitter = Math.floor(Math.random() * 2000);
170
- const delay = 3000 * consecutive4002 + jitter;
171
- log(
172
- `palz[${accountId}]: replaced (code=4002, attempt=${consecutive4002}), retry in ${delay}ms`,
173
- );
174
- reconnectTimer = setTimeout(connect, delay);
175
- return;
176
- }
177
-
178
- // 4001: bot_id 缺失,不重连
179
- if (code === 4001) {
180
- error(`palz[${accountId}]: bot_id missing (code=4001), not reconnecting`);
181
- cleanup();
182
- resolve();
183
- return;
184
- }
185
-
186
- // 其他断开:指数退避重连
187
- if (stableMs > 10_000) reconnectDelay = 1000;
188
- log(
189
- `palz[${accountId}]: disconnected (code=${code}, reason=${reasonStr}, uptime=${Math.round(stableMs / 1000)}s, msgs=${messageCount}), reconnecting in ${reconnectDelay}ms`,
190
- );
191
- scheduleReconnect();
227
+ handleSocketEnd(code, reason?.toString() || "");
192
228
  });
193
229
 
194
230
  ws.on("error", (err) => {
195
231
  error(`palz[${accountId}]: WebSocket error: ${err.message}`);
196
- if (!closeFired) {
197
- closeFired = true;
198
- if (pingInterval) {
199
- clearInterval(pingInterval);
200
- pingInterval = null;
201
- }
202
- scheduleReconnect();
203
- }
232
+ handleSocketEnd(1006, err.message);
204
233
  });
205
234
  }
206
235
 
package/src/types.ts CHANGED
@@ -39,6 +39,8 @@ export interface PalzMessageEvent {
39
39
  group_id?: string;
40
40
  /** 群主用户 ID,群聊时由 IM 下发 */
41
41
  group_owner_id?: string;
42
+ /** 群聊名称,群聊时由 IM 下发 */
43
+ group_name?: string;
42
44
  /** Lobster ID,标识 agent 身份(IM 通过此字段区分不同 agent) */
43
45
  lobster_id?: string;
44
46
  /** 群组类型,由 IM 下发 */
@@ -62,6 +64,10 @@ export interface PalzConfig {
62
64
  botId?: string;
63
65
  streamUrl?: string;
64
66
  apiBaseUrl?: string;
67
+ /** claw-gateway 服务基础地址,例如 https://example.com */
68
+ clawGatewayUrl?: string;
69
+ /** 是否向 claw-gateway 上报 IM 消息接收事件,默认 false */
70
+ activityReportEnabled?: boolean;
65
71
  sessionTimeout?: number;
66
72
  /** 群聊上下文缓存开关:true=未@消息缓存为上下文(默认),false=所有群聊消息直接发送给AI */
67
73
  groupContextCache?: boolean;