palz-connector 1.6.2 → 1.6.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/palz-connector.config.json +2 -1
- package/palz-connector.dev.config.json +2 -1
- package/palz-connector.prod.config.json +2 -1
- package/palz-connector.staging.config.json +2 -1
- package/src/activity.js +11 -15
- package/src/channel.js +30 -2
- package/src/config.js +1 -0
- package/src/control-server.js +81 -0
- package/src/monitor.js +8 -10
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/activity.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Palz IM
|
|
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,
|
|
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: "
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
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) {
|