palz-connector 1.5.4 → 1.5.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/activity.js +1 -1
- package/src/bot.js +31 -19
- package/src/channel.js +11 -44
- package/src/config.js +14 -19
- package/src/monitor.js +5 -8
- package/src/outbound.js +1 -13
- package/src/reply-dispatcher.js +0 -2
- package/src/send.js +5 -7
- package/src/agent-logger.js +0 -84
- package/src/connection-manager.js +0 -135
- package/src/workspace-scanner.js +0 -97
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/activity.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function reportPalzActivity(params) {
|
|
|
30
30
|
if (config.activityReportEnabled !== true)
|
|
31
31
|
return;
|
|
32
32
|
const baseUrl = config.clawGatewayUrl?.trim();
|
|
33
|
-
const releaseName = (config.botId || "").trim();
|
|
33
|
+
const releaseName = (process.env.botID || config.botId || "").trim();
|
|
34
34
|
if (!baseUrl || !releaseName) {
|
|
35
35
|
const warnKey = `${accountId ?? ""}:${baseUrl ? "hasBaseUrl" : "missingBaseUrl"}:${releaseName ? "hasRelease" : "missingRelease"}`;
|
|
36
36
|
if (!warnedMissingConfig.has(warnKey)) {
|
package/src/bot.js
CHANGED
|
@@ -14,6 +14,7 @@ import { resolvePalzAccount } from "./config.js";
|
|
|
14
14
|
import { tryClaimMessage } from "./dedup.js";
|
|
15
15
|
import { createPalzReplyDispatcher } from "./reply-dispatcher.js";
|
|
16
16
|
import { resolvePalzMediaList, resolveMediaLocalRoots } from "./media.js";
|
|
17
|
+
import { sendToPalzIM } from "./send.js";
|
|
17
18
|
import { tracer, trace, context, SpanStatusCode } from "./tracing.js";
|
|
18
19
|
// ============ 原始消息透传字段 ============
|
|
19
20
|
const PASSTHROUGH_EXCLUDE = new Set(["event", "content", "timestamp"]);
|
|
@@ -209,7 +210,7 @@ export async function handlePalzMessage(params) {
|
|
|
209
210
|
span.setAttribute("msg_id", msg.msg_id);
|
|
210
211
|
span.setAttribute("sender_id", msg.sender_id);
|
|
211
212
|
span.setAttribute("conversation_type", msg.conversation_type);
|
|
212
|
-
span.setAttribute("agent_id",
|
|
213
|
+
span.setAttribute("agent_id", msg.agent_id || "main");
|
|
213
214
|
span.setAttribute("is_group", msg.conversation_type === "group");
|
|
214
215
|
try {
|
|
215
216
|
await _handlePalzMessageInner(params);
|
|
@@ -224,12 +225,12 @@ export async function handlePalzMessage(params) {
|
|
|
224
225
|
});
|
|
225
226
|
}
|
|
226
227
|
async function _handlePalzMessageInner(params) {
|
|
227
|
-
const { cfg, msg, runtime, accountId
|
|
228
|
+
const { cfg, msg, runtime, accountId } = params;
|
|
228
229
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
229
230
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
230
231
|
const tag = `palz[${accountId}]`;
|
|
231
232
|
const isGroup = msg.conversation_type === "group";
|
|
232
|
-
const effectiveAgentId =
|
|
233
|
+
const effectiveAgentId = msg.agent_id || "main";
|
|
233
234
|
const step1Filter = `[STEP 1/6 入站过滤] msg_id=${msg.msg_id} sender=${msg.sender_id} conv=${msg.conversation_id} type=${msg.conversation_type} agent=${effectiveAgentId}`;
|
|
234
235
|
log(`${tag}: ${step1Filter}`);
|
|
235
236
|
const span = trace.getActiveSpan();
|
|
@@ -306,6 +307,28 @@ async function _handlePalzMessageInner(params) {
|
|
|
306
307
|
span?.addEvent(`[STEP 2 跳过] 原因=消息已被处理(去重)`);
|
|
307
308
|
return;
|
|
308
309
|
}
|
|
310
|
+
// 立即发送确认消息(在入队之前,不受队列阻塞影响)
|
|
311
|
+
const groupId = isGroup ? msg.conversation_id : undefined;
|
|
312
|
+
try {
|
|
313
|
+
log(`${tag}: [ACK] 发送确认消息 conv=${msg.conversation_id} sender=${msg.sender_id}`);
|
|
314
|
+
await sendToPalzIM({
|
|
315
|
+
config: account.config,
|
|
316
|
+
conversationId: msg.conversation_id,
|
|
317
|
+
content: "收到,正在思考中…",
|
|
318
|
+
conversationType: msg.conversation_type || "direct",
|
|
319
|
+
msgId: msg.msg_id,
|
|
320
|
+
senderId: msg.sender_id,
|
|
321
|
+
msgType: msg.msg_type,
|
|
322
|
+
groupId,
|
|
323
|
+
lobsterId: msg.lobster_id,
|
|
324
|
+
palzMsgType: "thinking",
|
|
325
|
+
passthrough: buildPassthroughFromMsg(msg),
|
|
326
|
+
});
|
|
327
|
+
log(`${tag}: [ACK] 确认消息发送成功`);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
log(`${tag}: [ACK] 确认消息发送失败(不影响主流程): ${String(err)}`);
|
|
331
|
+
}
|
|
309
332
|
// 入队(按 agentId 隔离,不同 agent 并行处理)
|
|
310
333
|
const queueKey = isGroup
|
|
311
334
|
? `${effectiveAgentId}:${msg.conversation_id}`
|
|
@@ -325,7 +348,7 @@ async function _handlePalzMessageInner(params) {
|
|
|
325
348
|
span?.addEvent(waitLog);
|
|
326
349
|
}
|
|
327
350
|
try {
|
|
328
|
-
await context.with(capturedCtx, () => dispatchPalzMessage({ cfg, msg, runtime, accountId
|
|
351
|
+
await context.with(capturedCtx, () => dispatchPalzMessage({ cfg, msg, runtime, accountId }));
|
|
329
352
|
const doneLog = `[完成] msg_id=${msg.msg_id} 总耗时=${Date.now() - startMs}ms queueWait=${queueWaitMs}ms`;
|
|
330
353
|
log(`${tag}: ${doneLog}`);
|
|
331
354
|
span?.addEvent(doneLog);
|
|
@@ -357,7 +380,7 @@ async function dispatchPalzMessage(params) {
|
|
|
357
380
|
});
|
|
358
381
|
}
|
|
359
382
|
async function _dispatchPalzMessageInner(params) {
|
|
360
|
-
const { cfg, msg, runtime, accountId
|
|
383
|
+
const { cfg, msg, runtime, accountId } = params;
|
|
361
384
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
362
385
|
const tag = `palz[${accountId}]`;
|
|
363
386
|
const core = getPalzRuntime();
|
|
@@ -403,12 +426,13 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
403
426
|
accountId,
|
|
404
427
|
peer: { kind: peerKind, id: peerId },
|
|
405
428
|
});
|
|
406
|
-
|
|
429
|
+
// IM 指定 agent_id 时走指定 agent,否则强制走 main
|
|
430
|
+
const effectiveAgentId = msg.agent_id || "main";
|
|
407
431
|
if (route.agentId !== effectiveAgentId) {
|
|
408
432
|
const oldAgentId = route.agentId;
|
|
409
433
|
route.agentId = effectiveAgentId;
|
|
410
434
|
route.sessionKey = route.sessionKey.replace(`agent:${oldAgentId}:`, `agent:${effectiveAgentId}:`);
|
|
411
|
-
const step5Override = `[STEP 5 覆盖] agentId: ${oldAgentId} -> ${effectiveAgentId}, sessionKey=${route.sessionKey}`;
|
|
435
|
+
const step5Override = `[STEP 5 覆盖] agentId: ${oldAgentId} -> ${effectiveAgentId}, sessionKey=${route.sessionKey} (${msg.agent_id ? "IM指定" : "默认main"})`;
|
|
412
436
|
log(`${tag}: ${step5Override}`);
|
|
413
437
|
span?.addEvent(step5Override);
|
|
414
438
|
}
|
|
@@ -735,15 +759,3 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
735
759
|
log(`${tag}: [TRACE] 解析 sessionFile 失败: ${String(err)}`);
|
|
736
760
|
}
|
|
737
761
|
}
|
|
738
|
-
export function cleanupAgentCaches(agentIdToClean) {
|
|
739
|
-
const prefix = `${agentIdToClean}:`;
|
|
740
|
-
for (const key of chatHistories.keys()) {
|
|
741
|
-
if (key.startsWith(prefix))
|
|
742
|
-
chatHistories.delete(key);
|
|
743
|
-
}
|
|
744
|
-
for (const key of reasoningActivated.keys()) {
|
|
745
|
-
if (key.includes(`:agent:${agentIdToClean}:`))
|
|
746
|
-
reasoningActivated.delete(key);
|
|
747
|
-
}
|
|
748
|
-
reasoningScannedAgents.delete(agentIdToClean);
|
|
749
|
-
}
|
package/src/channel.js
CHANGED
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
* 实现 OpenClaw ChannelPlugin 接口,与飞书插件采用相同的架构模式。
|
|
5
5
|
* 包含 config、messaging、outbound、gateway 等全套适配器。
|
|
6
6
|
*/
|
|
7
|
-
import { listPalzAccountIds, resolvePalzAccount, resolveDefaultPalzAccountId,
|
|
7
|
+
import { listPalzAccountIds, resolvePalzAccount, resolveDefaultPalzAccountId, } from "./config.js";
|
|
8
8
|
import { palzOutbound } from "./outbound.js";
|
|
9
9
|
import { normalizePalzTarget, looksLikePalzId } from "./targets.js";
|
|
10
|
-
import { ConnectionManager } from "./connection-manager.js";
|
|
11
|
-
import { startWorkspaceScanner } from "./workspace-scanner.js";
|
|
12
10
|
export const palzPlugin = {
|
|
13
11
|
id: "palz-connector",
|
|
14
12
|
meta: {
|
|
@@ -71,53 +69,22 @@ export const palzPlugin = {
|
|
|
71
69
|
startAccount: async (ctx) => {
|
|
72
70
|
const log = typeof ctx.runtime?.log === "function" ? ctx.runtime.log : console.log;
|
|
73
71
|
const logInfo = typeof ctx.log?.info === "function" ? ctx.log.info.bind(ctx.log) : log;
|
|
74
|
-
const error = typeof ctx.runtime?.error === "function" ? ctx.runtime.error : console.error;
|
|
75
72
|
logInfo("palz-gateway: [startAccount] 输入: accountId=" + JSON.stringify(ctx.accountId));
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
ctx.abortSignal?.addEventListener("abort", () => resolve(), { once: true });
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
const baseConfig = resolvePalzConfig(ctx.cfg);
|
|
87
|
-
if (!baseConfig.streamUrl || !baseConfig.apiBaseUrl) {
|
|
88
|
-
const errMsg = "Palz manager not configured (missing streamUrl/apiBaseUrl)";
|
|
73
|
+
const { monitorPalzProvider } = await import("./monitor.js");
|
|
74
|
+
const account = resolvePalzAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
75
|
+
logInfo("palz-gateway: [startAccount] account解析: " + JSON.stringify({ accountId: account.accountId, enabled: account.enabled, configured: account.configured, name: account.name, botId: account.config.botId, streamUrl: account.config.streamUrl }));
|
|
76
|
+
if (!account.configured) {
|
|
77
|
+
const errMsg = "Palz account " + JSON.stringify(ctx.accountId) + " not configured (missing botId/streamUrl/apiBaseUrl)";
|
|
89
78
|
logInfo("palz-gateway: [startAccount] 失败: " + errMsg);
|
|
90
79
|
throw new Error(errMsg);
|
|
91
80
|
}
|
|
92
|
-
logInfo("palz-gateway: [startAccount] 启动
|
|
93
|
-
|
|
81
|
+
logInfo("palz-gateway: [startAccount] 启动WebSocket监听 botId=" + account.config.botId + " streamUrl=" + account.config.streamUrl);
|
|
82
|
+
return monitorPalzProvider({
|
|
94
83
|
cfg: ctx.cfg,
|
|
95
|
-
|
|
84
|
+
config: account.config,
|
|
96
85
|
runtime: ctx.runtime,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
const scanner = startWorkspaceScanner({
|
|
101
|
-
onSnapshot: (releaseNames) => {
|
|
102
|
-
manager.reconcile(releaseNames).catch((err) => {
|
|
103
|
-
error("palz-gateway: reconcile error: " + String(err));
|
|
104
|
-
});
|
|
105
|
-
},
|
|
106
|
-
log: logInfo,
|
|
107
|
-
error,
|
|
108
|
-
});
|
|
109
|
-
return new Promise((resolve) => {
|
|
110
|
-
const onAbort = () => {
|
|
111
|
-
logInfo("palz-gateway: [startAccount] abort received, shutting down manager");
|
|
112
|
-
scanner.stop();
|
|
113
|
-
manager.shutdown();
|
|
114
|
-
resolve();
|
|
115
|
-
};
|
|
116
|
-
if (ctx.abortSignal?.aborted) {
|
|
117
|
-
onAbort();
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
abortSignal: ctx.abortSignal,
|
|
87
|
+
accountId: ctx.accountId,
|
|
121
88
|
});
|
|
122
89
|
},
|
|
123
90
|
},
|
package/src/config.js
CHANGED
|
@@ -12,7 +12,7 @@ import path from "path";
|
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
14
|
const __dirname = path.dirname(__filename);
|
|
15
|
-
|
|
15
|
+
const DEFAULT_ACCOUNT_ID = "__default__";
|
|
16
16
|
const DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
17
17
|
const DEFAULT_CONFIG_FILENAME = "palz-connector.config.json";
|
|
18
18
|
let _cachedFileConfig = null;
|
|
@@ -55,11 +55,11 @@ function loadConfigFile() {
|
|
|
55
55
|
_cachedFileConfig = {};
|
|
56
56
|
return _cachedFileConfig;
|
|
57
57
|
}
|
|
58
|
-
export function resolvePalzConfig(_cfg
|
|
58
|
+
export function resolvePalzConfig(_cfg) {
|
|
59
59
|
const file = loadConfigFile();
|
|
60
60
|
const config = {
|
|
61
61
|
enabled: file.enabled !== false,
|
|
62
|
-
botId:
|
|
62
|
+
botId: process.env.botID || "",
|
|
63
63
|
streamUrl: file.streamUrl || "",
|
|
64
64
|
apiBaseUrl: file.apiBaseUrl || "",
|
|
65
65
|
clawGatewayUrl: file.clawGatewayUrl || "",
|
|
@@ -70,46 +70,41 @@ export function resolvePalzConfig(_cfg, botId) {
|
|
|
70
70
|
};
|
|
71
71
|
if (!_configLoggedOnce) {
|
|
72
72
|
_configLoggedOnce = true;
|
|
73
|
-
console.log("palz-config: [resolve] input:
|
|
73
|
+
console.log("palz-config: [resolve] input: env.botID=" + JSON.stringify(process.env.botID) + " configFile=" + JSON.stringify(file));
|
|
74
74
|
console.log("palz-config: [resolve] output: " + JSON.stringify(config));
|
|
75
75
|
}
|
|
76
76
|
return config;
|
|
77
77
|
}
|
|
78
78
|
export function listPalzAccountIds(_cfg) {
|
|
79
79
|
const config = resolvePalzConfig();
|
|
80
|
-
const ids = config.enabled ? [
|
|
80
|
+
const ids = !config.enabled || !config.botId ? [] : [config.botId];
|
|
81
81
|
if (!_configAdapterLoggedOnce) {
|
|
82
82
|
console.log("palz-config: [listAccountIds] output: " + JSON.stringify(ids));
|
|
83
83
|
}
|
|
84
84
|
return ids;
|
|
85
85
|
}
|
|
86
86
|
export function resolveDefaultPalzAccountId(_cfg) {
|
|
87
|
+
const config = resolvePalzConfig();
|
|
88
|
+
const id = config.botId || DEFAULT_ACCOUNT_ID;
|
|
87
89
|
if (!_configAdapterLoggedOnce) {
|
|
88
|
-
console.log("palz-config: [defaultAccountId] output: " + JSON.stringify(
|
|
90
|
+
console.log("palz-config: [defaultAccountId] output: " + JSON.stringify(id));
|
|
89
91
|
}
|
|
90
|
-
return
|
|
92
|
+
return id;
|
|
91
93
|
}
|
|
92
94
|
export function resolvePalzAccount(params) {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const accountId = isManager ? PALZ_MANAGER_ACCOUNT_ID : rawAccountId;
|
|
96
|
-
const botId = isManager ? "" : rawAccountId;
|
|
97
|
-
const config = resolvePalzConfig(params.cfg, botId);
|
|
95
|
+
const config = resolvePalzConfig();
|
|
96
|
+
const accountId = params.accountId?.trim() || config.botId || DEFAULT_ACCOUNT_ID;
|
|
98
97
|
const result = {
|
|
99
98
|
accountId,
|
|
100
99
|
enabled: config.enabled !== false,
|
|
101
|
-
configured:
|
|
102
|
-
|
|
103
|
-
: Boolean(botId && config.streamUrl && config.apiBaseUrl),
|
|
104
|
-
name: isManager
|
|
105
|
-
? "Palz Connector (manager)"
|
|
106
|
-
: "Palz Connector (" + botId + ")",
|
|
100
|
+
configured: Boolean(config.botId && config.streamUrl && config.apiBaseUrl),
|
|
101
|
+
name: "Palz Connector (" + (config.botId || "unconfigured") + ")",
|
|
107
102
|
config,
|
|
108
103
|
};
|
|
109
104
|
if (!_configAdapterLoggedOnce) {
|
|
110
105
|
_configAdapterLoggedOnce = true;
|
|
111
106
|
console.log("palz-config: [resolveAccount] input: accountId=" + JSON.stringify(params.accountId));
|
|
112
|
-
console.log("palz-config: [resolveAccount] output: " + JSON.stringify({ accountId: result.accountId, enabled: result.enabled, configured: result.configured, name: result.name
|
|
107
|
+
console.log("palz-config: [resolveAccount] output: " + JSON.stringify({ accountId: result.accountId, enabled: result.enabled, configured: result.configured, name: result.name }));
|
|
113
108
|
}
|
|
114
109
|
return result;
|
|
115
110
|
}
|
package/src/monitor.js
CHANGED
|
@@ -9,11 +9,10 @@ import { handlePalzMessage } from "./bot.js";
|
|
|
9
9
|
import { reportPalzActivity } from "./activity.js";
|
|
10
10
|
import { tracer, extractTraceparentContext, SpanStatusCode } from "./tracing.js";
|
|
11
11
|
const WS_CONNECT_TIMEOUT_MS = 15_000;
|
|
12
|
-
const WS_PING_INTERVAL_MS =
|
|
12
|
+
const WS_PING_INTERVAL_MS = 30_000;
|
|
13
13
|
const WS_PONG_TIMEOUT_MS = 70_000;
|
|
14
14
|
export async function monitorPalzProvider(params) {
|
|
15
|
-
const { cfg, config, runtime, abortSignal, accountId
|
|
16
|
-
const effectiveAgentId = agentId || accountId;
|
|
15
|
+
const { cfg, config, runtime, abortSignal, accountId } = params;
|
|
17
16
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
18
17
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
19
18
|
if (!config.botId || !config.streamUrl) {
|
|
@@ -184,6 +183,7 @@ export async function monitorPalzProvider(params) {
|
|
|
184
183
|
try {
|
|
185
184
|
lastPingAt = Date.now();
|
|
186
185
|
ws.ping();
|
|
186
|
+
log(`palz[${accountId}]: [WS_HEARTBEAT] ping sent, last_pong_ago=${timeSinceLastPong}ms`);
|
|
187
187
|
}
|
|
188
188
|
catch (err) {
|
|
189
189
|
error(`palz[${accountId}]: [WS_HEARTBEAT] ping failed: ${err.message ?? err}`);
|
|
@@ -193,11 +193,9 @@ export async function monitorPalzProvider(params) {
|
|
|
193
193
|
});
|
|
194
194
|
ws.on("pong", () => {
|
|
195
195
|
const now = Date.now();
|
|
196
|
-
const rttMs = lastPingAt > 0 ? now - lastPingAt :
|
|
196
|
+
const rttMs = lastPingAt > 0 ? now - lastPingAt : null;
|
|
197
197
|
lastPongAt = now;
|
|
198
|
-
|
|
199
|
-
log(`palz[${accountId}]: [WS_HEARTBEAT] slow pong, rtt=${rttMs}ms`);
|
|
200
|
-
}
|
|
198
|
+
log(`palz[${accountId}]: [WS_HEARTBEAT] pong received${rttMs === null ? "" : `, rtt=${rttMs}ms`}`);
|
|
201
199
|
});
|
|
202
200
|
ws.on("message", (data) => {
|
|
203
201
|
const receivedAt = new Date();
|
|
@@ -229,7 +227,6 @@ export async function monitorPalzProvider(params) {
|
|
|
229
227
|
msg,
|
|
230
228
|
runtime,
|
|
231
229
|
accountId,
|
|
232
|
-
agentId: effectiveAgentId,
|
|
233
230
|
}).catch((err) => {
|
|
234
231
|
error(`palz[${accountId}]: [WS_RECV] handlePalzMessage unhandled error: ${err.message ?? err}`);
|
|
235
232
|
});
|
package/src/outbound.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 实现 ChannelOutboundAdapter 接口,供 OpenClaw Runtime 发送主动消息
|
|
5
5
|
* (如定时任务 cron delivery、跨渠道路由等)。
|
|
6
6
|
*/
|
|
7
|
-
import { resolvePalzAccount
|
|
7
|
+
import { resolvePalzAccount } from "./config.js";
|
|
8
8
|
import { sendToPalzIM } from "./send.js";
|
|
9
9
|
import { loadMediaAsOssUrl } from "./media.js";
|
|
10
10
|
import { parsePalzTarget } from "./targets.js";
|
|
@@ -25,11 +25,6 @@ export const palzOutbound = {
|
|
|
25
25
|
const { cfg, to, text, accountId } = ctx;
|
|
26
26
|
const log = typeof ctx.log === "function" ? ctx.log : console.log;
|
|
27
27
|
log(`palz-outbound: [sendText] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} text="${(text || "").slice(0, 120)}"`);
|
|
28
|
-
if (accountId === PALZ_MANAGER_ACCOUNT_ID) {
|
|
29
|
-
const err = new Error("Cannot send messages with manager account " + PALZ_MANAGER_ACCOUNT_ID);
|
|
30
|
-
log(`palz-outbound: [sendText] 拒绝: ${err.message}`);
|
|
31
|
-
throw err;
|
|
32
|
-
}
|
|
33
28
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
34
29
|
const { senderId, lobsterId, conversationId, conversationType } = parsePalzTarget(to);
|
|
35
30
|
log(`palz-outbound: [sendText] 解析: senderId="${senderId}" lobsterId="${lobsterId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
|
|
@@ -42,7 +37,6 @@ export const palzOutbound = {
|
|
|
42
37
|
senderId,
|
|
43
38
|
conversationType,
|
|
44
39
|
lobsterId,
|
|
45
|
-
log,
|
|
46
40
|
});
|
|
47
41
|
const output = { channel: "palz-connector", messageId: Date.now().toString() };
|
|
48
42
|
log(`palz-outbound: [sendText] 输出: ${JSON.stringify(output)} sendResult=${JSON.stringify(result)}`);
|
|
@@ -51,11 +45,6 @@ export const palzOutbound = {
|
|
|
51
45
|
sendMedia: async (ctx) => {
|
|
52
46
|
const { cfg, to, text, mediaUrl, accountId, mediaLocalRoots } = ctx;
|
|
53
47
|
const log = typeof ctx.log === "function" ? ctx.log : console.log;
|
|
54
|
-
if (accountId === PALZ_MANAGER_ACCOUNT_ID) {
|
|
55
|
-
const err = new Error("Cannot send media with manager account " + PALZ_MANAGER_ACCOUNT_ID);
|
|
56
|
-
log(`palz-outbound: [sendMedia] 拒绝: ${err.message}`);
|
|
57
|
-
throw err;
|
|
58
|
-
}
|
|
59
48
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
60
49
|
const { senderId, lobsterId, conversationId, conversationType } = parsePalzTarget(to);
|
|
61
50
|
log(`palz-outbound: [sendMedia] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} mediaUrl="${(mediaUrl || "").slice(0, 200)}"`);
|
|
@@ -83,7 +72,6 @@ export const palzOutbound = {
|
|
|
83
72
|
senderId,
|
|
84
73
|
conversationType,
|
|
85
74
|
lobsterId,
|
|
86
|
-
log,
|
|
87
75
|
});
|
|
88
76
|
const output = { channel: "palz-connector", messageId: Date.now().toString() };
|
|
89
77
|
log(`palz-outbound: [sendMedia] 输出: ${JSON.stringify(output)} sendResult=${JSON.stringify(result)}`);
|
package/src/reply-dispatcher.js
CHANGED
package/src/send.js
CHANGED
|
@@ -36,9 +36,7 @@ function normalizeContent(content) {
|
|
|
36
36
|
return content;
|
|
37
37
|
}
|
|
38
38
|
async function _sendToPalzIMInner(params) {
|
|
39
|
-
const { config, conversationId, content: rawContent, conversationType, msgId, senderId, stream, msgType, groupId, lobsterId, palzMsgType, toolContent, passthrough
|
|
40
|
-
const log = paramLog ?? console.log;
|
|
41
|
-
const error = paramError ?? console.error;
|
|
39
|
+
const { config, conversationId, content: rawContent, conversationType, msgId, senderId, stream, msgType, groupId, lobsterId, palzMsgType, toolContent, passthrough } = params;
|
|
42
40
|
const content = normalizeContent(rawContent);
|
|
43
41
|
const url = `${config.apiBaseUrl}/bot/send`;
|
|
44
42
|
const resolvedMsgId = msgId || nextMsgId();
|
|
@@ -77,7 +75,7 @@ async function _sendToPalzIMInner(params) {
|
|
|
77
75
|
}
|
|
78
76
|
const reqBodyStr = JSON.stringify(reqBody);
|
|
79
77
|
const reqLog = `[HTTP_REQ] POST ${url} body_length=${reqBodyStr.length}\n request_body=${reqBodyStr}`;
|
|
80
|
-
log(`palz-send: ${reqLog}`);
|
|
78
|
+
console.log(`palz-send: ${reqLog}`);
|
|
81
79
|
span?.addEvent(reqLog);
|
|
82
80
|
// 构建请求 headers,注入 Traceparent
|
|
83
81
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -101,7 +99,7 @@ async function _sendToPalzIMInner(params) {
|
|
|
101
99
|
const elapsedMs = Date.now() - startMs;
|
|
102
100
|
const reason = err?.name === "AbortError" ? `timeout ${SEND_TIMEOUT_MS}ms` : (err?.message ?? String(err));
|
|
103
101
|
const failLog = `[HTTP_RES] ERROR elapsed=${elapsedMs}ms reason=${reason}`;
|
|
104
|
-
error(`palz-send: ${failLog}`);
|
|
102
|
+
console.error(`palz-send: ${failLog}`);
|
|
105
103
|
span?.addEvent(failLog);
|
|
106
104
|
throw err?.name === "AbortError"
|
|
107
105
|
? new Error(`Palz send timeout after ${SEND_TIMEOUT_MS}ms`)
|
|
@@ -119,12 +117,12 @@ async function _sendToPalzIMInner(params) {
|
|
|
119
117
|
catch { }
|
|
120
118
|
if (!response.ok) {
|
|
121
119
|
const failLog = `[HTTP_RES] FAILED status=${response.status} elapsed=${elapsedMs}ms\n response_headers=${JSON.stringify(Object.fromEntries(response.headers.entries()))}\n response_body=${rawText.slice(0, 500)}`;
|
|
122
|
-
error(`palz-send: ${failLog}`);
|
|
120
|
+
console.error(`palz-send: ${failLog}`);
|
|
123
121
|
span?.addEvent(failLog);
|
|
124
122
|
throw new Error(`Palz send failed: ${response.status} ${rawText}`);
|
|
125
123
|
}
|
|
126
124
|
const okLog = `[HTTP_RES] OK status=${response.status} elapsed=${elapsedMs}ms msg_id=${resolvedMsgId}${traceparent ? ` Traceparent=${traceparent}` : ""}\n response_body=${rawText.slice(0, 500)}`;
|
|
127
|
-
log(`palz-send: ${okLog}`);
|
|
125
|
+
console.log(`palz-send: ${okLog}`);
|
|
128
126
|
span?.addEvent(okLog);
|
|
129
127
|
return body;
|
|
130
128
|
}
|
package/src/agent-logger.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import os from "os";
|
|
4
|
-
const DEFAULT_OPENCLAW_DIR = path.join(os.homedir(), ".openclaw");
|
|
5
|
-
const LOG_DIR_NAME = "palz-log";
|
|
6
|
-
function todayStr() {
|
|
7
|
-
const d = new Date();
|
|
8
|
-
const y = d.getFullYear();
|
|
9
|
-
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
10
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
11
|
-
return `${y}-${m}-${day}`;
|
|
12
|
-
}
|
|
13
|
-
function formatTimestamp() {
|
|
14
|
-
const d = new Date();
|
|
15
|
-
const offset = -d.getTimezoneOffset();
|
|
16
|
-
const sign = offset >= 0 ? "+" : "-";
|
|
17
|
-
const absOff = Math.abs(offset);
|
|
18
|
-
const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
|
|
19
|
-
const mm = String(absOff % 60).padStart(2, "0");
|
|
20
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
21
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${sign}${hh}:${mm}`;
|
|
22
|
-
}
|
|
23
|
-
function argsToString(args) {
|
|
24
|
-
return args.map((a) => (typeof a === "string" ? a : String(a))).join(" ");
|
|
25
|
-
}
|
|
26
|
-
export function createAgentLogger(agentId, openclawDir) {
|
|
27
|
-
const baseDir = openclawDir || DEFAULT_OPENCLAW_DIR;
|
|
28
|
-
const logDir = path.join(baseDir, `workspace-${agentId}`, LOG_DIR_NAME);
|
|
29
|
-
let currentDate = "";
|
|
30
|
-
let fd = null;
|
|
31
|
-
function ensureFd() {
|
|
32
|
-
const date = todayStr();
|
|
33
|
-
if (fd !== null && date === currentDate)
|
|
34
|
-
return fd;
|
|
35
|
-
if (fd !== null) {
|
|
36
|
-
try {
|
|
37
|
-
fs.closeSync(fd);
|
|
38
|
-
}
|
|
39
|
-
catch { }
|
|
40
|
-
fd = null;
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
44
|
-
const filePath = path.join(logDir, `palz-${date}.log`);
|
|
45
|
-
fd = fs.openSync(filePath, "a");
|
|
46
|
-
currentDate = date;
|
|
47
|
-
return fd;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function writeLine(level, args) {
|
|
54
|
-
const fileFd = ensureFd();
|
|
55
|
-
if (fileFd !== null) {
|
|
56
|
-
const line = `${formatTimestamp()} [${level}] ${argsToString(args)}\n`;
|
|
57
|
-
try {
|
|
58
|
-
fs.writeSync(fileFd, line);
|
|
59
|
-
}
|
|
60
|
-
catch { }
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const logger = {
|
|
64
|
-
log: (...args) => {
|
|
65
|
-
console.log(...args);
|
|
66
|
-
writeLine("INFO", args);
|
|
67
|
-
},
|
|
68
|
-
error: (...args) => {
|
|
69
|
-
console.error(...args);
|
|
70
|
-
writeLine("ERROR", args);
|
|
71
|
-
},
|
|
72
|
-
close: () => {
|
|
73
|
-
if (fd !== null) {
|
|
74
|
-
try {
|
|
75
|
-
fs.closeSync(fd);
|
|
76
|
-
}
|
|
77
|
-
catch { }
|
|
78
|
-
fd = null;
|
|
79
|
-
currentDate = "";
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
return logger;
|
|
84
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { monitorPalzProvider } from "./monitor.js";
|
|
2
|
-
import { cleanupAgentCaches } from "./bot.js";
|
|
3
|
-
import { createAgentLogger } from "./agent-logger.js";
|
|
4
|
-
const BATCH_SIZE = 10;
|
|
5
|
-
const BATCH_DELAY_MS = 500;
|
|
6
|
-
function sleep(ms) {
|
|
7
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
-
}
|
|
9
|
-
export class ConnectionManager {
|
|
10
|
-
connections = new Map();
|
|
11
|
-
cfg;
|
|
12
|
-
baseConfig;
|
|
13
|
-
runtime;
|
|
14
|
-
log;
|
|
15
|
-
error;
|
|
16
|
-
reconciling = false;
|
|
17
|
-
shutdownCalled = false;
|
|
18
|
-
constructor(params) {
|
|
19
|
-
this.cfg = params.cfg;
|
|
20
|
-
this.baseConfig = params.baseConfig;
|
|
21
|
-
this.runtime = params.runtime;
|
|
22
|
-
this.log = params.log ?? console.log;
|
|
23
|
-
this.error = params.error ?? console.error;
|
|
24
|
-
}
|
|
25
|
-
async reconcile(desiredReleaseNames) {
|
|
26
|
-
if (this.shutdownCalled)
|
|
27
|
-
return;
|
|
28
|
-
if (this.reconciling) {
|
|
29
|
-
this.log("connection-manager: reconcile skipped (already in progress)");
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
this.reconciling = true;
|
|
33
|
-
try {
|
|
34
|
-
const desired = new Set(desiredReleaseNames);
|
|
35
|
-
const current = new Set(this.connections.keys());
|
|
36
|
-
const toRemove = [...current].filter((name) => !desired.has(name));
|
|
37
|
-
const toAdd = [...desired].filter((name) => !current.has(name));
|
|
38
|
-
for (const name of toRemove) {
|
|
39
|
-
this.removeAgent(name);
|
|
40
|
-
}
|
|
41
|
-
for (let i = 0; i < toAdd.length; i++) {
|
|
42
|
-
if (this.shutdownCalled)
|
|
43
|
-
break;
|
|
44
|
-
this.addAgent(toAdd[i]);
|
|
45
|
-
if (i > 0 && i % BATCH_SIZE === 0 && i < toAdd.length - 1) {
|
|
46
|
-
this.log(`connection-manager: batch pause after ${i} connections`);
|
|
47
|
-
await sleep(BATCH_DELAY_MS);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (toAdd.length > 0 || toRemove.length > 0) {
|
|
51
|
-
this.log(`connection-manager: reconcile done, added=${toAdd.length} removed=${toRemove.length} active=${this.connections.size}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
this.error(`connection-manager: reconcile error: ${err}`);
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
this.reconciling = false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
addAgent(releasename) {
|
|
62
|
-
if (this.connections.has(releasename))
|
|
63
|
-
return;
|
|
64
|
-
if (this.shutdownCalled)
|
|
65
|
-
return;
|
|
66
|
-
const abortController = new AbortController();
|
|
67
|
-
const config = { ...this.baseConfig, botId: releasename };
|
|
68
|
-
const logger = createAgentLogger(releasename);
|
|
69
|
-
const agentRuntime = { ...this.runtime, log: logger.log, error: logger.error };
|
|
70
|
-
this.log(`connection-manager: adding agent "${releasename}"`);
|
|
71
|
-
const promise = monitorPalzProvider({
|
|
72
|
-
cfg: this.cfg,
|
|
73
|
-
config,
|
|
74
|
-
runtime: agentRuntime,
|
|
75
|
-
abortSignal: abortController.signal,
|
|
76
|
-
accountId: releasename,
|
|
77
|
-
agentId: releasename,
|
|
78
|
-
});
|
|
79
|
-
const entry = {
|
|
80
|
-
releasename,
|
|
81
|
-
abortController,
|
|
82
|
-
promise,
|
|
83
|
-
startedAt: Date.now(),
|
|
84
|
-
logger,
|
|
85
|
-
};
|
|
86
|
-
this.connections.set(releasename, entry);
|
|
87
|
-
promise
|
|
88
|
-
.catch((err) => {
|
|
89
|
-
this.error(`connection-manager: agent "${releasename}" exited with error: ${err}`);
|
|
90
|
-
})
|
|
91
|
-
.finally(() => {
|
|
92
|
-
if (this.connections.get(releasename) === entry) {
|
|
93
|
-
this.connections.delete(releasename);
|
|
94
|
-
this.log(`connection-manager: agent "${releasename}" cleaned up from map`);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
removeAgent(releasename) {
|
|
99
|
-
const entry = this.connections.get(releasename);
|
|
100
|
-
if (!entry)
|
|
101
|
-
return;
|
|
102
|
-
this.log(`connection-manager: removing agent "${releasename}"`);
|
|
103
|
-
this.connections.delete(releasename);
|
|
104
|
-
entry.abortController.abort();
|
|
105
|
-
entry.logger.close();
|
|
106
|
-
try {
|
|
107
|
-
cleanupAgentCaches(releasename);
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
this.error(`connection-manager: cache cleanup error for "${releasename}": ${err}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
getActiveAgents() {
|
|
114
|
-
return [...this.connections.keys()].sort();
|
|
115
|
-
}
|
|
116
|
-
shutdown() {
|
|
117
|
-
if (this.shutdownCalled)
|
|
118
|
-
return;
|
|
119
|
-
this.shutdownCalled = true;
|
|
120
|
-
this.log(`connection-manager: shutting down ${this.connections.size} connection(s)`);
|
|
121
|
-
for (const [name, entry] of this.connections) {
|
|
122
|
-
try {
|
|
123
|
-
entry.abortController.abort();
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
this.error(`connection-manager: abort error for "${name}": ${err}`);
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
entry.logger.close();
|
|
130
|
-
}
|
|
131
|
-
catch { }
|
|
132
|
-
}
|
|
133
|
-
this.connections.clear();
|
|
134
|
-
}
|
|
135
|
-
}
|
package/src/workspace-scanner.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import os from "os";
|
|
4
|
-
const DEFAULT_OPENCLAW_DIR = path.join(os.homedir(), ".openclaw");
|
|
5
|
-
const WORKSPACE_PREFIX = "workspace-";
|
|
6
|
-
const DEFAULT_SCAN_INTERVAL_MS = 5000;
|
|
7
|
-
const VALID_RELEASENAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
8
|
-
export function scanWorkspaceReleaseNames(openclawDir) {
|
|
9
|
-
const dir = openclawDir || DEFAULT_OPENCLAW_DIR;
|
|
10
|
-
let entries;
|
|
11
|
-
try {
|
|
12
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
const releaseNames = [];
|
|
18
|
-
for (const entry of entries) {
|
|
19
|
-
if (!entry.isDirectory()) {
|
|
20
|
-
if (entry.isSymbolicLink()) {
|
|
21
|
-
try {
|
|
22
|
-
const fullPath = path.join(dir, entry.name);
|
|
23
|
-
const stat = fs.statSync(fullPath);
|
|
24
|
-
if (!stat.isDirectory())
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (!entry.name.startsWith(WORKSPACE_PREFIX))
|
|
36
|
-
continue;
|
|
37
|
-
const releasename = entry.name.slice(WORKSPACE_PREFIX.length);
|
|
38
|
-
if (!releasename)
|
|
39
|
-
continue;
|
|
40
|
-
if (!VALID_RELEASENAME_RE.test(releasename))
|
|
41
|
-
continue;
|
|
42
|
-
releaseNames.push(releasename);
|
|
43
|
-
}
|
|
44
|
-
return releaseNames.sort();
|
|
45
|
-
}
|
|
46
|
-
export function startWorkspaceScanner(opts) {
|
|
47
|
-
const dir = opts.openclawDir || DEFAULT_OPENCLAW_DIR;
|
|
48
|
-
const intervalMs = opts.intervalMs ?? DEFAULT_SCAN_INTERVAL_MS;
|
|
49
|
-
const log = opts.log ?? console.log;
|
|
50
|
-
const error = opts.error ?? console.error;
|
|
51
|
-
let stopped = false;
|
|
52
|
-
let scanning = false;
|
|
53
|
-
let timer = null;
|
|
54
|
-
function doScan() {
|
|
55
|
-
if (stopped)
|
|
56
|
-
return [];
|
|
57
|
-
if (scanning)
|
|
58
|
-
return [];
|
|
59
|
-
scanning = true;
|
|
60
|
-
try {
|
|
61
|
-
const names = scanWorkspaceReleaseNames(dir);
|
|
62
|
-
try {
|
|
63
|
-
opts.onSnapshot(names);
|
|
64
|
-
}
|
|
65
|
-
catch (err) {
|
|
66
|
-
error(`workspace-scanner: onSnapshot error: ${err}`);
|
|
67
|
-
}
|
|
68
|
-
return names;
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
error(`workspace-scanner: scan error: ${err}`);
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
finally {
|
|
75
|
-
scanning = false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
log(`workspace-scanner: starting, dir=${dir} interval=${intervalMs}ms`);
|
|
79
|
-
const initialNames = doScan();
|
|
80
|
-
log(`workspace-scanner: initial scan found ${initialNames.length} workspace(s): [${initialNames.join(", ")}]`);
|
|
81
|
-
timer = setInterval(doScan, intervalMs);
|
|
82
|
-
return {
|
|
83
|
-
stop() {
|
|
84
|
-
if (stopped)
|
|
85
|
-
return;
|
|
86
|
-
stopped = true;
|
|
87
|
-
if (timer) {
|
|
88
|
-
clearInterval(timer);
|
|
89
|
-
timer = null;
|
|
90
|
-
}
|
|
91
|
-
log("workspace-scanner: stopped");
|
|
92
|
-
},
|
|
93
|
-
scanNow() {
|
|
94
|
-
return doScan();
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|