palz-connector 1.5.4 → 1.5.6

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