@spzhongwin/skill-logger-plugin 1.0.11 → 1.0.13
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/dist/active-skills.js +67 -0
- package/dist/active-skills.test.js +29 -0
- package/dist/config-sync.js +439 -0
- package/dist/config-sync.test.js +145 -0
- package/dist/hooks.js +337 -0
- package/dist/hooks.test.js +123 -0
- package/dist/http.js +54 -0
- package/dist/identity.js +56 -0
- package/dist/index.js +240 -78
- package/dist/index.test.js +39 -0
- package/dist/integration.test.js +102 -0
- package/dist/matcher.js +362 -0
- package/dist/matcher.test.js +139 -0
- package/dist/paths.js +62 -0
- package/dist/paths.test.js +49 -0
- package/dist/reporter.js +267 -0
- package/dist/reporter.test.js +128 -0
- package/dist/semver.js +64 -0
- package/dist/semver.test.js +21 -0
- package/dist/skill-version.js +23 -0
- package/dist/types.js +9 -0
- package/dist/updater.js +352 -0
- package/dist/updater.test.js +212 -0
- package/dist/ws-client.js +484 -0
- package/openclaw.plugin.json +50 -50
- package/package.json +37 -37
- package/src/active-skills.test.ts +32 -32
- package/src/active-skills.ts +77 -77
- package/src/config-sync.test.ts +165 -165
- package/src/config-sync.ts +544 -544
- package/src/hooks.test.ts +251 -251
- package/src/hooks.ts +517 -517
- package/src/http.ts +61 -61
- package/src/identity.ts +64 -64
- package/src/index.test.ts +53 -53
- package/src/index.ts +226 -226
- package/src/integration.test.ts +119 -119
- package/src/matcher.test.ts +170 -170
- package/src/matcher.ts +393 -393
- package/src/paths.test.ts +57 -57
- package/src/paths.ts +84 -84
- package/src/reporter.test.ts +139 -139
- package/src/reporter.ts +298 -298
- package/src/sample-config.json +72 -72
- package/src/semver.test.ts +23 -23
- package/src/semver.ts +60 -60
- package/src/skill-version.ts +53 -53
- package/src/types.ts +198 -198
- package/src/updater.test.ts +325 -237
- package/src/updater.ts +549 -433
- package/src/ws-client.test.ts +48 -37
- package/src/ws-client.ts +717 -642
- package/test-ws.ts +17 -17
- package/tsconfig.json +14 -14
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { openclawHome } from "./paths.ts";
|
|
5
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
6
|
+
const HEARTBEAT_ACK_TIMEOUT_MS = 75_000;
|
|
7
|
+
const AGENT_SCAN_INTERVAL_MS = 3 * 60 * 1000;
|
|
8
|
+
export class GatewayWsClient {
|
|
9
|
+
ws = null;
|
|
10
|
+
options;
|
|
11
|
+
reconnectTimer = null;
|
|
12
|
+
agentScanTimer = null;
|
|
13
|
+
pingTimer = null;
|
|
14
|
+
connectTimeoutTimer = null;
|
|
15
|
+
lastServerAckAt = 0;
|
|
16
|
+
reconnectAttempts = 0;
|
|
17
|
+
isDestroyed = false;
|
|
18
|
+
// 本地缓存的 agent ID 列表
|
|
19
|
+
currentAgentIds = new Set();
|
|
20
|
+
appendLogToFile(level, category, message, payload) {
|
|
21
|
+
if (!this.options.enableFileLog)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
const ts = new Date().toISOString();
|
|
25
|
+
let logLine = `[${ts}] [${level}] [${category}] ${message}`;
|
|
26
|
+
if (payload !== undefined && payload !== null) {
|
|
27
|
+
// 如果是 Error 对象,主动提取 stack
|
|
28
|
+
if (payload instanceof Error) {
|
|
29
|
+
logLine += `\n Stack: ${payload.stack || payload.message}`;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
logLine += ` | Data: ${typeof payload === 'object' ? JSON.stringify(payload) : payload}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
logLine += '\n';
|
|
36
|
+
const logsDir = path.join(openclawHome(), "logs");
|
|
37
|
+
fs.mkdir(logsDir, { recursive: true }).then(() => {
|
|
38
|
+
const logPath = path.join(logsDir, "skill-logger.err");
|
|
39
|
+
fs.appendFile(logPath, logLine).catch(() => { });
|
|
40
|
+
}).catch(() => { });
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
// ignore
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.options = options;
|
|
48
|
+
}
|
|
49
|
+
connect() {
|
|
50
|
+
if (this.isDestroyed)
|
|
51
|
+
return;
|
|
52
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN ||
|
|
53
|
+
this.ws.readyState === WebSocket.CONNECTING)) {
|
|
54
|
+
this.appendLogToFile("INFO", "Connection", "Connect skipped: websocket already active", { readyState: this.ws.readyState });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.clearConnectTimeout();
|
|
58
|
+
const msgConnect = `Connecting to Central Service: ${this.options.serverUrl}`;
|
|
59
|
+
console.log(`[skill-logger-plugin][WS] ${msgConnect}`);
|
|
60
|
+
this.appendLogToFile("INFO", "Connection", msgConnect);
|
|
61
|
+
const headers = {
|
|
62
|
+
"X-Gateway-Id": this.options.gatewayId,
|
|
63
|
+
};
|
|
64
|
+
if (this.options.authToken) {
|
|
65
|
+
headers["Authorization"] = this.options.authToken;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const ws = new WebSocket(this.options.serverUrl, { headers });
|
|
69
|
+
this.ws = ws;
|
|
70
|
+
this.connectTimeoutTimer = setTimeout(() => {
|
|
71
|
+
if (this.ws === ws && ws.readyState === WebSocket.CONNECTING) {
|
|
72
|
+
this.appendLogToFile("WARN", "Connection", "Connection timed out before open; terminating socket.");
|
|
73
|
+
ws.terminate();
|
|
74
|
+
}
|
|
75
|
+
}, 15000);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error(`[skill-logger-plugin][WS] Sync init error:`, err);
|
|
79
|
+
this.appendLogToFile("ERROR", "Connection", "Sync initialization error", err);
|
|
80
|
+
this.scheduleReconnect();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const ws = this.ws;
|
|
84
|
+
ws.on("open", async () => {
|
|
85
|
+
if (this.ws !== ws)
|
|
86
|
+
return;
|
|
87
|
+
console.log(`[skill-logger-plugin][WS] Connected successfully!`);
|
|
88
|
+
this.appendLogToFile("INFO", "Connection", "Connected successfully!");
|
|
89
|
+
this.clearConnectTimeout();
|
|
90
|
+
this.reconnectAttempts = 0;
|
|
91
|
+
this.lastServerAckAt = Date.now();
|
|
92
|
+
this.clearReconnectTimer();
|
|
93
|
+
// 首次连接,全量扫描并上报,同时进行应用层握手,确保服务端能把 DB 在线态刷新成真实状态。
|
|
94
|
+
await this.scanAndReportAgents(true);
|
|
95
|
+
this.sendGatewayHello();
|
|
96
|
+
this.startHeartbeat();
|
|
97
|
+
// 开启 3 分钟定期的自动扫码增量同步
|
|
98
|
+
if (!this.agentScanTimer) {
|
|
99
|
+
this.agentScanTimer = setInterval(() => {
|
|
100
|
+
this.scanAndReportAgents(true);
|
|
101
|
+
}, AGENT_SCAN_INTERVAL_MS);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
ws.on("pong", () => {
|
|
105
|
+
this.lastServerAckAt = Date.now();
|
|
106
|
+
});
|
|
107
|
+
ws.on("message", async (data) => {
|
|
108
|
+
if (this.ws !== ws)
|
|
109
|
+
return;
|
|
110
|
+
try {
|
|
111
|
+
const msg = JSON.parse(data.toString());
|
|
112
|
+
if (msg?.type === "GATEWAY_HELLO_ACK" || msg?.type === "HEARTBEAT_ACK") {
|
|
113
|
+
this.lastServerAckAt = Date.now();
|
|
114
|
+
this.appendLogToFile("INFO", "Heartbeat", "Server heartbeat acknowledged", { type: msg.type, connectionId: msg.connectionId });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await this.handleMessage(msg);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(`[skill-logger-plugin][WS] Failed to parse/handle message:`, err);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
ws.on("close", () => {
|
|
124
|
+
console.warn(`[skill-logger-plugin][WS] Connection closed.`);
|
|
125
|
+
this.appendLogToFile("WARN", "Connection", "Connection closed. Starting reconnect timer.");
|
|
126
|
+
if (this.ws === ws) {
|
|
127
|
+
this.ws = null;
|
|
128
|
+
this.clearConnectTimeout();
|
|
129
|
+
this.clearAgentScanTimer();
|
|
130
|
+
this.clearHeartbeat();
|
|
131
|
+
this.scheduleReconnect();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
ws.on("error", (err) => {
|
|
135
|
+
console.error(`[skill-logger-plugin][WS] Connection error:`, err);
|
|
136
|
+
this.appendLogToFile("ERROR", "Connection", "Connection error", err);
|
|
137
|
+
if (this.ws === ws) {
|
|
138
|
+
ws.close(); // 触发 close 事件进行重连
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 扫描 OpenClaw 根目录下的 workspace-assistant-{userId} 目录
|
|
144
|
+
*/
|
|
145
|
+
async scanAndReportAgents(isInitialReport) {
|
|
146
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
const rootPath = openclawHome();
|
|
150
|
+
let entries = [];
|
|
151
|
+
try {
|
|
152
|
+
entries = await fs.readdir(rootPath);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
this.currentAgentIds = new Set();
|
|
156
|
+
this.appendLogToFile("WARN", "AgentScan", "OpenClaw home is not readable; reporting empty agent list", e);
|
|
157
|
+
this.sendAgentListReport("AGENT_LIST_REPORT");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const newAgentIds = new Set();
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (entry.startsWith("workspace-assistant-")) {
|
|
163
|
+
const suffix = entry.replace("workspace-assistant-", "").trim();
|
|
164
|
+
if (suffix && suffix === path.basename(suffix) && !suffix.startsWith(".")) {
|
|
165
|
+
newAgentIds.add(`assistant-${suffix}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let changed = false;
|
|
170
|
+
if (newAgentIds.size !== this.currentAgentIds.size) {
|
|
171
|
+
changed = true;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
for (const id of newAgentIds) {
|
|
175
|
+
if (!this.currentAgentIds.has(id)) {
|
|
176
|
+
changed = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.currentAgentIds = newAgentIds;
|
|
182
|
+
if (isInitialReport) {
|
|
183
|
+
this.appendLogToFile("INFO", "AgentScan", `Reporting full agent list`, { count: this.currentAgentIds.size, agents: Array.from(this.currentAgentIds) });
|
|
184
|
+
this.sendAgentListReport("AGENT_LIST_REPORT");
|
|
185
|
+
}
|
|
186
|
+
else if (changed) {
|
|
187
|
+
this.appendLogToFile("INFO", "AgentScan", `Syncing changed agent list`, { count: this.currentAgentIds.size, agents: Array.from(this.currentAgentIds) });
|
|
188
|
+
this.sendAgentListReport("AGENT_LIST_SYNC");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
console.error(`[skill-logger-plugin][WS] Failed to scan agents:`, err);
|
|
193
|
+
this.appendLogToFile("ERROR", "AgentScan", `Failed to scan agents`, err);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
startHeartbeat() {
|
|
197
|
+
this.clearHeartbeat();
|
|
198
|
+
this.pingTimer = setInterval(() => {
|
|
199
|
+
const ws = this.ws;
|
|
200
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
201
|
+
return;
|
|
202
|
+
const ackAge = Date.now() - this.lastServerAckAt;
|
|
203
|
+
if (ackAge > HEARTBEAT_ACK_TIMEOUT_MS) {
|
|
204
|
+
this.appendLogToFile("WARN", "Heartbeat", "Server heartbeat ACK timed out; terminating socket for reconnect", { ackAge });
|
|
205
|
+
ws.terminate();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
ws.ping();
|
|
209
|
+
this.sendClientHeartbeat();
|
|
210
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
211
|
+
this.sendClientHeartbeat();
|
|
212
|
+
}
|
|
213
|
+
clearHeartbeat() {
|
|
214
|
+
if (this.pingTimer) {
|
|
215
|
+
clearInterval(this.pingTimer);
|
|
216
|
+
this.pingTimer = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
scheduleReconnect() {
|
|
220
|
+
if (this.isDestroyed || this.reconnectTimer)
|
|
221
|
+
return;
|
|
222
|
+
// 闭环完善:引入随机 Jitter 抖动,打散服务端重启时可能引发的瞬间重连风暴
|
|
223
|
+
const jitter = Math.floor(Math.random() * 5000);
|
|
224
|
+
const baseDelay = Math.min(30000, 2000 * Math.max(1, 2 ** this.reconnectAttempts));
|
|
225
|
+
const delay = baseDelay + jitter;
|
|
226
|
+
this.reconnectAttempts += 1;
|
|
227
|
+
console.log(`[skill-logger-plugin][WS] Reconnecting in ${delay}ms...`);
|
|
228
|
+
this.appendLogToFile("INFO", "Connection", "Reconnect scheduled", { delay, attempt: this.reconnectAttempts });
|
|
229
|
+
this.reconnectTimer = setTimeout(() => {
|
|
230
|
+
this.reconnectTimer = null;
|
|
231
|
+
this.connect();
|
|
232
|
+
}, delay);
|
|
233
|
+
}
|
|
234
|
+
clearReconnectTimer() {
|
|
235
|
+
if (this.reconnectTimer) {
|
|
236
|
+
clearTimeout(this.reconnectTimer);
|
|
237
|
+
this.reconnectTimer = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
clearConnectTimeout() {
|
|
241
|
+
if (this.connectTimeoutTimer) {
|
|
242
|
+
clearTimeout(this.connectTimeoutTimer);
|
|
243
|
+
this.connectTimeoutTimer = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
terminateCurrentSocket(reason, payload) {
|
|
247
|
+
const ws = this.ws;
|
|
248
|
+
if (!ws)
|
|
249
|
+
return;
|
|
250
|
+
this.appendLogToFile("WARN", "Connection", `Terminating websocket: ${reason}`, payload);
|
|
251
|
+
try {
|
|
252
|
+
ws.terminate();
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
this.appendLogToFile("WARN", "Connection", "Terminate websocket failed", err);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
sendJson(payload, category) {
|
|
259
|
+
const ws = this.ws;
|
|
260
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
261
|
+
return false;
|
|
262
|
+
try {
|
|
263
|
+
ws.send(JSON.stringify(payload), (err) => {
|
|
264
|
+
if (!err)
|
|
265
|
+
return;
|
|
266
|
+
this.appendLogToFile("WARN", category, "WebSocket send failed", err);
|
|
267
|
+
if (this.ws === ws) {
|
|
268
|
+
this.terminateCurrentSocket("send_failed", { category, message: err.message });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
this.appendLogToFile("WARN", category, "WebSocket send threw", err);
|
|
275
|
+
if (this.ws === ws) {
|
|
276
|
+
this.terminateCurrentSocket("send_threw", err);
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
sendGatewayHello() {
|
|
282
|
+
this.sendJson({
|
|
283
|
+
type: "GATEWAY_HELLO",
|
|
284
|
+
gatewayId: this.options.gatewayId,
|
|
285
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
286
|
+
clientTime: Date.now(),
|
|
287
|
+
}, "Heartbeat");
|
|
288
|
+
}
|
|
289
|
+
sendClientHeartbeat() {
|
|
290
|
+
this.sendJson({
|
|
291
|
+
type: "CLIENT_HEARTBEAT",
|
|
292
|
+
gatewayId: this.options.gatewayId,
|
|
293
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
294
|
+
clientTime: Date.now(),
|
|
295
|
+
}, "Heartbeat");
|
|
296
|
+
}
|
|
297
|
+
sendAgentListReport(type) {
|
|
298
|
+
this.sendJson({
|
|
299
|
+
type,
|
|
300
|
+
gatewayId: this.options.gatewayId,
|
|
301
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
302
|
+
clientTime: Date.now(),
|
|
303
|
+
}, "AgentScan");
|
|
304
|
+
}
|
|
305
|
+
clearAgentScanTimer() {
|
|
306
|
+
if (this.agentScanTimer) {
|
|
307
|
+
clearInterval(this.agentScanTimer);
|
|
308
|
+
this.agentScanTimer = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 核心指令分发中心:完全跳过沙盒,基于 userId 直接进行底层物理文件操作
|
|
313
|
+
*/
|
|
314
|
+
async handleMessage(msg) {
|
|
315
|
+
const { action, userId, code, url, force, version, replyId } = msg;
|
|
316
|
+
this.appendLogToFile("INFO", "Command", `Received WS message`, { action, userId, code, version, replyId });
|
|
317
|
+
if (!action || !userId) {
|
|
318
|
+
this.appendLogToFile("WARN", "Command", `Message dropped: missing action or userId`, msg);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// 闭环完善:强制清理 userId 和 code 字符串,防止恶意指令通过 '../' 引发宿主机目录穿越攻击
|
|
322
|
+
const safeUserId = path.basename(userId);
|
|
323
|
+
const safeCode = code ? path.basename(code) : undefined;
|
|
324
|
+
// 100% 确定性安全寻址 (去掉 userId 中可能带有的 assistant- 前缀,统一用 workspace-assistant- 拼接)
|
|
325
|
+
const pureId = safeUserId.replace(/^assistant-/, "");
|
|
326
|
+
const targetDir = path.join(openclawHome(), `workspace-assistant-${pureId}`, "skills");
|
|
327
|
+
try {
|
|
328
|
+
if (action === "INSTALL_SKILL") {
|
|
329
|
+
if (!safeCode)
|
|
330
|
+
throw new Error("Missing code parameter");
|
|
331
|
+
console.log(`[skill-logger-plugin][WS] Executing INSTALL for user ${userId}, code: ${safeCode}`);
|
|
332
|
+
this.appendLogToFile("INFO", "Command", `INSTALL_SKILL received`, { userId, code: safeCode, version });
|
|
333
|
+
const result = await this.options.updater.manualInstall({
|
|
334
|
+
code: safeCode, url, version, force: force !== false, targetDir
|
|
335
|
+
});
|
|
336
|
+
this.reply(replyId, { success: result.success, message: result.message, action });
|
|
337
|
+
}
|
|
338
|
+
else if (action === "UNINSTALL_SKILL") {
|
|
339
|
+
if (!safeCode)
|
|
340
|
+
throw new Error("Missing code parameter");
|
|
341
|
+
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL for user ${userId}, code: ${safeCode}`);
|
|
342
|
+
this.appendLogToFile("INFO", "Command", `UNINSTALL_SKILL received`, { userId, code: safeCode });
|
|
343
|
+
const skillPath = path.join(targetDir, safeCode);
|
|
344
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
345
|
+
this.reply(replyId, { success: true, message: `Skill ${safeCode} removed`, action });
|
|
346
|
+
}
|
|
347
|
+
else if (action === "LIST_SKILLS") {
|
|
348
|
+
let list = [];
|
|
349
|
+
let targetDirExists = false;
|
|
350
|
+
try {
|
|
351
|
+
const targetStat = await fs.stat(targetDir);
|
|
352
|
+
targetDirExists = targetStat.isDirectory();
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
if (err?.code !== "ENOENT")
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
if (!targetDirExists) {
|
|
359
|
+
throw new Error(`Target skills directory does not exist: ${targetDir}`);
|
|
360
|
+
}
|
|
361
|
+
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
362
|
+
const dirs = entries.filter(e => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith("."));
|
|
363
|
+
for (const e of dirs) {
|
|
364
|
+
const skillDir = path.join(targetDir, e.name);
|
|
365
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
366
|
+
try {
|
|
367
|
+
const stat = await fs.stat(skillMdPath);
|
|
368
|
+
if (!stat.isFile())
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const metaPath = path.join(skillDir, '.meta.json');
|
|
375
|
+
let isPlatform = false;
|
|
376
|
+
let isBuiltIn = e.isSymbolicLink();
|
|
377
|
+
let metaData = null;
|
|
378
|
+
let name = e.name;
|
|
379
|
+
let description = "";
|
|
380
|
+
let skillVersion = "";
|
|
381
|
+
try {
|
|
382
|
+
const mdContent = await fs.readFile(skillMdPath, 'utf8');
|
|
383
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdContent)?.[1] ?? "";
|
|
384
|
+
const parsedName = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim();
|
|
385
|
+
if (parsedName)
|
|
386
|
+
name = parsedName;
|
|
387
|
+
const parsedVersion = /(^|\n)version:\s*(.+)/i.exec(fm)?.[2]?.trim();
|
|
388
|
+
if (parsedVersion)
|
|
389
|
+
skillVersion = parsedVersion.replace(/^['"]|['"]$/g, '').replace(/\s+#.*$/, '');
|
|
390
|
+
const descMatch = /(^|\n)description:\s*(?:>\s*\n\s*)?(.*?)(?=\n[a-z]+:|\n---|$)/is.exec(fm);
|
|
391
|
+
if (descMatch && descMatch[2]) {
|
|
392
|
+
description = descMatch[2].replace(/\n\s+/g, ' ').trim();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (err) { }
|
|
396
|
+
try {
|
|
397
|
+
const metaContent = await fs.readFile(metaPath, 'utf8');
|
|
398
|
+
const parsed = JSON.parse(metaContent);
|
|
399
|
+
if (parsed) {
|
|
400
|
+
if (parsed.ownerId === 'CMS' || parsed.ownerId === 'CMS_COMPAT')
|
|
401
|
+
isPlatform = true;
|
|
402
|
+
if (parsed.isBuiltIn === true || parsed.ownerId === 'built-in')
|
|
403
|
+
isBuiltIn = true;
|
|
404
|
+
metaData = parsed;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (err) { }
|
|
408
|
+
if (isPlatform) {
|
|
409
|
+
list.push({
|
|
410
|
+
code: e.name,
|
|
411
|
+
isPlatform: true,
|
|
412
|
+
isBuiltIn: isBuiltIn,
|
|
413
|
+
version: metaData?.version || skillVersion,
|
|
414
|
+
name,
|
|
415
|
+
description,
|
|
416
|
+
publishedAt: metaData?.publishedAt
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
list.push({
|
|
421
|
+
code: e.name,
|
|
422
|
+
isPlatform: false,
|
|
423
|
+
isBuiltIn: isBuiltIn,
|
|
424
|
+
version: skillVersion,
|
|
425
|
+
name: name,
|
|
426
|
+
description: description
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
this.reply(replyId, { success: true, data: list, action });
|
|
431
|
+
}
|
|
432
|
+
else if (action === "UPDATE_SKILL") {
|
|
433
|
+
if (!safeCode)
|
|
434
|
+
throw new Error("Missing code parameter");
|
|
435
|
+
// 更新可能是大批量广播,增加防阻塞的随机延时 (0~5秒)
|
|
436
|
+
const delayMs = Math.random() * 5000;
|
|
437
|
+
console.log(`[skill-logger-plugin][WS] Scheduled UPDATE for user ${userId}, code: ${safeCode} in ${Math.round(delayMs)}ms`);
|
|
438
|
+
this.appendLogToFile("INFO", "Command", `Scheduled UPDATE_SKILL`, { userId, code: safeCode, version, delayMs: Math.round(delayMs) });
|
|
439
|
+
setTimeout(async () => {
|
|
440
|
+
try {
|
|
441
|
+
await this.options.updater.manualInstall({
|
|
442
|
+
code: safeCode, url, version, force: true, targetDir
|
|
443
|
+
});
|
|
444
|
+
// 广播更新一般无需强阻塞等待 replyId,若有需要也可以回传
|
|
445
|
+
if (replyId) {
|
|
446
|
+
this.reply(replyId, { success: true, message: `Skill ${safeCode} updated successfully`, action });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch (e) {
|
|
450
|
+
if (replyId)
|
|
451
|
+
this.reply(replyId, { success: false, message: e.message, action });
|
|
452
|
+
}
|
|
453
|
+
}, delayMs);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
console.warn(`[skill-logger-plugin][WS] Unknown action: ${action}`);
|
|
457
|
+
this.appendLogToFile("WARN", "Command", `Unknown action: ${action}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
this.appendLogToFile("ERROR", "Command", `Error executing action ${action}`, err);
|
|
462
|
+
this.reply(replyId, { success: false, message: err.message, action });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
reply(replyId, payload) {
|
|
466
|
+
this.appendLogToFile("INFO", "Command", `Replying to command`, { replyId, payload });
|
|
467
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !replyId) {
|
|
468
|
+
this.appendLogToFile("WARN", "Command", `Cannot reply: WS closed or missing replyId`, { replyId });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.ws.send(JSON.stringify({ type: "REPLY", replyId, ...payload }));
|
|
472
|
+
}
|
|
473
|
+
destroy() {
|
|
474
|
+
this.isDestroyed = true;
|
|
475
|
+
this.clearReconnectTimer();
|
|
476
|
+
this.clearAgentScanTimer();
|
|
477
|
+
this.clearHeartbeat();
|
|
478
|
+
this.clearConnectTimeout();
|
|
479
|
+
if (this.ws) {
|
|
480
|
+
this.ws.terminate(); // 强行销毁,斩断半开连接残留
|
|
481
|
+
this.ws = null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "skill-logger-plugin",
|
|
3
|
-
"name": "Skill Logger",
|
|
4
|
-
"description": "追踪 openclaw skill 内功能点(脚本/命令/工具/HTTP)使用与报错,落本地并批量上报",
|
|
5
|
-
"activation": {
|
|
6
|
-
"onStartup": true
|
|
7
|
-
},
|
|
8
|
-
"configSchema": {
|
|
9
|
-
"type": "object",
|
|
10
|
-
"additionalProperties": true,
|
|
11
|
-
"properties": {
|
|
12
|
-
"pluginId": {
|
|
13
|
-
"type": "string",
|
|
14
|
-
"description": "自定义插件 ID"
|
|
15
|
-
},
|
|
16
|
-
"pluginName": {
|
|
17
|
-
"type": "string",
|
|
18
|
-
"description": "自定义插件名称"
|
|
19
|
-
},
|
|
20
|
-
"platformBaseUrl": {
|
|
21
|
-
"type": "string",
|
|
22
|
-
"description": "拉取 skill 标准配置的服务地址;缺省时使用内置静态桩"
|
|
23
|
-
},
|
|
24
|
-
"reportBaseUrl": {
|
|
25
|
-
"type": "string",
|
|
26
|
-
"description": "批量上报的服务地址;缺省时只落本地、不上报"
|
|
27
|
-
},
|
|
28
|
-
"authToken": {
|
|
29
|
-
"type": "string",
|
|
30
|
-
"description": "拉取/上报接口的 Authorization 头值(如 'Bearer xxx')"
|
|
31
|
-
},
|
|
32
|
-
"recordUnattributed": {
|
|
33
|
-
"type": "boolean",
|
|
34
|
-
"description": "是否记录无法归属到具体功能点的 exec 调用(默认 true)"
|
|
35
|
-
},
|
|
36
|
-
"enableFileLog": {
|
|
37
|
-
"type": "boolean",
|
|
38
|
-
"description": "是否开启插件专属文件日志(落盘为 skill-logger.err),用于排查 WS 连接和指令运行情况"
|
|
39
|
-
},
|
|
40
|
-
"autoUpdateSkills": {
|
|
41
|
-
"type": "boolean",
|
|
42
|
-
"description": "是否开启 skill 版本自动更新(检测到本地落后于平台最新版本时下载并覆盖各 workspace 下的副本);默认开启,仅显式设为 false 才关闭"
|
|
43
|
-
},
|
|
44
|
-
"versionCheckIntervalMinutes": {
|
|
45
|
-
"type": "number",
|
|
46
|
-
"description": "版本检查+自动更新的周期(分钟)。每隔该周期按本地所有已装 skill 向服务端拉最新版本并按需更新;默认 30。本地扫描发现固定 3 分钟,与此独立"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "skill-logger-plugin",
|
|
3
|
+
"name": "Skill Logger",
|
|
4
|
+
"description": "追踪 openclaw skill 内功能点(脚本/命令/工具/HTTP)使用与报错,落本地并批量上报",
|
|
5
|
+
"activation": {
|
|
6
|
+
"onStartup": true
|
|
7
|
+
},
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": true,
|
|
11
|
+
"properties": {
|
|
12
|
+
"pluginId": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "自定义插件 ID"
|
|
15
|
+
},
|
|
16
|
+
"pluginName": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "自定义插件名称"
|
|
19
|
+
},
|
|
20
|
+
"platformBaseUrl": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "拉取 skill 标准配置的服务地址;缺省时使用内置静态桩"
|
|
23
|
+
},
|
|
24
|
+
"reportBaseUrl": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "批量上报的服务地址;缺省时只落本地、不上报"
|
|
27
|
+
},
|
|
28
|
+
"authToken": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "拉取/上报接口的 Authorization 头值(如 'Bearer xxx')"
|
|
31
|
+
},
|
|
32
|
+
"recordUnattributed": {
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"description": "是否记录无法归属到具体功能点的 exec 调用(默认 true)"
|
|
35
|
+
},
|
|
36
|
+
"enableFileLog": {
|
|
37
|
+
"type": "boolean",
|
|
38
|
+
"description": "是否开启插件专属文件日志(落盘为 skill-logger.err),用于排查 WS 连接和指令运行情况"
|
|
39
|
+
},
|
|
40
|
+
"autoUpdateSkills": {
|
|
41
|
+
"type": "boolean",
|
|
42
|
+
"description": "是否开启 skill 版本自动更新(检测到本地落后于平台最新版本时下载并覆盖各 workspace 下的副本);默认开启,仅显式设为 false 才关闭"
|
|
43
|
+
},
|
|
44
|
+
"versionCheckIntervalMinutes": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"description": "版本检查+自动更新的周期(分钟)。每隔该周期按本地所有已装 skill 向服务端拉最新版本并按需更新;默认 30。本地扫描发现固定 3 分钟,与此独立"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@spzhongwin/skill-logger-plugin",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"exports": "./dist/index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "node --experimental-strip-types --test src/*.test.ts",
|
|
8
|
-
"typecheck": "tsc --noEmit",
|
|
9
|
-
"build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:ws"
|
|
10
|
-
},
|
|
11
|
-
"openclaw": {
|
|
12
|
-
"extensions": [
|
|
13
|
-
"./dist/index.js"
|
|
14
|
-
],
|
|
15
|
-
"contracts": {
|
|
16
|
-
"tools": [
|
|
17
|
-
"report_skill_error"
|
|
18
|
-
]
|
|
19
|
-
},
|
|
20
|
-
"compat": {
|
|
21
|
-
"pluginApi": ">=2026.3.28",
|
|
22
|
-
"minGatewayVersion": "2026.3.28"
|
|
23
|
-
},
|
|
24
|
-
"build": {
|
|
25
|
-
"openclawVersion": "2026.3.28",
|
|
26
|
-
"pluginSdkVersion": "2026.3.28"
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@types/node": "^22.19.20",
|
|
31
|
-
"@types/ws": "^8.18.1",
|
|
32
|
-
"typescript": "^5.9.3"
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"ws": "^8.21.0"
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@spzhongwin/skill-logger-plugin",
|
|
3
|
+
"version": "1.0.13",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": "./dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --experimental-strip-types --test src/*.test.ts",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:ws"
|
|
10
|
+
},
|
|
11
|
+
"openclaw": {
|
|
12
|
+
"extensions": [
|
|
13
|
+
"./dist/index.js"
|
|
14
|
+
],
|
|
15
|
+
"contracts": {
|
|
16
|
+
"tools": [
|
|
17
|
+
"report_skill_error"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"compat": {
|
|
21
|
+
"pluginApi": ">=2026.3.28",
|
|
22
|
+
"minGatewayVersion": "2026.3.28"
|
|
23
|
+
},
|
|
24
|
+
"build": {
|
|
25
|
+
"openclawVersion": "2026.3.28",
|
|
26
|
+
"pluginSdkVersion": "2026.3.28"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.19.20",
|
|
31
|
+
"@types/ws": "^8.18.1",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"ws": "^8.21.0"
|
|
36
|
+
}
|
|
37
|
+
}
|