@spzhongwin/skill-logger-plugin 1.0.2 → 1.0.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/dist/index.js +149 -130
- package/package.json +1 -1
- package/src/config-sync.ts +2 -2
- package/src/hooks.ts +2 -2
- package/src/skill-version.ts +33 -2
- package/src/updater.ts +16 -18
- package/src/ws-client.ts +5 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs7 from "node:fs";
|
|
3
|
+
import path9 from "node:path";
|
|
4
4
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5
5
|
import os4 from "node:os";
|
|
6
6
|
|
|
@@ -101,17 +101,38 @@ var ActiveSkills = class {
|
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
// src/updater.ts
|
|
104
|
-
import
|
|
104
|
+
import fs3 from "node:fs/promises";
|
|
105
105
|
import fsSync from "node:fs";
|
|
106
|
-
import
|
|
106
|
+
import path3 from "node:path";
|
|
107
107
|
import os2 from "node:os";
|
|
108
108
|
import { execFile } from "node:child_process";
|
|
109
109
|
import { promisify } from "node:util";
|
|
110
110
|
import { randomUUID, createHash } from "node:crypto";
|
|
111
111
|
|
|
112
112
|
// src/skill-version.ts
|
|
113
|
+
import fs2 from "node:fs/promises";
|
|
114
|
+
import path2 from "node:path";
|
|
115
|
+
async function readSkillVersion(rootDir, skillMdContent) {
|
|
116
|
+
try {
|
|
117
|
+
const metaPath = path2.join(rootDir, ".meta.json");
|
|
118
|
+
const metaContent = await fs2.readFile(metaPath, "utf-8");
|
|
119
|
+
const meta = JSON.parse(metaContent);
|
|
120
|
+
if (meta && typeof meta.version === "string" && meta.version.trim() && meta.version !== "unknown") {
|
|
121
|
+
return meta.version.trim();
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
if (!skillMdContent) {
|
|
126
|
+
try {
|
|
127
|
+
skillMdContent = await fs2.readFile(path2.join(rootDir, "SKILL.md"), "utf-8");
|
|
128
|
+
} catch {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return parseSkillVersion(skillMdContent);
|
|
133
|
+
}
|
|
113
134
|
function parseSkillVersion(content) {
|
|
114
|
-
const m = /(?:^|\r?\n)[ \t]*(?:version
|
|
135
|
+
const m = /(?:^|\r?\n)[ \t]*(?:version|当前版本号|当前版本|版本号|版本)[ \t]*[::][ \t]*(.+)/i.exec(content);
|
|
115
136
|
if (!m) return void 0;
|
|
116
137
|
const v = m[1].replace(/\s+#.*$/, "").trim().replace(/^["']|["']$/g, "").trim();
|
|
117
138
|
return v || void 0;
|
|
@@ -121,7 +142,7 @@ function parseSkillVersion(content) {
|
|
|
121
142
|
var execFileAsync = promisify(execFile);
|
|
122
143
|
var ATTEMPT_COOLDOWN_MS = 30 * 60 * 1e3;
|
|
123
144
|
async function systemUnzip(zipPath, destDir) {
|
|
124
|
-
await
|
|
145
|
+
await fs3.mkdir(destDir, { recursive: true });
|
|
125
146
|
await execFileAsync("unzip", ["-o", "-q", zipPath, "-d", destDir]);
|
|
126
147
|
}
|
|
127
148
|
var SkillUpdater = class {
|
|
@@ -170,7 +191,7 @@ var SkillUpdater = class {
|
|
|
170
191
|
for (const [k, v] of this.lastAttempt) {
|
|
171
192
|
obj[k] = v;
|
|
172
193
|
}
|
|
173
|
-
await
|
|
194
|
+
await fs3.writeFile(this.cooldownStatePath, JSON.stringify(obj), "utf-8");
|
|
174
195
|
} catch {
|
|
175
196
|
}
|
|
176
197
|
}
|
|
@@ -209,46 +230,45 @@ var SkillUpdater = class {
|
|
|
209
230
|
async updateOne(skillName, version, targets) {
|
|
210
231
|
const dl = await this.fetchDownloadUrl(skillName, version);
|
|
211
232
|
if (!dl?.url) return;
|
|
212
|
-
const work =
|
|
213
|
-
await
|
|
233
|
+
const work = path3.join(this.tmpDir, `slp-update-${randomUUID()}`);
|
|
234
|
+
await fs3.mkdir(work, { recursive: true });
|
|
214
235
|
try {
|
|
215
|
-
const zipPath =
|
|
236
|
+
const zipPath = path3.join(work, "pkg.zip");
|
|
216
237
|
const res = await this.fetchImpl(dl.url);
|
|
217
238
|
if (!res.ok) {
|
|
218
239
|
console.warn(`[skill-logger-plugin] \u4E0B\u8F7D ${skillName}@${version} \u5931\u8D25 HTTP`, res.status);
|
|
219
240
|
return;
|
|
220
241
|
}
|
|
221
242
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
222
|
-
await
|
|
223
|
-
const staging =
|
|
243
|
+
await fs3.writeFile(zipPath, buf);
|
|
244
|
+
const staging = path3.join(work, "staging");
|
|
224
245
|
await this.unzip(zipPath, staging);
|
|
225
246
|
const srcRoot = await this.locateSkillRoot(staging, 0);
|
|
226
247
|
if (!srcRoot) {
|
|
227
248
|
console.warn(`[skill-logger-plugin] ${skillName}@${version} \u4E0B\u8F7D\u5305\u5185\u672A\u627E\u5230 SKILL.md\uFF0C\u653E\u5F03\u8986\u76D6`);
|
|
228
249
|
return;
|
|
229
250
|
}
|
|
230
|
-
const metaPath = path2.join(srcRoot, ".meta.json");
|
|
231
|
-
if (!await this.exists(metaPath)) {
|
|
232
|
-
this.debug(`[skill-logger-plugin] \u4E3A ${skillName}@${version} \u81EA\u52A8\u751F\u6210 .meta.json`);
|
|
233
|
-
await fs2.writeFile(metaPath, JSON.stringify({
|
|
234
|
-
ownerId: "CMS_COMPAT",
|
|
235
|
-
slug: skillName,
|
|
236
|
-
version,
|
|
237
|
-
publishedAt: Date.now()
|
|
238
|
-
}, null, 2));
|
|
239
|
-
}
|
|
240
251
|
let packageVersion;
|
|
241
252
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.warn(`[skill-logger-plugin] ${skillName}@${version} \u8BFB\u53D6\u4E0B\u8F7D\u5305 SKILL.md \u5931\u8D25\uFF0C\u653E\u5F03\u8986\u76D6`);
|
|
253
|
+
packageVersion = await readSkillVersion(srcRoot);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.warn(`[skill-logger-plugin] ${skillName}@${version} \u8BFB\u53D6\u4E0B\u8F7D\u5305\u7248\u672C\u53F7\u5931\u8D25\uFF0C\u653E\u5F03\u8986\u76D6`, err);
|
|
246
256
|
return;
|
|
247
257
|
}
|
|
248
258
|
if (!packageVersion) {
|
|
249
|
-
console.warn(`[skill-logger-plugin] ${skillName}@${version} \u4E0B\u8F7D\u5305
|
|
259
|
+
console.warn(`[skill-logger-plugin] ${skillName}@${version} \u4E0B\u8F7D\u5305\u65E0\u7248\u672C\u53F7\uFF0C\u653E\u5F03\u8986\u76D6`);
|
|
250
260
|
return;
|
|
251
261
|
}
|
|
262
|
+
const metaPath = path3.join(srcRoot, ".meta.json");
|
|
263
|
+
if (!await this.exists(metaPath)) {
|
|
264
|
+
this.debug(`[skill-logger-plugin] \u4E3A ${skillName}@${version} \u81EA\u52A8\u751F\u6210 .meta.json`);
|
|
265
|
+
await fs3.writeFile(metaPath, JSON.stringify({
|
|
266
|
+
ownerId: "CMS_COMPAT",
|
|
267
|
+
slug: skillName,
|
|
268
|
+
version: packageVersion,
|
|
269
|
+
publishedAt: Date.now()
|
|
270
|
+
}, null, 2));
|
|
271
|
+
}
|
|
252
272
|
for (const target of targets) {
|
|
253
273
|
try {
|
|
254
274
|
await this.replaceDir(srcRoot, target);
|
|
@@ -258,7 +278,7 @@ var SkillUpdater = class {
|
|
|
258
278
|
}
|
|
259
279
|
}
|
|
260
280
|
} finally {
|
|
261
|
-
await
|
|
281
|
+
await fs3.rm(work, { recursive: true, force: true });
|
|
262
282
|
}
|
|
263
283
|
}
|
|
264
284
|
/** POST 下载接口取包 URL。返回 undefined 表示拿不到(调用方放弃本次更新)。 */
|
|
@@ -287,14 +307,14 @@ var SkillUpdater = class {
|
|
|
287
307
|
if (depth > 2) return void 0;
|
|
288
308
|
let entries;
|
|
289
309
|
try {
|
|
290
|
-
entries = await
|
|
310
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
291
311
|
} catch {
|
|
292
312
|
return void 0;
|
|
293
313
|
}
|
|
294
314
|
if (entries.some((e) => e.isFile() && e.name === "SKILL.md")) return dir;
|
|
295
315
|
for (const e of entries) {
|
|
296
316
|
if (e.isDirectory()) {
|
|
297
|
-
const found = await this.locateSkillRoot(
|
|
317
|
+
const found = await this.locateSkillRoot(path3.join(dir, e.name), depth + 1);
|
|
298
318
|
if (found) return found;
|
|
299
319
|
}
|
|
300
320
|
}
|
|
@@ -306,30 +326,30 @@ var SkillUpdater = class {
|
|
|
306
326
|
* 换入失败时把旧目录还原,避免目标被清空。过程中的临时/旧目录都是隐藏且即时清理,不保留 .bak。
|
|
307
327
|
*/
|
|
308
328
|
async replaceDir(src, target) {
|
|
309
|
-
const parent =
|
|
310
|
-
await
|
|
329
|
+
const parent = path3.dirname(target);
|
|
330
|
+
await fs3.mkdir(parent, { recursive: true });
|
|
311
331
|
const tag = `${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
312
|
-
const stage =
|
|
313
|
-
const old =
|
|
314
|
-
await
|
|
315
|
-
await
|
|
332
|
+
const stage = path3.join(parent, `.${path3.basename(target)}.new-${tag}`);
|
|
333
|
+
const old = path3.join(parent, `.${path3.basename(target)}.old-${tag}`);
|
|
334
|
+
await fs3.rm(stage, { recursive: true, force: true });
|
|
335
|
+
await fs3.cp(src, stage, { recursive: true });
|
|
316
336
|
const hadTarget = await this.exists(target);
|
|
317
|
-
if (hadTarget) await
|
|
337
|
+
if (hadTarget) await fs3.rename(target, old);
|
|
318
338
|
try {
|
|
319
|
-
await
|
|
339
|
+
await fs3.rename(stage, target);
|
|
320
340
|
} catch (err) {
|
|
321
|
-
if (hadTarget) await
|
|
341
|
+
if (hadTarget) await fs3.rename(old, target).catch(() => {
|
|
322
342
|
});
|
|
323
|
-
await
|
|
343
|
+
await fs3.rm(stage, { recursive: true, force: true }).catch(() => {
|
|
324
344
|
});
|
|
325
345
|
throw err;
|
|
326
346
|
}
|
|
327
|
-
await
|
|
347
|
+
await fs3.rm(old, { recursive: true, force: true }).catch(() => {
|
|
328
348
|
});
|
|
329
349
|
}
|
|
330
350
|
async exists(p) {
|
|
331
351
|
try {
|
|
332
|
-
await
|
|
352
|
+
await fs3.access(p);
|
|
333
353
|
return true;
|
|
334
354
|
} catch {
|
|
335
355
|
return false;
|
|
@@ -349,38 +369,37 @@ var SkillUpdater = class {
|
|
|
349
369
|
url = dl.url;
|
|
350
370
|
version = dl.version || version;
|
|
351
371
|
}
|
|
352
|
-
const targetSkillPath =
|
|
372
|
+
const targetSkillPath = path3.join(targetDir, code);
|
|
353
373
|
if (!force && await this.exists(targetSkillPath)) {
|
|
354
374
|
return { success: false, message: `\u6280\u80FD ${code} \u5DF2\u5B58\u5728\u4E8E\u76EE\u6807\u76EE\u5F55` };
|
|
355
375
|
}
|
|
356
|
-
const work =
|
|
357
|
-
await
|
|
376
|
+
const work = path3.join(this.tmpDir, `slp-manual-${randomUUID()}`);
|
|
377
|
+
await fs3.mkdir(work, { recursive: true });
|
|
358
378
|
try {
|
|
359
|
-
const zipPath =
|
|
379
|
+
const zipPath = path3.join(work, "pkg.zip");
|
|
360
380
|
const res = await this.fetchImpl(url);
|
|
361
381
|
if (!res.ok) {
|
|
362
382
|
return { success: false, message: `\u4E0B\u8F7D\u5931\u8D25: HTTP ${res.status}` };
|
|
363
383
|
}
|
|
364
384
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
365
|
-
await
|
|
366
|
-
const staging =
|
|
385
|
+
await fs3.writeFile(zipPath, buf);
|
|
386
|
+
const staging = path3.join(work, "staging");
|
|
367
387
|
await this.unzip(zipPath, staging);
|
|
368
388
|
const srcRoot = await this.locateSkillRoot(staging, 0);
|
|
369
389
|
if (!srcRoot) {
|
|
370
390
|
return { success: false, message: `\u4E0B\u8F7D\u5305\u5185\u672A\u627E\u5230 SKILL.md\uFF0C\u975E\u6CD5\u7684\u6280\u80FD\u5305\u7ED3\u6784` };
|
|
371
391
|
}
|
|
372
|
-
const metaPath =
|
|
392
|
+
const metaPath = path3.join(srcRoot, ".meta.json");
|
|
373
393
|
if (!await this.exists(metaPath)) {
|
|
374
394
|
let parsedVersion = version || "unknown";
|
|
375
395
|
if (!version) {
|
|
376
396
|
try {
|
|
377
|
-
|
|
378
|
-
parsedVersion = parseSkillVersion(newSkillMd) || "unknown";
|
|
397
|
+
parsedVersion = await readSkillVersion(srcRoot) || "unknown";
|
|
379
398
|
} catch {
|
|
380
399
|
}
|
|
381
400
|
}
|
|
382
401
|
this.debug(`[skill-logger-plugin] \u4E3A\u624B\u52A8\u5B89\u88C5\u7684 ${code}@${parsedVersion} \u81EA\u52A8\u751F\u6210 .meta.json`);
|
|
383
|
-
await
|
|
402
|
+
await fs3.writeFile(metaPath, JSON.stringify({
|
|
384
403
|
ownerId: "CMS_COMPAT",
|
|
385
404
|
slug: code,
|
|
386
405
|
version: parsedVersion,
|
|
@@ -392,19 +411,19 @@ var SkillUpdater = class {
|
|
|
392
411
|
} catch (err) {
|
|
393
412
|
return { success: false, message: `\u6267\u884C\u51FA\u9519: ${err.message}` };
|
|
394
413
|
} finally {
|
|
395
|
-
await
|
|
414
|
+
await fs3.rm(work, { recursive: true, force: true }).catch(() => {
|
|
396
415
|
});
|
|
397
416
|
}
|
|
398
417
|
}
|
|
399
418
|
};
|
|
400
419
|
|
|
401
420
|
// src/config-sync.ts
|
|
402
|
-
import
|
|
403
|
-
import
|
|
421
|
+
import fs4 from "node:fs/promises";
|
|
422
|
+
import path5 from "node:path";
|
|
404
423
|
import { fileURLToPath } from "node:url";
|
|
405
424
|
|
|
406
425
|
// src/matcher.ts
|
|
407
|
-
import
|
|
426
|
+
import path4 from "node:path";
|
|
408
427
|
var INTERPRETERS = /* @__PURE__ */ new Set([
|
|
409
428
|
"python",
|
|
410
429
|
"python3",
|
|
@@ -452,7 +471,7 @@ function buildIndex(configs) {
|
|
|
452
471
|
};
|
|
453
472
|
switch (fn.match.type) {
|
|
454
473
|
case "script":
|
|
455
|
-
pushBucket(index.scriptByBasename,
|
|
474
|
+
pushBucket(index.scriptByBasename, path4.basename(fn.match.script), indexed);
|
|
456
475
|
break;
|
|
457
476
|
case "command":
|
|
458
477
|
pushBucket(index.commandByHead, fn.match.command, indexed);
|
|
@@ -536,7 +555,7 @@ function commandHead(tokens) {
|
|
|
536
555
|
for (let i = 0; i < tokens.length; i++) {
|
|
537
556
|
const t = tokens[i];
|
|
538
557
|
if (/^[A-Za-z_][\w]*=/.test(t)) continue;
|
|
539
|
-
const base =
|
|
558
|
+
const base = path4.basename(t);
|
|
540
559
|
if (INTERPRETERS.has(base)) continue;
|
|
541
560
|
return base;
|
|
542
561
|
}
|
|
@@ -640,7 +659,7 @@ function match(call, activeSkills, index) {
|
|
|
640
659
|
const tokens = tokenize(command);
|
|
641
660
|
const flags = parseFlags(tokens);
|
|
642
661
|
for (const tok of tokens) {
|
|
643
|
-
const fns = index.scriptByBasename.get(
|
|
662
|
+
const fns = index.scriptByBasename.get(path4.basename(tok));
|
|
644
663
|
if (!fns) continue;
|
|
645
664
|
for (const f of fns) {
|
|
646
665
|
const rule = f.rule;
|
|
@@ -818,7 +837,7 @@ var ConfigSync = class {
|
|
|
818
837
|
/** 从磁盘加载已缓存的同步状态并重建索引(gateway 启动时调一次)。 */
|
|
819
838
|
async load() {
|
|
820
839
|
try {
|
|
821
|
-
const raw = await
|
|
840
|
+
const raw = await fs4.readFile(this.paths.syncStatePath, "utf-8");
|
|
822
841
|
const state = JSON.parse(raw);
|
|
823
842
|
if (state.skills) {
|
|
824
843
|
for (const [name, entry] of Object.entries(state.skills)) {
|
|
@@ -860,16 +879,16 @@ var ConfigSync = class {
|
|
|
860
879
|
if (depth > MAX_SCAN_DEPTH) return;
|
|
861
880
|
let entries;
|
|
862
881
|
try {
|
|
863
|
-
entries = await
|
|
882
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
864
883
|
} catch {
|
|
865
884
|
return;
|
|
866
885
|
}
|
|
867
886
|
for (const e of entries) {
|
|
868
887
|
if (e.isDirectory()) {
|
|
869
888
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
870
|
-
await walk(
|
|
889
|
+
await walk(path5.join(dir, e.name), depth + 1);
|
|
871
890
|
} else if (e.name === "SKILL.md") {
|
|
872
|
-
const skillMd =
|
|
891
|
+
const skillMd = path5.join(dir, e.name);
|
|
873
892
|
const skill = await this.readSkill(dir, skillMd);
|
|
874
893
|
if (skill) {
|
|
875
894
|
out.push(skill);
|
|
@@ -912,10 +931,10 @@ var ConfigSync = class {
|
|
|
912
931
|
}
|
|
913
932
|
async readSkill(rootDir, skillMdPath) {
|
|
914
933
|
try {
|
|
915
|
-
const content = await
|
|
934
|
+
const content = await fs4.readFile(skillMdPath, "utf-8");
|
|
916
935
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content)?.[1] ?? "";
|
|
917
|
-
const name = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim() ||
|
|
918
|
-
const version =
|
|
936
|
+
const name = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim() || path5.basename(rootDir);
|
|
937
|
+
const version = await readSkillVersion(rootDir, content);
|
|
919
938
|
const signature = await this.computeSignature(rootDir, skillMdPath, version);
|
|
920
939
|
return { name, version, rootDir, signature };
|
|
921
940
|
} catch {
|
|
@@ -926,16 +945,16 @@ var ConfigSync = class {
|
|
|
926
945
|
async computeSignature(rootDir, skillMdPath, version) {
|
|
927
946
|
const parts = [version ?? ""];
|
|
928
947
|
try {
|
|
929
|
-
const st = await
|
|
948
|
+
const st = await fs4.stat(skillMdPath);
|
|
930
949
|
parts.push(`md:${st.mtimeMs}:${st.size}`);
|
|
931
950
|
} catch {
|
|
932
951
|
}
|
|
933
952
|
try {
|
|
934
|
-
const scriptsDir =
|
|
935
|
-
const entries = await
|
|
953
|
+
const scriptsDir = path5.join(rootDir, "scripts");
|
|
954
|
+
const entries = await fs4.readdir(scriptsDir, { withFileTypes: true });
|
|
936
955
|
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
937
956
|
if (!e.isFile()) continue;
|
|
938
|
-
const st = await
|
|
957
|
+
const st = await fs4.stat(path5.join(scriptsDir, e.name));
|
|
939
958
|
parts.push(`s:${e.name}:${st.mtimeMs}:${st.size}`);
|
|
940
959
|
}
|
|
941
960
|
} catch {
|
|
@@ -1018,7 +1037,7 @@ var ConfigSync = class {
|
|
|
1018
1037
|
/** 懒触发:仅检查某个被触发的 skill,缺配置/签名变才拉。`ident` 可为规范名或目录名。 */
|
|
1019
1038
|
async lazyCheck(ident) {
|
|
1020
1039
|
try {
|
|
1021
|
-
const find = (list) => list.find((x) => x.name === ident ||
|
|
1040
|
+
const find = (list) => list.find((x) => x.name === ident || path5.basename(x.rootDir) === ident);
|
|
1022
1041
|
let s = find(await this.scanInstalledSkillsCached());
|
|
1023
1042
|
if (!s) {
|
|
1024
1043
|
const fresh = await this.scanInstalledSkills();
|
|
@@ -1094,8 +1113,8 @@ var ConfigSync = class {
|
|
|
1094
1113
|
async loadSampleConfigs() {
|
|
1095
1114
|
if (this.sampleConfigs) return this.sampleConfigs;
|
|
1096
1115
|
try {
|
|
1097
|
-
const here =
|
|
1098
|
-
const raw = await
|
|
1116
|
+
const here = path5.dirname(fileURLToPath(import.meta.url));
|
|
1117
|
+
const raw = await fs4.readFile(path5.join(here, "sample-config.json"), "utf-8");
|
|
1099
1118
|
this.sampleConfigs = JSON.parse(raw).configs;
|
|
1100
1119
|
} catch {
|
|
1101
1120
|
this.sampleConfigs = [];
|
|
@@ -1117,10 +1136,10 @@ var ConfigSync = class {
|
|
|
1117
1136
|
for (const [name, copies] of this.installations) {
|
|
1118
1137
|
state.installations[name] = copies.map((c) => ({ rootDir: c.rootDir, version: c.version }));
|
|
1119
1138
|
}
|
|
1120
|
-
await
|
|
1139
|
+
await fs4.mkdir(path5.dirname(this.paths.syncStatePath), { recursive: true });
|
|
1121
1140
|
const tmp = `${this.paths.syncStatePath}.tmp-${process.pid}-${Date.now()}`;
|
|
1122
|
-
await
|
|
1123
|
-
await
|
|
1141
|
+
await fs4.writeFile(tmp, JSON.stringify(state));
|
|
1142
|
+
await fs4.rename(tmp, this.paths.syncStatePath);
|
|
1124
1143
|
} catch (err) {
|
|
1125
1144
|
console.warn("[skill-logger-plugin] \u6301\u4E45\u5316\u540C\u6B65\u72B6\u6001\u5931\u8D25", err);
|
|
1126
1145
|
}
|
|
@@ -1128,8 +1147,8 @@ var ConfigSync = class {
|
|
|
1128
1147
|
};
|
|
1129
1148
|
|
|
1130
1149
|
// src/reporter.ts
|
|
1131
|
-
import
|
|
1132
|
-
import
|
|
1150
|
+
import fs5 from "node:fs/promises";
|
|
1151
|
+
import path6 from "node:path";
|
|
1133
1152
|
|
|
1134
1153
|
// src/identity.ts
|
|
1135
1154
|
import os3 from "node:os";
|
|
@@ -1198,9 +1217,9 @@ var Reporter = class {
|
|
|
1198
1217
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1199
1218
|
const logLine = `[${ts}] [${level}] [skill-logger-plugin] ${msg}
|
|
1200
1219
|
`;
|
|
1201
|
-
const logDir =
|
|
1202
|
-
await
|
|
1203
|
-
await
|
|
1220
|
+
const logDir = path6.dirname(this.paths.eventsLogPath);
|
|
1221
|
+
await fs5.mkdir(logDir, { recursive: true });
|
|
1222
|
+
await fs5.appendFile(path6.join(logDir, "skill-logger.err.log"), logLine);
|
|
1204
1223
|
if (level === "INFO" && this.isDebug) {
|
|
1205
1224
|
console.log("[skill-logger-plugin/reporter]", ...args);
|
|
1206
1225
|
} else if (level === "WARN" || level === "ERROR") {
|
|
@@ -1233,8 +1252,8 @@ var Reporter = class {
|
|
|
1233
1252
|
} catch {
|
|
1234
1253
|
return;
|
|
1235
1254
|
}
|
|
1236
|
-
await
|
|
1237
|
-
await
|
|
1255
|
+
await fs5.mkdir(path6.dirname(this.paths.eventsLogPath), { recursive: true });
|
|
1256
|
+
await fs5.appendFile(this.paths.eventsLogPath, line + "\n");
|
|
1238
1257
|
} catch (err) {
|
|
1239
1258
|
void this.writeFallbackLog("ERROR", "\u5199\u4E8B\u4EF6\u65E5\u5FD7\u5931\u8D25", this.paths.eventsLogPath, err);
|
|
1240
1259
|
}
|
|
@@ -1304,19 +1323,19 @@ var Reporter = class {
|
|
|
1304
1323
|
void this.writeFallbackLog("WARN", "flush skipped: reportBaseUrl \u6CA1\u6709\u914D\u7F6E\uFF01\u8BF7\u68C0\u67E5 openclaw.json \u63D2\u4EF6\u914D\u7F6E\u662F\u5426\u6B63\u786E\u52A0\u8F7D\u3002");
|
|
1305
1324
|
return;
|
|
1306
1325
|
}
|
|
1307
|
-
const logDir =
|
|
1326
|
+
const logDir = path6.dirname(this.paths.eventsLogPath);
|
|
1308
1327
|
try {
|
|
1309
|
-
await
|
|
1328
|
+
await fs5.access(this.paths.eventsLogPath);
|
|
1310
1329
|
const timestamp = Date.now();
|
|
1311
|
-
const rotatedPath =
|
|
1312
|
-
await
|
|
1313
|
-
this.debug(`Rotated active log to ${
|
|
1330
|
+
const rotatedPath = path6.join(logDir, `events.${timestamp}.jsonl`);
|
|
1331
|
+
await fs5.rename(this.paths.eventsLogPath, rotatedPath);
|
|
1332
|
+
this.debug(`Rotated active log to ${path6.basename(rotatedPath)}`);
|
|
1314
1333
|
} catch {
|
|
1315
1334
|
}
|
|
1316
1335
|
let files = [];
|
|
1317
1336
|
try {
|
|
1318
|
-
const dirEntries = await
|
|
1319
|
-
files = dirEntries.filter((f) => f.startsWith("events.") && f.endsWith(".jsonl") && f !== "events.jsonl").map((f) =>
|
|
1337
|
+
const dirEntries = await fs5.readdir(logDir);
|
|
1338
|
+
files = dirEntries.filter((f) => f.startsWith("events.") && f.endsWith(".jsonl") && f !== "events.jsonl").map((f) => path6.join(logDir, f));
|
|
1320
1339
|
} catch {
|
|
1321
1340
|
return;
|
|
1322
1341
|
}
|
|
@@ -1329,10 +1348,10 @@ var Reporter = class {
|
|
|
1329
1348
|
const url = config.reportBaseUrl.replace(/\/$/, "") + "/skill_report/batch";
|
|
1330
1349
|
for (const filePath of files) {
|
|
1331
1350
|
try {
|
|
1332
|
-
const content = await
|
|
1351
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
1333
1352
|
const lines = this.linesOf(content);
|
|
1334
1353
|
if (lines.length === 0) {
|
|
1335
|
-
await
|
|
1354
|
+
await fs5.unlink(filePath);
|
|
1336
1355
|
continue;
|
|
1337
1356
|
}
|
|
1338
1357
|
let cursor = 0;
|
|
@@ -1362,7 +1381,7 @@ var Reporter = class {
|
|
|
1362
1381
|
const res = await this.fetchImpl(url, { method: "POST", headers, body });
|
|
1363
1382
|
if (!res.ok) {
|
|
1364
1383
|
const errBody = await res.text().catch(() => "\u65E0\u6CD5\u8BFB\u53D6\u54CD\u5E94\u4F53");
|
|
1365
|
-
await this.writeFallbackLog("ERROR", `\u6279\u91CF\u4E0A\u62A5\u5931\u8D25 (\u6587\u4EF6 ${
|
|
1384
|
+
await this.writeFallbackLog("ERROR", `\u6279\u91CF\u4E0A\u62A5\u5931\u8D25 (\u6587\u4EF6 ${path6.basename(filePath)}), HTTP`, res.status, "\u670D\u52A1\u7AEF\u8FD4\u56DE\u4FE1\u606F:", errBody);
|
|
1366
1385
|
if (res.status === 400 || res.status === 413 || res.status === 422) {
|
|
1367
1386
|
await this.writeFallbackLog("WARN", `\u62A5\u6587\u88AB\u670D\u52A1\u5668\u6C38\u4E45\u62D2\u7EDD\uFF0C\u4E22\u5F03\u8BE5\u6279\u6B21\u4EE5\u91CA\u653E\u961F\u5217`);
|
|
1368
1387
|
cursor += slice.length;
|
|
@@ -1371,17 +1390,17 @@ var Reporter = class {
|
|
|
1371
1390
|
allSuccess = false;
|
|
1372
1391
|
break;
|
|
1373
1392
|
}
|
|
1374
|
-
this.debug(`Successfully reported batch of ${events.length} events from ${
|
|
1393
|
+
this.debug(`Successfully reported batch of ${events.length} events from ${path6.basename(filePath)}`);
|
|
1375
1394
|
cursor += slice.length;
|
|
1376
1395
|
}
|
|
1377
1396
|
if (allSuccess) {
|
|
1378
|
-
await
|
|
1379
|
-
this.debug(`Deleted fully processed file: ${
|
|
1397
|
+
await fs5.unlink(filePath);
|
|
1398
|
+
this.debug(`Deleted fully processed file: ${path6.basename(filePath)}`);
|
|
1380
1399
|
} else {
|
|
1381
|
-
this.debug(`File ${
|
|
1400
|
+
this.debug(`File ${path6.basename(filePath)} partially failed. Keeping it for next flush.`);
|
|
1382
1401
|
}
|
|
1383
1402
|
} catch (err) {
|
|
1384
|
-
await this.writeFallbackLog("ERROR", `\u5904\u7406\u6587\u4EF6 ${
|
|
1403
|
+
await this.writeFallbackLog("ERROR", `\u5904\u7406\u6587\u4EF6 ${path6.basename(filePath)} \u5F02\u5E38:`, err);
|
|
1385
1404
|
}
|
|
1386
1405
|
}
|
|
1387
1406
|
} catch (err) {
|
|
@@ -1394,8 +1413,8 @@ var Reporter = class {
|
|
|
1394
1413
|
|
|
1395
1414
|
// src/ws-client.ts
|
|
1396
1415
|
import WebSocket from "ws";
|
|
1397
|
-
import
|
|
1398
|
-
import
|
|
1416
|
+
import path7 from "path";
|
|
1417
|
+
import fs6 from "fs/promises";
|
|
1399
1418
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
1400
1419
|
var HEARTBEAT_ACK_TIMEOUT_MS = 75e3;
|
|
1401
1420
|
var AGENT_SCAN_INTERVAL_MS = 3 * 60 * 1e3;
|
|
@@ -1425,10 +1444,10 @@ var GatewayWsClient = class {
|
|
|
1425
1444
|
}
|
|
1426
1445
|
}
|
|
1427
1446
|
logLine += "\n";
|
|
1428
|
-
const logsDir =
|
|
1429
|
-
|
|
1430
|
-
const logPath =
|
|
1431
|
-
|
|
1447
|
+
const logsDir = path7.join(openclawHome(), "logs");
|
|
1448
|
+
fs6.mkdir(logsDir, { recursive: true }).then(() => {
|
|
1449
|
+
const logPath = path7.join(logsDir, "skill-logger.err");
|
|
1450
|
+
fs6.appendFile(logPath, logLine).catch(() => {
|
|
1432
1451
|
});
|
|
1433
1452
|
}).catch(() => {
|
|
1434
1453
|
});
|
|
@@ -1532,7 +1551,7 @@ var GatewayWsClient = class {
|
|
|
1532
1551
|
const rootPath = openclawHome();
|
|
1533
1552
|
let entries = [];
|
|
1534
1553
|
try {
|
|
1535
|
-
entries = await
|
|
1554
|
+
entries = await fs6.readdir(rootPath);
|
|
1536
1555
|
} catch (e) {
|
|
1537
1556
|
this.currentAgentIds = /* @__PURE__ */ new Set();
|
|
1538
1557
|
this.appendLogToFile("WARN", "AgentScan", "OpenClaw home is not readable; reporting empty agent list", e);
|
|
@@ -1543,7 +1562,7 @@ var GatewayWsClient = class {
|
|
|
1543
1562
|
for (const entry of entries) {
|
|
1544
1563
|
if (entry.startsWith("workspace-assistant-")) {
|
|
1545
1564
|
const suffix = entry.replace("workspace-assistant-", "").trim();
|
|
1546
|
-
if (suffix && suffix ===
|
|
1565
|
+
if (suffix && suffix === path7.basename(suffix) && !suffix.startsWith(".")) {
|
|
1547
1566
|
newAgentIds.add(`assistant-${suffix}`);
|
|
1548
1567
|
}
|
|
1549
1568
|
}
|
|
@@ -1689,10 +1708,10 @@ var GatewayWsClient = class {
|
|
|
1689
1708
|
this.appendLogToFile("WARN", "Command", `Message dropped: missing action or userId`, msg);
|
|
1690
1709
|
return;
|
|
1691
1710
|
}
|
|
1692
|
-
const safeUserId =
|
|
1693
|
-
const safeCode = code ?
|
|
1711
|
+
const safeUserId = path7.basename(userId);
|
|
1712
|
+
const safeCode = code ? path7.basename(code) : void 0;
|
|
1694
1713
|
const pureId = safeUserId.replace(/^assistant-/, "");
|
|
1695
|
-
const targetDir =
|
|
1714
|
+
const targetDir = path7.join(openclawHome(), `workspace-assistant-${pureId}`, "skills");
|
|
1696
1715
|
try {
|
|
1697
1716
|
if (action === "INSTALL_SKILL") {
|
|
1698
1717
|
if (!safeCode) throw new Error("Missing code parameter");
|
|
@@ -1710,14 +1729,14 @@ var GatewayWsClient = class {
|
|
|
1710
1729
|
if (!safeCode) throw new Error("Missing code parameter");
|
|
1711
1730
|
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL for user ${userId}, code: ${safeCode}`);
|
|
1712
1731
|
this.appendLogToFile("INFO", "Command", `UNINSTALL_SKILL received`, { userId, code: safeCode });
|
|
1713
|
-
const skillPath =
|
|
1714
|
-
await
|
|
1732
|
+
const skillPath = path7.join(targetDir, safeCode);
|
|
1733
|
+
await fs6.rm(skillPath, { recursive: true, force: true });
|
|
1715
1734
|
this.reply(replyId, { success: true, message: `Skill ${safeCode} removed`, action });
|
|
1716
1735
|
} else if (action === "LIST_SKILLS") {
|
|
1717
1736
|
let list = [];
|
|
1718
1737
|
let targetDirExists = false;
|
|
1719
1738
|
try {
|
|
1720
|
-
const targetStat = await
|
|
1739
|
+
const targetStat = await fs6.stat(targetDir);
|
|
1721
1740
|
targetDirExists = targetStat.isDirectory();
|
|
1722
1741
|
} catch (err) {
|
|
1723
1742
|
if (err?.code !== "ENOENT") throw err;
|
|
@@ -1725,18 +1744,18 @@ var GatewayWsClient = class {
|
|
|
1725
1744
|
if (!targetDirExists) {
|
|
1726
1745
|
throw new Error(`Target skills directory does not exist: ${targetDir}`);
|
|
1727
1746
|
}
|
|
1728
|
-
const entries = await
|
|
1747
|
+
const entries = await fs6.readdir(targetDir, { withFileTypes: true });
|
|
1729
1748
|
const dirs = entries.filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith("."));
|
|
1730
1749
|
for (const e of dirs) {
|
|
1731
|
-
const skillDir =
|
|
1732
|
-
const skillMdPath =
|
|
1750
|
+
const skillDir = path7.join(targetDir, e.name);
|
|
1751
|
+
const skillMdPath = path7.join(skillDir, "SKILL.md");
|
|
1733
1752
|
try {
|
|
1734
|
-
const stat = await
|
|
1753
|
+
const stat = await fs6.stat(skillMdPath);
|
|
1735
1754
|
if (!stat.isFile()) continue;
|
|
1736
1755
|
} catch (err) {
|
|
1737
1756
|
continue;
|
|
1738
1757
|
}
|
|
1739
|
-
const metaPath =
|
|
1758
|
+
const metaPath = path7.join(skillDir, ".meta.json");
|
|
1740
1759
|
let isPlatform = false;
|
|
1741
1760
|
let isBuiltIn = e.isSymbolicLink();
|
|
1742
1761
|
let metaData = null;
|
|
@@ -1744,12 +1763,10 @@ var GatewayWsClient = class {
|
|
|
1744
1763
|
let description = "";
|
|
1745
1764
|
let skillVersion = "";
|
|
1746
1765
|
try {
|
|
1747
|
-
const mdContent = await
|
|
1766
|
+
const mdContent = await fs6.readFile(skillMdPath, "utf8");
|
|
1748
1767
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdContent)?.[1] ?? "";
|
|
1749
1768
|
const parsedName = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim();
|
|
1750
1769
|
if (parsedName) name = parsedName;
|
|
1751
|
-
const parsedVersion = /(^|\n)version:\s*(.+)/i.exec(fm)?.[2]?.trim();
|
|
1752
|
-
if (parsedVersion) skillVersion = parsedVersion.replace(/^['"]|['"]$/g, "").replace(/\s+#.*$/, "");
|
|
1753
1770
|
const descMatch = /(^|\n)description:\s*(?:>\s*\n\s*)?(.*?)(?=\n[a-z]+:|\n---|$)/is.exec(fm);
|
|
1754
1771
|
if (descMatch && descMatch[2]) {
|
|
1755
1772
|
description = descMatch[2].replace(/\n\s+/g, " ").trim();
|
|
@@ -1757,7 +1774,7 @@ var GatewayWsClient = class {
|
|
|
1757
1774
|
} catch (err) {
|
|
1758
1775
|
}
|
|
1759
1776
|
try {
|
|
1760
|
-
const metaContent = await
|
|
1777
|
+
const metaContent = await fs6.readFile(metaPath, "utf8");
|
|
1761
1778
|
const parsed = JSON.parse(metaContent);
|
|
1762
1779
|
if (parsed) {
|
|
1763
1780
|
if (parsed.ownerId === "CMS" || parsed.ownerId === "CMS_COMPAT") isPlatform = true;
|
|
@@ -1766,12 +1783,14 @@ var GatewayWsClient = class {
|
|
|
1766
1783
|
}
|
|
1767
1784
|
} catch (err) {
|
|
1768
1785
|
}
|
|
1786
|
+
const resolvedVersion = await readSkillVersion(skillDir);
|
|
1787
|
+
if (resolvedVersion) skillVersion = resolvedVersion;
|
|
1769
1788
|
if (isPlatform) {
|
|
1770
1789
|
list.push({
|
|
1771
1790
|
code: e.name,
|
|
1772
1791
|
isPlatform: true,
|
|
1773
1792
|
isBuiltIn,
|
|
1774
|
-
version:
|
|
1793
|
+
version: skillVersion,
|
|
1775
1794
|
name,
|
|
1776
1795
|
description,
|
|
1777
1796
|
publishedAt: metaData?.publishedAt
|
|
@@ -1840,7 +1859,7 @@ var GatewayWsClient = class {
|
|
|
1840
1859
|
};
|
|
1841
1860
|
|
|
1842
1861
|
// src/hooks.ts
|
|
1843
|
-
import
|
|
1862
|
+
import path8 from "node:path";
|
|
1844
1863
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1845
1864
|
var PENDING_TTL_MS = 30 * 60 * 1e3;
|
|
1846
1865
|
var PENDING_MAX = 5e3;
|
|
@@ -1849,12 +1868,12 @@ function toMySQLDateTime(d) {
|
|
|
1849
1868
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1850
1869
|
}
|
|
1851
1870
|
function isSkillMdReadPath(filePath) {
|
|
1852
|
-
return
|
|
1871
|
+
return path8.basename(filePath) === "SKILL.md";
|
|
1853
1872
|
}
|
|
1854
1873
|
function extractAppKey(event, ctx) {
|
|
1855
1874
|
try {
|
|
1856
1875
|
const content = JSON.stringify({ event, ctx });
|
|
1857
|
-
const match2 = content.match(/(?:app[-_\s]?key)(?:[^\n\r]{0,40}?(?:[:=:]|是|为|\bis\b|\bvalue\b))?[^a-zA-Z0-9_-]*([a-zA-Z0-9_-]{
|
|
1876
|
+
const match2 = content.match(/(?:app[-_\s]?key)[*]*(?:[^\n\r,{}]{0,40}?(?:[:=:]|是|为|\bis\b|\bvalue\b))?[^a-zA-Z0-9_,}-]*([a-zA-Z0-9_-]{12,})/i);
|
|
1858
1877
|
return match2 ? match2[1] : void 0;
|
|
1859
1878
|
} catch {
|
|
1860
1879
|
return void 0;
|
|
@@ -1971,8 +1990,8 @@ var Hooks = class {
|
|
|
1971
1990
|
if (toolName === "read") {
|
|
1972
1991
|
const filePath = params.path ?? params.file_path ?? "";
|
|
1973
1992
|
if (!isSkillMdReadPath(filePath)) return;
|
|
1974
|
-
const rootDir =
|
|
1975
|
-
const skillName = this.configSync.resolveSkillName(rootDir) ||
|
|
1993
|
+
const rootDir = path8.dirname(filePath);
|
|
1994
|
+
const skillName = this.configSync.resolveSkillName(rootDir) || path8.basename(rootDir);
|
|
1976
1995
|
if (!skillName) return;
|
|
1977
1996
|
this.debug(`Intercepted 'read' for SKILL.md. Attributed to skill: ${skillName}`);
|
|
1978
1997
|
this.recordTrigger(skillName, "inline", "read", ctx, appKey);
|
|
@@ -2153,9 +2172,9 @@ var definition = {
|
|
|
2153
2172
|
register(api) {
|
|
2154
2173
|
let pkgVersion = "unknown";
|
|
2155
2174
|
try {
|
|
2156
|
-
const dir =
|
|
2157
|
-
const pkgPath =
|
|
2158
|
-
const pkg = JSON.parse(
|
|
2175
|
+
const dir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
2176
|
+
const pkgPath = path9.join(dir, "..", "package.json");
|
|
2177
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
2159
2178
|
if (pkg.version) pkgVersion = pkg.version;
|
|
2160
2179
|
} catch {
|
|
2161
2180
|
}
|
package/package.json
CHANGED
package/src/config-sync.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { type PluginPaths, resolveAgentSkillDirs, openclawHome } from "./paths.t
|
|
|
18
18
|
import { buildIndex, emptyIndex, type MatchIndex } from "./matcher.ts";
|
|
19
19
|
import { defaultFetch } from "./http.ts";
|
|
20
20
|
import { isOutdated } from "./semver.ts";
|
|
21
|
-
import { parseSkillVersion } from "./skill-version.ts";
|
|
21
|
+
import { parseSkillVersion, readSkillVersion } from "./skill-version.ts";
|
|
22
22
|
import type { OutdatedCopy } from "./updater.ts";
|
|
23
23
|
|
|
24
24
|
// 保持对外导出位置不变(历史测试从 config-sync 导入)。
|
|
@@ -242,7 +242,7 @@ export class ConfigSync {
|
|
|
242
242
|
const content = await fs.readFile(skillMdPath, "utf-8");
|
|
243
243
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content)?.[1] ?? "";
|
|
244
244
|
const name = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim() || path.basename(rootDir);
|
|
245
|
-
const version =
|
|
245
|
+
const version = await readSkillVersion(rootDir, content);
|
|
246
246
|
const signature = await this.computeSignature(rootDir, skillMdPath, version);
|
|
247
247
|
return { name, version, rootDir, signature };
|
|
248
248
|
} catch {
|
package/src/hooks.ts
CHANGED
|
@@ -49,8 +49,8 @@ export function isSkillMdReadPath(filePath: string): boolean {
|
|
|
49
49
|
function extractAppKey(event: HookEvent, ctx: HookCtx): string | undefined {
|
|
50
50
|
try {
|
|
51
51
|
const content = JSON.stringify({ event, ctx });
|
|
52
|
-
// 终极增强正则:兼容
|
|
53
|
-
const match = content.match(/(?:app[-_\s]?key)(?:[^\n\r]{0,40}?(?:[:=:]|是|为|\bis\b|\bvalue\b))?[^a-zA-Z0-9_-]*([a-zA-Z0-9_-]{
|
|
52
|
+
// 终极增强正则:兼容 Markdown 的 **appKey**,防御 JSON Key 穿透,并且要求长度至少 12 位
|
|
53
|
+
const match = content.match(/(?:app[-_\s]?key)[*]*(?:[^\n\r,{}]{0,40}?(?:[:=:]|是|为|\bis\b|\bvalue\b))?[^a-zA-Z0-9_,}-]*([a-zA-Z0-9_-]{12,})/i);
|
|
54
54
|
return match ? match[1] : undefined;
|
|
55
55
|
} catch {
|
|
56
56
|
return undefined;
|
package/src/skill-version.ts
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 优先从 .meta.json 获取版本,如果没有,再回退到 SKILL.md 提取。
|
|
6
|
+
*/
|
|
7
|
+
export async function readSkillVersion(rootDir: string, skillMdContent?: string): Promise<string | undefined> {
|
|
8
|
+
// 1. 优先尝试从 .meta.json 中读取
|
|
9
|
+
try {
|
|
10
|
+
const metaPath = path.join(rootDir, ".meta.json");
|
|
11
|
+
const metaContent = await fs.readFile(metaPath, "utf-8");
|
|
12
|
+
const meta = JSON.parse(metaContent);
|
|
13
|
+
if (meta && typeof meta.version === "string" && meta.version.trim() && meta.version !== "unknown") {
|
|
14
|
+
return meta.version.trim();
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
// 忽略异常,继续 fallback
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 2. 降级到 SKILL.md 提取
|
|
21
|
+
if (!skillMdContent) {
|
|
22
|
+
try {
|
|
23
|
+
skillMdContent = await fs.readFile(path.join(rootDir, "SKILL.md"), "utf-8");
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return parseSkillVersion(skillMdContent);
|
|
30
|
+
}
|
|
31
|
+
|
|
1
32
|
/**
|
|
2
33
|
* 从 SKILL.md 文本中提取版本号(纯正则、不借助任何模型)。
|
|
3
34
|
*
|
|
4
35
|
* 兼容点:
|
|
5
|
-
* - 键名:英文 `version` / `Version`(大小写不敏感)、中文 `版本号` / `版本`;
|
|
36
|
+
* - 键名:英文 `version` / `Version`(大小写不敏感)、中文 `当前版本号` / `当前版本` / `版本号` / `版本`;
|
|
6
37
|
* - 冒号:半角 `:` 或全角 `:`;
|
|
7
38
|
* - 取值:去除包裹引号、行尾 ` # 注释`、首尾空白。
|
|
8
39
|
*
|
|
@@ -11,7 +42,7 @@
|
|
|
11
42
|
* 取不到返回 undefined(该 skill 不参与版本更新)。
|
|
12
43
|
*/
|
|
13
44
|
export function parseSkillVersion(content: string): string | undefined {
|
|
14
|
-
const m = /(?:^|\r?\n)[ \t]*(?:version
|
|
45
|
+
const m = /(?:^|\r?\n)[ \t]*(?:version|当前版本号|当前版本|版本号|版本)[ \t]*[::][ \t]*(.+)/i.exec(content);
|
|
15
46
|
if (!m) return undefined;
|
|
16
47
|
const v = m[1]
|
|
17
48
|
.replace(/\s+#.*$/, "") // 行尾注释
|
package/src/updater.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { execFile } from "node:child_process";
|
|
|
23
23
|
import { promisify } from "node:util";
|
|
24
24
|
import { randomUUID, createHash } from "node:crypto";
|
|
25
25
|
import type { PluginConfig } from "./types.ts";
|
|
26
|
-
import { parseSkillVersion } from "./skill-version.ts";
|
|
26
|
+
import { parseSkillVersion, readSkillVersion } from "./skill-version.ts";
|
|
27
27
|
|
|
28
28
|
const execFileAsync = promisify(execFile);
|
|
29
29
|
|
|
@@ -193,6 +193,19 @@ export class SkillUpdater {
|
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// 守卫:下载包必须能解析出版本号,否则不做覆盖(服务端要求:无版本号不更新)。
|
|
197
|
+
let packageVersion: string | undefined;
|
|
198
|
+
try {
|
|
199
|
+
packageVersion = await readSkillVersion(srcRoot);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.warn(`[skill-logger-plugin] ${skillName}@${version} 读取下载包版本号失败,放弃覆盖`, err);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!packageVersion) {
|
|
205
|
+
console.warn(`[skill-logger-plugin] ${skillName}@${version} 下载包无版本号,放弃覆盖`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
196
209
|
// [兼容处理] 缺少 .meta.json 时主动补齐
|
|
197
210
|
const metaPath = path.join(srcRoot, ".meta.json");
|
|
198
211
|
if (!(await this.exists(metaPath))) {
|
|
@@ -200,25 +213,11 @@ export class SkillUpdater {
|
|
|
200
213
|
await fs.writeFile(metaPath, JSON.stringify({
|
|
201
214
|
ownerId: 'CMS_COMPAT',
|
|
202
215
|
slug: skillName,
|
|
203
|
-
version:
|
|
216
|
+
version: packageVersion,
|
|
204
217
|
publishedAt: Date.now()
|
|
205
218
|
}, null, 2));
|
|
206
219
|
}
|
|
207
220
|
|
|
208
|
-
// 守卫:下载包的 SKILL.md 必须能解析出版本号,否则不做覆盖(服务端要求:无版本号不更新)。
|
|
209
|
-
let packageVersion: string | undefined;
|
|
210
|
-
try {
|
|
211
|
-
const newSkillMd = await fs.readFile(path.join(srcRoot, "SKILL.md"), "utf-8");
|
|
212
|
-
packageVersion = parseSkillVersion(newSkillMd);
|
|
213
|
-
} catch {
|
|
214
|
-
console.warn(`[skill-logger-plugin] ${skillName}@${version} 读取下载包 SKILL.md 失败,放弃覆盖`);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (!packageVersion) {
|
|
218
|
-
console.warn(`[skill-logger-plugin] ${skillName}@${version} 下载包 SKILL.md 无版本号,放弃覆盖`);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
221
|
for (const target of targets) {
|
|
223
222
|
try {
|
|
224
223
|
await this.replaceDir(srcRoot, target);
|
|
@@ -372,8 +371,7 @@ export class SkillUpdater {
|
|
|
372
371
|
let parsedVersion = version || "unknown";
|
|
373
372
|
if (!version) {
|
|
374
373
|
try {
|
|
375
|
-
|
|
376
|
-
parsedVersion = parseSkillVersion(newSkillMd) || "unknown";
|
|
374
|
+
parsedVersion = (await readSkillVersion(srcRoot)) || "unknown";
|
|
377
375
|
} catch {}
|
|
378
376
|
}
|
|
379
377
|
|
package/src/ws-client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from "path";
|
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import { SkillUpdater } from "./updater.ts";
|
|
5
5
|
import { openclawHome } from "./paths.ts";
|
|
6
|
+
import { readSkillVersion } from "./skill-version.ts";
|
|
6
7
|
|
|
7
8
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
8
9
|
const HEARTBEAT_ACK_TIMEOUT_MS = 75_000;
|
|
@@ -420,8 +421,6 @@ export class GatewayWsClient {
|
|
|
420
421
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdContent)?.[1] ?? "";
|
|
421
422
|
const parsedName = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim();
|
|
422
423
|
if (parsedName) name = parsedName;
|
|
423
|
-
const parsedVersion = /(^|\n)version:\s*(.+)/i.exec(fm)?.[2]?.trim();
|
|
424
|
-
if (parsedVersion) skillVersion = parsedVersion.replace(/^['"]|['"]$/g, '').replace(/\s+#.*$/, '');
|
|
425
424
|
|
|
426
425
|
const descMatch = /(^|\n)description:\s*(?:>\s*\n\s*)?(.*?)(?=\n[a-z]+:|\n---|$)/is.exec(fm);
|
|
427
426
|
if (descMatch && descMatch[2]) {
|
|
@@ -438,13 +437,16 @@ export class GatewayWsClient {
|
|
|
438
437
|
metaData = parsed;
|
|
439
438
|
}
|
|
440
439
|
} catch (err) {}
|
|
440
|
+
|
|
441
|
+
const resolvedVersion = await readSkillVersion(skillDir);
|
|
442
|
+
if (resolvedVersion) skillVersion = resolvedVersion;
|
|
441
443
|
|
|
442
444
|
if (isPlatform) {
|
|
443
445
|
list.push({
|
|
444
446
|
code: e.name,
|
|
445
447
|
isPlatform: true,
|
|
446
448
|
isBuiltIn: isBuiltIn,
|
|
447
|
-
version:
|
|
449
|
+
version: skillVersion,
|
|
448
450
|
name,
|
|
449
451
|
description,
|
|
450
452
|
publishedAt: metaData?.publishedAt
|