@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
package/dist/index.js
CHANGED
|
@@ -106,7 +106,6 @@ import fsSync from "node:fs";
|
|
|
106
106
|
import path3 from "node:path";
|
|
107
107
|
import os2 from "node:os";
|
|
108
108
|
import { execFile } from "node:child_process";
|
|
109
|
-
import { promisify } from "node:util";
|
|
110
109
|
import { randomUUID, createHash } from "node:crypto";
|
|
111
110
|
|
|
112
111
|
// src/skill-version.ts
|
|
@@ -139,14 +138,37 @@ function parseSkillVersion(content) {
|
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
// src/updater.ts
|
|
142
|
-
var
|
|
141
|
+
var EXTRACT_TIMEOUT_MS = 3e4;
|
|
143
142
|
var ATTEMPT_COOLDOWN_MS = 30 * 60 * 1e3;
|
|
144
143
|
function skillIdentityHash(code, version) {
|
|
145
144
|
return createHash("sha256").update(`${code} ${code} ${version}`).digest("hex");
|
|
146
145
|
}
|
|
146
|
+
function extractorCommandForPlatform(platform, zipPath, destDir) {
|
|
147
|
+
return platform === "darwin" ? { command: "ditto", args: ["-x", "-k", zipPath, destDir] } : { command: "unzip", args: ["-o", "-q", zipPath, "-d", destDir] };
|
|
148
|
+
}
|
|
149
|
+
function runExtractor(command, args) {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
const child = execFile(command, args, {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
timeout: EXTRACT_TIMEOUT_MS,
|
|
154
|
+
killSignal: "SIGKILL",
|
|
155
|
+
maxBuffer: 1024 * 1024
|
|
156
|
+
}, (error, _stdout, stderr) => {
|
|
157
|
+
if (!error) {
|
|
158
|
+
resolve();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const detail = String(stderr || "").trim();
|
|
162
|
+
const timeout = error.killed ? `\uFF0C\u8D85\u8FC7 ${EXTRACT_TIMEOUT_MS}ms \u5DF2\u7EC8\u6B62` : "";
|
|
163
|
+
reject(new Error(`${command} \u89E3\u538B\u5931\u8D25${timeout}: ${detail || error.message}`));
|
|
164
|
+
});
|
|
165
|
+
child.stdin?.end();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
147
168
|
async function systemUnzip(zipPath, destDir) {
|
|
148
169
|
await fs3.mkdir(destDir, { recursive: true });
|
|
149
|
-
|
|
170
|
+
const { command, args } = extractorCommandForPlatform(process.platform, zipPath, destDir);
|
|
171
|
+
await runExtractor(command, args);
|
|
150
172
|
}
|
|
151
173
|
var SkillUpdater = class {
|
|
152
174
|
getConfig;
|
|
@@ -395,36 +417,85 @@ var SkillUpdater = class {
|
|
|
395
417
|
}
|
|
396
418
|
}
|
|
397
419
|
async manualInstall(options) {
|
|
398
|
-
const { code, force, targetDir } = options;
|
|
420
|
+
const { code, force, targetDir, additionalTargetDirs = [], trace } = options;
|
|
399
421
|
let { url, version } = options;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
422
|
+
const startedAt = Date.now();
|
|
423
|
+
let currentStage = "install.start";
|
|
424
|
+
let work;
|
|
425
|
+
const emit = (stage, data) => {
|
|
426
|
+
currentStage = stage;
|
|
427
|
+
try {
|
|
428
|
+
trace?.(stage, data);
|
|
429
|
+
} catch {
|
|
404
430
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
await fs3.mkdir(work, { recursive: true });
|
|
431
|
+
};
|
|
432
|
+
emit("install.start", {
|
|
433
|
+
code,
|
|
434
|
+
version: version || "latest",
|
|
435
|
+
force,
|
|
436
|
+
hasDirectUrl: Boolean(url),
|
|
437
|
+
targetCount: 1 + additionalTargetDirs.length
|
|
438
|
+
});
|
|
414
439
|
try {
|
|
440
|
+
if (!url) {
|
|
441
|
+
const lookupStartedAt = Date.now();
|
|
442
|
+
emit("download_url.lookup.start", { code, version: version || "latest" });
|
|
443
|
+
const dl = await this.fetchDownloadUrl(code, version || "latest");
|
|
444
|
+
if (!dl?.url) {
|
|
445
|
+
const message = `\u65E0\u6CD5\u83B7\u53D6\u6280\u80FD ${code} \u7684\u4E0B\u8F7D\u5730\u5740`;
|
|
446
|
+
emit("install.failed", { stage: currentStage, message, elapsedMs: Date.now() - startedAt });
|
|
447
|
+
return { success: false, message };
|
|
448
|
+
}
|
|
449
|
+
url = dl.url;
|
|
450
|
+
version = dl.version || version;
|
|
451
|
+
emit("download_url.lookup.completed", {
|
|
452
|
+
version: version || "latest",
|
|
453
|
+
url,
|
|
454
|
+
elapsedMs: Date.now() - lookupStartedAt
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const targetSkillPath = path3.join(targetDir, code);
|
|
458
|
+
if (!force && await this.exists(targetSkillPath)) {
|
|
459
|
+
const message = `\u6280\u80FD ${code} \u5DF2\u5B58\u5728\u4E8E\u76EE\u6807\u76EE\u5F55`;
|
|
460
|
+
emit("install.failed", { stage: currentStage, message, elapsedMs: Date.now() - startedAt });
|
|
461
|
+
return { success: false, message };
|
|
462
|
+
}
|
|
463
|
+
work = path3.join(this.tmpDir, `slp-manual-${randomUUID()}`);
|
|
464
|
+
await fs3.mkdir(work, { recursive: true });
|
|
415
465
|
const zipPath = path3.join(work, "pkg.zip");
|
|
466
|
+
const downloadStartedAt = Date.now();
|
|
467
|
+
emit("download.request.start", { url });
|
|
416
468
|
const res = await this.fetchImpl(url);
|
|
469
|
+
emit("download.headers.received", {
|
|
470
|
+
status: res.status,
|
|
471
|
+
contentLength: res.headers?.get("content-length") || void 0,
|
|
472
|
+
contentType: res.headers?.get("content-type") || void 0,
|
|
473
|
+
elapsedMs: Date.now() - downloadStartedAt
|
|
474
|
+
});
|
|
417
475
|
if (!res.ok) {
|
|
418
|
-
|
|
476
|
+
const message = `\u4E0B\u8F7D\u5931\u8D25: HTTP ${res.status}`;
|
|
477
|
+
emit("install.failed", { stage: currentStage, message, elapsedMs: Date.now() - startedAt });
|
|
478
|
+
return { success: false, message };
|
|
419
479
|
}
|
|
420
480
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
481
|
+
emit("download.body.completed", {
|
|
482
|
+
bytes: buf.byteLength,
|
|
483
|
+
elapsedMs: Date.now() - downloadStartedAt
|
|
484
|
+
});
|
|
421
485
|
await fs3.writeFile(zipPath, buf);
|
|
486
|
+
emit("download.file.written", { bytes: buf.byteLength });
|
|
422
487
|
const staging = path3.join(work, "staging");
|
|
488
|
+
const unzipStartedAt = Date.now();
|
|
489
|
+
emit("unzip.start", { extractor: process.platform === "darwin" ? "ditto" : "unzip" });
|
|
423
490
|
await this.unzip(zipPath, staging);
|
|
491
|
+
emit("unzip.completed", { elapsedMs: Date.now() - unzipStartedAt });
|
|
424
492
|
const srcRoot = await this.locateSkillRoot(staging, 0);
|
|
425
493
|
if (!srcRoot) {
|
|
426
|
-
|
|
494
|
+
const message = `\u4E0B\u8F7D\u5305\u5185\u672A\u627E\u5230 SKILL.md\uFF0C\u975E\u6CD5\u7684\u6280\u80FD\u5305\u7ED3\u6784`;
|
|
495
|
+
emit("install.failed", { stage: currentStage, message, elapsedMs: Date.now() - startedAt });
|
|
496
|
+
return { success: false, message };
|
|
427
497
|
}
|
|
498
|
+
emit("skill_root.located");
|
|
428
499
|
const metaPath = path3.join(srcRoot, ".meta.json");
|
|
429
500
|
if (!await this.exists(metaPath)) {
|
|
430
501
|
let parsedVersion = version || "unknown";
|
|
@@ -442,13 +513,37 @@ var SkillUpdater = class {
|
|
|
442
513
|
publishedAt: Date.now()
|
|
443
514
|
}, null, 2));
|
|
444
515
|
}
|
|
445
|
-
|
|
516
|
+
const targets = [targetDir, ...additionalTargetDirs];
|
|
517
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
518
|
+
const rootDir = targets[index];
|
|
519
|
+
const targetPath = path3.join(rootDir, code);
|
|
520
|
+
const replaceStartedAt = Date.now();
|
|
521
|
+
emit("target.replace.start", { targetDir: rootDir, targetIndex: index, targetCount: targets.length });
|
|
522
|
+
await this.replaceDir(srcRoot, targetPath);
|
|
523
|
+
emit("target.replace.completed", {
|
|
524
|
+
targetDir: rootDir,
|
|
525
|
+
targetIndex: index,
|
|
526
|
+
targetCount: targets.length,
|
|
527
|
+
elapsedMs: Date.now() - replaceStartedAt
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
emit("install.completed", { elapsedMs: Date.now() - startedAt, targetCount: targets.length });
|
|
446
531
|
return { success: true, message: `\u6280\u80FD ${code} \u5B89\u88C5/\u66F4\u65B0\u6210\u529F` };
|
|
447
532
|
} catch (err) {
|
|
533
|
+
emit("install.failed", {
|
|
534
|
+
stage: currentStage,
|
|
535
|
+
errorName: err?.name,
|
|
536
|
+
message: err?.message || String(err),
|
|
537
|
+
stack: err?.stack,
|
|
538
|
+
elapsedMs: Date.now() - startedAt
|
|
539
|
+
});
|
|
448
540
|
return { success: false, message: `\u6267\u884C\u51FA\u9519: ${err.message}` };
|
|
449
541
|
} finally {
|
|
450
|
-
|
|
451
|
-
|
|
542
|
+
if (work) {
|
|
543
|
+
await fs3.rm(work, { recursive: true, force: true }).catch(() => {
|
|
544
|
+
});
|
|
545
|
+
emit("cleanup.completed", { elapsedMs: Date.now() - startedAt });
|
|
546
|
+
}
|
|
452
547
|
}
|
|
453
548
|
}
|
|
454
549
|
};
|
|
@@ -1281,11 +1376,11 @@ import path6 from "node:path";
|
|
|
1281
1376
|
// src/identity.ts
|
|
1282
1377
|
import os3 from "node:os";
|
|
1283
1378
|
import { execFile as execFile2 } from "node:child_process";
|
|
1284
|
-
import { promisify
|
|
1285
|
-
var
|
|
1379
|
+
import { promisify } from "node:util";
|
|
1380
|
+
var execFileAsync = promisify(execFile2);
|
|
1286
1381
|
async function getGitConfigValue(key) {
|
|
1287
1382
|
try {
|
|
1288
|
-
const { stdout } = await
|
|
1383
|
+
const { stdout } = await execFileAsync("git", ["config", "--global", key]);
|
|
1289
1384
|
return stdout.trim();
|
|
1290
1385
|
} catch {
|
|
1291
1386
|
console.warn(
|
|
@@ -1562,6 +1657,9 @@ function normalizeAssistantUserId(userId) {
|
|
|
1562
1657
|
if (!ASSISTANT_WORKSPACE_ID_RE.test(pureId)) return void 0;
|
|
1563
1658
|
return pureId;
|
|
1564
1659
|
}
|
|
1660
|
+
function shouldSyncBuiltInTemplate(action, isBuiltIn) {
|
|
1661
|
+
return action === "UPDATE_SKILL" && isBuiltIn === true;
|
|
1662
|
+
}
|
|
1565
1663
|
var GatewayWsClient = class {
|
|
1566
1664
|
ws = null;
|
|
1567
1665
|
options;
|
|
@@ -1598,6 +1696,14 @@ var GatewayWsClient = class {
|
|
|
1598
1696
|
} catch (e) {
|
|
1599
1697
|
}
|
|
1600
1698
|
}
|
|
1699
|
+
createInstallTrace(context) {
|
|
1700
|
+
return (stage, data) => {
|
|
1701
|
+
this.appendLogToFile(stage === "install.failed" ? "ERROR" : "INFO", "Install", stage, {
|
|
1702
|
+
...context,
|
|
1703
|
+
...data
|
|
1704
|
+
});
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1601
1707
|
constructor(options) {
|
|
1602
1708
|
this.options = options;
|
|
1603
1709
|
}
|
|
@@ -1849,8 +1955,16 @@ var GatewayWsClient = class {
|
|
|
1849
1955
|
* 核心指令分发中心:完全跳过沙盒,基于 userId 直接进行底层物理文件操作
|
|
1850
1956
|
*/
|
|
1851
1957
|
async handleMessage(msg) {
|
|
1852
|
-
const { action, userId, code, url, force, version, replyId } = msg;
|
|
1853
|
-
this.appendLogToFile("INFO", "Command", `Received WS message`, {
|
|
1958
|
+
const { action, userId, code, url, force, version, replyId, isBuiltIn } = msg;
|
|
1959
|
+
this.appendLogToFile("INFO", "Command", `Received WS message`, {
|
|
1960
|
+
action,
|
|
1961
|
+
userId,
|
|
1962
|
+
code,
|
|
1963
|
+
version,
|
|
1964
|
+
replyId,
|
|
1965
|
+
isBuiltIn,
|
|
1966
|
+
hasDirectUrl: Boolean(url)
|
|
1967
|
+
});
|
|
1854
1968
|
if (!action || !userId) {
|
|
1855
1969
|
this.appendLogToFile("WARN", "Command", `Message dropped: missing action or userId`, msg);
|
|
1856
1970
|
return;
|
|
@@ -1872,7 +1986,8 @@ var GatewayWsClient = class {
|
|
|
1872
1986
|
url,
|
|
1873
1987
|
version,
|
|
1874
1988
|
force: force !== false,
|
|
1875
|
-
targetDir
|
|
1989
|
+
targetDir,
|
|
1990
|
+
trace: this.createInstallTrace({ action, replyId, userId, code: safeCode })
|
|
1876
1991
|
});
|
|
1877
1992
|
this.reply(replyId, { success: result.success, message: result.message, action });
|
|
1878
1993
|
} else if (action === "UNINSTALL_SKILL") {
|
|
@@ -1907,7 +2022,7 @@ var GatewayWsClient = class {
|
|
|
1907
2022
|
}
|
|
1908
2023
|
const metaPath = path7.join(skillDir, ".meta.json");
|
|
1909
2024
|
let isPlatform = false;
|
|
1910
|
-
let
|
|
2025
|
+
let isBuiltIn2 = e.isSymbolicLink();
|
|
1911
2026
|
let metaData = null;
|
|
1912
2027
|
let name = e.name;
|
|
1913
2028
|
let description = "";
|
|
@@ -1928,7 +2043,7 @@ var GatewayWsClient = class {
|
|
|
1928
2043
|
const parsed = JSON.parse(metaContent);
|
|
1929
2044
|
if (parsed) {
|
|
1930
2045
|
if (parsed.ownerId === "CMS" || parsed.ownerId === "CMS_COMPAT") isPlatform = true;
|
|
1931
|
-
if (parsed.isBuiltIn === true || parsed.ownerId === "built-in")
|
|
2046
|
+
if (parsed.isBuiltIn === true || parsed.ownerId === "built-in") isBuiltIn2 = true;
|
|
1932
2047
|
metaData = parsed;
|
|
1933
2048
|
}
|
|
1934
2049
|
} catch (err) {
|
|
@@ -1939,7 +2054,7 @@ var GatewayWsClient = class {
|
|
|
1939
2054
|
list.push({
|
|
1940
2055
|
code: e.name,
|
|
1941
2056
|
isPlatform: true,
|
|
1942
|
-
isBuiltIn,
|
|
2057
|
+
isBuiltIn: isBuiltIn2,
|
|
1943
2058
|
version: skillVersion,
|
|
1944
2059
|
name,
|
|
1945
2060
|
description,
|
|
@@ -1949,7 +2064,7 @@ var GatewayWsClient = class {
|
|
|
1949
2064
|
list.push({
|
|
1950
2065
|
code: e.name,
|
|
1951
2066
|
isPlatform: false,
|
|
1952
|
-
isBuiltIn,
|
|
2067
|
+
isBuiltIn: isBuiltIn2,
|
|
1953
2068
|
version: skillVersion,
|
|
1954
2069
|
name,
|
|
1955
2070
|
description
|
|
@@ -1960,21 +2075,54 @@ var GatewayWsClient = class {
|
|
|
1960
2075
|
} else if (action === "UPDATE_SKILL") {
|
|
1961
2076
|
if (!safeCode) throw new Error("Missing code parameter");
|
|
1962
2077
|
const delayMs = Math.random() * 5e3;
|
|
2078
|
+
const syncBuiltInTemplate = shouldSyncBuiltInTemplate(action, isBuiltIn);
|
|
1963
2079
|
console.log(`[skill-logger-plugin][WS] Scheduled UPDATE for user ${userId}, code: ${safeCode} in ${Math.round(delayMs)}ms`);
|
|
1964
|
-
this.appendLogToFile("INFO", "Command", `Scheduled UPDATE_SKILL`, {
|
|
2080
|
+
this.appendLogToFile("INFO", "Command", `Scheduled UPDATE_SKILL`, {
|
|
2081
|
+
userId,
|
|
2082
|
+
code: safeCode,
|
|
2083
|
+
version,
|
|
2084
|
+
isBuiltIn,
|
|
2085
|
+
syncBuiltInTemplate,
|
|
2086
|
+
delayMs: Math.round(delayMs)
|
|
2087
|
+
});
|
|
1965
2088
|
setTimeout(async () => {
|
|
1966
2089
|
try {
|
|
1967
|
-
|
|
2090
|
+
const additionalTargetDirs = syncBuiltInTemplate ? [path7.join(openclawHome(), "workspace-xgjk-assistant-template", "skills")] : [];
|
|
2091
|
+
const result = await this.options.updater.manualInstall({
|
|
1968
2092
|
code: safeCode,
|
|
1969
2093
|
url,
|
|
1970
2094
|
version,
|
|
1971
2095
|
force: true,
|
|
1972
|
-
targetDir
|
|
2096
|
+
targetDir,
|
|
2097
|
+
additionalTargetDirs,
|
|
2098
|
+
trace: this.createInstallTrace({
|
|
2099
|
+
action,
|
|
2100
|
+
replyId,
|
|
2101
|
+
userId,
|
|
2102
|
+
code: safeCode,
|
|
2103
|
+
isBuiltIn,
|
|
2104
|
+
syncBuiltInTemplate
|
|
2105
|
+
})
|
|
2106
|
+
});
|
|
2107
|
+
this.appendLogToFile(result.success ? "INFO" : "ERROR", "Command", `UPDATE_SKILL completed`, {
|
|
2108
|
+
userId,
|
|
2109
|
+
code: safeCode,
|
|
2110
|
+
replyId,
|
|
2111
|
+
success: result.success,
|
|
2112
|
+
message: result.message,
|
|
2113
|
+
syncBuiltInTemplate
|
|
1973
2114
|
});
|
|
1974
2115
|
if (replyId) {
|
|
1975
|
-
this.reply(replyId, { success:
|
|
2116
|
+
this.reply(replyId, { success: result.success, message: result.message, action });
|
|
1976
2117
|
}
|
|
1977
2118
|
} catch (e) {
|
|
2119
|
+
this.appendLogToFile("ERROR", "Command", `UPDATE_SKILL threw`, {
|
|
2120
|
+
userId,
|
|
2121
|
+
code: safeCode,
|
|
2122
|
+
replyId,
|
|
2123
|
+
message: e?.message || String(e),
|
|
2124
|
+
stack: e?.stack
|
|
2125
|
+
});
|
|
1978
2126
|
if (replyId) this.reply(replyId, { success: false, message: e.message, action });
|
|
1979
2127
|
}
|
|
1980
2128
|
}, delayMs);
|
|
@@ -1985,11 +2133,25 @@ var GatewayWsClient = class {
|
|
|
1985
2133
|
this.appendLogToFile("INFO", "Command", `INSTALL_EXPERT received`, { userId, code: safeCode, version: version2, downloadUrl });
|
|
1986
2134
|
const userSkillRoot = path7.join(openclawHome(), `workspace-assistant-${pureId}`, ".user");
|
|
1987
2135
|
const expertTarget = path7.join(userSkillRoot, "experts", safeCode);
|
|
1988
|
-
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
2136
|
+
let skipInstall = false;
|
|
2137
|
+
const metaPath = path7.join(expertTarget, ".meta.json");
|
|
2138
|
+
try {
|
|
2139
|
+
const raw = await fs6.readFile(metaPath, "utf-8");
|
|
2140
|
+
const existing = JSON.parse(raw);
|
|
2141
|
+
if (existing.version && existing.version === (version2 || "1.0.0")) {
|
|
2142
|
+
skipInstall = true;
|
|
2143
|
+
}
|
|
2144
|
+
} catch {
|
|
2145
|
+
}
|
|
2146
|
+
if (!skipInstall) {
|
|
2147
|
+
await fs6.mkdir(path7.dirname(expertTarget), { recursive: true });
|
|
2148
|
+
const expertResult = await this.options.updater.installZipFromUrl(downloadUrl, expertTarget);
|
|
2149
|
+
if (!expertResult.success) {
|
|
2150
|
+
throw new Error(`\u4E13\u5BB6\u5B89\u88C5\u5931\u8D25: ${expertResult.message}`);
|
|
2151
|
+
}
|
|
1992
2152
|
}
|
|
2153
|
+
const meta = { code: safeCode, name, version: version2 || "1.0.0", installedAt: Date.now() };
|
|
2154
|
+
await fs6.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
1993
2155
|
const skillTargetRoot = path7.join(userSkillRoot, "skills");
|
|
1994
2156
|
await fs6.mkdir(skillTargetRoot, { recursive: true });
|
|
1995
2157
|
const skillResults = [];
|
|
@@ -2014,48 +2176,42 @@ var GatewayWsClient = class {
|
|
|
2014
2176
|
action,
|
|
2015
2177
|
data: { expertCode: safeCode, skills: skillResults }
|
|
2016
2178
|
});
|
|
2017
|
-
} else if (action === "
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2179
|
+
} else if (action === "UNINSTALL_EXPERT") {
|
|
2180
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
2181
|
+
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL_EXPERT for user ${userId}, code: ${safeCode}`);
|
|
2182
|
+
this.appendLogToFile("INFO", "Command", `UNINSTALL_EXPERT received`, { userId, code: safeCode });
|
|
2183
|
+
const expertPath = path7.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "experts", safeCode);
|
|
2184
|
+
await fs6.rm(expertPath, { recursive: true, force: true });
|
|
2185
|
+
this.reply(replyId, { success: true, message: `\u4E13\u5BB6 ${safeCode} \u5DF2\u5378\u8F7D`, action });
|
|
2186
|
+
} else if (action === "LIST_EXPERTS") {
|
|
2187
|
+
console.log(`[skill-logger-plugin][WS] Executing LIST_EXPERTS for user ${userId}`);
|
|
2188
|
+
this.appendLogToFile("INFO", "Command", `LIST_EXPERTS received`, { userId });
|
|
2189
|
+
const expertsDir = path7.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "experts");
|
|
2190
|
+
const list = [];
|
|
2023
2191
|
try {
|
|
2024
|
-
const stat = await fs6.stat(
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
continue;
|
|
2042
|
-
}
|
|
2043
|
-
if (currentExpert) {
|
|
2044
|
-
const nameMatch = line.match(/^\s*name:\s*(.+)$/);
|
|
2045
|
-
if (nameMatch) {
|
|
2046
|
-
currentExpert.name = nameMatch[1].trim();
|
|
2047
|
-
}
|
|
2048
|
-
const descMatch = line.match(/^\s*description:\s*(.+)$/);
|
|
2049
|
-
if (descMatch) {
|
|
2050
|
-
currentExpert.description = descMatch[1].trim();
|
|
2192
|
+
const stat = await fs6.stat(expertsDir);
|
|
2193
|
+
if (stat.isDirectory()) {
|
|
2194
|
+
const entries = await fs6.readdir(expertsDir, { withFileTypes: true });
|
|
2195
|
+
for (const e of entries) {
|
|
2196
|
+
if (!e.isDirectory()) continue;
|
|
2197
|
+
const metaPath = path7.join(expertsDir, e.name, ".meta.json");
|
|
2198
|
+
try {
|
|
2199
|
+
const raw = await fs6.readFile(metaPath, "utf-8");
|
|
2200
|
+
const meta = JSON.parse(raw);
|
|
2201
|
+
list.push({
|
|
2202
|
+
code: meta.code || e.name,
|
|
2203
|
+
name: meta.name || e.name,
|
|
2204
|
+
version: meta.version || "",
|
|
2205
|
+
installedAt: meta.installedAt
|
|
2206
|
+
});
|
|
2207
|
+
} catch {
|
|
2208
|
+
list.push({ code: e.name, name: e.name, version: "" });
|
|
2051
2209
|
}
|
|
2052
2210
|
}
|
|
2053
2211
|
}
|
|
2054
|
-
|
|
2055
|
-
this.reply(replyId, { success: true, data: expertsList, action });
|
|
2056
|
-
} else {
|
|
2057
|
-
this.reply(replyId, { success: false, message: `\u672A\u627E\u5230\u7528\u6237\u6280\u80FD\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u80FD\u5C1A\u672A\u6CE8\u518C\u6216\u6587\u4EF6\u5DF2\u4E22\u5931`, action });
|
|
2212
|
+
} catch {
|
|
2058
2213
|
}
|
|
2214
|
+
this.reply(replyId, { success: true, data: list, action });
|
|
2059
2215
|
} else {
|
|
2060
2216
|
console.warn(`[skill-logger-plugin][WS] Unknown action: ${action}`);
|
|
2061
2217
|
this.appendLogToFile("WARN", "Command", `Unknown action: ${action}`);
|
|
@@ -2072,7 +2228,13 @@ var GatewayWsClient = class {
|
|
|
2072
2228
|
this.appendLogToFile("WARN", "Command", `Cannot reply: WS closed or missing replyId`, { replyId });
|
|
2073
2229
|
return;
|
|
2074
2230
|
}
|
|
2075
|
-
this.ws.send(JSON.stringify({ type: "REPLY", replyId, ...payload }))
|
|
2231
|
+
this.ws.send(JSON.stringify({ type: "REPLY", replyId, ...payload }), (err) => {
|
|
2232
|
+
if (err) {
|
|
2233
|
+
this.appendLogToFile("ERROR", "Command", `Reply send failed`, { replyId, message: err.message });
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
this.appendLogToFile("INFO", "Command", `Reply sent`, { replyId });
|
|
2237
|
+
});
|
|
2076
2238
|
}
|
|
2077
2239
|
destroy() {
|
|
2078
2240
|
this.isDestroyed = true;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, before } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
let isSkillMdReadPath;
|
|
4
|
+
let extractApiPluginConfig;
|
|
5
|
+
describe("isSkillMdReadPath", () => {
|
|
6
|
+
before(async () => {
|
|
7
|
+
({ isSkillMdReadPath, extractApiPluginConfig } = await import("./index.ts"));
|
|
8
|
+
});
|
|
9
|
+
it("POSIX 路径 basename 为 SKILL.md 时为 true", () => {
|
|
10
|
+
assert.equal(isSkillMdReadPath("/x/y/my-skill/SKILL.md"), true);
|
|
11
|
+
});
|
|
12
|
+
it("Windows 风格路径", { skip: process.platform !== "win32" }, () => {
|
|
13
|
+
assert.equal(isSkillMdReadPath(String.raw `C:\x\y\my-skill\SKILL.md`), true);
|
|
14
|
+
});
|
|
15
|
+
it("fooSKILL.md / my-SKILL.md 不误判", () => {
|
|
16
|
+
assert.equal(isSkillMdReadPath("/tmp/fooSKILL.md"), false);
|
|
17
|
+
assert.equal(isSkillMdReadPath("/tmp/my-SKILL.md"), false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe("extractApiPluginConfig", () => {
|
|
21
|
+
it("从 OpenClaw 注入的 api.pluginConfig 初始化运行期配置", () => {
|
|
22
|
+
assert.deepEqual(extractApiPluginConfig({
|
|
23
|
+
pluginConfig: {
|
|
24
|
+
platformBaseUrl: "https://platform.example/api",
|
|
25
|
+
reportBaseUrl: "https://report.example/api",
|
|
26
|
+
authToken: "Bearer token",
|
|
27
|
+
recordUnattributed: true,
|
|
28
|
+
},
|
|
29
|
+
}), {
|
|
30
|
+
platformBaseUrl: "https://platform.example/api",
|
|
31
|
+
reportBaseUrl: "https://report.example/api",
|
|
32
|
+
authToken: "Bearer token",
|
|
33
|
+
recordUnattributed: true,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it("未配置时返回空对象", () => {
|
|
37
|
+
assert.deepEqual(extractApiPluginConfig({}), {});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { ConfigSync } from "./config-sync.ts";
|
|
8
|
+
import { SkillUpdater } from "./updater.ts";
|
|
9
|
+
const idHash = (code, version) => createHash("sha256").update(`${code} ${code} ${version}`).digest("hex");
|
|
10
|
+
let dir;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
dir = path.join(os.tmpdir(), `slp-int-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
13
|
+
await fs.mkdir(dir, { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
describe("版本更新闭环(端到端)", () => {
|
|
19
|
+
it("config-pull 报最新版 → 检测落后 → 下载覆盖 → 再查收敛", async () => {
|
|
20
|
+
// 本地装一个 demo@1.0.0
|
|
21
|
+
const ws = path.join(dir, "workspace");
|
|
22
|
+
const skillDir = path.join(ws, "skills", "demo");
|
|
23
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
24
|
+
await fs.writeFile(path.join(skillDir, "SKILL.md"), "---\nname: demo\nversion: 1.0.0\n---\nOLD\n");
|
|
25
|
+
const config = { platformBaseUrl: "https://api", autoUpdateSkills: true };
|
|
26
|
+
// 服务端最新版本(随测试推进而变化),模拟「更新后平台版本=本地版本」的收敛。
|
|
27
|
+
let platformLatest = "2.0.0";
|
|
28
|
+
// ConfigSync 的 fetch:/skill_config/pull → 带 latestVersion
|
|
29
|
+
const csFetch = async (_url, _init) => ({
|
|
30
|
+
ok: true,
|
|
31
|
+
status: 200,
|
|
32
|
+
json: async () => ({ configs: [{ skillName: "demo", version: "1.0.0", latestVersion: platformLatest, functions: [] }] }),
|
|
33
|
+
});
|
|
34
|
+
// Updater 的 fetch:/skill_package/pull → {url, sha256};GET → zip 字节
|
|
35
|
+
const upFetch = async (_url, init) => {
|
|
36
|
+
if (init?.method === "POST") {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
status: 200,
|
|
40
|
+
json: async () => ({ url: "https://pkg/skill.zip", version: platformLatest, sha256: idHash("demo", platformLatest) }),
|
|
41
|
+
arrayBuffer: async () => new ArrayBuffer(0),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, status: 200, json: async () => ({}), arrayBuffer: async () => new Uint8Array([1]).buffer };
|
|
45
|
+
};
|
|
46
|
+
// 解压:写出新版本的 SKILL.md(版本号与 platformLatest 一致)
|
|
47
|
+
const unzip = async (_zip, destDir) => {
|
|
48
|
+
const root = path.join(destDir, "demo");
|
|
49
|
+
await fs.mkdir(root, { recursive: true });
|
|
50
|
+
await fs.writeFile(path.join(root, "SKILL.md"), `---\nname: demo\nversion: ${platformLatest}\n---\nNEW\n`);
|
|
51
|
+
};
|
|
52
|
+
const updater = new SkillUpdater({ getConfig: () => config, fetchImpl: upFetch, unzip, tmpDir: dir });
|
|
53
|
+
const cs = new ConfigSync({
|
|
54
|
+
paths: { extensionsDir: path.join(dir, "ext"), syncStatePath: path.join(dir, "sync.json"), openclawConfigPath: path.join(dir, "openclaw.json") },
|
|
55
|
+
getConfig: () => config,
|
|
56
|
+
fetchImpl: csFetch,
|
|
57
|
+
resolveSkillDirs: () => [path.join(ws, "skills")],
|
|
58
|
+
updater,
|
|
59
|
+
});
|
|
60
|
+
// 第一轮:检测落后并覆盖到 2.0.0
|
|
61
|
+
await cs.checkVersionsAndUpdate();
|
|
62
|
+
const afterMd = await fs.readFile(path.join(skillDir, "SKILL.md"), "utf-8");
|
|
63
|
+
assert.ok(afterMd.includes("version: 2.0.0"), "应被更新到 2.0.0");
|
|
64
|
+
assert.ok(afterMd.includes("NEW"), "应为新内容");
|
|
65
|
+
// 第二轮:平台版本仍 2.0.0、本地已 2.0.0 → 收敛,无落后副本
|
|
66
|
+
platformLatest = "2.0.0";
|
|
67
|
+
await cs.checkVersionsAndUpdate();
|
|
68
|
+
assert.equal(cs.detectOutdated().length, 0, "更新后应收敛");
|
|
69
|
+
// sync.json 落了 installations 映射,且版本已是 2.0.0
|
|
70
|
+
const state = JSON.parse(await fs.readFile(path.join(dir, "sync.json"), "utf-8"));
|
|
71
|
+
assert.equal(state.installations.demo[0].version, "2.0.0");
|
|
72
|
+
});
|
|
73
|
+
it("autoUpdateSkills=false:检测到落后也不覆盖", async () => {
|
|
74
|
+
const ws = path.join(dir, "workspace");
|
|
75
|
+
const skillDir = path.join(ws, "skills", "demo");
|
|
76
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
77
|
+
await fs.writeFile(path.join(skillDir, "SKILL.md"), "---\nname: demo\nversion: 1.0.0\n---\nOLD\n");
|
|
78
|
+
const config = { platformBaseUrl: "https://api", autoUpdateSkills: false };
|
|
79
|
+
const csFetch = async () => ({
|
|
80
|
+
ok: true,
|
|
81
|
+
status: 200,
|
|
82
|
+
json: async () => ({ configs: [{ skillName: "demo", version: "1.0.0", latestVersion: "2.0.0", functions: [] }] }),
|
|
83
|
+
});
|
|
84
|
+
let upCalled = 0;
|
|
85
|
+
const upFetch = async () => {
|
|
86
|
+
upCalled++;
|
|
87
|
+
return { ok: true, status: 200, json: async () => ({ url: "" }), arrayBuffer: async () => new ArrayBuffer(0) };
|
|
88
|
+
};
|
|
89
|
+
const updater = new SkillUpdater({ getConfig: () => config, fetchImpl: upFetch, unzip: async () => { }, tmpDir: dir });
|
|
90
|
+
const cs = new ConfigSync({
|
|
91
|
+
paths: { extensionsDir: path.join(dir, "ext"), syncStatePath: path.join(dir, "sync.json"), openclawConfigPath: path.join(dir, "openclaw.json") },
|
|
92
|
+
getConfig: () => config,
|
|
93
|
+
fetchImpl: csFetch,
|
|
94
|
+
resolveSkillDirs: () => [path.join(ws, "skills")],
|
|
95
|
+
updater,
|
|
96
|
+
});
|
|
97
|
+
await cs.checkVersionsAndUpdate();
|
|
98
|
+
assert.equal(cs.detectOutdated().length, 1, "仍检测到落后");
|
|
99
|
+
assert.equal(upCalled, 0, "关闭时不应发起下载");
|
|
100
|
+
assert.ok((await fs.readFile(path.join(skillDir, "SKILL.md"), "utf-8")).includes("OLD"), "文件保持原样");
|
|
101
|
+
});
|
|
102
|
+
});
|