palz-connector 1.6.2 → 1.6.3

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.6.2",
4
+ "version": "1.6.3",
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.6.2",
3
+ "version": "1.6.3",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
@@ -6,5 +6,6 @@
6
6
  "activityReportEnabled": false,
7
7
  "sessionTimeout": 1800000,
8
8
  "groupContextCache": false,
9
- "showProcess": true
9
+ "showProcess": true,
10
+ "controlPort": 8008
10
11
  }
@@ -6,5 +6,6 @@
6
6
  "activityReportEnabled": true,
7
7
  "sessionTimeout": 1800000,
8
8
  "groupContextCache": false,
9
- "showProcess": true
9
+ "showProcess": true,
10
+ "controlPort": 8008
10
11
  }
@@ -6,5 +6,6 @@
6
6
  "activityReportEnabled": true,
7
7
  "sessionTimeout": 1800000,
8
8
  "groupContextCache": false,
9
- "showProcess": true
9
+ "showProcess": true,
10
+ "controlPort": 8008
10
11
  }
@@ -7,4 +7,5 @@
7
7
  "sessionTimeout": 1800000,
8
8
  "groupContextCache": false,
9
9
  "showProcess": true
10
+ "controlPort": 8008
10
11
  }
package/src/activity.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Palz IM 收到消息后的 claw-gateway 活动上报。
2
+ * Palz IM WebSocket 连接成功后的 claw-gateway 活动上报。
3
3
  */
4
4
  const ACTIVITY_REPORT_TIMEOUT_MS = 2000;
5
5
  const ACTIVITY_REPORT_PATH_PREFIX = "/openclaw-gateway/be/deployments";
6
6
  const warnedMissingConfig = new Set();
