palz-connector 1.5.5 → 1.5.7
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/agent-logger.js +84 -0
- package/src/bot.js +43 -43
- package/src/channel.js +44 -11
- package/src/config.js +19 -14
- package/src/connection-manager.js +131 -0
- package/src/media.js +7 -7
- package/src/message-utils.js +12 -0
- package/src/monitor.js +32 -10
- package/src/outbound.js +13 -1
- package/src/reply-dispatcher.js +2 -0
- package/src/send.js +7 -5
- package/src/workspace-scanner.js +93 -0
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 = (
|
|
33
|
+
const releaseName = (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)) {
|
|
@@ -0,0 +1,84 @@
|
|
|
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 createUserLogger(userId, openclawDir) {
|
|
27
|
+
const baseDir = openclawDir || DEFAULT_OPENCLAW_DIR;
|
|
28
|
+
const logDir = path.join(baseDir, userId, 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
|
+
}
|
package/src/bot.js
CHANGED
|
@@ -15,20 +15,8 @@ import { tryClaimMessage } from "./dedup.js";
|
|
|
15
15
|
import { createPalzReplyDispatcher } from "./reply-dispatcher.js";
|
|
16
16
|
import { resolvePalzMediaList, resolveMediaLocalRoots } from "./media.js";
|
|
17
17
|
import { sendToPalzIM } from "./send.js";
|
|
18
|
+
import { buildPassthroughFromMsg } from "./message-utils.js";
|
|
18
19
|
import { tracer, trace, context, SpanStatusCode } from "./tracing.js";
|
|
19
|
-
// ============ 原始消息透传字段 ============
|
|
20
|
-
const PASSTHROUGH_EXCLUDE = new Set(["event", "content", "timestamp"]);
|
|
21
|
-
function buildPassthroughFromMsg(msg) {
|
|
22
|
-
const out = {};
|
|
23
|
-
for (const [k, v] of Object.entries(msg)) {
|
|
24
|
-
if (PASSTHROUGH_EXCLUDE.has(k))
|
|
25
|
-
continue;
|
|
26
|
-
if (v === undefined)
|
|
27
|
-
continue;
|
|
28
|
-
out[k] = v;
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
20
|
// ============ 文本提取工具 ============
|
|
33
21
|
function extractPlainText(content) {
|
|
34
22
|
if (typeof content === "string")
|
|
@@ -210,7 +198,7 @@ export async function handlePalzMessage(params) {
|
|
|
210
198
|
span.setAttribute("msg_id", msg.msg_id);
|
|
211
199
|
span.setAttribute("sender_id", msg.sender_id);
|
|
212
200
|
span.setAttribute("conversation_type", msg.conversation_type);
|
|
213
|
-
span.setAttribute("agent_id",
|
|
201
|
+
span.setAttribute("agent_id", params.agentId);
|
|
214
202
|
span.setAttribute("is_group", msg.conversation_type === "group");
|
|
215
203
|
try {
|
|
216
204
|
await _handlePalzMessageInner(params);
|
|
@@ -225,12 +213,12 @@ export async function handlePalzMessage(params) {
|
|
|
225
213
|
});
|
|
226
214
|
}
|
|
227
215
|
async function _handlePalzMessageInner(params) {
|
|
228
|
-
const { cfg, msg, runtime, accountId } = params;
|
|
216
|
+
const { cfg, msg, runtime, accountId, agentId } = params;
|
|
229
217
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
230
218
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
231
219
|
const tag = `palz[${accountId}]`;
|
|
232
220
|
const isGroup = msg.conversation_type === "group";
|
|
233
|
-
const effectiveAgentId =
|
|
221
|
+
const effectiveAgentId = agentId;
|
|
234
222
|
const step1Filter = `[STEP 1/6 入站过滤] msg_id=${msg.msg_id} sender=${msg.sender_id} conv=${msg.conversation_id} type=${msg.conversation_type} agent=${effectiveAgentId}`;
|
|
235
223
|
log(`${tag}: ${step1Filter}`);
|
|
236
224
|
const span = trace.getActiveSpan();
|
|
@@ -307,6 +295,28 @@ async function _handlePalzMessageInner(params) {
|
|
|
307
295
|
span?.addEvent(`[STEP 2 跳过] 原因=消息已被处理(去重)`);
|
|
308
296
|
return;
|
|
309
297
|
}
|
|
298
|
+
// 立即发送确认消息(在入队之前,不受队列阻塞影响)
|
|
299
|
+
const groupId = isGroup ? msg.conversation_id : undefined;
|
|
300
|
+
try {
|
|
301
|
+
log(`${tag}: [ACK] 发送确认消息 conv=${msg.conversation_id} sender=${msg.sender_id}`);
|
|
302
|
+
await sendToPalzIM({
|
|
303
|
+
config: account.config,
|
|
304
|
+
conversationId: msg.conversation_id,
|
|
305
|
+
content: "收到,正在思考中…",
|
|
306
|
+
conversationType: msg.conversation_type || "direct",
|
|
307
|
+
msgId: msg.msg_id,
|
|
308
|
+
senderId: msg.sender_id,
|
|
309
|
+
msgType: msg.msg_type,
|
|
310
|
+
groupId,
|
|
311
|
+
lobsterId: msg.lobster_id,
|
|
312
|
+
palzMsgType: "thinking",
|
|
313
|
+
passthrough: buildPassthroughFromMsg(msg),
|
|
314
|
+
});
|
|
315
|
+
log(`${tag}: [ACK] 确认消息发送成功`);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
log(`${tag}: [ACK] 确认消息发送失败(不影响主流程): ${String(err)}`);
|
|
319
|
+
}
|
|
310
320
|
// 入队(按 agentId 隔离,不同 agent 并行处理)
|
|
311
321
|
const queueKey = isGroup
|
|
312
322
|
? `${effectiveAgentId}:${msg.conversation_id}`
|
|
@@ -326,7 +336,7 @@ async function _handlePalzMessageInner(params) {
|
|
|
326
336
|
span?.addEvent(waitLog);
|
|
327
337
|
}
|
|
328
338
|
try {
|
|
329
|
-
await context.with(capturedCtx, () => dispatchPalzMessage({ cfg, msg, runtime, accountId }));
|
|
339
|
+
await context.with(capturedCtx, () => dispatchPalzMessage({ cfg, msg, runtime, accountId, agentId }));
|
|
330
340
|
const doneLog = `[完成] msg_id=${msg.msg_id} 总耗时=${Date.now() - startMs}ms queueWait=${queueWaitMs}ms`;
|
|
331
341
|
log(`${tag}: ${doneLog}`);
|
|
332
342
|
span?.addEvent(doneLog);
|
|
@@ -358,7 +368,7 @@ async function dispatchPalzMessage(params) {
|
|
|
358
368
|
});
|
|
359
369
|
}
|
|
360
370
|
async function _dispatchPalzMessageInner(params) {
|
|
361
|
-
const { cfg, msg, runtime, accountId } = params;
|
|
371
|
+
const { cfg, msg, runtime, accountId, agentId } = params;
|
|
362
372
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
363
373
|
const tag = `palz[${accountId}]`;
|
|
364
374
|
const core = getPalzRuntime();
|
|
@@ -372,27 +382,6 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
372
382
|
if (isGroup) {
|
|
373
383
|
log(`${tag}: [group_id] resolved=${groupId ?? "(none)"} conv=${msg.conversation_id}`);
|
|
374
384
|
}
|
|
375
|
-
// 立即发送确认消息,让用户知道 agent 已收到并开始处理
|
|
376
|
-
try {
|
|
377
|
-
log(`${tag}: [ACK] 发送确认消息 conv=${msg.conversation_id} sender=${msg.sender_id}`);
|
|
378
|
-
await sendToPalzIM({
|
|
379
|
-
config,
|
|
380
|
-
conversationId: msg.conversation_id,
|
|
381
|
-
content: "收到,正在思考中…",
|
|
382
|
-
conversationType: msg.conversation_type || "direct",
|
|
383
|
-
msgId: msg.msg_id,
|
|
384
|
-
senderId: msg.sender_id,
|
|
385
|
-
msgType: msg.msg_type,
|
|
386
|
-
groupId,
|
|
387
|
-
lobsterId: msg.lobster_id,
|
|
388
|
-
palzMsgType: "thinking",
|
|
389
|
-
passthrough: buildPassthroughFromMsg(msg),
|
|
390
|
-
});
|
|
391
|
-
log(`${tag}: [ACK] 确认消息发送成功`);
|
|
392
|
-
}
|
|
393
|
-
catch (err) {
|
|
394
|
-
log(`${tag}: [ACK] 确认消息发送失败(不影响主流程): ${String(err)}`);
|
|
395
|
-
}
|
|
396
385
|
// peerId 使用 4 段格式,确保 cron delivery 推断 to 时包含完整路由信息(含 lobster_id)
|
|
397
386
|
// 格式: {conversationType}:{senderId}:{lobsterId}:{conversationId}
|
|
398
387
|
// 群聊中不同用户共享 session:senderId 固定为 "_"
|
|
@@ -425,13 +414,12 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
425
414
|
accountId,
|
|
426
415
|
peer: { kind: peerKind, id: peerId },
|
|
427
416
|
});
|
|
428
|
-
|
|
429
|
-
const effectiveAgentId = msg.agent_id || "main";
|
|
417
|
+
const effectiveAgentId = agentId;
|
|
430
418
|
if (route.agentId !== effectiveAgentId) {
|
|
431
419
|
const oldAgentId = route.agentId;
|
|
432
420
|
route.agentId = effectiveAgentId;
|
|
433
421
|
route.sessionKey = route.sessionKey.replace(`agent:${oldAgentId}:`, `agent:${effectiveAgentId}:`);
|
|
434
|
-
const step5Override = `[STEP 5 覆盖] agentId: ${oldAgentId} -> ${effectiveAgentId}, sessionKey=${route.sessionKey}
|
|
422
|
+
const step5Override = `[STEP 5 覆盖] agentId: ${oldAgentId} -> ${effectiveAgentId}, sessionKey=${route.sessionKey}`;
|
|
435
423
|
log(`${tag}: ${step5Override}`);
|
|
436
424
|
span?.addEvent(step5Override);
|
|
437
425
|
}
|
|
@@ -703,7 +691,7 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
703
691
|
msgId: msg.msg_id,
|
|
704
692
|
msgType: msg.msg_type,
|
|
705
693
|
groupId,
|
|
706
|
-
mediaLocalRoots: resolveMediaLocalRoots(effectiveAgentId),
|
|
694
|
+
mediaLocalRoots: resolveMediaLocalRoots(effectiveAgentId, accountId),
|
|
707
695
|
showProcess,
|
|
708
696
|
sessionKey: route.sessionKey,
|
|
709
697
|
passthrough: buildPassthroughFromMsg(msg),
|
|
@@ -758,3 +746,15 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
758
746
|
log(`${tag}: [TRACE] 解析 sessionFile 失败: ${String(err)}`);
|
|
759
747
|
}
|
|
760
748
|
}
|
|
749
|
+
export function cleanupAgentCaches(agentIdToClean) {
|
|
750
|
+
const prefix = `${agentIdToClean}:`;
|
|
751
|
+
for (const key of chatHistories.keys()) {
|
|
752
|
+
if (key.startsWith(prefix))
|
|
753
|
+
chatHistories.delete(key);
|
|
754
|
+
}
|
|
755
|
+
for (const key of reasoningActivated.keys()) {
|
|
756
|
+
if (key.includes(`:agent:${agentIdToClean}:`))
|
|
757
|
+
reasoningActivated.delete(key);
|
|
758
|
+
}
|
|
759
|
+
reasoningScannedAgents.delete(agentIdToClean);
|
|
760
|
+
}
|
package/src/channel.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* 实现 OpenClaw ChannelPlugin 接口,与飞书插件采用相同的架构模式。
|
|
5
5
|
* 包含 config、messaging、outbound、gateway 等全套适配器。
|
|
6
6
|
*/
|
|
7
|
-
import { listPalzAccountIds, resolvePalzAccount, resolveDefaultPalzAccountId, } from "./config.js";
|
|
7
|
+
import { listPalzAccountIds, resolvePalzAccount, resolveDefaultPalzAccountId, resolvePalzConfig, PALZ_MANAGER_ACCOUNT_ID, } 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 { startUserDirScanner } from "./workspace-scanner.js";
|
|
10
12
|
export const palzPlugin = {
|
|
11
13
|
id: "palz-connector",
|
|
12
14
|
meta: {
|
|
@@ -69,22 +71,53 @@ export const palzPlugin = {
|
|
|
69
71
|
startAccount: async (ctx) => {
|
|
70
72
|
const log = typeof ctx.runtime?.log === "function" ? ctx.runtime.log : console.log;
|
|
71
73
|
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;
|
|
72
75
|
logInfo("palz-gateway: [startAccount] 输入: accountId=" + JSON.stringify(ctx.accountId));
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if (ctx.accountId !== PALZ_MANAGER_ACCOUNT_ID) {
|
|
77
|
+
logInfo("palz-gateway: [startAccount] non-manager account " + JSON.stringify(ctx.accountId) + ", returning abort-pending promise");
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
if (ctx.abortSignal?.aborted) {
|
|
80
|
+
resolve();
|
|
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)";
|
|
78
89
|
logInfo("palz-gateway: [startAccount] 失败: " + errMsg);
|
|
79
90
|
throw new Error(errMsg);
|
|
80
91
|
}
|
|
81
|
-
logInfo("palz-gateway: [startAccount] 启动
|
|
82
|
-
|
|
92
|
+
logInfo("palz-gateway: [startAccount] 启动 manager, streamUrl=" + baseConfig.streamUrl);
|
|
93
|
+
const manager = new ConnectionManager({
|
|
83
94
|
cfg: ctx.cfg,
|
|
84
|
-
|
|
95
|
+
baseConfig,
|
|
85
96
|
runtime: ctx.runtime,
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
log: logInfo,
|
|
98
|
+
error,
|
|
99
|
+
});
|
|
100
|
+
const scanner = startUserDirScanner({
|
|
101
|
+
onSnapshot: (userIds) => {
|
|
102
|
+
manager.reconcile(userIds).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 });
|
|
88
121
|
});
|
|
89
122
|
},
|
|
90
123
|
},
|
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
|
-
const
|
|
15
|
+
export const PALZ_MANAGER_ACCOUNT_ID = "palz-manager";
|
|
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, botId) {
|
|
59
59
|
const file = loadConfigFile();
|
|
60
60
|
const config = {
|
|
61
61
|
enabled: file.enabled !== false,
|
|
62
|
-
botId:
|
|
62
|
+
botId: botId ?? "",
|
|
63
63
|
streamUrl: file.streamUrl || "",
|
|
64
64
|
apiBaseUrl: file.apiBaseUrl || "",
|
|
65
65
|
clawGatewayUrl: file.clawGatewayUrl || "",
|
|
@@ -70,41 +70,46 @@ export function resolvePalzConfig(_cfg) {
|
|
|
70
70
|
};
|
|
71
71
|
if (!_configLoggedOnce) {
|
|
72
72
|
_configLoggedOnce = true;
|
|
73
|
-
console.log("palz-config: [resolve] input:
|
|
73
|
+
console.log("palz-config: [resolve] input: botId=" + JSON.stringify(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 =
|
|
80
|
+
const ids = config.enabled ? [PALZ_MANAGER_ACCOUNT_ID] : [];
|
|
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;
|
|
89
87
|
if (!_configAdapterLoggedOnce) {
|
|
90
|
-
console.log("palz-config: [defaultAccountId] output: " + JSON.stringify(
|
|
88
|
+
console.log("palz-config: [defaultAccountId] output: " + JSON.stringify(PALZ_MANAGER_ACCOUNT_ID));
|
|
91
89
|
}
|
|
92
|
-
return
|
|
90
|
+
return PALZ_MANAGER_ACCOUNT_ID;
|
|
93
91
|
}
|
|
94
92
|
export function resolvePalzAccount(params) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
93
|
+
const rawAccountId = params.accountId?.trim() || "";
|
|
94
|
+
const isManager = !rawAccountId || rawAccountId === PALZ_MANAGER_ACCOUNT_ID;
|
|
95
|
+
const accountId = isManager ? PALZ_MANAGER_ACCOUNT_ID : rawAccountId;
|
|
96
|
+
const botId = isManager ? "" : rawAccountId;
|
|
97
|
+
const config = resolvePalzConfig(params.cfg, botId);
|
|
97
98
|
const result = {
|
|
98
99
|
accountId,
|
|
99
100
|
enabled: config.enabled !== false,
|
|
100
|
-
configured:
|
|
101
|
-
|
|
101
|
+
configured: isManager
|
|
102
|
+
? Boolean(config.streamUrl && config.apiBaseUrl)
|
|
103
|
+
: Boolean(botId && config.streamUrl && config.apiBaseUrl),
|
|
104
|
+
name: isManager
|
|
105
|
+
? "Palz Connector (manager)"
|
|
106
|
+
: "Palz Connector (" + botId + ")",
|
|
102
107
|
config,
|
|
103
108
|
};
|
|
104
109
|
if (!_configAdapterLoggedOnce) {
|
|
105
110
|
_configAdapterLoggedOnce = true;
|
|
106
111
|
console.log("palz-config: [resolveAccount] input: accountId=" + JSON.stringify(params.accountId));
|
|
107
|
-
console.log("palz-config: [resolveAccount] output: " + JSON.stringify({ accountId: result.accountId, enabled: result.enabled, configured: result.configured, name: result.name }));
|
|
112
|
+
console.log("palz-config: [resolveAccount] output: " + JSON.stringify({ accountId: result.accountId, enabled: result.enabled, configured: result.configured, name: result.name, botId }));
|
|
108
113
|
}
|
|
109
114
|
return result;
|
|
110
115
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { monitorPalzProvider } from "./monitor.js";
|
|
2
|
+
import { createUserLogger } from "./agent-logger.js";
|
|
3
|
+
const BATCH_SIZE = 10;
|
|
4
|
+
const BATCH_DELAY_MS = 500;
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
export class ConnectionManager {
|
|
9
|
+
connections = new Map();
|
|
10
|
+
cfg;
|
|
11
|
+
baseConfig;
|
|
12
|
+
runtime;
|
|
13
|
+
log;
|
|
14
|
+
error;
|
|
15
|
+
reconciling = false;
|
|
16
|
+
shutdownCalled = false;
|
|
17
|
+
constructor(params) {
|
|
18
|
+
this.cfg = params.cfg;
|
|
19
|
+
this.baseConfig = params.baseConfig;
|
|
20
|
+
this.runtime = params.runtime;
|
|
21
|
+
this.log = params.log ?? console.log;
|
|
22
|
+
this.error = params.error ?? console.error;
|
|
23
|
+
}
|
|
24
|
+
async reconcile(desiredUserIds) {
|
|
25
|
+
if (this.shutdownCalled)
|
|
26
|
+
return;
|
|
27
|
+
if (this.reconciling) {
|
|
28
|
+
this.log("connection-manager: reconcile skipped (already in progress)");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.reconciling = true;
|
|
32
|
+
try {
|
|
33
|
+
const desired = new Set(desiredUserIds);
|
|
34
|
+
const current = new Set(this.connections.keys());
|
|
35
|
+
const toRemove = [...current].filter((name) => !desired.has(name));
|
|
36
|
+
const toAdd = [...desired].filter((name) => !current.has(name));
|
|
37
|
+
for (const name of toRemove) {
|
|
38
|
+
this.removeUser(name);
|
|
39
|
+
}
|
|
40
|
+
for (let i = 0; i < toAdd.length; i++) {
|
|
41
|
+
if (this.shutdownCalled)
|
|
42
|
+
break;
|
|
43
|
+
this.addUser(toAdd[i]);
|
|
44
|
+
if (i > 0 && i % BATCH_SIZE === 0 && i < toAdd.length - 1) {
|
|
45
|
+
this.log(`connection-manager: batch pause after ${i} connections`);
|
|
46
|
+
await sleep(BATCH_DELAY_MS);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (toAdd.length > 0 || toRemove.length > 0) {
|
|
50
|
+
this.log(`connection-manager: reconcile done, added=${toAdd.length} removed=${toRemove.length} active=${this.connections.size}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
this.error(`connection-manager: reconcile error: ${err}`);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
this.reconciling = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
addUser(userId) {
|
|
61
|
+
if (this.connections.has(userId))
|
|
62
|
+
return;
|
|
63
|
+
if (this.shutdownCalled)
|
|
64
|
+
return;
|
|
65
|
+
const abortController = new AbortController();
|
|
66
|
+
const config = { ...this.baseConfig, botId: userId };
|
|
67
|
+
const logger = createUserLogger(userId);
|
|
68
|
+
const userRuntime = { ...this.runtime, log: logger.log, error: logger.error };
|
|
69
|
+
this.log(`connection-manager: adding user "${userId}"`);
|
|
70
|
+
const promise = monitorPalzProvider({
|
|
71
|
+
cfg: this.cfg,
|
|
72
|
+
config,
|
|
73
|
+
runtime: userRuntime,
|
|
74
|
+
abortSignal: abortController.signal,
|
|
75
|
+
// accountId 在 connector 内部层表示 WS 连接标识(= userId = 目录名),
|
|
76
|
+
// 与 OpenClaw 框架层的 accountId("palz-manager")不同。
|
|
77
|
+
accountId: userId,
|
|
78
|
+
});
|
|
79
|
+
const entry = {
|
|
80
|
+
userId,
|
|
81
|
+
abortController,
|
|
82
|
+
promise,
|
|
83
|
+
startedAt: Date.now(),
|
|
84
|
+
logger,
|
|
85
|
+
};
|
|
86
|
+
this.connections.set(userId, entry);
|
|
87
|
+
promise
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
this.error(`connection-manager: user "${userId}" exited with error: ${err}`);
|
|
90
|
+
})
|
|
91
|
+
.finally(() => {
|
|
92
|
+
if (this.connections.get(userId) === entry) {
|
|
93
|
+
this.connections.delete(userId);
|
|
94
|
+
this.log(`connection-manager: user "${userId}" cleaned up from map`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
removeUser(userId) {
|
|
99
|
+
const entry = this.connections.get(userId);
|
|
100
|
+
if (!entry)
|
|
101
|
+
return;
|
|
102
|
+
this.log(`connection-manager: removing user "${userId}"`);
|
|
103
|
+
this.connections.delete(userId);
|
|
104
|
+
entry.abortController.abort();
|
|
105
|
+
entry.logger.close();
|
|
106
|
+
// 不主动清理 agent 缓存:userId 不是 agentId,无法确定该 user 下有哪些 agent。
|
|
107
|
+
// 依赖 dedup TTL (5min) 和 chatHistories MAX_HISTORY_KEYS (3000) 自动淘汰。
|
|
108
|
+
}
|
|
109
|
+
getActiveUsers() {
|
|
110
|
+
return [...this.connections.keys()].sort();
|
|
111
|
+
}
|
|
112
|
+
shutdown() {
|
|
113
|
+
if (this.shutdownCalled)
|
|
114
|
+
return;
|
|
115
|
+
this.shutdownCalled = true;
|
|
116
|
+
this.log(`connection-manager: shutting down ${this.connections.size} connection(s)`);
|
|
117
|
+
for (const [name, entry] of this.connections) {
|
|
118
|
+
try {
|
|
119
|
+
entry.abortController.abort();
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
this.error(`connection-manager: abort error for "${name}": ${err}`);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
entry.logger.close();
|
|
126
|
+
}
|
|
127
|
+
catch { }
|
|
128
|
+
}
|
|
129
|
+
this.connections.clear();
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/media.js
CHANGED
|
@@ -16,16 +16,16 @@ const STATE_DIR = path.join(os.homedir(), ".openclaw");
|
|
|
16
16
|
* 根据 agentId 构造媒体文件搜索根路径列表,
|
|
17
17
|
* 与 OpenClaw 的 getAgentScopedMediaLocalRoots 逻辑对齐。
|
|
18
18
|
*/
|
|
19
|
-
export function resolveMediaLocalRoots(agentId) {
|
|
19
|
+
export function resolveMediaLocalRoots(agentId, accountId) {
|
|
20
20
|
const roots = [MEDIA_DIR, path.join(STATE_DIR, "workspace"), path.join(STATE_DIR, "sandboxes")];
|
|
21
|
+
// user 目录下的 agent workspace: ~/.openclaw/{userId}/workspace-{agentId}
|
|
22
|
+
if (accountId?.startsWith("user-") && agentId?.trim()) {
|
|
23
|
+
roots.push(path.join(STATE_DIR, accountId, `workspace-${agentId}`));
|
|
24
|
+
}
|
|
21
25
|
if (agentId?.trim()) {
|
|
22
|
-
|
|
23
|
-
const workspaceDirById = path.join(STATE_DIR, `workspace-${agentId}`);
|
|
24
|
-
if (!roots.includes(workspaceDirById)) {
|
|
25
|
-
roots.push(workspaceDirById);
|
|
26
|
-
}
|
|
26
|
+
roots.push(path.join(STATE_DIR, `workspace-${agentId}`));
|
|
27
27
|
}
|
|
28
|
-
return roots;
|
|
28
|
+
return [...new Set(roots)];
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* 将原始文件名清洁化:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const PASSTHROUGH_EXCLUDE = new Set(["event", "content", "timestamp"]);
|
|
2
|
+
export function buildPassthroughFromMsg(msg) {
|
|
3
|
+
const out = {};
|
|
4
|
+
for (const [k, v] of Object.entries(msg)) {
|
|
5
|
+
if (PASSTHROUGH_EXCLUDE.has(k))
|
|
6
|
+
continue;
|
|
7
|
+
if (v === undefined)
|
|
8
|
+
continue;
|
|
9
|
+
out[k] = v;
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
package/src/monitor.js
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import WebSocket from "ws";
|
|
8
8
|
import { handlePalzMessage } from "./bot.js";
|
|
9
|
+
import { sendToPalzIM } from "./send.js";
|
|
10
|
+
import { buildPassthroughFromMsg } from "./message-utils.js";
|
|
9
11
|
import { reportPalzActivity } from "./activity.js";
|
|
10
12
|
import { tracer, extractTraceparentContext, SpanStatusCode } from "./tracing.js";
|
|
11
13
|
const WS_CONNECT_TIMEOUT_MS = 15_000;
|
|
12
|
-
const WS_PING_INTERVAL_MS =
|
|
14
|
+
const WS_PING_INTERVAL_MS = 15_000;
|
|
13
15
|
const WS_PONG_TIMEOUT_MS = 70_000;
|
|
14
16
|
export async function monitorPalzProvider(params) {
|
|
15
17
|
const { cfg, config, runtime, abortSignal, accountId } = params;
|
|
@@ -47,9 +49,8 @@ export async function monitorPalzProvider(params) {
|
|
|
47
49
|
currentWs.close();
|
|
48
50
|
}
|
|
49
51
|
else if (currentWs.readyState === WebSocket.CONNECTING) {
|
|
50
|
-
// WebSocket 还在连接中,等 open 后再关,或者连接失败自动清理
|
|
51
52
|
currentWs.once("open", () => currentWs?.close());
|
|
52
|
-
currentWs.once("error", () => { });
|
|
53
|
+
currentWs.once("error", () => { });
|
|
53
54
|
}
|
|
54
55
|
currentWs = null;
|
|
55
56
|
}
|
|
@@ -127,7 +128,6 @@ export async function monitorPalzProvider(params) {
|
|
|
127
128
|
return;
|
|
128
129
|
const wasConnected = connectedAt > 0;
|
|
129
130
|
const stableMs = wasConnected ? Date.now() - connectedAt : 0;
|
|
130
|
-
// 4002: 被新连接替代,带退避重试
|
|
131
131
|
if (code === 4002) {
|
|
132
132
|
consecutive4002++;
|
|
133
133
|
if (consecutive4002 >= MAX_CONSECUTIVE_4002) {
|
|
@@ -142,14 +142,12 @@ export async function monitorPalzProvider(params) {
|
|
|
142
142
|
scheduleReconnect(delay, false);
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
|
-
// 4001: bot_id 缺失,不重连
|
|
146
145
|
if (code === 4001) {
|
|
147
146
|
error(`palz[${accountId}]: bot_id missing (code=4001), not reconnecting`);
|
|
148
147
|
cleanup();
|
|
149
148
|
resolve();
|
|
150
149
|
return;
|
|
151
150
|
}
|
|
152
|
-
// 其他断开:指数退避重连
|
|
153
151
|
if (wasConnected && stableMs > 10_000)
|
|
154
152
|
reconnectDelay = 1000;
|
|
155
153
|
log(`palz[${accountId}]: disconnected (code=${code}, reason=${reasonStr}, uptime=${Math.round(stableMs / 1000)}s, msgs=${messageCount}), reconnecting in ${reconnectDelay}ms`);
|
|
@@ -183,7 +181,6 @@ export async function monitorPalzProvider(params) {
|
|
|
183
181
|
try {
|
|
184
182
|
lastPingAt = Date.now();
|
|
185
183
|
ws.ping();
|
|
186
|
-
log(`palz[${accountId}]: [WS_HEARTBEAT] ping sent, last_pong_ago=${timeSinceLastPong}ms`);
|
|
187
184
|
}
|
|
188
185
|
catch (err) {
|
|
189
186
|
error(`palz[${accountId}]: [WS_HEARTBEAT] ping failed: ${err.message ?? err}`);
|
|
@@ -193,9 +190,11 @@ export async function monitorPalzProvider(params) {
|
|
|
193
190
|
});
|
|
194
191
|
ws.on("pong", () => {
|
|
195
192
|
const now = Date.now();
|
|
196
|
-
const rttMs = lastPingAt > 0 ? now - lastPingAt :
|
|
193
|
+
const rttMs = lastPingAt > 0 ? now - lastPingAt : 0;
|
|
197
194
|
lastPongAt = now;
|
|
198
|
-
|
|
195
|
+
if (rttMs > 1000) {
|
|
196
|
+
log(`palz[${accountId}]: [WS_HEARTBEAT] slow pong, rtt=${rttMs}ms`);
|
|
197
|
+
}
|
|
199
198
|
});
|
|
200
199
|
ws.on("message", (data) => {
|
|
201
200
|
const receivedAt = new Date();
|
|
@@ -207,7 +206,6 @@ export async function monitorPalzProvider(params) {
|
|
|
207
206
|
return;
|
|
208
207
|
}
|
|
209
208
|
messageCount++;
|
|
210
|
-
// 从 IM 上游的 traceparent 恢复 trace context
|
|
211
209
|
const parentCtx = extractTraceparentContext(msg.traceparent);
|
|
212
210
|
tracer.startActiveSpan("palz.ws_recv", {}, parentCtx, (span) => {
|
|
213
211
|
try {
|
|
@@ -222,11 +220,35 @@ export async function monitorPalzProvider(params) {
|
|
|
222
220
|
}).catch((err) => {
|
|
223
221
|
error(`palz[${accountId}]: [ACTIVITY_REPORT] unhandled error: ${err.message ?? err}`);
|
|
224
222
|
});
|
|
223
|
+
// agent_id 必填:从消息中取,缺失时回复 IM 错误并跳过 AI 分发
|
|
224
|
+
const messageAgentId = typeof msg.agent_id === "string" ? msg.agent_id.trim() : "";
|
|
225
|
+
if (!messageAgentId) {
|
|
226
|
+
error(`palz[${accountId}]: missing agent_id, skipping message msg_id=${msg.msg_id}`);
|
|
227
|
+
sendToPalzIM({
|
|
228
|
+
config,
|
|
229
|
+
conversationId: msg.conversation_id,
|
|
230
|
+
content: "消息缺少 agent_id,无法路由到 Agent。",
|
|
231
|
+
conversationType: msg.conversation_type || "direct",
|
|
232
|
+
msgId: msg.msg_id,
|
|
233
|
+
senderId: msg.sender_id,
|
|
234
|
+
msgType: msg.msg_type,
|
|
235
|
+
groupId: msg.conversation_type === "group" ? msg.conversation_id : undefined,
|
|
236
|
+
lobsterId: msg.lobster_id,
|
|
237
|
+
palzMsgType: "error",
|
|
238
|
+
passthrough: buildPassthroughFromMsg(msg),
|
|
239
|
+
log,
|
|
240
|
+
error,
|
|
241
|
+
}).catch((err) => {
|
|
242
|
+
error(`palz[${accountId}]: missing agent_id error reply failed: ${err.message ?? err}`);
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
225
246
|
handlePalzMessage({
|
|
226
247
|
cfg,
|
|
227
248
|
msg,
|
|
228
249
|
runtime,
|
|
229
250
|
accountId,
|
|
251
|
+
agentId: messageAgentId,
|
|
230
252
|
}).catch((err) => {
|
|
231
253
|
error(`palz[${accountId}]: [WS_RECV] handlePalzMessage unhandled error: ${err.message ?? err}`);
|
|
232
254
|
});
|
package/src/outbound.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 实现 ChannelOutboundAdapter 接口,供 OpenClaw Runtime 发送主动消息
|
|
5
5
|
* (如定时任务 cron delivery、跨渠道路由等)。
|
|
6
6
|
*/
|
|
7
|
-
import { resolvePalzAccount } from "./config.js";
|
|
7
|
+
import { resolvePalzAccount, PALZ_MANAGER_ACCOUNT_ID } 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,6 +25,11 @@ 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
|
+
}
|
|
28
33
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
29
34
|
const { senderId, lobsterId, conversationId, conversationType } = parsePalzTarget(to);
|
|
30
35
|
log(`palz-outbound: [sendText] 解析: senderId="${senderId}" lobsterId="${lobsterId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
|
|
@@ -37,6 +42,7 @@ export const palzOutbound = {
|
|
|
37
42
|
senderId,
|
|
38
43
|
conversationType,
|
|
39
44
|
lobsterId,
|
|
45
|
+
log,
|
|
40
46
|
});
|
|
41
47
|
const output = { channel: "palz-connector", messageId: Date.now().toString() };
|
|
42
48
|
log(`palz-outbound: [sendText] 输出: ${JSON.stringify(output)} sendResult=${JSON.stringify(result)}`);
|
|
@@ -45,6 +51,11 @@ export const palzOutbound = {
|
|
|
45
51
|
sendMedia: async (ctx) => {
|
|
46
52
|
const { cfg, to, text, mediaUrl, accountId, mediaLocalRoots } = ctx;
|
|
47
53
|
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
|
+
}
|
|
48
59
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
49
60
|
const { senderId, lobsterId, conversationId, conversationType } = parsePalzTarget(to);
|
|
50
61
|
log(`palz-outbound: [sendMedia] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} mediaUrl="${(mediaUrl || "").slice(0, 200)}"`);
|
|
@@ -72,6 +83,7 @@ export const palzOutbound = {
|
|
|
72
83
|
senderId,
|
|
73
84
|
conversationType,
|
|
74
85
|
lobsterId,
|
|
86
|
+
log,
|
|
75
87
|
});
|
|
76
88
|
const output = { channel: "palz-connector", messageId: Date.now().toString() };
|
|
77
89
|
log(`palz-outbound: [sendMedia] 输出: ${JSON.stringify(output)} sendResult=${JSON.stringify(result)}`);
|
package/src/reply-dispatcher.js
CHANGED
package/src/send.js
CHANGED
|
@@ -36,7 +36,9 @@ 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 } = params;
|
|
39
|
+
const { config, conversationId, content: rawContent, conversationType, msgId, senderId, stream, msgType, groupId, lobsterId, palzMsgType, toolContent, passthrough, log: paramLog, error: paramError } = params;
|
|
40
|
+
const log = paramLog ?? console.log;
|
|
41
|
+
const error = paramError ?? console.error;
|
|
40
42
|
const content = normalizeContent(rawContent);
|
|
41
43
|
const url = `${config.apiBaseUrl}/bot/send`;
|
|
42
44
|
const resolvedMsgId = msgId || nextMsgId();
|
|
@@ -75,7 +77,7 @@ async function _sendToPalzIMInner(params) {
|
|
|
75
77
|
}
|
|
76
78
|
const reqBodyStr = JSON.stringify(reqBody);
|
|
77
79
|
const reqLog = `[HTTP_REQ] POST ${url} body_length=${reqBodyStr.length}\n request_body=${reqBodyStr}`;
|
|
78
|
-
|
|
80
|
+
log(`palz-send: ${reqLog}`);
|
|
79
81
|
span?.addEvent(reqLog);
|
|
80
82
|
// 构建请求 headers,注入 Traceparent
|
|
81
83
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -99,7 +101,7 @@ async function _sendToPalzIMInner(params) {
|
|
|
99
101
|
const elapsedMs = Date.now() - startMs;
|
|
100
102
|
const reason = err?.name === "AbortError" ? `timeout ${SEND_TIMEOUT_MS}ms` : (err?.message ?? String(err));
|
|
101
103
|
const failLog = `[HTTP_RES] ERROR elapsed=${elapsedMs}ms reason=${reason}`;
|
|
102
|
-
|
|
104
|
+
error(`palz-send: ${failLog}`);
|
|
103
105
|
span?.addEvent(failLog);
|
|
104
106
|
throw err?.name === "AbortError"
|
|
105
107
|
? new Error(`Palz send timeout after ${SEND_TIMEOUT_MS}ms`)
|
|
@@ -117,12 +119,12 @@ async function _sendToPalzIMInner(params) {
|
|
|
117
119
|
catch { }
|
|
118
120
|
if (!response.ok) {
|
|
119
121
|
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)}`;
|
|
120
|
-
|
|
122
|
+
error(`palz-send: ${failLog}`);
|
|
121
123
|
span?.addEvent(failLog);
|
|
122
124
|
throw new Error(`Palz send failed: ${response.status} ${rawText}`);
|
|
123
125
|
}
|
|
124
126
|
const okLog = `[HTTP_RES] OK status=${response.status} elapsed=${elapsedMs}ms msg_id=${resolvedMsgId}${traceparent ? ` Traceparent=${traceparent}` : ""}\n response_body=${rawText.slice(0, 500)}`;
|
|
125
|
-
|
|
127
|
+
log(`palz-send: ${okLog}`);
|
|
126
128
|
span?.addEvent(okLog);
|
|
127
129
|
return body;
|
|
128
130
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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 USER_PREFIX = "user-";
|
|
6
|
+
const DEFAULT_SCAN_INTERVAL_MS = 5000;
|
|
7
|
+
export function scanUserDirs(openclawDir) {
|
|
8
|
+
const dir = openclawDir || DEFAULT_OPENCLAW_DIR;
|
|
9
|
+
let entries;
|
|
10
|
+
try {
|
|
11
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const userIds = [];
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (!entry.isDirectory()) {
|
|
19
|
+
if (entry.isSymbolicLink()) {
|
|
20
|
+
try {
|
|
21
|
+
const fullPath = path.join(dir, entry.name);
|
|
22
|
+
const stat = fs.statSync(fullPath);
|
|
23
|
+
if (!stat.isDirectory())
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!entry.name.startsWith(USER_PREFIX))
|
|
35
|
+
continue;
|
|
36
|
+
if (entry.name.length <= USER_PREFIX.length)
|
|
37
|
+
continue;
|
|
38
|
+
userIds.push(entry.name);
|
|
39
|
+
}
|
|
40
|
+
return userIds.sort();
|
|
41
|
+
}
|
|
42
|
+
export function startUserDirScanner(opts) {
|
|
43
|
+
const dir = opts.openclawDir || DEFAULT_OPENCLAW_DIR;
|
|
44
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_SCAN_INTERVAL_MS;
|
|
45
|
+
const log = opts.log ?? console.log;
|
|
46
|
+
const error = opts.error ?? console.error;
|
|
47
|
+
let stopped = false;
|
|
48
|
+
let scanning = false;
|
|
49
|
+
let timer = null;
|
|
50
|
+
function doScan() {
|
|
51
|
+
if (stopped)
|
|
52
|
+
return [];
|
|
53
|
+
if (scanning)
|
|
54
|
+
return [];
|
|
55
|
+
scanning = true;
|
|
56
|
+
try {
|
|
57
|
+
const names = scanUserDirs(dir);
|
|
58
|
+
try {
|
|
59
|
+
opts.onSnapshot(names);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
error(`user-dir-scanner: onSnapshot error: ${err}`);
|
|
63
|
+
}
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
error(`user-dir-scanner: scan error: ${err}`);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
scanning = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
log(`user-dir-scanner: starting, dir=${dir} interval=${intervalMs}ms`);
|
|
75
|
+
const initialNames = doScan();
|
|
76
|
+
log(`user-dir-scanner: initial scan found ${initialNames.length} user(s): [${initialNames.join(", ")}]`);
|
|
77
|
+
timer = setInterval(doScan, intervalMs);
|
|
78
|
+
return {
|
|
79
|
+
stop() {
|
|
80
|
+
if (stopped)
|
|
81
|
+
return;
|
|
82
|
+
stopped = true;
|
|
83
|
+
if (timer) {
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
timer = null;
|
|
86
|
+
}
|
|
87
|
+
log("user-dir-scanner: stopped");
|
|
88
|
+
},
|
|
89
|
+
scanNow() {
|
|
90
|
+
return doScan();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|