palz-connector 1.7.0 → 1.7.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.7.0",
4
+ "version": "1.7.2",
5
5
  "description": "Palz IM 接入 OpenClaw",
6
6
  "channels": [
7
7
  "palz-connector"
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "palz-connector",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
7
7
  "scripts": {
8
- "build": "tsc"
8
+ "build": "tsc",
9
+ "prepack": "npm run build"
9
10
  },
10
11
  "files": [
11
12
  "index.js",
@@ -0,0 +1,12 @@
1
+ {
2
+ "enabled": true,
3
+ "streamUrl": "ws://claw-server:8787/ws/bot",
4
+ "apiBaseUrl": "http://claw-server:8787/api",
5
+ "clawGatewayUrl": "http://claw-gateway:8080",
6
+ "activityReportEnabled": true,
7
+ "sessionTimeout": 1800000,
8
+ "groupContextCache": false,
9
+ "showProcess": true,
10
+ "kbEngineUrl": "http://uniclaw-kb-engine:8070",
11
+ "controlPort": 8008
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "enabled": true,
3
+ "streamUrl": "ws://claw-server:8787/ws/bot",
4
+ "apiBaseUrl": "http://claw-server:8787/api",
5
+ "clawGatewayUrl": "http://claw-gateway:8080",
6
+ "activityReportEnabled": true,
7
+ "sessionTimeout": 1800000,
8
+ "groupContextCache": false,
9
+ "showProcess": true,
10
+ "kbEngineUrl": "http://uniclaw-kb-engine:8070",
11
+ "controlPort": 8008
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "enabled": true,
3
+ "streamUrl": "ws://claw-server:8787/ws/bot",
4
+ "apiBaseUrl": "http://claw-server:8787/api",
5
+ "clawGatewayUrl": "http://claw-gateway:8080",
6
+ "activityReportEnabled": true,
7
+ "sessionTimeout": 1800000,
8
+ "groupContextCache": false,
9
+ "showProcess": true,
10
+ "kbEngineUrl": "http://uniclaw-kb-engine:8070",
11
+ "controlPort": 8008
12
+ }
package/src/activity.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Palz IM 收到消息后的 claw-gateway 活动上报。
2
+ * Palz IM WebSocket 连接成功后的 claw-gateway 活动上报。
3
3
  */
4
4
  const ACTIVITY_REPORT_TIMEOUT_MS = 2000;
5
5
  const ACTIVITY_REPORT_PATH_PREFIX = "/openclaw-gateway/be/deployments";
6
6
  const warnedMissingConfig = new Set();
7
+ const SERVICE_NAME = process.env.OPENCLAW_WORKSPACE_POOL_SERVICE_NAME ?? "";
7
8
  function formatDateTimeWithOffset(date) {
8
9
  const pad = (n, width = 2) => String(n).padStart(width, "0");
9
10
  const offsetMinutes = -date.getTimezoneOffset();
@@ -23,7 +24,7 @@ function resolveActivityUrl(baseUrl, releaseName) {
23
24
  return `${normalizedBase}${ACTIVITY_REPORT_PATH_PREFIX}/${encodeURIComponent(releaseName)}/activity`;
24
25
  }
25
26
  export async function reportPalzActivity(params) {
26
- const { config, msg, receivedAt, runtime, accountId } = params;
27
+ const { config, connectedAt, runtime, accountId } = params;
27
28
  const log = typeof runtime?.log === "function" ? runtime.log : console.log;
28
29
  const error = typeof runtime?.error === "function" ? runtime.error : console.error;
29
30
  const tag = `palz[${accountId ?? config.botId ?? "default"}]`;
@@ -41,22 +42,17 @@ export async function reportPalzActivity(params) {
41
42
  }
42
43
  const payload = {
43
44
  source: "palz-connector",
44
- eventType: "im_message_received",
45
- messageId: msg.msg_id,
46
- conversationId: msg.conversation_id,
47
- conversationType: msg.conversation_type || "direct",
48
- userId: msg.owner_id ?? "",
49
- senderId: msg.sender_id,
50
- receivedAt: formatDateTimeWithOffset(receivedAt),
45
+ eventType: "ws_connected",
46
+ releaseName,
47
+ serviceName: SERVICE_NAME,
48
+ connectedAt: formatDateTimeWithOffset(connectedAt),
51
49
  };
52
- if (msg.traceparent) {
53
- payload.traceId = msg.traceparent;
54
- }
55
50
  const url = resolveActivityUrl(baseUrl, releaseName);
56
51
  const body = JSON.stringify(payload);
57
52
  const controller = new AbortController();
58
53
  const timeout = setTimeout(() => controller.abort(), ACTIVITY_REPORT_TIMEOUT_MS);
59
54
  const startMs = Date.now();
55
+ log(`${tag}: [ACTIVITY_REPORT] reporting eventType=ws_connected url=${url} body=${body}`);
60
56
  try {
61
57
  const response = await fetch(url, {
62
58
  method: "POST",
@@ -67,15 +63,15 @@ export async function reportPalzActivity(params) {
67
63
  const elapsedMs = Date.now() - startMs;
68
64
  const responseText = await response.text().catch(() => "");
69
65
  if (!response.ok) {
70
- error(`${tag}: [ACTIVITY_REPORT] failed status=${response.status} elapsed=${elapsedMs}ms msg_id=${msg.msg_id} response=${responseText.slice(0, 300)}`);
66
+ error(`${tag}: [ACTIVITY_REPORT] failed status=${response.status} elapsed=${elapsedMs}ms response=${responseText.slice(0, 300)}`);
71
67
  return;
72
68
  }
73
- log(`${tag}: [ACTIVITY_REPORT] ok status=${response.status} elapsed=${elapsedMs}ms msg_id=${msg.msg_id}`);
69
+ log(`${tag}: [ACTIVITY_REPORT] ok status=${response.status} elapsed=${elapsedMs}ms eventType=ws_connected`);
74
70
  }
75
71
  catch (err) {
76
72
  const elapsedMs = Date.now() - startMs;
77
73
  const reason = err?.name === "AbortError" ? `timeout ${ACTIVITY_REPORT_TIMEOUT_MS}ms` : (err?.message ?? String(err));
78
- error(`${tag}: [ACTIVITY_REPORT] error elapsed=${elapsedMs}ms msg_id=${msg.msg_id} reason=${reason}`);
74
+ error(`${tag}: [ACTIVITY_REPORT] error elapsed=${elapsedMs}ms reason=${reason}`);
79
75
  }
80
76
  finally {
81
77
  clearTimeout(timeout);
package/src/bot.js CHANGED
@@ -19,6 +19,7 @@ import { extractFileUrls, ingestFilesToKbEngine } from "./kb-ingest.js";
19
19
  import { consumeKbTaskContext, extractKbTaskCallbackContent, extractKbTaskId, resolveKbTaskContext, } from "./kb-task-callback.js";
20
20
  import { sendToPalzIM } from "./send.js";
21
21
  import { buildPassthroughFromMsg } from "./message-utils.js";
22
+ import { buildPalzGatewayRelayContext, runWithPalzGatewayRelay } from "./gateway-relay.js";
22
23
  import { tracer, trace, context, SpanStatusCode } from "./tracing.js";
23
24
  // ============ 文本提取工具 ============
24
25
  function extractPlainText(content) {
@@ -209,6 +210,24 @@ function buildMediaPayload(mediaList) {
209
210
  MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
210
211
  };
211
212
  }
213
+ function buildUniclawEventContext(params) {
214
+ return {
215
+ label: "Uniclaw event",
216
+ source: "palz-connector",
217
+ type: "server.message_event.v1",
218
+ payload: {
219
+ raw: params.msg,
220
+ route: {
221
+ agent_id: params.effectiveAgentId,
222
+ session_key: params.route?.sessionKey,
223
+ account_id: params.route?.accountId || params.accountId,
224
+ chat_type: params.chatType,
225
+ group_id: params.groupId || "",
226
+ was_mentioned: params.wasMentioned,
227
+ },
228
+ },
229
+ };
230
+ }
212
231
  function buildFileSharingSystemPrompt() {
213
232
  return [
214
233
  "## File Sharing",
@@ -678,6 +697,15 @@ async function _dispatchPalzMessageInner(params) {
678
697
  SenderId: msg.sender_id,
679
698
  SenderName: senderName,
680
699
  UntrustedContext: untrustedContext,
700
+ UntrustedStructuredContext: [buildUniclawEventContext({
701
+ msg,
702
+ effectiveAgentId,
703
+ route,
704
+ accountId,
705
+ chatType,
706
+ groupId,
707
+ wasMentioned,
708
+ })],
681
709
  Provider: "palz-connector",
682
710
  Surface: "palz-connector",
683
711
  MessageSid: msg.msg_id,
@@ -810,6 +838,13 @@ async function _dispatchPalzMessageInner(params) {
810
838
  log(`${tag}: ${step6dStart}`);
811
839
  span?.addEvent(step6dStart);
812
840
  const dispatchStartMs = Date.now();
841
+ const palzRelay = buildPalzGatewayRelayContext({
842
+ msg,
843
+ accountId,
844
+ agentId: effectiveAgentId,
845
+ relayInput: { config, route, ctx, plainText, useStream, groupId },
846
+ runtime,
847
+ });
813
848
  let queuedFinal = false;
814
849
  let counts = {};
815
850
  let attempt = 0; // 已重生成次数
@@ -818,7 +853,7 @@ async function _dispatchPalzMessageInner(params) {
818
853
  let blockedByRiskControl = false; // 最近一轮是否仍被风控拦截
819
854
  while (true) {
820
855
  const { dispatcher, replyOptions, markDispatchIdle, riskControl } = makeDispatcher();
821
- const result = await core.channel.reply.withReplyDispatcher({
856
+ const result = await runWithPalzGatewayRelay(palzRelay, () => core.channel.reply.withReplyDispatcher({
822
857
  dispatcher,
823
858
  onSettled: () => markDispatchIdle(),
824
859
  run: () => core.channel.reply.dispatchReplyFromConfig({
@@ -827,7 +862,7 @@ async function _dispatchPalzMessageInner(params) {
827
862
  dispatcher,
828
863
  replyOptions,
829
864
  }),
830
- });
865
+ }));
831
866
  queuedFinal = result.queuedFinal;
832
867
  counts = result.counts;
833
868
  // withReplyDispatcher 返回时 sendChain(含 HTTP send)已 settle,风控结果已确定。
@@ -0,0 +1,340 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const PALZ_RELAY_HOP_HEADER = "X-Uniclaw-Palz-Relay-Hop";
3
+ const PALZ_RELAY_SOURCE_PHYSICAL_HEADER = "X-Uniclaw-Palz-Relay-Source-Physical";
4
+ const PALZ_RELAY_HOP_OPENCLAW_AGENT = "openclaw-agent";
5
+ const PALZ_GATEWAY_RELAY_GLOBAL_KEY = "__uniclawPalzGatewayRelay";
6
+ const PALZ_GATEWAY_RELAY_NATIVE_VERSION = "1.8.0";
7
+ const gatewayRelayNotConfiguredError = "OPENCLAW_PALZ_RELAY_URL is required for palz gateway relay";
8
+ const relayStorage = new AsyncLocalStorage();
9
+ let fetchPatched = false;
10
+ function exposePalzGatewayRelayGlobal() {
11
+ const existing = globalThis[PALZ_GATEWAY_RELAY_GLOBAL_KEY] ?? {};
12
+ if (existing.__palzConnectorNativeVersion === PALZ_GATEWAY_RELAY_NATIVE_VERSION)
13
+ return;
14
+ globalThis[PALZ_GATEWAY_RELAY_GLOBAL_KEY] = {
15
+ ...existing,
16
+ __palzConnectorNativeVersion: PALZ_GATEWAY_RELAY_NATIVE_VERSION,
17
+ currentPalzGatewayRelay: () => currentPalzGatewayRelay(),
18
+ runWithPalzGatewayRelay: (context, fn) => {
19
+ if (!context || !normalize(context.relayUrl))
20
+ throw new Error(gatewayRelayNotConfiguredError);
21
+ return relayStorage.run(context, fn);
22
+ },
23
+ };
24
+ }
25
+ function normalize(value) {
26
+ return String(value ?? "").trim();
27
+ }
28
+ function randomRelayKey() {
29
+ try {
30
+ const bytes = new Uint8Array(16);
31
+ crypto.getRandomValues(bytes);
32
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
33
+ }
34
+ catch {
35
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
36
+ }
37
+ }
38
+ function palzMessageContent(msg, relayInput) {
39
+ const direct = normalize(relayInput.plainText || msg.plainText || msg.text || msg.body || msg.Body);
40
+ if (direct)
41
+ return direct;
42
+ const content = msg.content;
43
+ if (typeof content === "string")
44
+ return normalize(content);
45
+ if (Array.isArray(content)) {
46
+ return content
47
+ .map((item) => typeof item === "string" ? item : normalize(item?.text || item?.content))
48
+ .filter(Boolean)
49
+ .join("\n");
50
+ }
51
+ return "";
52
+ }
53
+ export function buildPalzGatewayRelayContext(params) {
54
+ const relayUrl = normalize(process.env.OPENCLAW_PALZ_RELAY_URL);
55
+ if (!relayUrl) {
56
+ const runtime = params.runtime;
57
+ const logError = typeof runtime?.error === "function" ? runtime.error : console.error;
58
+ logError(`[GATEWAY_RELAY] ${gatewayRelayNotConfiguredError}`);
59
+ throw new Error(gatewayRelayNotConfiguredError);
60
+ }
61
+ const { msg, accountId, agentId, relayInput, runtime } = params;
62
+ const ctx = relayInput.ctx ?? {};
63
+ const releaseName = normalize(accountId || msg.release_name || msg.releaseName);
64
+ const userId = releaseName.replace(/^user-/, "");
65
+ const agent = normalize(agentId || msg.agent_id);
66
+ return {
67
+ relayUrl,
68
+ apiKey: normalize(process.env.CLAW_GATEWAY_API_KEY || process.env.OPENCLAW_GATEWAY_API_KEY),
69
+ namespace: normalize(process.env.OPENCLAW_POD_NAMESPACE || process.env.POD_NAMESPACE),
70
+ releaseName,
71
+ logicalReleaseName: releaseName,
72
+ physicalReleaseName: normalize(process.env.OPENCLAW_WORKSPACE_POOL_PHYSICAL_RELEASE || process.env.botID),
73
+ userId,
74
+ agentId: agent,
75
+ runtimeAgentId: agent,
76
+ prewarmKey: randomRelayKey(),
77
+ conversationId: normalize(msg.conversation_id || msg.conversationId || relayInput.groupId || ctx.conversation_id || ctx.conversationId),
78
+ conversationType: normalize(msg.conversation_type || msg.conversationType || (relayInput.groupId ? "group" : "")),
79
+ senderId: normalize(msg.sender_id || msg.senderId || msg.from || ctx.sender_id || ctx.senderId),
80
+ senderName: normalize(msg.sender_name || msg.senderName || msg.nick || msg.nickname || ctx.sender_name || ctx.senderName),
81
+ senderAccountType: normalize(msg.sender_account_type || msg.senderAccountType || ctx.sender_account_type || ctx.senderAccountType),
82
+ ownerId: normalize(msg.owner_id || msg.ownerId || ctx.owner_id || ctx.ownerId),
83
+ ownerName: normalize(msg.owner_name || msg.ownerName || ctx.owner_name || ctx.ownerName),
84
+ groupId: normalize(relayInput.groupId || msg.group_id || msg.groupId),
85
+ groupKind: ctx.group_kind ?? ctx.groupKind ?? msg.group_kind ?? msg.groupKind,
86
+ msgId: normalize(msg.msg_id || msg.msgId || msg.message_id || msg.messageId),
87
+ mentionedBot: msg.mentioned_bot ?? msg.mentionedBot ?? ctx.mentioned_bot ?? ctx.mentionedBot,
88
+ lobsterId: normalize(msg.lobster_id || msg.lobsterId || msg.agent_id || msg.agentId || agent),
89
+ lobsterName: normalize(msg.lobster_name || msg.lobsterName || msg.agent_name || msg.agentName),
90
+ lobsterType: normalize(msg.lobster_type || msg.lobsterType),
91
+ groupMembersList: ctx.group_members_list || ctx.groupMembersList || msg.group_members_list || msg.groupMembersList || null,
92
+ groupLobsters: ctx.group_lobsters || ctx.groupLobsters || msg.group_lobsters || msg.groupLobsters || null,
93
+ content: palzMessageContent(msg, relayInput),
94
+ message: msg,
95
+ context: ctx || null,
96
+ route: relayInput.route || null,
97
+ relayInput,
98
+ log: typeof runtime?.log === "function" ? runtime.log : console.log,
99
+ error: typeof runtime?.error === "function" ? runtime.error : console.error,
100
+ };
101
+ }
102
+ export function currentPalzGatewayRelay() {
103
+ return relayStorage.getStore() ?? null;
104
+ }
105
+ export async function runWithPalzGatewayRelay(relay, fn) {
106
+ if (!relay)
107
+ throw new Error("palz gateway relay context is required");
108
+ exposePalzGatewayRelayGlobal();
109
+ installPalzGatewayRelayFetchPatch();
110
+ relay.log?.(`[GATEWAY_RELAY] armed transparent transport relay url=${relay.relayUrl} release=${relay.releaseName ?? ""} agent=${relay.agentId ?? ""} msg_id=${relay.msgId ?? ""}`);
111
+ return relayStorage.run(relay, fn);
112
+ }
113
+ function requestURL(url) {
114
+ if (url && typeof url === "object" && "url" in url) {
115
+ return normalize(url.url);
116
+ }
117
+ return normalize(url);
118
+ }
119
+ function shouldInterceptModelRequest(url) {
120
+ const raw = requestURL(url);
121
+ return /\/v1\/api\/.+\/(chat\/completions|images\/generations|video\/generations)|\/v1\/api\/(chat\/completions|images\/generations|video\/generations)/.test(raw);
122
+ }
123
+ function withLocatorHeaders(init, relay) {
124
+ if (!normalize(relay.userId))
125
+ return init;
126
+ const next = { ...(init ?? {}) };
127
+ const headers = new Headers(next.headers);
128
+ headers.set("X-CLAW-USER", `uniclaw-user-locator:${normalize(relay.userId)}`);
129
+ headers.set("X-OpenClaw-User-ID", normalize(relay.userId));
130
+ if (normalize(relay.agentId)) {
131
+ headers.set("X-OpenClaw-Agent-ID", normalize(relay.agentId));
132
+ headers.set("X-Uniclaw-Agent-Id", normalize(relay.agentId));
133
+ }
134
+ if (normalize(relay.releaseName))
135
+ headers.set("X-Uniclaw-Owner-Ref", normalize(relay.releaseName));
136
+ if (normalize(relay.physicalReleaseName))
137
+ headers.set("X-OpenClaw-Physical-Release", normalize(relay.physicalReleaseName));
138
+ if (normalize(relay.prewarmKey))
139
+ headers.set("X-Uniclaw-Palz-Prewarm-Key", normalize(relay.prewarmKey));
140
+ next.headers = headers;
141
+ return next;
142
+ }
143
+ function parseRequestBodyText(text) {
144
+ const trimmed = text.trim();
145
+ if (!trimmed)
146
+ return null;
147
+ try {
148
+ return JSON.parse(trimmed);
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ function parseGatewayRelayResponseText(raw) {
155
+ try {
156
+ return raw ? JSON.parse(raw) : null;
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ async function requestBodyFromInit(init) {
163
+ const body = init?.body;
164
+ if (body == null)
165
+ return null;
166
+ let text = "";
167
+ if (typeof body === "string")
168
+ text = body;
169
+ else if (body instanceof Uint8Array)
170
+ text = new TextDecoder().decode(body);
171
+ else if (body instanceof ArrayBuffer)
172
+ text = new TextDecoder().decode(new Uint8Array(body));
173
+ else if (ArrayBuffer.isView(body))
174
+ text = new TextDecoder().decode(new Uint8Array(body.buffer, body.byteOffset, body.byteLength));
175
+ else if (typeof Blob !== "undefined" && body instanceof Blob)
176
+ text = await body.text().catch(() => "");
177
+ else if (body && typeof body.getReader === "function")
178
+ text = await new Response(body).text().catch(() => "");
179
+ else if (body && typeof body.text === "function")
180
+ text = await body.text().catch(() => "");
181
+ return parseRequestBodyText(text);
182
+ }
183
+ async function requestBodyFromFetchInput(input, init) {
184
+ if (typeof Request !== "undefined" && input instanceof Request) {
185
+ try {
186
+ const parsed = parseRequestBodyText(await input.clone().text());
187
+ if (parsed)
188
+ return parsed;
189
+ }
190
+ catch { }
191
+ }
192
+ return requestBodyFromInit(init);
193
+ }
194
+ function headersFromFetchInput(input, init) {
195
+ const out = {};
196
+ try {
197
+ const headers = new Headers(typeof Request !== "undefined" && input instanceof Request ? input.headers : undefined);
198
+ const initHeaders = new Headers(init?.headers);
199
+ initHeaders.forEach((value, key) => headers.set(key, value));
200
+ headers.forEach((value, key) => {
201
+ out[key] = value;
202
+ });
203
+ }
204
+ catch { }
205
+ return out;
206
+ }
207
+ function isGatewayOpenClawAgentRelayHop(headers) {
208
+ const h = new Headers(headers);
209
+ return normalize(h.get(PALZ_RELAY_HOP_HEADER)) === PALZ_RELAY_HOP_OPENCLAW_AGENT &&
210
+ Boolean(normalize(h.get(PALZ_RELAY_SOURCE_PHYSICAL_HEADER)));
211
+ }
212
+ function openClawPathFromURL(url) {
213
+ let pathname = "";
214
+ try {
215
+ pathname = new URL(requestURL(url), "http://local").pathname;
216
+ }
217
+ catch {
218
+ pathname = "";
219
+ }
220
+ if (!pathname)
221
+ return "";
222
+ if (pathname.startsWith("/v1/api/v1/"))
223
+ return `/v1/${pathname.slice("/v1/api/v1/".length)}`;
224
+ if (pathname.startsWith("/v1/api/"))
225
+ return `/v1/${pathname.slice("/v1/api/".length)}`;
226
+ return pathname;
227
+ }
228
+ function modelResponseFromGatewayData(data) {
229
+ if (!data)
230
+ throw new Error("gateway relay response is empty");
231
+ let body = "";
232
+ if (typeof data.openclawResponseText === "string" && data.openclawResponseText) {
233
+ body = data.openclawResponseText;
234
+ }
235
+ else if (data.openclawResponse !== undefined && data.openclawResponse !== null) {
236
+ body = typeof data.openclawResponse === "string" ? data.openclawResponse : JSON.stringify(data.openclawResponse);
237
+ }
238
+ else {
239
+ throw new Error("gateway relay response missing openclawResponse");
240
+ }
241
+ const status = Number(data.statusCode || 200) || 200;
242
+ const contentType = normalize(data.openclawContentType) || "application/json";
243
+ return new Response(body, { status, headers: { "Content-Type": contentType } });
244
+ }
245
+ async function relayOpenClawRequest(input, url, init, relay) {
246
+ const requestBody = await requestBodyFromFetchInput(input, init);
247
+ if (!normalize(relay.relayUrl))
248
+ throw new Error(gatewayRelayNotConfiguredError);
249
+ if (!requestBody)
250
+ throw new Error("failed to capture OpenClaw request body for palz gateway relay");
251
+ const inboundHeaders = headersFromFetchInput(input, init);
252
+ if (isGatewayOpenClawAgentRelayHop(inboundHeaders)) {
253
+ throw new Error("palz gateway relay captured gateway-originated OpenClaw agent hop");
254
+ }
255
+ const headers = { "Content-Type": "application/json" };
256
+ if (normalize(relay.apiKey))
257
+ headers["X-API-Key"] = normalize(relay.apiKey);
258
+ const payload = {
259
+ namespace: relay.namespace,
260
+ releaseName: relay.releaseName,
261
+ logicalReleaseName: relay.logicalReleaseName || relay.releaseName,
262
+ physicalReleaseName: relay.physicalReleaseName,
263
+ userId: relay.userId,
264
+ agentId: relay.agentId,
265
+ runtimeAgentId: relay.runtimeAgentId,
266
+ prewarmKey: relay.prewarmKey,
267
+ conversationId: relay.conversationId,
268
+ conversationType: relay.conversationType,
269
+ senderId: relay.senderId,
270
+ senderName: relay.senderName,
271
+ senderAccountType: relay.senderAccountType,
272
+ ownerId: relay.ownerId,
273
+ ownerName: relay.ownerName,
274
+ groupId: relay.groupId,
275
+ groupKind: relay.groupKind,
276
+ msgId: relay.msgId,
277
+ mentionedBot: relay.mentionedBot,
278
+ lobsterId: relay.lobsterId,
279
+ lobsterName: relay.lobsterName,
280
+ lobsterType: relay.lobsterType,
281
+ groupMembersList: relay.groupMembersList,
282
+ groupLobsters: relay.groupLobsters,
283
+ content: relay.content,
284
+ message: relay.message,
285
+ context: relay.context,
286
+ route: relay.route,
287
+ relayInput: relay.relayInput,
288
+ openclawPath: openClawPathFromURL(url),
289
+ headers: inboundHeaders,
290
+ openclawRequest: requestBody,
291
+ };
292
+ try {
293
+ relay.log?.(`[GATEWAY_RELAY] forwarding generated OpenClaw request release=${relay.releaseName ?? ""} agent=${relay.agentId ?? ""}`);
294
+ const resp = await fetch(relay.relayUrl, {
295
+ method: "POST",
296
+ headers,
297
+ body: JSON.stringify(payload),
298
+ });
299
+ const raw = await resp.text();
300
+ const parsed = parseGatewayRelayResponseText(raw);
301
+ const data = parsed?.data ? parsed.data : parsed;
302
+ if (data?.skippedInternalRelay) {
303
+ throw new Error("gateway relay skipped internal shared OpenClaw loop");
304
+ }
305
+ if (!resp.ok || !data || data.ok === false) {
306
+ throw new Error(`gateway relay failed status=${resp.status} body=${raw.slice(0, 500)}`);
307
+ }
308
+ const modelResponse = modelResponseFromGatewayData(data);
309
+ relay.log?.(`[GATEWAY_RELAY] gateway relay returned OpenClaw response status=${modelResponse.status}`);
310
+ return modelResponse;
311
+ }
312
+ catch (err) {
313
+ relay.error?.(`[GATEWAY_RELAY] gateway relay error=${String(err?.message || err)}`);
314
+ throw err;
315
+ }
316
+ }
317
+ export function installPalzGatewayRelayFetchPatch() {
318
+ if (fetchPatched || typeof globalThis.fetch !== "function")
319
+ return;
320
+ const originalFetch = globalThis.fetch.bind(globalThis);
321
+ globalThis.fetch = async (input, init) => {
322
+ const relay = currentPalzGatewayRelay();
323
+ if (!relay || !shouldInterceptModelRequest(input)) {
324
+ return originalFetch(input, init);
325
+ }
326
+ const nextInit = withLocatorHeaders(init, relay);
327
+ return relayOpenClawRequest(input, requestURL(input), nextInit, relay);
328
+ };
329
+ fetchPatched = true;
330
+ }
331
+ export function resolveIMSendProxy() {
332
+ const url = normalize(process.env.OPENCLAW_IM_SEND_PROXY_URL);
333
+ if (!url)
334
+ return null;
335
+ return {
336
+ url,
337
+ apiKey: normalize(process.env.CLAW_GATEWAY_API_KEY || process.env.OPENCLAW_GATEWAY_API_KEY),
338
+ };
339
+ }
340
+ exposePalzGatewayRelayGlobal();
package/src/monitor.js CHANGED
@@ -167,6 +167,14 @@ export async function monitorPalzProvider(params) {
167
167
  lastPongAt = connectedAt;
168
168
  consecutive4002 = 0;
169
169
  log(`palz[${accountId}]: WebSocket connected, bot_id=${config.botId}`);
170
+ reportPalzActivity({
171
+ config,
172
+ connectedAt: new Date(connectedAt),
173
+ runtime,
174
+ accountId,
175
+ }).catch((err) => {
176
+ error(`palz[${accountId}]: [ACTIVITY_REPORT] unhandled error: ${err.message ?? err}`);
177
+ });
170
178
  pingInterval = setInterval(() => {
171
179
  if (ws.readyState === WebSocket.OPEN) {
172
180
  const timeSinceLastPong = Date.now() - lastPongAt;
@@ -195,7 +203,6 @@ export async function monitorPalzProvider(params) {
195
203
  }
196
204
  });
197
205
  ws.on("message", (data) => {
198
- const receivedAt = new Date();
199
206
  const raw = data.toString();
200
207
  try {
201
208
  const msg = JSON.parse(raw);
@@ -209,15 +216,6 @@ export async function monitorPalzProvider(params) {
209
216
  try {
210
217
  span.setAttribute("raw_message", raw);
211
218
  log(`palz[${accountId}]: [WS_RECV] #${messageCount} full_message=${raw} traceId=${span.spanContext().traceId}`);
212
- reportPalzActivity({
213
- config,
214
- msg,
215
- receivedAt,
216
- runtime,
217
- accountId,
218
- }).catch((err) => {
219
- error(`palz[${accountId}]: [ACTIVITY_REPORT] unhandled error: ${err.message ?? err}`);
220
- });
221
219
  // agent_id:从消息中取,缺失/空时默认为 {userId}-main
222
220
  let messageAgentId = typeof msg.agent_id === "string" ? msg.agent_id.trim() : "";
223
221
  if (!messageAgentId) {
package/src/send.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { tracer, trace, SpanStatusCode, buildTraceparentHeader } from "./tracing.js";
8
8
  import { contentPartsToOpenAIContent } from "./content.js";
9
+ import { resolveIMSendProxy } from "./gateway-relay.js";
9
10
  const SEND_TIMEOUT_MS = 40_000;
10
11
  /**
11
12
  * IM 后端转调腾讯 IM SDK 时可能命中内容风控(敏感词),此时 HTTP 200 但 body
@@ -71,7 +72,8 @@ async function _sendToPalzIMInner(params) {
71
72
  const log = paramLog ?? console.log;
72
73
  const error = paramError ?? console.error;
73
74
  const content = normalizeContent(rawContent);
74
- const url = `${config.apiBaseUrl}/bot/send`;
75
+ const imSendProxy = resolveIMSendProxy();
76
+ const url = imSendProxy?.url || `${config.apiBaseUrl}/bot/send`;
75
77
  const resolvedMsgId = msgId || nextMsgId();
76
78
  const span = trace.getActiveSpan();
77
79
  const reqBody = {
@@ -116,6 +118,9 @@ async function _sendToPalzIMInner(params) {
116
118
  if (traceparent) {
117
119
  headers["Traceparent"] = traceparent;
118
120
  }
121
+ if (imSendProxy?.apiKey) {
122
+ headers["X-API-Key"] = imSendProxy.apiKey;
123
+ }
119
124
  const startMs = Date.now();
120
125
  const controller = new AbortController();
121
126
  const timeout = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "rootDir": "./",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true
11
+ },
12
+ "include": [
13
+ "index.ts",
14
+ "src/**/*"
15
+ ],
16
+ "exclude": [
17
+ "node_modules"
18
+ ]
19
+ }