palz-connector 1.7.1 → 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.1",
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.1",
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",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "enabled": true,
3
- "streamUrl": "ws://claw-server:8787/ws/bot",
4
- "apiBaseUrl": "http://claw-server:8787/api",
3
+ "streamUrl": "ws://14.103.148.99:8090/ws/bot",
4
+ "apiBaseUrl": "http://14.103.148.99:8090/api",
5
5
  "clawGatewayUrl": "http://claw-gateway:8080",
6
- "activityReportEnabled": true,
6
+ "activityReportEnabled": false,
7
7
  "sessionTimeout": 1800000,
8
8
  "groupContextCache": false,
9
9
  "showProcess": true,
10
- "kbEngineUrl": "http://uniclaw-kb-engine:8070",
10
+ "kbEngineUrl": "http://127.0.0.1:8070",
11
11
  "controlPort": 8008
12
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
+ }
@@ -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/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/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
+ }