palz-connector 1.5.3 → 1.5.4
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 +19 -8
- package/src/channel.js +44 -11
- package/src/config.js +19 -14
- package/src/connection-manager.js +135 -0
- package/src/monitor.js +8 -5
- package/src/outbound.js +13 -1
- package/src/reply-dispatcher.js +2 -0
- package/src/send.js +7 -5
- package/src/workspace-scanner.js +97 -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 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
|
+
}
|
package/src/bot.js
CHANGED
|
@@ -209,7 +209,7 @@ export async function handlePalzMessage(params) {
|
|
|
209
209
|
span.setAttribute("msg_id", msg.msg_id);
|
|
210
210
|
span.setAttribute("sender_id", msg.sender_id);
|
|
211
211
|
span.setAttribute("conversation_type", msg.conversation_type);
|
|
212
|
-
span.setAttribute("agent_id",
|
|
212
|
+
span.setAttribute("agent_id", params.agentId);
|
|
213
213
|
span.setAttribute("is_group", msg.conversation_type === "group");
|
|
214
214
|
try {
|
|
215
215
|
await _handlePalzMessageInner(params);
|
|
@@ -224,12 +224,12 @@ export async function handlePalzMessage(params) {
|
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
226
|
async function _handlePalzMessageInner(params) {
|
|
227
|
-
const { cfg, msg, runtime, accountId } = params;
|
|
227
|
+
const { cfg, msg, runtime, accountId, agentId } = params;
|
|
228
228
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
229
229
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
230
230
|
const tag = `palz[${accountId}]`;
|
|
231
231
|
const isGroup = msg.conversation_type === "group";
|
|
232
|
-
const effectiveAgentId =
|
|
232
|
+
const effectiveAgentId = agentId;
|
|
233
233
|
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
234
|
log(`${tag}: ${step1Filter}`);
|
|
235
235
|
const span = trace.getActiveSpan();
|
|
@@ -325,7 +325,7 @@ async function _handlePalzMessageInner(params) {
|
|
|
325
325
|
span?.addEvent(waitLog);
|
|
326
326
|
}
|
|
327
327
|
try {
|
|
328
|
-
await context.with(capturedCtx, () => dispatchPalzMessage({ cfg, msg, runtime, accountId }));
|
|
328
|
+
await context.with(capturedCtx, () => dispatchPalzMessage({ cfg, msg, runtime, accountId, agentId }));
|
|
329
329
|
const doneLog = `[完成] msg_id=${msg.msg_id} 总耗时=${Date.now() - startMs}ms queueWait=${queueWaitMs}ms`;
|
|
330
330
|
log(`${tag}: ${doneLog}`);
|
|
331
331
|
span?.addEvent(doneLog);
|
|
@@ -357,7 +357,7 @@ async function dispatchPalzMessage(params) {
|
|
|
357
357
|
});
|
|
358
358
|
}
|
|
359
359
|
async function _dispatchPalzMessageInner(params) {
|
|
360
|
-
const { cfg, msg, runtime, accountId } = params;
|
|
360
|
+
const { cfg, msg, runtime, accountId, agentId } = params;
|
|
361
361
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
362
362
|
const tag = `palz[${accountId}]`;
|
|
363
363
|
const core = getPalzRuntime();
|
|
@@ -403,13 +403,12 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
403
403
|
accountId,
|
|
404
404
|
peer: { kind: peerKind, id: peerId },
|
|
405
405
|
});
|
|
406
|
-
|
|
407
|
-
const effectiveAgentId = msg.agent_id || "main";
|
|
406
|
+
const effectiveAgentId = agentId;
|
|
408
407
|
if (route.agentId !== effectiveAgentId) {
|
|
409
408
|
const oldAgentId = route.agentId;
|
|
410
409
|
route.agentId = effectiveAgentId;
|
|
411
410
|
route.sessionKey = route.sessionKey.replace(`agent:${oldAgentId}:`, `agent:${effectiveAgentId}:`);
|
|
412
|
-
const step5Override = `[STEP 5 覆盖] agentId: ${oldAgentId} -> ${effectiveAgentId}, sessionKey=${route.sessionKey}
|
|
411
|
+
const step5Override = `[STEP 5 覆盖] agentId: ${oldAgentId} -> ${effectiveAgentId}, sessionKey=${route.sessionKey}`;
|
|
413
412
|
log(`${tag}: ${step5Override}`);
|
|
414
413
|
span?.addEvent(step5Override);
|
|
415
414
|
}
|
|
@@ -736,3 +735,15 @@ async function _dispatchPalzMessageInner(params) {
|
|
|
736
735
|
log(`${tag}: [TRACE] 解析 sessionFile 失败: ${String(err)}`);
|
|
737
736
|
}
|
|
738
737
|
}
|
|
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,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 { startWorkspaceScanner } 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 = 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 });
|
|
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,135 @@
|
|
|
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/monitor.js
CHANGED
|
@@ -9,10 +9,11 @@ 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 = 15_000;
|
|
13
13
|
const WS_PONG_TIMEOUT_MS = 70_000;
|
|
14
14
|
export async function monitorPalzProvider(params) {
|
|
15
|
-
const { cfg, config, runtime, abortSignal, accountId } = params;
|
|
15
|
+
const { cfg, config, runtime, abortSignal, accountId, agentId } = params;
|
|
16
|
+
const effectiveAgentId = agentId || accountId;
|
|
16
17
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
17
18
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
18
19
|
if (!config.botId || !config.streamUrl) {
|
|
@@ -183,7 +184,6 @@ export async function monitorPalzProvider(params) {
|
|
|
183
184
|
try {
|
|
184
185
|
lastPingAt = Date.now();
|
|
185
186
|
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,9 +193,11 @@ 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 : 0;
|
|
197
197
|
lastPongAt = now;
|
|
198
|
-
|
|
198
|
+
if (rttMs > 1000) {
|
|
199
|
+
log(`palz[${accountId}]: [WS_HEARTBEAT] slow pong, rtt=${rttMs}ms`);
|
|
200
|
+
}
|
|
199
201
|
});
|
|
200
202
|
ws.on("message", (data) => {
|
|
201
203
|
const receivedAt = new Date();
|
|
@@ -227,6 +229,7 @@ export async function monitorPalzProvider(params) {
|
|
|
227
229
|
msg,
|
|
228
230
|
runtime,
|
|
229
231
|
accountId,
|
|
232
|
+
agentId: effectiveAgentId,
|
|
230
233
|
}).catch((err) => {
|
|
231
234
|
error(`palz[${accountId}]: [WS_RECV] handlePalzMessage unhandled error: ${err.message ?? err}`);
|
|
232
235
|
});
|
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,97 @@
|
|
|
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
|
+
}
|