@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/src/sample-config.json
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
{
|
|
2
|
-
"configs": [
|
|
3
|
-
{
|
|
4
|
-
"skillName": "model-usage",
|
|
5
|
-
"version": "0.0.0-stub",
|
|
6
|
-
"description": "查看各 provider 模型用量(本地静态桩)",
|
|
7
|
-
"functions": [
|
|
8
|
-
{
|
|
9
|
-
"id": "usage_current",
|
|
10
|
-
"name": "当前用量",
|
|
11
|
-
"description": "查看当前模型用量",
|
|
12
|
-
"match": {
|
|
13
|
-
"type": "script",
|
|
14
|
-
"script": "scripts/model_usage.py",
|
|
15
|
-
"argRules": [{ "flag": "--mode", "value": "current" }]
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"id": "usage_all",
|
|
20
|
-
"name": "全部用量",
|
|
21
|
-
"description": "查看全部模型用量",
|
|
22
|
-
"match": {
|
|
23
|
-
"type": "script",
|
|
24
|
-
"script": "scripts/model_usage.py",
|
|
25
|
-
"argRules": [{ "flag": "--mode", "value": "all" }]
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"skillName": "openai-whisper-api",
|
|
32
|
-
"version": "0.0.0-stub",
|
|
33
|
-
"description": "Whisper 音频转写(本地静态桩)",
|
|
34
|
-
"functions": [
|
|
35
|
-
{
|
|
36
|
-
"id": "transcribe",
|
|
37
|
-
"name": "音频转写",
|
|
38
|
-
"match": { "type": "script", "script": "scripts/transcribe.sh" }
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"skillName": "mcporter",
|
|
44
|
-
"version": "0.0.0-stub",
|
|
45
|
-
"description": "MCP CLI 包装示例(本地静态桩)",
|
|
46
|
-
"functions": [
|
|
47
|
-
{
|
|
48
|
-
"id": "linear_list_issues",
|
|
49
|
-
"name": "列 Linear issue",
|
|
50
|
-
"match": { "type": "command", "command": "mcporter", "targetPattern": "call linear.list_issues" }
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"skillName": "linear-mcp",
|
|
56
|
-
"version": "0.0.0-stub",
|
|
57
|
-
"description": "MCP/SSE 工具直调 + HTTP 端点示例(本地静态桩)",
|
|
58
|
-
"functions": [
|
|
59
|
-
{
|
|
60
|
-
"id": "create_issue",
|
|
61
|
-
"name": "创建 issue(MCP工具直调)",
|
|
62
|
-
"match": { "type": "tool", "toolName": "linear_create_issue" }
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
"id": "transcribe_http",
|
|
66
|
-
"name": "转写 HTTP 端点",
|
|
67
|
-
"match": { "type": "http", "urlContains": "/v1/audio/transcriptions" }
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"configs": [
|
|
3
|
+
{
|
|
4
|
+
"skillName": "model-usage",
|
|
5
|
+
"version": "0.0.0-stub",
|
|
6
|
+
"description": "查看各 provider 模型用量(本地静态桩)",
|
|
7
|
+
"functions": [
|
|
8
|
+
{
|
|
9
|
+
"id": "usage_current",
|
|
10
|
+
"name": "当前用量",
|
|
11
|
+
"description": "查看当前模型用量",
|
|
12
|
+
"match": {
|
|
13
|
+
"type": "script",
|
|
14
|
+
"script": "scripts/model_usage.py",
|
|
15
|
+
"argRules": [{ "flag": "--mode", "value": "current" }]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "usage_all",
|
|
20
|
+
"name": "全部用量",
|
|
21
|
+
"description": "查看全部模型用量",
|
|
22
|
+
"match": {
|
|
23
|
+
"type": "script",
|
|
24
|
+
"script": "scripts/model_usage.py",
|
|
25
|
+
"argRules": [{ "flag": "--mode", "value": "all" }]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"skillName": "openai-whisper-api",
|
|
32
|
+
"version": "0.0.0-stub",
|
|
33
|
+
"description": "Whisper 音频转写(本地静态桩)",
|
|
34
|
+
"functions": [
|
|
35
|
+
{
|
|
36
|
+
"id": "transcribe",
|
|
37
|
+
"name": "音频转写",
|
|
38
|
+
"match": { "type": "script", "script": "scripts/transcribe.sh" }
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"skillName": "mcporter",
|
|
44
|
+
"version": "0.0.0-stub",
|
|
45
|
+
"description": "MCP CLI 包装示例(本地静态桩)",
|
|
46
|
+
"functions": [
|
|
47
|
+
{
|
|
48
|
+
"id": "linear_list_issues",
|
|
49
|
+
"name": "列 Linear issue",
|
|
50
|
+
"match": { "type": "command", "command": "mcporter", "targetPattern": "call linear.list_issues" }
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"skillName": "linear-mcp",
|
|
56
|
+
"version": "0.0.0-stub",
|
|
57
|
+
"description": "MCP/SSE 工具直调 + HTTP 端点示例(本地静态桩)",
|
|
58
|
+
"functions": [
|
|
59
|
+
{
|
|
60
|
+
"id": "create_issue",
|
|
61
|
+
"name": "创建 issue(MCP工具直调)",
|
|
62
|
+
"match": { "type": "tool", "toolName": "linear_create_issue" }
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "transcribe_http",
|
|
66
|
+
"name": "转写 HTTP 端点",
|
|
67
|
+
"match": { "type": "http", "urlContains": "/v1/audio/transcriptions" }
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
package/src/semver.test.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { compareVersions, isOutdated } from "./semver.ts";
|
|
4
|
-
|
|
5
|
-
describe("compareVersions", () => {
|
|
6
|
-
it("数值逐段比较:1.10 > 1.9", () => assert.equal(compareVersions("1.10.0", "1.9.0"), 1));
|
|
7
|
-
it("缺省段按 0 补齐:1.2 == 1.2.0", () => assert.equal(compareVersions("1.2", "1.2.0"), 0));
|
|
8
|
-
it("容忍 v 前缀", () => assert.equal(compareVersions("v2.0.0", "2.0.0"), 0));
|
|
9
|
-
it("a < b", () => assert.equal(compareVersions("1.0.0", "1.0.1"), -1));
|
|
10
|
-
it("忽略预发布后缀(核心相等)", () => assert.equal(compareVersions("1.0.0-beta", "1.0.0"), 0));
|
|
11
|
-
it("非数字版本退化为字符串比较", () => assert.equal(compareVersions("abc", "abc"), 0));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe("isOutdated", () => {
|
|
15
|
-
it("本地落后 → true", () => assert.equal(isOutdated("1.0.0", "1.1.0"), true));
|
|
16
|
-
it("本地更新 → false", () => assert.equal(isOutdated("2.0.0", "1.9.0"), false));
|
|
17
|
-
it("相等 → false", () => assert.equal(isOutdated("1.2.3", "1.2.3"), false));
|
|
18
|
-
it("1.9 vs 1.10:应判过期(数值语义)", () => assert.equal(isOutdated("1.9.0", "1.10.0"), true));
|
|
19
|
-
it("缺本地版本 → 不判过期", () => assert.equal(isOutdated(undefined, "1.0.0"), false));
|
|
20
|
-
it("缺最新版本 → 不判过期", () => assert.equal(isOutdated("1.0.0", undefined), false));
|
|
21
|
-
it("非 semver 且不同 → 过期(保守)", () => assert.equal(isOutdated("alpha", "beta"), true));
|
|
22
|
-
it("带预发布后缀核心相同 → 不过期", () => assert.equal(isOutdated("2026-01", "2026-02"), false));
|
|
23
|
-
});
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { compareVersions, isOutdated } from "./semver.ts";
|
|
4
|
+
|
|
5
|
+
describe("compareVersions", () => {
|
|
6
|
+
it("数值逐段比较:1.10 > 1.9", () => assert.equal(compareVersions("1.10.0", "1.9.0"), 1));
|
|
7
|
+
it("缺省段按 0 补齐:1.2 == 1.2.0", () => assert.equal(compareVersions("1.2", "1.2.0"), 0));
|
|
8
|
+
it("容忍 v 前缀", () => assert.equal(compareVersions("v2.0.0", "2.0.0"), 0));
|
|
9
|
+
it("a < b", () => assert.equal(compareVersions("1.0.0", "1.0.1"), -1));
|
|
10
|
+
it("忽略预发布后缀(核心相等)", () => assert.equal(compareVersions("1.0.0-beta", "1.0.0"), 0));
|
|
11
|
+
it("非数字版本退化为字符串比较", () => assert.equal(compareVersions("abc", "abc"), 0));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("isOutdated", () => {
|
|
15
|
+
it("本地落后 → true", () => assert.equal(isOutdated("1.0.0", "1.1.0"), true));
|
|
16
|
+
it("本地更新 → false", () => assert.equal(isOutdated("2.0.0", "1.9.0"), false));
|
|
17
|
+
it("相等 → false", () => assert.equal(isOutdated("1.2.3", "1.2.3"), false));
|
|
18
|
+
it("1.9 vs 1.10:应判过期(数值语义)", () => assert.equal(isOutdated("1.9.0", "1.10.0"), true));
|
|
19
|
+
it("缺本地版本 → 不判过期", () => assert.equal(isOutdated(undefined, "1.0.0"), false));
|
|
20
|
+
it("缺最新版本 → 不判过期", () => assert.equal(isOutdated("1.0.0", undefined), false));
|
|
21
|
+
it("非 semver 且不同 → 过期(保守)", () => assert.equal(isOutdated("alpha", "beta"), true));
|
|
22
|
+
it("带预发布后缀核心相同 → 不过期", () => assert.equal(isOutdated("2026-01", "2026-02"), false));
|
|
23
|
+
});
|
package/src/semver.ts
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 轻量语义化版本比较(纯函数,无依赖)。
|
|
3
|
-
*
|
|
4
|
-
* 仅用于「本地 skill 版本 vs 平台最新版本」的过期判断,不追求完整 semver 规范:
|
|
5
|
-
* - 容忍前缀 `v`/`V`(如 `v1.2.3`);
|
|
6
|
-
* - 比较 `主.次.修订…` 的数值段,缺省段按 0 补齐(`1.2` == `1.2.0`);
|
|
7
|
-
* - 预发布/构建元数据(`-beta`、`+build`)只取核心版本比较,忽略其后缀;
|
|
8
|
-
* - 任一侧无法解析为「点分数字」时,退化为字符串相等判断。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/** 解析核心数字段;非「点分数字」返回 null(触发字符串回退)。 */
|
|
12
|
-
function parseCore(v: string): number[] | null {
|
|
13
|
-
const core = v.trim().replace(/^[vV]/, "").split(/[-+]/, 1)[0];
|
|
14
|
-
if (!core) return null;
|
|
15
|
-
const parts = core.split(".");
|
|
16
|
-
const nums: number[] = [];
|
|
17
|
-
for (const p of parts) {
|
|
18
|
-
if (!/^\d+$/.test(p)) return null;
|
|
19
|
-
nums.push(Number(p));
|
|
20
|
-
}
|
|
21
|
-
return nums.length > 0 ? nums : null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 比较 a 与 b:a<b 返回 -1,a>b 返回 1,相等返回 0。
|
|
26
|
-
* 两侧都能解析为点分数字时按数值逐段比较;否则退化为字符串比较。
|
|
27
|
-
*/
|
|
28
|
-
export function compareVersions(a: string, b: string): -1 | 0 | 1 {
|
|
29
|
-
const na = parseCore(a);
|
|
30
|
-
const nb = parseCore(b);
|
|
31
|
-
if (na && nb) {
|
|
32
|
-
const len = Math.max(na.length, nb.length);
|
|
33
|
-
for (let i = 0; i < len; i++) {
|
|
34
|
-
const x = na[i] ?? 0;
|
|
35
|
-
const y = nb[i] ?? 0;
|
|
36
|
-
if (x < y) return -1;
|
|
37
|
-
if (x > y) return 1;
|
|
38
|
-
}
|
|
39
|
-
return 0;
|
|
40
|
-
}
|
|
41
|
-
// 字符串回退:仅区分相等/不等(不臆测大小关系)。
|
|
42
|
-
const sa = a.trim();
|
|
43
|
-
const sb = b.trim();
|
|
44
|
-
if (sa === sb) return 0;
|
|
45
|
-
return sa < sb ? -1 : 1;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 本地版本是否「落后于」最新版本(即需要更新)。
|
|
50
|
-
* - 两侧都是合法 semver:latest 数值更大才算过期;
|
|
51
|
-
* - 无法数值比较时:字符串不相等即视为过期(保守,宁可多更新一次)。
|
|
52
|
-
* 任一侧为空 → 不判过期(缺版本号则不参与更新)。
|
|
53
|
-
*/
|
|
54
|
-
export function isOutdated(local: string | undefined, latest: string | undefined): boolean {
|
|
55
|
-
if (!local || !latest) return false;
|
|
56
|
-
const na = parseCore(local);
|
|
57
|
-
const nb = parseCore(latest);
|
|
58
|
-
if (na && nb) return compareVersions(local, latest) < 0;
|
|
59
|
-
return local.trim() !== latest.trim();
|
|
60
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 轻量语义化版本比较(纯函数,无依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 仅用于「本地 skill 版本 vs 平台最新版本」的过期判断,不追求完整 semver 规范:
|
|
5
|
+
* - 容忍前缀 `v`/`V`(如 `v1.2.3`);
|
|
6
|
+
* - 比较 `主.次.修订…` 的数值段,缺省段按 0 补齐(`1.2` == `1.2.0`);
|
|
7
|
+
* - 预发布/构建元数据(`-beta`、`+build`)只取核心版本比较,忽略其后缀;
|
|
8
|
+
* - 任一侧无法解析为「点分数字」时,退化为字符串相等判断。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** 解析核心数字段;非「点分数字」返回 null(触发字符串回退)。 */
|
|
12
|
+
function parseCore(v: string): number[] | null {
|
|
13
|
+
const core = v.trim().replace(/^[vV]/, "").split(/[-+]/, 1)[0];
|
|
14
|
+
if (!core) return null;
|
|
15
|
+
const parts = core.split(".");
|
|
16
|
+
const nums: number[] = [];
|
|
17
|
+
for (const p of parts) {
|
|
18
|
+
if (!/^\d+$/.test(p)) return null;
|
|
19
|
+
nums.push(Number(p));
|
|
20
|
+
}
|
|
21
|
+
return nums.length > 0 ? nums : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 比较 a 与 b:a<b 返回 -1,a>b 返回 1,相等返回 0。
|
|
26
|
+
* 两侧都能解析为点分数字时按数值逐段比较;否则退化为字符串比较。
|
|
27
|
+
*/
|
|
28
|
+
export function compareVersions(a: string, b: string): -1 | 0 | 1 {
|
|
29
|
+
const na = parseCore(a);
|
|
30
|
+
const nb = parseCore(b);
|
|
31
|
+
if (na && nb) {
|
|
32
|
+
const len = Math.max(na.length, nb.length);
|
|
33
|
+
for (let i = 0; i < len; i++) {
|
|
34
|
+
const x = na[i] ?? 0;
|
|
35
|
+
const y = nb[i] ?? 0;
|
|
36
|
+
if (x < y) return -1;
|
|
37
|
+
if (x > y) return 1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
// 字符串回退:仅区分相等/不等(不臆测大小关系)。
|
|
42
|
+
const sa = a.trim();
|
|
43
|
+
const sb = b.trim();
|
|
44
|
+
if (sa === sb) return 0;
|
|
45
|
+
return sa < sb ? -1 : 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 本地版本是否「落后于」最新版本(即需要更新)。
|
|
50
|
+
* - 两侧都是合法 semver:latest 数值更大才算过期;
|
|
51
|
+
* - 无法数值比较时:字符串不相等即视为过期(保守,宁可多更新一次)。
|
|
52
|
+
* 任一侧为空 → 不判过期(缺版本号则不参与更新)。
|
|
53
|
+
*/
|
|
54
|
+
export function isOutdated(local: string | undefined, latest: string | undefined): boolean {
|
|
55
|
+
if (!local || !latest) return false;
|
|
56
|
+
const na = parseCore(local);
|
|
57
|
+
const nb = parseCore(latest);
|
|
58
|
+
if (na && nb) return compareVersions(local, latest) < 0;
|
|
59
|
+
return local.trim() !== latest.trim();
|
|
60
|
+
}
|
package/src/skill-version.ts
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
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
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 从 SKILL.md 文本中提取版本号(纯正则、不借助任何模型)。
|
|
34
|
-
*
|
|
35
|
-
* 兼容点:
|
|
36
|
-
* - 键名:英文 `version` / `Version`(大小写不敏感)、中文 `当前版本号` / `当前版本` / `版本号` / `版本`;
|
|
37
|
-
* - 冒号:半角 `:` 或全角 `:`;
|
|
38
|
-
* - 取值:去除包裹引号、行尾 ` # 注释`、首尾空白。
|
|
39
|
-
*
|
|
40
|
-
* 范围:在整篇 SKILL.md 内按「单独一行」匹配(frontmatter 在最前,故天然优先命中)。
|
|
41
|
-
* 因要求行首即键名(前面只允许空白),Markdown 标题 `## Version: x`(带 `#` 前缀)不会被误命中。
|
|
42
|
-
* 取不到返回 undefined(该 skill 不参与版本更新)。
|
|
43
|
-
*/
|
|
44
|
-
export function parseSkillVersion(content: string): string | undefined {
|
|
45
|
-
const m = /(?:^|\r?\n)[ \t]*(?:version|当前版本号|当前版本|版本号|版本)[ \t]*[::][ \t]*(.+)/i.exec(content);
|
|
46
|
-
if (!m) return undefined;
|
|
47
|
-
const v = m[1]
|
|
48
|
-
.replace(/\s+#.*$/, "") // 行尾注释
|
|
49
|
-
.trim()
|
|
50
|
-
.replace(/^["']|["']$/g, "") // 包裹引号
|
|
51
|
-
.trim();
|
|
52
|
-
return v || undefined;
|
|
53
|
-
}
|
|
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
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 从 SKILL.md 文本中提取版本号(纯正则、不借助任何模型)。
|
|
34
|
+
*
|
|
35
|
+
* 兼容点:
|
|
36
|
+
* - 键名:英文 `version` / `Version`(大小写不敏感)、中文 `当前版本号` / `当前版本` / `版本号` / `版本`;
|
|
37
|
+
* - 冒号:半角 `:` 或全角 `:`;
|
|
38
|
+
* - 取值:去除包裹引号、行尾 ` # 注释`、首尾空白。
|
|
39
|
+
*
|
|
40
|
+
* 范围:在整篇 SKILL.md 内按「单独一行」匹配(frontmatter 在最前,故天然优先命中)。
|
|
41
|
+
* 因要求行首即键名(前面只允许空白),Markdown 标题 `## Version: x`(带 `#` 前缀)不会被误命中。
|
|
42
|
+
* 取不到返回 undefined(该 skill 不参与版本更新)。
|
|
43
|
+
*/
|
|
44
|
+
export function parseSkillVersion(content: string): string | undefined {
|
|
45
|
+
const m = /(?:^|\r?\n)[ \t]*(?:version|当前版本号|当前版本|版本号|版本)[ \t]*[::][ \t]*(.+)/i.exec(content);
|
|
46
|
+
if (!m) return undefined;
|
|
47
|
+
const v = m[1]
|
|
48
|
+
.replace(/\s+#.*$/, "") // 行尾注释
|
|
49
|
+
.trim()
|
|
50
|
+
.replace(/^["']|["']$/g, "") // 包裹引号
|
|
51
|
+
.trim();
|
|
52
|
+
return v || undefined;
|
|
53
|
+
}
|