@mison/wecom-cleaner 1.3.0 → 1.3.3
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/README.md +45 -7
- package/docs/IMPLEMENTATION_PLAN.md +6 -6
- package/docs/NON_INTERACTIVE_SPEC.md +59 -0
- package/docs/TEST_MATRIX.md +17 -10
- package/docs/releases/v1.3.1.md +31 -0
- package/docs/releases/v1.3.2.md +38 -0
- package/docs/releases/v1.3.3.md +32 -0
- package/native/bin/darwin-arm64/wecom-cleaner-core +0 -0
- package/native/bin/darwin-x64/wecom-cleaner-core +0 -0
- package/native/manifest.json +5 -5
- package/native/zig/build.sh +3 -0
- package/native/zig/src/main.zig +2 -1
- package/native/zig/src/version.zig +1 -0
- package/package.json +2 -1
- package/skills/wecom-cleaner-agent/SKILL.md +10 -4
- package/skills/wecom-cleaner-agent/agents/openai.yaml +1 -1
- package/skills/wecom-cleaner-agent/references/commands.md +47 -11
- package/skills/wecom-cleaner-agent/scripts/check_update_report.sh +15 -0
- package/skills/wecom-cleaner-agent/scripts/cleanup_monthly_report.sh +37 -53
- package/skills/wecom-cleaner-agent/scripts/doctor_report.sh +3 -0
- package/skills/wecom-cleaner-agent/scripts/recycle_maintain_report.sh +39 -53
- package/skills/wecom-cleaner-agent/scripts/restore_batch_report.sh +34 -39
- package/skills/wecom-cleaner-agent/scripts/space_governance_report.sh +34 -49
- package/skills/wecom-cleaner-agent/scripts/upgrade_report.sh +15 -0
- package/skills/wecom-cleaner-agent/version.json +6 -0
- package/src/cli.js +1395 -28
- package/src/config.js +35 -0
- package/src/constants.js +1 -0
- package/src/doctor.js +41 -0
- package/src/skill-cli.js +111 -4
- package/src/skill-installer.js +204 -1
- package/src/updater.js +106 -3
package/src/config.js
CHANGED
|
@@ -11,6 +11,9 @@ const ALLOWED_EXTERNAL_ROOT_SOURCES = new Set(['preset', 'configured', 'auto', '
|
|
|
11
11
|
const ALLOWED_GOVERNANCE_TIERS = new Set(['safe', 'caution', 'protected']);
|
|
12
12
|
const ALLOWED_UPGRADE_METHODS = new Set(['npm', 'github-script']);
|
|
13
13
|
const ALLOWED_UPGRADE_CHANNELS = new Set(['stable', 'pre']);
|
|
14
|
+
const ALLOWED_SKILL_SYNC_METHODS = new Set(['npm', 'github-script']);
|
|
15
|
+
const ALLOWED_RUN_TASK_MODES = new Set(['preview', 'execute', 'preview-execute-verify']);
|
|
16
|
+
const ALLOWED_SCAN_DEBUG_LEVELS = new Set(['off', 'summary', 'full']);
|
|
14
17
|
const ACTION_FLAG_MAP = new Map([
|
|
15
18
|
['--cleanup-monthly', 'cleanup_monthly'],
|
|
16
19
|
['--analysis-only', 'analysis_only'],
|
|
@@ -18,6 +21,7 @@ const ACTION_FLAG_MAP = new Map([
|
|
|
18
21
|
['--recycle-maintain', 'recycle_maintain'],
|
|
19
22
|
['--doctor', 'doctor'],
|
|
20
23
|
['--check-update', 'check_update'],
|
|
24
|
+
['--sync-skills', 'sync_skills'],
|
|
21
25
|
]);
|
|
22
26
|
const MODE_TO_ACTION_MAP = new Map([
|
|
23
27
|
['cleanup_monthly', 'cleanup_monthly'],
|
|
@@ -28,6 +32,7 @@ const MODE_TO_ACTION_MAP = new Map([
|
|
|
28
32
|
['doctor', 'doctor'],
|
|
29
33
|
['check_update', 'check_update'],
|
|
30
34
|
['upgrade', 'upgrade'],
|
|
35
|
+
['sync_skills', 'sync_skills'],
|
|
31
36
|
]);
|
|
32
37
|
|
|
33
38
|
export class CliArgError extends Error {
|
|
@@ -196,6 +201,11 @@ export function parseCliArgs(argv) {
|
|
|
196
201
|
upgradeVersion: null,
|
|
197
202
|
upgradeChannel: null,
|
|
198
203
|
upgradeYes: false,
|
|
204
|
+
upgradeSyncSkills: null,
|
|
205
|
+
skillSyncMethod: null,
|
|
206
|
+
skillSyncRef: null,
|
|
207
|
+
runTask: null,
|
|
208
|
+
scanDebug: 'off',
|
|
199
209
|
};
|
|
200
210
|
const actionValues = [];
|
|
201
211
|
|
|
@@ -300,6 +310,31 @@ export function parseCliArgs(argv) {
|
|
|
300
310
|
parsed.upgradeYes = true;
|
|
301
311
|
continue;
|
|
302
312
|
}
|
|
313
|
+
if (token === '--upgrade-sync-skills') {
|
|
314
|
+
parsed.upgradeSyncSkills = parseBooleanFlag(token, takeValue(token, i));
|
|
315
|
+
i += 1;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (token === '--skill-sync-method') {
|
|
319
|
+
parsed.skillSyncMethod = parseEnumValue(token, takeValue(token, i), ALLOWED_SKILL_SYNC_METHODS);
|
|
320
|
+
i += 1;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (token === '--skill-sync-ref') {
|
|
324
|
+
parsed.skillSyncRef = takeValue(token, i);
|
|
325
|
+
i += 1;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (token === '--run-task') {
|
|
329
|
+
parsed.runTask = parseEnumValue(token, takeValue(token, i), ALLOWED_RUN_TASK_MODES);
|
|
330
|
+
i += 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (token === '--scan-debug') {
|
|
334
|
+
parsed.scanDebug = parseEnumValue(token, takeValue(token, i), ALLOWED_SCAN_DEBUG_LEVELS);
|
|
335
|
+
i += 1;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
303
338
|
if (token === '--theme') {
|
|
304
339
|
const theme = normalizeTheme(takeValue(token, i));
|
|
305
340
|
if (!theme) {
|
package/src/constants.js
CHANGED
package/src/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { collectRecycleStats, normalizeRecycleRetention } from './recycle-maintenance.js';
|
|
5
5
|
import { detectExternalStorageRoots, discoverAccounts } from './scanner.js';
|
|
6
|
+
import { inspectSkillBinding, skillBindingStatusLabel } from './skill-installer.js';
|
|
6
7
|
import { normalizeSelfUpdateConfig } from './updater.js';
|
|
7
8
|
|
|
8
9
|
const STATUS_PASS = 'pass';
|
|
@@ -291,6 +292,43 @@ export async function runDoctor({ config, aliases, projectRoot, appVersion }) {
|
|
|
291
292
|
)
|
|
292
293
|
);
|
|
293
294
|
|
|
295
|
+
let skillBinding = null;
|
|
296
|
+
try {
|
|
297
|
+
skillBinding = await inspectSkillBinding({
|
|
298
|
+
appVersion: appVersion || '',
|
|
299
|
+
targetRoot: process.env.WECOM_CLEANER_SKILLS_ROOT || '',
|
|
300
|
+
});
|
|
301
|
+
} catch (error) {
|
|
302
|
+
skillBinding = {
|
|
303
|
+
status: 'invalid_skill_dir',
|
|
304
|
+
matched: false,
|
|
305
|
+
installed: false,
|
|
306
|
+
expectedAppVersion: String(appVersion || ''),
|
|
307
|
+
installedManifest: null,
|
|
308
|
+
recommendation: `技能检测失败:${error instanceof Error ? error.message : String(error)}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const skillDetail = (() => {
|
|
312
|
+
if (!skillBinding) {
|
|
313
|
+
return '未获取到技能状态';
|
|
314
|
+
}
|
|
315
|
+
const installedVersion = skillBinding.installedManifest?.skillVersion || '-';
|
|
316
|
+
const requiredAppVersion = skillBinding.installedManifest?.requiredAppVersion || '-';
|
|
317
|
+
return `${skillBindingStatusLabel(skillBinding.status)},技能版本 ${installedVersion},绑定程序版本 ${requiredAppVersion}`;
|
|
318
|
+
})();
|
|
319
|
+
checks.push(
|
|
320
|
+
buildCheck(
|
|
321
|
+
'skills_binding',
|
|
322
|
+
'Agent Skills 版本绑定',
|
|
323
|
+
skillBinding?.matched ? STATUS_PASS : STATUS_WARN,
|
|
324
|
+
skillDetail,
|
|
325
|
+
skillBinding?.matched
|
|
326
|
+
? ''
|
|
327
|
+
: skillBinding?.recommendation ||
|
|
328
|
+
'建议执行 wecom-cleaner-skill install --force 重新同步 skills 版本。'
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
|
|
294
332
|
const target = resolveRuntimeTarget();
|
|
295
333
|
const manifest = await readManifest(projectRoot);
|
|
296
334
|
const manifestTarget = manifest.parsed?.targets?.[target.targetTag] || null;
|
|
@@ -384,6 +422,9 @@ export async function runDoctor({ config, aliases, projectRoot, appVersion }) {
|
|
|
384
422
|
recycleBytes: recycleStats.totalBytes,
|
|
385
423
|
recycleThresholdBytes: thresholdBytes,
|
|
386
424
|
recycleOverThreshold,
|
|
425
|
+
skillsStatus: skillBinding?.status || 'unknown',
|
|
426
|
+
skillsMatched: Boolean(skillBinding?.matched),
|
|
427
|
+
skillsInstalled: Boolean(skillBinding?.installed),
|
|
387
428
|
},
|
|
388
429
|
recommendations,
|
|
389
430
|
};
|
package/src/skill-cli.js
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
installSkill,
|
|
7
|
+
inspectSkillBinding,
|
|
8
|
+
resolveDefaultSkillsRoot,
|
|
9
|
+
skillBindingStatusLabel,
|
|
10
|
+
} from './skill-installer.js';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
async function loadAppVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const packagePath = path.join(PROJECT_ROOT, 'package.json');
|
|
19
|
+
const text = await readFile(packagePath, 'utf-8');
|
|
20
|
+
const pkg = JSON.parse(text);
|
|
21
|
+
return String(pkg.version || '').trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
3
26
|
|
|
4
27
|
function printHelp() {
|
|
5
28
|
console.log(`wecom-cleaner-skill
|
|
6
29
|
|
|
7
30
|
用法:
|
|
8
|
-
wecom-cleaner-skill install [--target <目录>] [--force] [--dry-run]
|
|
31
|
+
wecom-cleaner-skill install [--target <目录>] [--force] [--dry-run] [--json]
|
|
32
|
+
wecom-cleaner-skill sync [--target <目录>] [--dry-run] [--json]
|
|
33
|
+
wecom-cleaner-skill status [--target <目录>] [--app-version <x.y.z>] [--json]
|
|
9
34
|
wecom-cleaner-skill path
|
|
10
35
|
|
|
11
36
|
说明:
|
|
12
37
|
- install: 安装 wecom-cleaner-agent 到 Codex 技能目录
|
|
38
|
+
- sync: 同步/升级技能版本(等价 install --force)
|
|
39
|
+
- status: 检查技能是否与主程序版本匹配
|
|
13
40
|
- path: 输出默认技能目录(由 CODEX_HOME 或 ~/.codex 推导)
|
|
14
41
|
`);
|
|
15
42
|
}
|
|
@@ -20,6 +47,8 @@ function parseArgs(argv) {
|
|
|
20
47
|
target: '',
|
|
21
48
|
force: false,
|
|
22
49
|
dryRun: false,
|
|
50
|
+
json: false,
|
|
51
|
+
appVersion: '',
|
|
23
52
|
help: false,
|
|
24
53
|
};
|
|
25
54
|
|
|
@@ -44,6 +73,10 @@ function parseArgs(argv) {
|
|
|
44
73
|
parsed.dryRun = true;
|
|
45
74
|
continue;
|
|
46
75
|
}
|
|
76
|
+
if (token === '--json') {
|
|
77
|
+
parsed.json = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
47
80
|
if (token === '--target') {
|
|
48
81
|
const next = tokens[i + 1];
|
|
49
82
|
if (!next || next.startsWith('-')) {
|
|
@@ -53,15 +86,47 @@ function parseArgs(argv) {
|
|
|
53
86
|
i += 1;
|
|
54
87
|
continue;
|
|
55
88
|
}
|
|
89
|
+
if (token === '--app-version') {
|
|
90
|
+
const next = tokens[i + 1];
|
|
91
|
+
if (!next || next.startsWith('-')) {
|
|
92
|
+
throw new Error('参数 --app-version 缺少版本值');
|
|
93
|
+
}
|
|
94
|
+
parsed.appVersion = next;
|
|
95
|
+
i += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
56
98
|
throw new Error(`未知参数: ${token}`);
|
|
57
99
|
}
|
|
58
100
|
|
|
59
101
|
return parsed;
|
|
60
102
|
}
|
|
61
103
|
|
|
104
|
+
function printStatusText(result) {
|
|
105
|
+
console.log(`技能: ${result.skillName}`);
|
|
106
|
+
console.log(`状态: ${skillBindingStatusLabel(result.status)}`);
|
|
107
|
+
console.log(`匹配: ${result.matched ? '是' : '否'}`);
|
|
108
|
+
console.log(`主程序版本: ${result.expectedAppVersion || '-'}`);
|
|
109
|
+
console.log(`已安装: ${result.installed ? '是' : '否'}`);
|
|
110
|
+
if (result.installedManifest?.skillVersion) {
|
|
111
|
+
console.log(`技能版本: ${result.installedManifest.skillVersion}`);
|
|
112
|
+
} else {
|
|
113
|
+
console.log('技能版本: -');
|
|
114
|
+
}
|
|
115
|
+
if (result.installedManifest?.requiredAppVersion) {
|
|
116
|
+
console.log(`技能绑定版本: ${result.installedManifest.requiredAppVersion}`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log('技能绑定版本: -');
|
|
119
|
+
}
|
|
120
|
+
console.log(`目标目录: ${result.targetSkillDir}`);
|
|
121
|
+
if (result.recommendation) {
|
|
122
|
+
console.log(`建议: ${result.recommendation}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
62
126
|
async function main() {
|
|
63
127
|
try {
|
|
64
128
|
const args = parseArgs(process.argv.slice(2));
|
|
129
|
+
const appVersion = args.appVersion || (await loadAppVersion());
|
|
65
130
|
|
|
66
131
|
if (args.help) {
|
|
67
132
|
printHelp();
|
|
@@ -73,21 +138,63 @@ async function main() {
|
|
|
73
138
|
return;
|
|
74
139
|
}
|
|
75
140
|
|
|
76
|
-
if (args.command
|
|
141
|
+
if (args.command === 'status') {
|
|
142
|
+
const status = await inspectSkillBinding({
|
|
143
|
+
targetRoot: args.target,
|
|
144
|
+
appVersion,
|
|
145
|
+
});
|
|
146
|
+
if (args.json) {
|
|
147
|
+
console.log(JSON.stringify(status, null, 2));
|
|
148
|
+
process.exitCode = status.matched ? 0 : 1;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
printStatusText(status);
|
|
152
|
+
process.exitCode = status.matched ? 0 : 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (args.command !== 'install' && args.command !== 'sync') {
|
|
77
157
|
throw new Error(`不支持的命令: ${args.command}`);
|
|
78
158
|
}
|
|
79
159
|
|
|
160
|
+
const force = args.command === 'sync' ? true : args.force;
|
|
80
161
|
const result = await installSkill({
|
|
81
162
|
targetRoot: args.target,
|
|
82
|
-
force
|
|
163
|
+
force,
|
|
83
164
|
dryRun: args.dryRun,
|
|
165
|
+
appVersion,
|
|
166
|
+
});
|
|
167
|
+
const status = await inspectSkillBinding({
|
|
168
|
+
targetRoot: args.target,
|
|
169
|
+
appVersion,
|
|
84
170
|
});
|
|
85
171
|
|
|
172
|
+
if (args.json) {
|
|
173
|
+
console.log(
|
|
174
|
+
JSON.stringify(
|
|
175
|
+
{
|
|
176
|
+
action: args.command,
|
|
177
|
+
result,
|
|
178
|
+
status,
|
|
179
|
+
},
|
|
180
|
+
null,
|
|
181
|
+
2
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
86
187
|
const mode = result.dryRun ? '预演' : '安装';
|
|
87
188
|
const replaceText = result.replaced ? '(覆盖已存在版本)' : '';
|
|
88
189
|
console.log(`${mode}成功${replaceText}`);
|
|
89
190
|
console.log(`技能: ${result.skillName}`);
|
|
191
|
+
console.log(`技能版本: ${result.targetManifest.skillVersion}`);
|
|
192
|
+
console.log(`绑定程序版本: ${result.targetManifest.requiredAppVersion}`);
|
|
90
193
|
console.log(`目标: ${result.targetSkillDir}`);
|
|
194
|
+
console.log(`匹配状态: ${skillBindingStatusLabel(status.status)}`);
|
|
195
|
+
if (!status.matched && status.recommendation) {
|
|
196
|
+
console.log(`建议: ${status.recommendation}`);
|
|
197
|
+
}
|
|
91
198
|
} catch (error) {
|
|
92
199
|
console.error(`执行失败: ${error.message}`);
|
|
93
200
|
process.exitCode = 1;
|
package/src/skill-installer.js
CHANGED
|
@@ -1,15 +1,60 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { access, cp, mkdir, rm } from 'node:fs/promises';
|
|
4
|
+
import { access, cp, mkdir, readFile, rm } from 'node:fs/promises';
|
|
5
5
|
import { constants as fsConstants } from 'node:fs';
|
|
6
6
|
|
|
7
7
|
export const SKILL_NAME = 'wecom-cleaner-agent';
|
|
8
|
+
export const SKILL_VERSION_FILE = 'version.json';
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = path.dirname(__filename);
|
|
11
12
|
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
12
13
|
const DEFAULT_SOURCE_SKILL_DIR = path.join(PROJECT_ROOT, 'skills', SKILL_NAME);
|
|
14
|
+
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
15
|
+
|
|
16
|
+
function normalizeSemver(rawValue) {
|
|
17
|
+
const text = String(rawValue || '').trim();
|
|
18
|
+
if (!text) {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
const matched = text.match(SEMVER_RE);
|
|
22
|
+
if (!matched) {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
const base = `${matched[1]}.${matched[2]}.${matched[3]}`;
|
|
26
|
+
return matched[4] ? `${base}-${matched[4]}` : base;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildFallbackManifest(appVersion = '') {
|
|
30
|
+
const normalizedAppVersion = normalizeSemver(appVersion) || '0.0.0';
|
|
31
|
+
return {
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
skillName: SKILL_NAME,
|
|
34
|
+
skillVersion: normalizedAppVersion,
|
|
35
|
+
requiredAppVersion: normalizedAppVersion,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeSkillManifest(rawManifest, options = {}) {
|
|
40
|
+
const fallback = buildFallbackManifest(options.appVersion);
|
|
41
|
+
const source = rawManifest && typeof rawManifest === 'object' ? rawManifest : {};
|
|
42
|
+
const schemaVersion = Number.isFinite(Number(source.schemaVersion))
|
|
43
|
+
? Number(source.schemaVersion)
|
|
44
|
+
: fallback.schemaVersion;
|
|
45
|
+
const skillName =
|
|
46
|
+
typeof source.skillName === 'string' && source.skillName.trim()
|
|
47
|
+
? source.skillName.trim()
|
|
48
|
+
: fallback.skillName;
|
|
49
|
+
const skillVersion = normalizeSemver(source.skillVersion) || fallback.skillVersion;
|
|
50
|
+
const requiredAppVersion = normalizeSemver(source.requiredAppVersion) || fallback.requiredAppVersion;
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion,
|
|
53
|
+
skillName,
|
|
54
|
+
skillVersion,
|
|
55
|
+
requiredAppVersion,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
13
58
|
|
|
14
59
|
export function resolveDefaultSkillsRoot(env = process.env) {
|
|
15
60
|
const codexHome = typeof env.CODEX_HOME === 'string' ? env.CODEX_HOME.trim() : '';
|
|
@@ -35,6 +80,142 @@ async function exists(targetPath) {
|
|
|
35
80
|
}
|
|
36
81
|
}
|
|
37
82
|
|
|
83
|
+
export function skillBindingStatusLabel(status) {
|
|
84
|
+
if (status === 'matched') {
|
|
85
|
+
return '已匹配';
|
|
86
|
+
}
|
|
87
|
+
if (status === 'mismatch') {
|
|
88
|
+
return '版本不匹配';
|
|
89
|
+
}
|
|
90
|
+
if (status === 'legacy_unversioned') {
|
|
91
|
+
return '旧版技能(缺少版本信息)';
|
|
92
|
+
}
|
|
93
|
+
if (status === 'not_installed') {
|
|
94
|
+
return '未安装';
|
|
95
|
+
}
|
|
96
|
+
if (status === 'invalid_skill_dir') {
|
|
97
|
+
return '安装目录异常';
|
|
98
|
+
}
|
|
99
|
+
return '未知';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function readSkillManifestFromDir(skillDir, options = {}) {
|
|
103
|
+
const resolvedSkillDir = path.resolve(String(skillDir || ''));
|
|
104
|
+
const manifestPath = path.join(resolvedSkillDir, SKILL_VERSION_FILE);
|
|
105
|
+
const manifestExists = await exists(manifestPath);
|
|
106
|
+
if (!manifestExists) {
|
|
107
|
+
return {
|
|
108
|
+
path: manifestPath,
|
|
109
|
+
exists: false,
|
|
110
|
+
parseError: '',
|
|
111
|
+
manifest: normalizeSkillManifest(null, options),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const text = await readFile(manifestPath, 'utf-8');
|
|
117
|
+
const parsed = JSON.parse(text);
|
|
118
|
+
return {
|
|
119
|
+
path: manifestPath,
|
|
120
|
+
exists: true,
|
|
121
|
+
parseError: '',
|
|
122
|
+
manifest: normalizeSkillManifest(parsed, options),
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return {
|
|
126
|
+
path: manifestPath,
|
|
127
|
+
exists: true,
|
|
128
|
+
parseError: error instanceof Error ? error.message : String(error),
|
|
129
|
+
manifest: normalizeSkillManifest(null, options),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function inspectSkillBinding(options = {}) {
|
|
135
|
+
const appVersion = normalizeSemver(options.appVersion);
|
|
136
|
+
const sourceSkillDir = path.resolve(options.sourceSkillDir || DEFAULT_SOURCE_SKILL_DIR);
|
|
137
|
+
const sourceSkillFile = path.join(sourceSkillDir, 'SKILL.md');
|
|
138
|
+
if (!(await exists(sourceSkillFile))) {
|
|
139
|
+
throw new Error(`技能源目录无效,缺少 SKILL.md: ${sourceSkillDir}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const sourceManifestState = await readSkillManifestFromDir(sourceSkillDir, {
|
|
143
|
+
appVersion,
|
|
144
|
+
});
|
|
145
|
+
const expectedAppVersion = appVersion || sourceManifestState.manifest.requiredAppVersion;
|
|
146
|
+
const targetRoot = resolveTargetSkillsRoot(options.targetRoot, options.env || process.env);
|
|
147
|
+
const targetSkillDir = path.join(targetRoot, SKILL_NAME);
|
|
148
|
+
const targetSkillExists = await exists(targetSkillDir);
|
|
149
|
+
const targetSkillFile = path.join(targetSkillDir, 'SKILL.md');
|
|
150
|
+
const targetSkillFileExists = targetSkillExists ? await exists(targetSkillFile) : false;
|
|
151
|
+
const installedManifestState = targetSkillExists
|
|
152
|
+
? await readSkillManifestFromDir(targetSkillDir, {
|
|
153
|
+
appVersion: expectedAppVersion,
|
|
154
|
+
})
|
|
155
|
+
: null;
|
|
156
|
+
|
|
157
|
+
const recommendation = (() => {
|
|
158
|
+
if (!targetSkillExists) {
|
|
159
|
+
return '执行 wecom-cleaner-skill install 安装技能。';
|
|
160
|
+
}
|
|
161
|
+
if (!targetSkillFileExists) {
|
|
162
|
+
return '目标目录缺少 SKILL.md,建议执行 wecom-cleaner-skill install --force 修复。';
|
|
163
|
+
}
|
|
164
|
+
if (installedManifestState && !installedManifestState.exists) {
|
|
165
|
+
return '检测到旧版技能(缺少 version.json),建议执行 wecom-cleaner-skill install --force。';
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
installedManifestState &&
|
|
169
|
+
installedManifestState.manifest.skillName === SKILL_NAME &&
|
|
170
|
+
installedManifestState.manifest.requiredAppVersion === expectedAppVersion
|
|
171
|
+
) {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
return '建议执行 wecom-cleaner-skill install --force 同步技能版本。';
|
|
175
|
+
})();
|
|
176
|
+
|
|
177
|
+
let status = 'matched';
|
|
178
|
+
let matched = true;
|
|
179
|
+
if (!targetSkillExists) {
|
|
180
|
+
status = 'not_installed';
|
|
181
|
+
matched = false;
|
|
182
|
+
} else if (!targetSkillFileExists) {
|
|
183
|
+
status = 'invalid_skill_dir';
|
|
184
|
+
matched = false;
|
|
185
|
+
} else if (installedManifestState && !installedManifestState.exists) {
|
|
186
|
+
status = 'legacy_unversioned';
|
|
187
|
+
matched = false;
|
|
188
|
+
} else if (
|
|
189
|
+
!installedManifestState ||
|
|
190
|
+
installedManifestState.manifest.skillName !== SKILL_NAME ||
|
|
191
|
+
installedManifestState.manifest.requiredAppVersion !== expectedAppVersion
|
|
192
|
+
) {
|
|
193
|
+
status = 'mismatch';
|
|
194
|
+
matched = false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
skillName: SKILL_NAME,
|
|
199
|
+
status,
|
|
200
|
+
matched,
|
|
201
|
+
recommendation,
|
|
202
|
+
expectedAppVersion: expectedAppVersion || '',
|
|
203
|
+
sourceSkillDir,
|
|
204
|
+
sourceManifestPath: sourceManifestState.path,
|
|
205
|
+
sourceManifest: sourceManifestState.manifest,
|
|
206
|
+
sourceManifestExists: sourceManifestState.exists,
|
|
207
|
+
sourceManifestParseError: sourceManifestState.parseError || '',
|
|
208
|
+
targetRoot,
|
|
209
|
+
targetSkillDir,
|
|
210
|
+
installed: targetSkillExists,
|
|
211
|
+
installedSkillFileExists: targetSkillFileExists,
|
|
212
|
+
installedManifestPath: installedManifestState?.path || path.join(targetSkillDir, SKILL_VERSION_FILE),
|
|
213
|
+
installedManifest: installedManifestState?.manifest || null,
|
|
214
|
+
installedManifestExists: Boolean(installedManifestState?.exists),
|
|
215
|
+
installedManifestParseError: installedManifestState?.parseError || '',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
38
219
|
export async function installSkill(options = {}) {
|
|
39
220
|
const sourceSkillDir = path.resolve(options.sourceSkillDir || DEFAULT_SOURCE_SKILL_DIR);
|
|
40
221
|
const targetRoot = resolveTargetSkillsRoot(options.targetRoot);
|
|
@@ -46,9 +227,17 @@ export async function installSkill(options = {}) {
|
|
|
46
227
|
if (!sourceExists) {
|
|
47
228
|
throw new Error(`技能源目录无效,缺少 SKILL.md: ${sourceSkillDir}`);
|
|
48
229
|
}
|
|
230
|
+
const sourceManifestState = await readSkillManifestFromDir(sourceSkillDir, {
|
|
231
|
+
appVersion: options.appVersion,
|
|
232
|
+
});
|
|
49
233
|
|
|
50
234
|
const targetSkillDir = path.join(targetRoot, SKILL_NAME);
|
|
51
235
|
const targetExists = await exists(targetSkillDir);
|
|
236
|
+
const previousManifestState = targetExists
|
|
237
|
+
? await readSkillManifestFromDir(targetSkillDir, {
|
|
238
|
+
appVersion: options.appVersion || sourceManifestState.manifest.requiredAppVersion,
|
|
239
|
+
})
|
|
240
|
+
: null;
|
|
52
241
|
if (targetExists && !force) {
|
|
53
242
|
throw new Error(`目标技能已存在:${targetSkillDir}。如需覆盖请加 --force`);
|
|
54
243
|
}
|
|
@@ -65,6 +254,12 @@ export async function installSkill(options = {}) {
|
|
|
65
254
|
});
|
|
66
255
|
}
|
|
67
256
|
|
|
257
|
+
const targetManifestState = dryRun
|
|
258
|
+
? sourceManifestState
|
|
259
|
+
: await readSkillManifestFromDir(targetSkillDir, {
|
|
260
|
+
appVersion: options.appVersion || sourceManifestState.manifest.requiredAppVersion,
|
|
261
|
+
});
|
|
262
|
+
|
|
68
263
|
return {
|
|
69
264
|
skillName: SKILL_NAME,
|
|
70
265
|
sourceSkillDir,
|
|
@@ -72,5 +267,13 @@ export async function installSkill(options = {}) {
|
|
|
72
267
|
targetSkillDir,
|
|
73
268
|
dryRun,
|
|
74
269
|
replaced: targetExists,
|
|
270
|
+
sourceManifest: sourceManifestState.manifest,
|
|
271
|
+
sourceManifestPath: sourceManifestState.path,
|
|
272
|
+
previousManifest: previousManifestState?.manifest || null,
|
|
273
|
+
previousManifestPath: previousManifestState?.path || null,
|
|
274
|
+
previousManifestExists: Boolean(previousManifestState?.exists),
|
|
275
|
+
targetManifest: targetManifestState.manifest,
|
|
276
|
+
targetManifestPath: targetManifestState.path,
|
|
277
|
+
targetManifestExists: targetManifestState.exists,
|
|
75
278
|
};
|
|
76
279
|
}
|