palz-connector 1.5.2 → 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.
@@ -1,11 +1,21 @@
1
1
  {
2
2
  "id": "palz-connector",
3
3
  "name": "Palz Connector Channel",
4
- "version": "1.5.2",
4
+ "version": "1.5.4",
5
5
  "description": "Palz IM 接入 OpenClaw",
6
6
  "channels": [
7
7
  "palz-connector"
8
8
  ],
9
+ "channelConfigs": {
10
+ "palz-connector": {
11
+ "schema": {
12
+ "type": "object",
13
+ "additionalProperties": true
14
+ },
15
+ "label": "palz-connector",
16
+ "description": "palz channel configuration."
17
+ }
18
+ },
9
19
  "configSchema": {
10
20
  "type": "object",
11
21
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palz-connector",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "enabled": true,
3
- "streamUrl": "wss://claw-server.csaiagent.com/ws/bot",
4
- "apiBaseUrl": "https://claw-server.csaiagent.com/api",
3
+ "streamUrl": "ws://claw-server:8787/ws/bot",
4
+ "apiBaseUrl": "http://claw-server:8787/api",
5
5
  "clawGatewayUrl": "http://claw-gateway:8080",
6
6
  "activityReportEnabled": true,
7
7
  "sessionTimeout": 1800000,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "enabled": true,
3
- "streamUrl": "wss://claw-server.csagentai.com/ws/bot",
4
- "apiBaseUrl": "https://claw-server.csagentai.com/api",
3
+ "streamUrl": "ws://claw-server:8787/ws/bot",
4
+ "apiBaseUrl": "http://claw-server:8787/api",
5
5
  "clawGatewayUrl": "http://claw-gateway:8080",
6
6
  "activityReportEnabled": true,
7
7
  "sessionTimeout": 1800000,
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "enabled": true,
3
- "streamUrl": "wss://claw-server.csjkagent.com/ws/bot",
4
- "apiBaseUrl": "https://claw-server.csjkagent.com/api",
5
- "clawGatewayUrl": "https://claw-server.csjkagent.com/api",
3
+ "streamUrl": "ws://claw-server:8787/ws/bot",
4
+ "apiBaseUrl": "http://claw-server:8787/api",
5
+ "clawGatewayUrl": "http://claw-gateway:8080",
6
6
  "activityReportEnabled": true,
7
7
  "sessionTimeout": 1800000,
8
8
  "groupContextCache": false,
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 = (process.env.botID || config.botId || "").trim();
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", msg.agent_id || "main");
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 = msg.agent_id || "main";
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
- // IM 指定 agent_id 时走指定 agent,否则强制走 main
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} (${msg.agent_id ? "IM指定" : "默认main"})`;
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
- 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)";
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] 启动WebSocket监听 botId=" + account.config.botId + " streamUrl=" + account.config.streamUrl);
82
- return monitorPalzProvider({
92
+ logInfo("palz-gateway: [startAccount] 启动 manager, streamUrl=" + baseConfig.streamUrl);
93
+ const manager = new ConnectionManager({
83
94
  cfg: ctx.cfg,
84
- config: account.config,
95
+ baseConfig,
85
96
  runtime: ctx.runtime,
86
- abortSignal: ctx.abortSignal,
87
- accountId: ctx.accountId,
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 DEFAULT_ACCOUNT_ID = "__default__";
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: process.env.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: env.botID=" + JSON.stringify(process.env.botID) + " configFile=" + JSON.stringify(file));
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 = !config.enabled || !config.botId ? [] : [config.botId];
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(id));
88
+ console.log("palz-config: [defaultAccountId] output: " + JSON.stringify(PALZ_MANAGER_ACCOUNT_ID));
91
89
  }
92
- return id;
90
+ return PALZ_MANAGER_ACCOUNT_ID;
93
91
  }
94
92
  export function resolvePalzAccount(params) {
95
- const config = resolvePalzConfig();
96
- const accountId = params.accountId?.trim() || config.botId || DEFAULT_ACCOUNT_ID;
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: Boolean(config.botId && config.streamUrl && config.apiBaseUrl),
101
- name: "Palz Connector (" + (config.botId || "unconfigured") + ")",
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 = 30_000;
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 : null;
196
+ const rttMs = lastPingAt > 0 ? now - lastPingAt : 0;
197
197
  lastPongAt = now;
198
- log(`palz[${accountId}]: [WS_HEARTBEAT] pong received${rttMs === null ? "" : `, rtt=${rttMs}ms`}`);
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)}`);
@@ -62,6 +62,8 @@ export function createPalzReplyDispatcher(params) {
62
62
  palzMsgType,
63
63
  toolContent,
64
64
  passthrough,
65
+ log,
66
+ error,
65
67
  });
66
68
  log(`${tag}: [DISPATCHER←sendToIM] 输出: ${JSON.stringify(result)}`);
67
69
  return result;
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
- console.log(`palz-send: ${reqLog}`);
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
- console.error(`palz-send: ${failLog}`);
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
- console.error(`palz-send: ${failLog}`);
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
- console.log(`palz-send: ${okLog}`);
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
+ }