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.
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/palz-connector.config.json +4 -4
- package/palz-connector.global.dev.config.json +12 -0
- package/palz-connector.global.prod.config.json +12 -0
- package/palz-connector.global.staging.config.json +12 -0
- package/src/bot.js +37 -2
- package/src/gateway-relay.js +340 -0
- package/src/send.js +6 -1
- package/tsconfig.json +19 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palz-connector",
|
|
3
|
-
"version": "1.7.
|
|
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://
|
|
4
|
-
"apiBaseUrl": "http://
|
|
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":
|
|
6
|
+
"activityReportEnabled": false,
|
|
7
7
|
"sessionTimeout": 1800000,
|
|
8
8
|
"groupContextCache": false,
|
|
9
9
|
"showProcess": true,
|
|
10
|
-
"kbEngineUrl": "http://
|
|
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
|
|
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
|
+
}
|