7
+ const SERVICE_NAME = process.env.OPENCLAW_WORKSPACE_POOL_SERVICE_NAME ?? "";
7
8
  function formatDateTimeWithOffset(date) {
8
9
  const pad = (n, width = 2) => String(n).padStart(width, "0");
9
10
  const offsetMinutes = -date.getTimezoneOffset();
@@ -23,7 +24,7 @@ function resolveActivityUrl(baseUrl, releaseName) {
23
24
  return `${normalizedBase}${ACTIVITY_REPORT_PATH_PREFIX}/${encodeURIComponent(releaseName)}/activity`;
24
25
  }
25
26
  export async function reportPalzActivity(params) {
26
- const { config, msg, receivedAt, runtime, accountId } = params;
27
+ const { config, connectedAt, runtime, accountId } = params;
27
28
  const log = typeof runtime?.log === "function" ? runtime.log : console.log;
28
29
  const error = typeof runtime?.error === "function" ? runtime.error : console.error;
29
30
  const tag = `palz[${accountId ?? config.botId ?? "default"}]`;
@@ -41,22 +42,17 @@ export async function reportPalzActivity(params) {
41
42
  }
42
43
  const payload = {
43
44
  source: "palz-connector",
44
- eventType: "im_message_received",
45
- messageId: msg.msg_id,
46
- conversationId: msg.conversation_id,
47
- conversationType: msg.conversation_type || "direct",
48
- userId: msg.owner_id ?? "",
49
- senderId: msg.sender_id,
50
- receivedAt: formatDateTimeWithOffset(receivedAt),
45
+ eventType: "ws_connected",
46
+ releaseName,
47
+ serviceName: SERVICE_NAME,
48
+ connectedAt: formatDateTimeWithOffset(connectedAt),
51
49
  };
52
- if (msg.traceparent) {
53
- payload.traceId = msg.traceparent;
54
- }
55
50
  const url = resolveActivityUrl(baseUrl, releaseName);
56
51
  const body = JSON.stringify(payload);
57
52
  const controller = new AbortController();
58
53
  const timeout = setTimeout(() => controller.abort(), ACTIVITY_REPORT_TIMEOUT_MS);
59
54
  const startMs = Date.now();
55
+ log(`${tag}: [ACTIVITY_REPORT] reporting eventType=ws_connected url=${url} body=${body}`);
60
56
  try {
61
57
  const response = await fetch(url, {
62
58
  method: "POST",
@@ -67,15 +63,15 @@ export async function reportPalzActivity(params) {
67
63
  const elapsedMs = Date.now() - startMs;
68
64
  const responseText = await response.text().catch(() => "");
69
65
  if (!response.ok) {
70
- error(`${tag}: [ACTIVITY_REPORT] failed status=${response.status} elapsed=${elapsedMs}ms msg_id=${msg.msg_id} response=${responseText.slice(0, 300)}`);
66
+ error(`${tag}: [ACTIVITY_REPORT] failed status=${response.status} elapsed=${elapsedMs}ms response=${responseText.slice(0, 300)}`);
71
67
  return;
72
68
  }
73
- log(`${tag}: [ACTIVITY_REPORT] ok status=${response.status} elapsed=${elapsedMs}ms msg_id=${msg.msg_id}`);
69
+ log(`${tag}: [ACTIVITY_REPORT] ok status=${response.status} elapsed=${elapsedMs}ms eventType=ws_connected`);
74
70
  }
75
71
  catch (err) {
76
72
  const elapsedMs = Date.now() - startMs;
77
73
  const reason = err?.name === "AbortError" ? `timeout ${ACTIVITY_REPORT_TIMEOUT_MS}ms` : (err?.message ?? String(err));
78
- error(`${tag}: [ACTIVITY_REPORT] error elapsed=${elapsedMs}ms msg_id=${msg.msg_id} reason=${reason}`);
74
+ error(`${tag}: [ACTIVITY_REPORT] error elapsed=${elapsedMs}ms reason=${reason}`);
79
75
  }
80
76
  finally {
81
77
  clearTimeout(timeout);
package/src/channel.js CHANGED
@@ -9,6 +9,7 @@ import { palzOutbound } from "./outbound.js";
9
9
  import { normalizePalzTarget, looksLikePalzId } from "./targets.js";
10
10
  import { ConnectionManager } from "./connection-manager.js";
11
11
  import { startUserDirScanner } from "./workspace-scanner.js";
12
+ import { startControlServer } from "./control-server.js";
12
13
  export const palzPlugin = {
13
14
  id: "palz-connector",
14
15
  meta: {
@@ -39,6 +40,7 @@ export const palzPlugin = {
39
40
  clawGatewayUrl: { type: "string" },
40
41
  activityReportEnabled: { type: "boolean" },
41
42
  sessionTimeout: { type: "integer", minimum: 0 },
43
+ controlPort: { type: "integer", minimum: 0 },
42
44
  },
43
45
  },
44
46
  },
@@ -106,11 +108,37 @@ export const palzPlugin = {
106
108
  log: logInfo,
107
109
  error,
108
110
  });
111
+ // 停止服务:断开所有 WS 且不再重连。幂等,可被 HTTP 控制接口或框架层 abort 多次调用。
112
+ let stopped = false;
113
+ const doStop = () => {
114
+ if (stopped)
115
+ return;
116
+ stopped = true;
117
+ logInfo("palz-gateway: [doStop] stopping service, shutting down scanner + manager");
118
+ scanner.stop();
119
+ manager.shutdown();
120
+ };
121
+ // 可选启动 HTTP 控制服务(仅当配置了 controlPort)。停止服务时保留该 HTTP 服务,
122
+ // 仅在框架层 abort 时才关闭。
123
+ let controlServer = null;
124
+ const controlPort = baseConfig.controlPort;
125
+ if (typeof controlPort === "number" && controlPort > 0) {
126
+ controlServer = startControlServer({
127
+ port: controlPort,
128
+ onStop: doStop,
129
+ getStatus: () => {
130
+ const activeUsers = manager.getActiveUsers();
131
+ return { stopped, activeConnections: activeUsers.length, activeUsers };
132
+ },
133
+ log: logInfo,
134
+ error,
135
+ });
136
+ }
109
137
  return new Promise((resolve) => {
110
138
  const onAbort = () => {
111
139
  logInfo("palz-gateway: [startAccount] abort received, shutting down manager");
112
- scanner.stop();
113
- manager.shutdown();
140
+ controlServer?.close();
141
+ doStop();
114
142
  resolve();
115
143
  };
116
144
  if (ctx.abortSignal?.aborted) {
package/src/config.js CHANGED
@@ -67,6 +67,7 @@ export function resolvePalzConfig(_cfg, botId) {
67
67
  sessionTimeout: file.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT,
68
68
  groupContextCache: file.groupContextCache !== false,
69
69
  showProcess: file.showProcess === true,
70
+ controlPort: file.controlPort,
70
71
  };
71
72
  if (!_configLoggedOnce) {
72
73
  _configLoggedOnce = true;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Palz Connector HTTP 控制服务
3
+ *
4
+ * 提供一个独立的 HTTP 端口,供外部主动控制插件:
5
+ * - POST /admin/stop 断开所有 WS 连接且不再重连(停止服务),幂等
6
+ * - GET /admin/health 查询当前状态(是否已停止 / 活跃连接数)
7
+ *
8
+ * 与 WS 连接 / 消息处理 / reconcile 完全隔离:只持有 onStop / getStatus 回调,
9
+ * 不碰任何业务数据;只有配置了端口才启动;启动错误只记日志不影响插件主流程。
10
+ *
11
+ * 统一响应结构:{ code, err_message, data },code=0 表示成功,非 0 为错误码。
12
+ * HTTP status 一律 200,业务结果由 code 表达。
13
+ */
14
+ import http from "node:http";
15
+ function sendJson(res, body) {
16
+ const payload = JSON.stringify(body);
17
+ res.writeHead(200, { "Content-Type": "application/json" });
18
+ res.end(payload);
19
+ }
20
+ export function startControlServer(opts) {
21
+ const log = opts.log ?? console.log;
22
+ const error = opts.error ?? console.error;
23
+ const server = http.createServer((req, res) => {
24
+ const method = (req.method || "GET").toUpperCase();
25
+ // 去掉查询串,只取 path
26
+ const path = (req.url || "/").split("?")[0].replace(/\/+$/, "") || "/";
27
+ try {
28
+ if (path === "/admin/stop") {
29
+ if (method !== "POST") {
30
+ sendJson(res, { code: 405, err_message: "method not allowed", data: {} });
31
+ return;
32
+ }
33
+ const before = opts.getStatus();
34
+ opts.onStop();
35
+ const after = opts.getStatus();
36
+ log(`control-server: [stop] requested, activeBefore=${before.activeConnections} stopped=${after.stopped}`);
37
+ sendJson(res, {
38
+ code: 0,
39
+ err_message: "",
40
+ data: { stopped: after.stopped, stoppedConnections: before.activeConnections },
41
+ });
42
+ return;
43
+ }
44
+ if (path === "/admin/health") {
45
+ if (method !== "GET") {
46
+ sendJson(res, { code: 405, err_message: "method not allowed", data: {} });
47
+ return;
48
+ }
49
+ const status = opts.getStatus();
50
+ sendJson(res, { code: 0, err_message: "", data: { ...status } });
51
+ return;
52
+ }
53
+ sendJson(res, { code: 404, err_message: "not found", data: {} });
54
+ }
55
+ catch (err) {
56
+ error(`control-server: request handling error: ${err?.message ?? err}`);
57
+ try {
58
+ sendJson(res, { code: 500, err_message: String(err?.message ?? err), data: {} });
59
+ }
60
+ catch { }
61
+ }
62
+ });
63
+ // 端口占用等错误只记日志,不让插件崩溃。
64
+ server.on("error", (err) => {
65
+ error(`control-server: server error on port ${opts.port}: ${err?.message ?? err}`);
66
+ });
67
+ server.listen(opts.port, () => {
68
+ log(`control-server: listening on port ${opts.port}`);
69
+ });
70
+ return {
71
+ close() {
72
+ try {
73
+ server.close();
74
+ log(`control-server: closed (port ${opts.port})`);
75
+ }
76
+ catch (err) {
77
+ error(`control-server: close error: ${err?.message ?? err}`);
78
+ }
79
+ },
80
+ };
81
+ }
package/src/monitor.js CHANGED
@@ -167,6 +167,14 @@ export async function monitorPalzProvider(params) {
167
167
  lastPongAt = connectedAt;
168
168
  consecutive4002 = 0;
169
169
  log(`palz[${accountId}]: WebSocket connected, bot_id=${config.botId}`);
170
+ reportPalzActivity({
171
+ config,
172
+ connectedAt: new Date(connectedAt),
173
+ runtime,
174
+ accountId,
175
+ }).catch((err) => {
176
+ error(`palz[${accountId}]: [ACTIVITY_REPORT] unhandled error: ${err.message ?? err}`);
177
+ });
170
178
  pingInterval = setInterval(() => {
171
179
  if (ws.readyState === WebSocket.OPEN) {
172
180
  const timeSinceLastPong = Date.now() - lastPongAt;
@@ -195,7 +203,6 @@ export async function monitorPalzProvider(params) {
195
203
  }
196
204
  });
197
205
  ws.on("message", (data) => {
198
- const receivedAt = new Date();
199
206
  const raw = data.toString();
200
207
  try {
201
208
  const msg = JSON.parse(raw);
@@ -209,15 +216,6 @@ export async function monitorPalzProvider(params) {
209
216
  try {
210
217
  span.setAttribute("raw_message", raw);
211
218
  log(`palz[${accountId}]: [WS_RECV] #${messageCount} full_message=${raw.slice(0, 1000)} traceId=${span.spanContext().traceId}`);
212
- reportPalzActivity({
213
- config,
214
- msg,
215
- receivedAt,
216
- runtime,
217
- accountId,
218
- }).catch((err) => {
219
- error(`palz[${accountId}]: [ACTIVITY_REPORT] unhandled error: ${err.message ?? err}`);
220
- });
221
219
  // agent_id:从消息中取,缺失/空时默认为 {userId}-main
222
220
  let messageAgentId = typeof msg.agent_id === "string" ? msg.agent_id.trim() : "";
223
221
  if (!messageAgentId) {