@spzhongwin/skill-logger-plugin 1.0.11 → 1.0.12
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 +212 -72
- 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 +314 -237
- package/src/updater.ts +518 -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
|
@@ -395,36 +395,85 @@ var SkillUpdater = class {
|
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
async manualInstall(options) {
|
|
398
|
-
const { code, force, targetDir } = options;
|
|
398
|
+
const { code, force, targetDir, additionalTargetDirs = [], trace } = options;
|
|
399
399
|
let { url, version } = options;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
400
|
+
const startedAt = Date.now();
|
|
401
|
+
let currentStage = "install.start";
|
|
402
|
+
let work;
|
|
403
|
+
const emit = (stage, data) => {
|
|
404
|
+
currentStage = stage;
|
|
405
|
+
try {
|
|
406
|
+
trace?.(stage, data);
|
|
407
|
+
} catch {
|
|
404
408
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
await fs3.mkdir(work, { recursive: true });
|
|
409
|
+
};
|
|
410
|
+
emit("install.start", {
|
|
411
|
+
code,
|
|
412
|
+
version: version || "latest",
|
|
413
|
+
force,
|
|
414
|
+
hasDirectUrl: Boolean(url),
|
|
415
|
+
targetCount: 1 + additionalTargetDirs.length
|
|
416
|
+
});
|
|
414
417
|
try {
|
|
418
|
+
if (!url) {
|
|
419
|
+
const lookupStartedAt = Date.now();
|
|
420
|
+
emit("download_url.lookup.start", { code, version: version || "latest" });
|
|
421
|
+
const dl = await this.fetchDownloadUrl(code, version || "latest");
|
|
422
|
+
if (!dl?.url) {
|
|
423
|
+
const message = `\u65E0\u6CD5\u83B7\u53D6\u6280\u80FD ${code} \u7684\u4E0B\u8F7D\u5730\u5740`;
|
|
424
|
+
emit("install.failed", { stage: currentStage, message, elapsedMs: Date.now() - startedAt });
|
|
425
|
+
return { success: false, message };
|
|
426
|
+
}
|
|
427
|
+
url = dl.url;
|
|
428
|
+
version = dl.version || version;
|
|
429
|
+
emit("download_url.lookup.completed", {
|
|
430
|
+
version: version || "latest",
|
|
431
|
+
url,
|
|
432
|
+
elapsedMs: Date.now() - lookupStartedAt
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
const targetSkillPath = path3.join(targetDir, code);
|
|
436
|
+
if (!force && await this.exists(targetSkillPath)) {
|
|
437
|
+
const message = `\u6280\u80FD ${code} \u5DF2\u5B58\u5728\u4E8E\u76EE\u6807\u76EE\u5F55`;
|
|
438
|
+
emit("install.failed", { stage: currentStage, message, elapsedMs: Date.now() - startedAt });
|
|
439
|
+
return { success: false, message };
|
|
440
|
+
}
|
|
441
|
+
work = path3.join(this.tmpDir, `slp-manual-${randomUUID()}`);
|
|
442
|
+
await fs3.mkdir(work, { recursive: true });
|
|
415
443
|
const zipPath = path3.join(work, "pkg.zip");
|
|
444
|
+
const downloadStartedAt = Date.now();
|
|
445
|
+
emit("download.request.start", { url });
|
|
416
446
|
const res = await this.fetchImpl(url);
|
|
447
|
+
emit("download.headers.received", {
|
|
448
|
+
status: res.status,
|
|
449
|
+
contentLength: res.headers?.get("content-length") || void 0,
|
|
450
|
+
contentType: res.headers?.get("content-type") || void 0,
|
|
451
|
+
elapsedMs: Date.now() - downloadStartedAt
|
|
452
|
+
});
|
|
417
453
|
if (!res.ok) {
|
|
418
|
-
|
|
454
|
+
const message = `\u4E0B\u8F7D\u5931\u8D25: HTTP ${res.status}`;
|
|
455
|
+
emit("install.failed", { stage: currentStage, message, elapsedMs: Date.now() - startedAt });
|
|
456
|
+
return { success: false, message };
|
|
419
457
|
}
|
|
420
458
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
459
|
+
emit("download.body.completed", {
|
|
460
|
+
bytes: buf.byteLength,
|
|
461
|
+
elapsedMs: Date.now() - downloadStartedAt
|
|
462
|
+
});
|
|
421
463
|
await fs3.writeFile(zipPath, buf);
|
|
464
|
+
emit("download.file.written", { bytes: buf.byteLength });
|
|
422
465
|
const staging = path3.join(work, "staging");
|
|
466
|
+
const unzipStartedAt = Date.now();
|
|
467
|
+
emit("unzip.start");
|
|
423
468
|
await this.unzip(zipPath, staging);
|
|
469
|
+
emit("unzip.completed", { elapsedMs: Date.now() - unzipStartedAt });
|
|
424
470
|
const srcRoot = await this.locateSkillRoot(staging, 0);
|
|
425
471
|
if (!srcRoot) {
|
|
426
|
-
|
|
472
|
+
const message = `\u4E0B\u8F7D\u5305\u5185\u672A\u627E\u5230 SKILL.md\uFF0C\u975E\u6CD5\u7684\u6280\u80FD\u5305\u7ED3\u6784`;
|
|
473
|
+
emit("install.failed", { stage: currentStage, message, elapsedMs: Date.now() - startedAt });
|
|
474
|
+
return { success: false, message };
|
|
427
475
|
}
|
|
476
|
+
emit("skill_root.located");
|
|
428
477
|
const metaPath = path3.join(srcRoot, ".meta.json");
|
|
429
478
|
if (!await this.exists(metaPath)) {
|
|
430
479
|
let parsedVersion = version || "unknown";
|
|
@@ -442,13 +491,37 @@ var SkillUpdater = class {
|
|
|
442
491
|
publishedAt: Date.now()
|
|
443
492
|
}, null, 2));
|
|
444
493
|
}
|
|
445
|
-
|
|
494
|
+
const targets = [targetDir, ...additionalTargetDirs];
|
|
495
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
496
|
+
const rootDir = targets[index];
|
|
497
|
+
const targetPath = path3.join(rootDir, code);
|
|
498
|
+
const replaceStartedAt = Date.now();
|
|
499
|
+
emit("target.replace.start", { targetDir: rootDir, targetIndex: index, targetCount: targets.length });
|
|
500
|
+
await this.replaceDir(srcRoot, targetPath);
|
|
501
|
+
emit("target.replace.completed", {
|
|
502
|
+
targetDir: rootDir,
|
|
503
|
+
targetIndex: index,
|
|
504
|
+
targetCount: targets.length,
|
|
505
|
+
elapsedMs: Date.now() - replaceStartedAt
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
emit("install.completed", { elapsedMs: Date.now() - startedAt, targetCount: targets.length });
|
|
446
509
|
return { success: true, message: `\u6280\u80FD ${code} \u5B89\u88C5/\u66F4\u65B0\u6210\u529F` };
|
|
447
510
|
} catch (err) {
|
|
511
|
+
emit("install.failed", {
|
|
512
|
+
stage: currentStage,
|
|
513
|
+
errorName: err?.name,
|
|
514
|
+
message: err?.message || String(err),
|
|
515
|
+
stack: err?.stack,
|
|
516
|
+
elapsedMs: Date.now() - startedAt
|
|
517
|
+
});
|
|
448
518
|
return { success: false, message: `\u6267\u884C\u51FA\u9519: ${err.message}` };
|
|
449
519
|
} finally {
|
|
450
|
-
|
|
451
|
-
|
|
520
|
+
if (work) {
|
|
521
|
+
await fs3.rm(work, { recursive: true, force: true }).catch(() => {
|
|
522
|
+
});
|
|
523
|
+
emit("cleanup.completed", { elapsedMs: Date.now() - startedAt });
|
|
524
|
+
}
|
|
452
525
|
}
|
|
453
526
|
}
|
|
454
527
|
};
|
|
@@ -1562,6 +1635,9 @@ function normalizeAssistantUserId(userId) {
|
|
|
1562
1635
|
if (!ASSISTANT_WORKSPACE_ID_RE.test(pureId)) return void 0;
|
|
1563
1636
|
return pureId;
|
|
1564
1637
|
}
|
|
1638
|
+
function shouldSyncBuiltInTemplate(action, isBuiltIn) {
|
|
1639
|
+
return action === "UPDATE_SKILL" && isBuiltIn === true;
|
|
1640
|
+
}
|
|
1565
1641
|
var GatewayWsClient = class {
|
|
1566
1642
|
ws = null;
|
|
1567
1643
|
options;
|
|
@@ -1598,6 +1674,14 @@ var GatewayWsClient = class {
|
|
|
1598
1674
|
} catch (e) {
|
|
1599
1675
|
}
|
|
1600
1676
|
}
|
|
1677
|
+
createInstallTrace(context) {
|
|
1678
|
+
return (stage, data) => {
|
|
1679
|
+
this.appendLogToFile(stage === "install.failed" ? "ERROR" : "INFO", "Install", stage, {
|
|
1680
|
+
...context,
|
|
1681
|
+
...data
|
|
1682
|
+
});
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1601
1685
|
constructor(options) {
|
|
1602
1686
|
this.options = options;
|
|
1603
1687
|
}
|
|
@@ -1849,8 +1933,16 @@ var GatewayWsClient = class {
|
|
|
1849
1933
|
* 核心指令分发中心:完全跳过沙盒,基于 userId 直接进行底层物理文件操作
|
|
1850
1934
|
*/
|
|
1851
1935
|
async handleMessage(msg) {
|
|
1852
|
-
const { action, userId, code, url, force, version, replyId } = msg;
|
|
1853
|
-
this.appendLogToFile("INFO", "Command", `Received WS message`, {
|
|
1936
|
+
const { action, userId, code, url, force, version, replyId, isBuiltIn } = msg;
|
|
1937
|
+
this.appendLogToFile("INFO", "Command", `Received WS message`, {
|
|
1938
|
+
action,
|
|
1939
|
+
userId,
|
|
1940
|
+
code,
|
|
1941
|
+
version,
|
|
1942
|
+
replyId,
|
|
1943
|
+
isBuiltIn,
|
|
1944
|
+
hasDirectUrl: Boolean(url)
|
|
1945
|
+
});
|
|
1854
1946
|
if (!action || !userId) {
|
|
1855
1947
|
this.appendLogToFile("WARN", "Command", `Message dropped: missing action or userId`, msg);
|
|
1856
1948
|
return;
|
|
@@ -1872,7 +1964,8 @@ var GatewayWsClient = class {
|
|
|
1872
1964
|
url,
|
|
1873
1965
|
version,
|
|
1874
1966
|
force: force !== false,
|
|
1875
|
-
targetDir
|
|
1967
|
+
targetDir,
|
|
1968
|
+
trace: this.createInstallTrace({ action, replyId, userId, code: safeCode })
|
|
1876
1969
|
});
|
|
1877
1970
|
this.reply(replyId, { success: result.success, message: result.message, action });
|
|
1878
1971
|
} else if (action === "UNINSTALL_SKILL") {
|
|
@@ -1907,7 +2000,7 @@ var GatewayWsClient = class {
|
|
|
1907
2000
|
}
|
|
1908
2001
|
const metaPath = path7.join(skillDir, ".meta.json");
|
|
1909
2002
|
let isPlatform = false;
|
|
1910
|
-
let
|
|
2003
|
+
let isBuiltIn2 = e.isSymbolicLink();
|
|
1911
2004
|
let metaData = null;
|
|
1912
2005
|
let name = e.name;
|
|
1913
2006
|
let description = "";
|
|
@@ -1928,7 +2021,7 @@ var GatewayWsClient = class {
|
|
|
1928
2021
|
const parsed = JSON.parse(metaContent);
|
|
1929
2022
|
if (parsed) {
|
|
1930
2023
|
if (parsed.ownerId === "CMS" || parsed.ownerId === "CMS_COMPAT") isPlatform = true;
|
|
1931
|
-
if (parsed.isBuiltIn === true || parsed.ownerId === "built-in")
|
|
2024
|
+
if (parsed.isBuiltIn === true || parsed.ownerId === "built-in") isBuiltIn2 = true;
|
|
1932
2025
|
metaData = parsed;
|
|
1933
2026
|
}
|
|
1934
2027
|
} catch (err) {
|
|
@@ -1939,7 +2032,7 @@ var GatewayWsClient = class {
|
|
|
1939
2032
|
list.push({
|
|
1940
2033
|
code: e.name,
|
|
1941
2034
|
isPlatform: true,
|
|
1942
|
-
isBuiltIn,
|
|
2035
|
+
isBuiltIn: isBuiltIn2,
|
|
1943
2036
|
version: skillVersion,
|
|
1944
2037
|
name,
|
|
1945
2038
|
description,
|
|
@@ -1949,7 +2042,7 @@ var GatewayWsClient = class {
|
|
|
1949
2042
|
list.push({
|
|
1950
2043
|
code: e.name,
|
|
1951
2044
|
isPlatform: false,
|
|
1952
|
-
isBuiltIn,
|
|
2045
|
+
isBuiltIn: isBuiltIn2,
|
|
1953
2046
|
version: skillVersion,
|
|
1954
2047
|
name,
|
|
1955
2048
|
description
|
|
@@ -1960,21 +2053,54 @@ var GatewayWsClient = class {
|
|
|
1960
2053
|
} else if (action === "UPDATE_SKILL") {
|
|
1961
2054
|
if (!safeCode) throw new Error("Missing code parameter");
|
|
1962
2055
|
const delayMs = Math.random() * 5e3;
|
|
2056
|
+
const syncBuiltInTemplate = shouldSyncBuiltInTemplate(action, isBuiltIn);
|
|
1963
2057
|
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`, {
|
|
2058
|
+
this.appendLogToFile("INFO", "Command", `Scheduled UPDATE_SKILL`, {
|
|
2059
|
+
userId,
|
|
2060
|
+
code: safeCode,
|
|
2061
|
+
version,
|
|
2062
|
+
isBuiltIn,
|
|
2063
|
+
syncBuiltInTemplate,
|
|
2064
|
+
delayMs: Math.round(delayMs)
|
|
2065
|
+
});
|
|
1965
2066
|
setTimeout(async () => {
|
|
1966
2067
|
try {
|
|
1967
|
-
|
|
2068
|
+
const additionalTargetDirs = syncBuiltInTemplate ? [path7.join(openclawHome(), "workspace-xgjk-assistant-template", "skills")] : [];
|
|
2069
|
+
const result = await this.options.updater.manualInstall({
|
|
1968
2070
|
code: safeCode,
|
|
1969
2071
|
url,
|
|
1970
2072
|
version,
|
|
1971
2073
|
force: true,
|
|
1972
|
-
targetDir
|
|
2074
|
+
targetDir,
|
|
2075
|
+
additionalTargetDirs,
|
|
2076
|
+
trace: this.createInstallTrace({
|
|
2077
|
+
action,
|
|
2078
|
+
replyId,
|
|
2079
|
+
userId,
|
|
2080
|
+
code: safeCode,
|
|
2081
|
+
isBuiltIn,
|
|
2082
|
+
syncBuiltInTemplate
|
|
2083
|
+
})
|
|
2084
|
+
});
|
|
2085
|
+
this.appendLogToFile(result.success ? "INFO" : "ERROR", "Command", `UPDATE_SKILL completed`, {
|
|
2086
|
+
userId,
|
|
2087
|
+
code: safeCode,
|
|
2088
|
+
replyId,
|
|
2089
|
+
success: result.success,
|
|
2090
|
+
message: result.message,
|
|
2091
|
+
syncBuiltInTemplate
|
|
1973
2092
|
});
|
|
1974
2093
|
if (replyId) {
|
|
1975
|
-
this.reply(replyId, { success:
|
|
2094
|
+
this.reply(replyId, { success: result.success, message: result.message, action });
|
|
1976
2095
|
}
|
|
1977
2096
|
} catch (e) {
|
|
2097
|
+
this.appendLogToFile("ERROR", "Command", `UPDATE_SKILL threw`, {
|
|
2098
|
+
userId,
|
|
2099
|
+
code: safeCode,
|
|
2100
|
+
replyId,
|
|
2101
|
+
message: e?.message || String(e),
|
|
2102
|
+
stack: e?.stack
|
|
2103
|
+
});
|
|
1978
2104
|
if (replyId) this.reply(replyId, { success: false, message: e.message, action });
|
|
1979
2105
|
}
|
|
1980
2106
|
}, delayMs);
|
|
@@ -1985,11 +2111,25 @@ var GatewayWsClient = class {
|
|
|
1985
2111
|
this.appendLogToFile("INFO", "Command", `INSTALL_EXPERT received`, { userId, code: safeCode, version: version2, downloadUrl });
|
|
1986
2112
|
const userSkillRoot = path7.join(openclawHome(), `workspace-assistant-${pureId}`, ".user");
|
|
1987
2113
|
const expertTarget = path7.join(userSkillRoot, "experts", safeCode);
|
|
1988
|
-
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
2114
|
+
let skipInstall = false;
|
|
2115
|
+
const metaPath = path7.join(expertTarget, ".meta.json");
|
|
2116
|
+
try {
|
|
2117
|
+
const raw = await fs6.readFile(metaPath, "utf-8");
|
|
2118
|
+
const existing = JSON.parse(raw);
|
|
2119
|
+
if (existing.version && existing.version === (version2 || "1.0.0")) {
|
|
2120
|
+
skipInstall = true;
|
|
2121
|
+
}
|
|
2122
|
+
} catch {
|
|
1992
2123
|
}
|
|
2124
|
+
if (!skipInstall) {
|
|
2125
|
+
await fs6.mkdir(path7.dirname(expertTarget), { recursive: true });
|
|
2126
|
+
const expertResult = await this.options.updater.installZipFromUrl(downloadUrl, expertTarget);
|
|
2127
|
+
if (!expertResult.success) {
|
|
2128
|
+
throw new Error(`\u4E13\u5BB6\u5B89\u88C5\u5931\u8D25: ${expertResult.message}`);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const meta = { code: safeCode, name, version: version2 || "1.0.0", installedAt: Date.now() };
|
|
2132
|
+
await fs6.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
1993
2133
|
const skillTargetRoot = path7.join(userSkillRoot, "skills");
|
|
1994
2134
|
await fs6.mkdir(skillTargetRoot, { recursive: true });
|
|
1995
2135
|
const skillResults = [];
|
|
@@ -2014,48 +2154,42 @@ var GatewayWsClient = class {
|
|
|
2014
2154
|
action,
|
|
2015
2155
|
data: { expertCode: safeCode, skills: skillResults }
|
|
2016
2156
|
});
|
|
2017
|
-
} else if (action === "
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2157
|
+
} else if (action === "UNINSTALL_EXPERT") {
|
|
2158
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
2159
|
+
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL_EXPERT for user ${userId}, code: ${safeCode}`);
|
|
2160
|
+
this.appendLogToFile("INFO", "Command", `UNINSTALL_EXPERT received`, { userId, code: safeCode });
|
|
2161
|
+
const expertPath = path7.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "experts", safeCode);
|
|
2162
|
+
await fs6.rm(expertPath, { recursive: true, force: true });
|
|
2163
|
+
this.reply(replyId, { success: true, message: `\u4E13\u5BB6 ${safeCode} \u5DF2\u5378\u8F7D`, action });
|
|
2164
|
+
} else if (action === "LIST_EXPERTS") {
|
|
2165
|
+
console.log(`[skill-logger-plugin][WS] Executing LIST_EXPERTS for user ${userId}`);
|
|
2166
|
+
this.appendLogToFile("INFO", "Command", `LIST_EXPERTS received`, { userId });
|
|
2167
|
+
const expertsDir = path7.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "experts");
|
|
2168
|
+
const list = [];
|
|
2023
2169
|
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();
|
|
2170
|
+
const stat = await fs6.stat(expertsDir);
|
|
2171
|
+
if (stat.isDirectory()) {
|
|
2172
|
+
const entries = await fs6.readdir(expertsDir, { withFileTypes: true });
|
|
2173
|
+
for (const e of entries) {
|
|
2174
|
+
if (!e.isDirectory()) continue;
|
|
2175
|
+
const metaPath = path7.join(expertsDir, e.name, ".meta.json");
|
|
2176
|
+
try {
|
|
2177
|
+
const raw = await fs6.readFile(metaPath, "utf-8");
|
|
2178
|
+
const meta = JSON.parse(raw);
|
|
2179
|
+
list.push({
|
|
2180
|
+
code: meta.code || e.name,
|
|
2181
|
+
name: meta.name || e.name,
|
|
2182
|
+
version: meta.version || "",
|
|
2183
|
+
installedAt: meta.installedAt
|
|
2184
|
+
});
|
|
2185
|
+
} catch {
|
|
2186
|
+
list.push({ code: e.name, name: e.name, version: "" });
|
|
2051
2187
|
}
|
|
2052
2188
|
}
|
|
2053
2189
|
}
|
|
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 });
|
|
2190
|
+
} catch {
|
|
2058
2191
|
}
|
|
2192
|
+
this.reply(replyId, { success: true, data: list, action });
|
|
2059
2193
|
} else {
|
|
2060
2194
|
console.warn(`[skill-logger-plugin][WS] Unknown action: ${action}`);
|
|
2061
2195
|
this.appendLogToFile("WARN", "Command", `Unknown action: ${action}`);
|
|
@@ -2072,7 +2206,13 @@ var GatewayWsClient = class {
|
|
|
2072
2206
|
this.appendLogToFile("WARN", "Command", `Cannot reply: WS closed or missing replyId`, { replyId });
|
|
2073
2207
|
return;
|
|
2074
2208
|
}
|
|
2075
|
-
this.ws.send(JSON.stringify({ type: "REPLY", replyId, ...payload }))
|
|
2209
|
+
this.ws.send(JSON.stringify({ type: "REPLY", replyId, ...payload }), (err) => {
|
|
2210
|
+
if (err) {
|
|
2211
|
+
this.appendLogToFile("ERROR", "Command", `Reply send failed`, { replyId, message: err.message });
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
this.appendLogToFile("INFO", "Command", `Reply sent`, { replyId });
|
|
2215
|
+
});
|
|
2076
2216
|
}
|
|
2077
2217
|
destroy() {
|
|
2078
2218
|
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
|
+
});
|