@adversity/coding-tool-x 3.0.6 → 3.1.1
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/CHANGELOG.md +38 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +92 -13
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/ui.js +8 -1
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +39 -2
- package/src/config/loader.js +74 -8
- package/src/config/paths.js +105 -33
- package/src/index.js +67 -4
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +198 -0
- package/src/server/api/opencode-sessions.js +403 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +32 -19
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +17 -3
- package/src/server/index.js +164 -48
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +30 -19
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +70 -12
- package/src/server/services/codex-channels.js +61 -23
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +26 -12
- package/src/server/services/env-manager.js +126 -18
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +37 -15
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +132 -3
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-BxudHPiX.js +0 -1
- package/dist/web/assets/index-D2VfwJBa.js +0 -14
- package/dist/web/assets/index-oXBzu0bd.css +0 -41
- package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/permissions.js +0 -385
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -7,15 +7,26 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
10
11
|
|
|
11
12
|
// Prompts 配置文件路径
|
|
12
|
-
const CC_TOOL_DIR = path.join(os.homedir(), '.
|
|
13
|
+
const CC_TOOL_DIR = path.join(os.homedir(), '.cc-tool');
|
|
13
14
|
const PROMPTS_FILE = path.join(CC_TOOL_DIR, 'prompts.json');
|
|
14
15
|
|
|
15
16
|
// 各平台提示词文件路径
|
|
16
17
|
const CLAUDE_PROMPT_PATH = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
17
18
|
const CODEX_PROMPT_PATH = path.join(os.homedir(), '.codex', 'AGENTS.md');
|
|
18
19
|
const GEMINI_PROMPT_PATH = path.join(os.homedir(), '.gemini', 'GEMINI.md');
|
|
20
|
+
const OPENCODE_PROMPT_PATH = path.join(NATIVE_PATHS.opencode.config, 'AGENTS.md');
|
|
21
|
+
|
|
22
|
+
function normalizeApps(apps = {}, defaults = { claude: true, codex: true, gemini: true, opencode: false }) {
|
|
23
|
+
return {
|
|
24
|
+
claude: apps.claude !== undefined ? !!apps.claude : defaults.claude,
|
|
25
|
+
codex: apps.codex !== undefined ? !!apps.codex : defaults.codex,
|
|
26
|
+
gemini: apps.gemini !== undefined ? !!apps.gemini : defaults.gemini,
|
|
27
|
+
opencode: apps.opencode !== undefined ? !!apps.opencode : defaults.opencode
|
|
28
|
+
};
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
// 内置模板(不是"默认",只是可选模板)
|
|
21
32
|
const BUILTIN_TEMPLATES = [
|
|
@@ -38,7 +49,7 @@ const BUILTIN_TEMPLATES = [
|
|
|
38
49
|
2. 具体问题列表(按严重程度排序)
|
|
39
50
|
3. 改进建议和示例代码
|
|
40
51
|
`,
|
|
41
|
-
apps: { claude: true, codex: true, gemini: true },
|
|
52
|
+
apps: { claude: true, codex: true, gemini: true, opencode: true },
|
|
42
53
|
isBuiltin: true
|
|
43
54
|
},
|
|
44
55
|
{
|
|
@@ -62,7 +73,7 @@ const BUILTIN_TEMPLATES = [
|
|
|
62
73
|
- 建议的解决方案
|
|
63
74
|
- 预防措施
|
|
64
75
|
`,
|
|
65
|
-
apps: { claude: true, codex: true, gemini: true },
|
|
76
|
+
apps: { claude: true, codex: true, gemini: true, opencode: true },
|
|
66
77
|
isBuiltin: true
|
|
67
78
|
},
|
|
68
79
|
{
|
|
@@ -85,7 +96,7 @@ const BUILTIN_TEMPLATES = [
|
|
|
85
96
|
- 简化条件表达式
|
|
86
97
|
- 消除重复代码
|
|
87
98
|
`,
|
|
88
|
-
apps: { claude: true, codex: true, gemini: true },
|
|
99
|
+
apps: { claude: true, codex: true, gemini: true, opencode: true },
|
|
89
100
|
isBuiltin: true
|
|
90
101
|
}
|
|
91
102
|
];
|
|
@@ -198,7 +209,7 @@ function initPromptsData() {
|
|
|
198
209
|
name: '当前使用',
|
|
199
210
|
description: '从现有配置导入',
|
|
200
211
|
content: existingContent,
|
|
201
|
-
apps: { claude: true, codex: false, gemini: false },
|
|
212
|
+
apps: normalizeApps({ claude: true, codex: false, gemini: false, opencode: false }),
|
|
202
213
|
isBuiltin: false,
|
|
203
214
|
isImported: true,
|
|
204
215
|
createdAt: Date.now(),
|
|
@@ -212,6 +223,25 @@ function initPromptsData() {
|
|
|
212
223
|
return initialData;
|
|
213
224
|
}
|
|
214
225
|
|
|
226
|
+
let updated = false;
|
|
227
|
+
for (const preset of Object.values(data.presets || {})) {
|
|
228
|
+
const normalizedApps = normalizeApps(preset.apps);
|
|
229
|
+
if (
|
|
230
|
+
!preset.apps ||
|
|
231
|
+
preset.apps.claude !== normalizedApps.claude ||
|
|
232
|
+
preset.apps.codex !== normalizedApps.codex ||
|
|
233
|
+
preset.apps.gemini !== normalizedApps.gemini ||
|
|
234
|
+
preset.apps.opencode !== normalizedApps.opencode
|
|
235
|
+
) {
|
|
236
|
+
preset.apps = normalizedApps;
|
|
237
|
+
updated = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (updated) {
|
|
242
|
+
writeJsonFile(PROMPTS_FILE, data);
|
|
243
|
+
}
|
|
244
|
+
|
|
215
245
|
return data;
|
|
216
246
|
}
|
|
217
247
|
|
|
@@ -288,9 +318,7 @@ function savePreset(preset) {
|
|
|
288
318
|
preset.updatedAt = Date.now();
|
|
289
319
|
|
|
290
320
|
// 确保 apps 字段存在
|
|
291
|
-
|
|
292
|
-
preset.apps = { claude: true, codex: true, gemini: true };
|
|
293
|
-
}
|
|
321
|
+
preset.apps = normalizeApps(preset.apps);
|
|
294
322
|
|
|
295
323
|
data.presets[preset.id] = preset;
|
|
296
324
|
writeJsonFile(PROMPTS_FILE, data);
|
|
@@ -359,7 +387,8 @@ async function deactivatePrompt() {
|
|
|
359
387
|
const results = {
|
|
360
388
|
claude: deleteFile(CLAUDE_PROMPT_PATH),
|
|
361
389
|
codex: deleteFile(CODEX_PROMPT_PATH),
|
|
362
|
-
gemini: deleteFile(GEMINI_PROMPT_PATH)
|
|
390
|
+
gemini: deleteFile(GEMINI_PROMPT_PATH),
|
|
391
|
+
opencode: deleteFile(OPENCODE_PROMPT_PATH)
|
|
363
392
|
};
|
|
364
393
|
|
|
365
394
|
console.log('[Prompts] Deactivated and removed prompt files:', results);
|
|
@@ -375,7 +404,8 @@ async function deactivatePrompt() {
|
|
|
375
404
|
* 同步预设到所有已启用的平台
|
|
376
405
|
*/
|
|
377
406
|
async function syncPresetToAllPlatforms(preset) {
|
|
378
|
-
const
|
|
407
|
+
const apps = normalizeApps(preset.apps);
|
|
408
|
+
const { content } = preset;
|
|
379
409
|
|
|
380
410
|
if (apps.claude) {
|
|
381
411
|
writeTextFile(CLAUDE_PROMPT_PATH, content);
|
|
@@ -391,6 +421,11 @@ async function syncPresetToAllPlatforms(preset) {
|
|
|
391
421
|
writeTextFile(GEMINI_PROMPT_PATH, content);
|
|
392
422
|
console.log('[Prompts] Synced to Gemini:', GEMINI_PROMPT_PATH);
|
|
393
423
|
}
|
|
424
|
+
|
|
425
|
+
if (apps.opencode) {
|
|
426
|
+
writeTextFile(OPENCODE_PROMPT_PATH, content);
|
|
427
|
+
console.log('[Prompts] Synced to OpenCode:', OPENCODE_PROMPT_PATH);
|
|
428
|
+
}
|
|
394
429
|
}
|
|
395
430
|
|
|
396
431
|
/**
|
|
@@ -404,6 +439,8 @@ function readPlatformPrompt(platform) {
|
|
|
404
439
|
return readTextFile(CODEX_PROMPT_PATH, '');
|
|
405
440
|
case 'gemini':
|
|
406
441
|
return readTextFile(GEMINI_PROMPT_PATH, '');
|
|
442
|
+
case 'opencode':
|
|
443
|
+
return readTextFile(OPENCODE_PROMPT_PATH, '');
|
|
407
444
|
default:
|
|
408
445
|
throw new Error(`无效的平台: ${platform}`);
|
|
409
446
|
}
|
|
@@ -428,6 +465,11 @@ function getPlatformStatus() {
|
|
|
428
465
|
path: GEMINI_PROMPT_PATH,
|
|
429
466
|
exists: fs.existsSync(GEMINI_PROMPT_PATH),
|
|
430
467
|
content: readTextFile(GEMINI_PROMPT_PATH, '')
|
|
468
|
+
},
|
|
469
|
+
opencode: {
|
|
470
|
+
path: OPENCODE_PROMPT_PATH,
|
|
471
|
+
exists: fs.existsSync(OPENCODE_PROMPT_PATH),
|
|
472
|
+
content: readTextFile(OPENCODE_PROMPT_PATH, '')
|
|
431
473
|
}
|
|
432
474
|
};
|
|
433
475
|
}
|
|
@@ -450,7 +492,7 @@ function importFromPlatform(platform, presetName) {
|
|
|
450
492
|
name: presetName || `从 ${platform} 导入`,
|
|
451
493
|
description: `从 ${platform} 导入的提示词`,
|
|
452
494
|
content,
|
|
453
|
-
apps: { claude: false, codex: false, gemini: false },
|
|
495
|
+
apps: normalizeApps({ claude: false, codex: false, gemini: false, opencode: false }),
|
|
454
496
|
isBuiltin: false,
|
|
455
497
|
createdAt: Date.now(),
|
|
456
498
|
updatedAt: Date.now()
|
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
|
|
5
5
|
function getRuntimeFilePath(proxyType) {
|
|
6
|
-
const ccToolDir = path.join(os.homedir(), '.
|
|
6
|
+
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
7
7
|
if (!fs.existsSync(ccToolDir)) {
|
|
8
8
|
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
9
9
|
}
|
|
@@ -42,7 +42,7 @@ class RepoScannerBase {
|
|
|
42
42
|
this.fileExtension = options.fileExtension || '.md';
|
|
43
43
|
this.defaultRepos = options.defaultRepos || [];
|
|
44
44
|
|
|
45
|
-
this.configDir = path.join(os.homedir(), '.
|
|
45
|
+
this.configDir = path.join(os.homedir(), '.cc-tool');
|
|
46
46
|
this.reposConfigPath = path.join(this.configDir, `${this.type}-repos.json`);
|
|
47
47
|
this.cachePath = path.join(this.configDir, `${this.type}-cache.json`);
|
|
48
48
|
|
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
|
|
6
|
-
const SECURITY_DIR = path.join(os.homedir(), '.
|
|
6
|
+
const SECURITY_DIR = path.join(os.homedir(), '.cc-tool');
|
|
7
7
|
const SECURITY_FILE = path.join(SECURITY_DIR, 'security.json');
|
|
8
8
|
|
|
9
9
|
const DEFAULT_SECURITY_CONFIG = {
|
|
@@ -19,22 +19,61 @@ const {
|
|
|
19
19
|
convertSkillToCodex,
|
|
20
20
|
convertSkillToClaude
|
|
21
21
|
} = require('./format-converter');
|
|
22
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'opencode'];
|
|
25
|
+
const OPENCODE_SKILL_NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
26
|
+
|
|
27
|
+
function normalizePlatform(platform) {
|
|
28
|
+
return SUPPORTED_PLATFORMS.includes(platform) ? platform : 'claude';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cloneRepos(repos = []) {
|
|
32
|
+
return repos.map(repo => ({ ...repo }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_REPOS_BY_PLATFORM = {
|
|
36
|
+
claude: [
|
|
37
|
+
{ owner: 'anthropics', name: 'skills', branch: 'main', directory: '', enabled: true }
|
|
38
|
+
],
|
|
39
|
+
codex: [
|
|
40
|
+
{ owner: 'openai', name: 'skills', branch: 'main', directory: 'skills/.curated', enabled: true }
|
|
41
|
+
],
|
|
42
|
+
opencode: [
|
|
43
|
+
{ owner: 'darrenhinde', name: 'OpenAgentsControl', branch: 'main', directory: '.opencode/skill', enabled: true }
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const PLATFORM_CONFIG = {
|
|
48
|
+
claude: {
|
|
49
|
+
installDir: path.join(os.homedir(), '.claude', 'skills'),
|
|
50
|
+
reposFile: 'skill-repos.json',
|
|
51
|
+
cacheFile: 'skills-cache.json'
|
|
52
|
+
},
|
|
53
|
+
codex: {
|
|
54
|
+
installDir: path.join(os.homedir(), '.codex', 'skills'),
|
|
55
|
+
reposFile: 'codex-skill-repos.json',
|
|
56
|
+
cacheFile: 'codex-skills-cache.json'
|
|
57
|
+
},
|
|
58
|
+
opencode: {
|
|
59
|
+
installDir: path.join(NATIVE_PATHS.opencode.config, 'skills'),
|
|
60
|
+
reposFile: 'opencode-skill-repos.json',
|
|
61
|
+
cacheFile: 'opencode-skills-cache.json'
|
|
62
|
+
}
|
|
63
|
+
};
|
|
28
64
|
|
|
29
65
|
// 缓存有效期(5分钟)
|
|
30
66
|
const CACHE_TTL = 5 * 60 * 1000;
|
|
31
67
|
|
|
32
68
|
class SkillService {
|
|
33
|
-
constructor() {
|
|
34
|
-
this.
|
|
35
|
-
this.configDir = path.join(os.homedir(), '.
|
|
36
|
-
|
|
37
|
-
|
|
69
|
+
constructor(platform = 'claude') {
|
|
70
|
+
this.platform = normalizePlatform(platform);
|
|
71
|
+
this.configDir = path.join(os.homedir(), '.cc-tool');
|
|
72
|
+
|
|
73
|
+
const platformConfig = PLATFORM_CONFIG[this.platform];
|
|
74
|
+
this.installDir = platformConfig.installDir;
|
|
75
|
+
this.reposConfigPath = path.join(this.configDir, platformConfig.reposFile);
|
|
76
|
+
this.cachePath = path.join(this.configDir, platformConfig.cacheFile);
|
|
38
77
|
|
|
39
78
|
// 内存缓存
|
|
40
79
|
this.skillsCache = null;
|
|
@@ -60,12 +99,14 @@ class SkillService {
|
|
|
60
99
|
try {
|
|
61
100
|
if (fs.existsSync(this.reposConfigPath)) {
|
|
62
101
|
const data = JSON.parse(fs.readFileSync(this.reposConfigPath, 'utf-8'));
|
|
63
|
-
|
|
102
|
+
if (Array.isArray(data.repos)) {
|
|
103
|
+
return data.repos;
|
|
104
|
+
}
|
|
64
105
|
}
|
|
65
106
|
} catch (err) {
|
|
66
107
|
console.error('[SkillService] Load repos config error:', err.message);
|
|
67
108
|
}
|
|
68
|
-
return
|
|
109
|
+
return cloneRepos(DEFAULT_REPOS_BY_PLATFORM[this.platform] || DEFAULT_REPOS_BY_PLATFORM.claude);
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
/**
|
|
@@ -624,6 +665,46 @@ class SkillService {
|
|
|
624
665
|
};
|
|
625
666
|
}
|
|
626
667
|
|
|
668
|
+
normalizeSkillDirectoryName(directory) {
|
|
669
|
+
if (!directory) return '';
|
|
670
|
+
return String(directory).replace(/\\/g, '/').split('/').pop();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
validateOpenCodeSkillMetadata({ name, description }, directory) {
|
|
674
|
+
const expectedName = this.normalizeSkillDirectoryName(directory);
|
|
675
|
+
const normalizedName = typeof name === 'string' ? name.trim() : '';
|
|
676
|
+
const normalizedDescription = typeof description === 'string' ? description.trim() : '';
|
|
677
|
+
|
|
678
|
+
if (!expectedName) {
|
|
679
|
+
return '技能目录不能为空';
|
|
680
|
+
}
|
|
681
|
+
if (!normalizedName) {
|
|
682
|
+
return 'SKILL.md frontmatter 缺少 name';
|
|
683
|
+
}
|
|
684
|
+
if (!normalizedDescription) {
|
|
685
|
+
return 'SKILL.md frontmatter 缺少 description';
|
|
686
|
+
}
|
|
687
|
+
if (normalizedName.length < 1 || normalizedName.length > 64) {
|
|
688
|
+
return 'name 必须为 1-64 个字符';
|
|
689
|
+
}
|
|
690
|
+
if (!OPENCODE_SKILL_NAME_REGEX.test(normalizedName)) {
|
|
691
|
+
return 'name 必须为小写字母/数字,并使用单个连字符连接';
|
|
692
|
+
}
|
|
693
|
+
if (normalizedName !== expectedName) {
|
|
694
|
+
return `name 必须与目录名一致(期望: ${expectedName})`;
|
|
695
|
+
}
|
|
696
|
+
if (normalizedDescription.length < 1 || normalizedDescription.length > 1024) {
|
|
697
|
+
return 'description 必须为 1-1024 个字符';
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
validateOpenCodeSkillContent(content, directory) {
|
|
704
|
+
const metadata = this.parseSkillMd(content);
|
|
705
|
+
return this.validateOpenCodeSkillMetadata(metadata, directory);
|
|
706
|
+
}
|
|
707
|
+
|
|
627
708
|
/**
|
|
628
709
|
* 转换技能格式
|
|
629
710
|
* @param {string} content - 技能内容
|
|
@@ -799,6 +880,22 @@ class SkillService {
|
|
|
799
880
|
fs.mkdirSync(dest, { recursive: true });
|
|
800
881
|
this.copyDirRecursive(sourceDir, dest);
|
|
801
882
|
|
|
883
|
+
if (this.platform === 'codex') {
|
|
884
|
+
this.convertInstalledSkillToCodex(dest);
|
|
885
|
+
} else if (this.platform === 'opencode') {
|
|
886
|
+
const skillMdPath = path.join(dest, 'SKILL.md');
|
|
887
|
+
if (fs.existsSync(skillMdPath)) {
|
|
888
|
+
const validationError = this.validateOpenCodeSkillContent(
|
|
889
|
+
fs.readFileSync(skillMdPath, 'utf-8'),
|
|
890
|
+
directory
|
|
891
|
+
);
|
|
892
|
+
if (validationError) {
|
|
893
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
894
|
+
throw new Error(`OpenCode skill 格式不符合要求: ${validationError}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
802
899
|
// 清除缓存,让列表刷新
|
|
803
900
|
this.skillsCache = null;
|
|
804
901
|
this.cacheTime = 0;
|
|
@@ -879,24 +976,73 @@ class SkillService {
|
|
|
879
976
|
}
|
|
880
977
|
}
|
|
881
978
|
|
|
979
|
+
/**
|
|
980
|
+
* 将安装后的 SKILL.md 转换为 Codex 兼容格式
|
|
981
|
+
*/
|
|
982
|
+
convertInstalledSkillToCodex(skillDir) {
|
|
983
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
984
|
+
if (!fs.existsSync(skillMdPath)) return;
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
988
|
+
const converted = convertSkillToCodex(content);
|
|
989
|
+
fs.writeFileSync(skillMdPath, converted.content, 'utf-8');
|
|
990
|
+
} catch (err) {
|
|
991
|
+
console.warn('[SkillService] Convert skill to codex format failed:', err.message);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
882
995
|
/**
|
|
883
996
|
* 创建自定义技能
|
|
884
997
|
*/
|
|
885
998
|
createCustomSkill({ name, directory, description, content }) {
|
|
886
999
|
const dest = path.join(this.installDir, directory);
|
|
1000
|
+
const normalizedDirectory = this.normalizeSkillDirectoryName(directory);
|
|
887
1001
|
|
|
888
1002
|
// 检查是否已存在
|
|
889
1003
|
if (fs.existsSync(dest)) {
|
|
890
1004
|
throw new Error(`技能目录 "${directory}" 已存在`);
|
|
891
1005
|
}
|
|
892
1006
|
|
|
1007
|
+
if (this.platform === 'opencode') {
|
|
1008
|
+
if (!OPENCODE_SKILL_NAME_REGEX.test(normalizedDirectory)) {
|
|
1009
|
+
throw new Error('OpenCode skill 目录名必须是小写字母/数字,并使用单个连字符连接');
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const normalizedDescription = (description || '').trim();
|
|
1014
|
+
const skillName = this.platform === 'opencode'
|
|
1015
|
+
? normalizedDirectory
|
|
1016
|
+
: (name || directory);
|
|
1017
|
+
|
|
1018
|
+
if (this.platform === 'opencode') {
|
|
1019
|
+
const validationError = this.validateOpenCodeSkillMetadata(
|
|
1020
|
+
{
|
|
1021
|
+
name: skillName,
|
|
1022
|
+
description: normalizedDescription
|
|
1023
|
+
},
|
|
1024
|
+
normalizedDirectory
|
|
1025
|
+
);
|
|
1026
|
+
if (validationError) {
|
|
1027
|
+
throw new Error(`OpenCode skill 格式不符合要求: ${validationError}`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
893
1031
|
// 创建目录
|
|
894
1032
|
fs.mkdirSync(dest, { recursive: true });
|
|
895
1033
|
|
|
896
1034
|
// 生成 SKILL.md 内容
|
|
897
|
-
const skillMdContent =
|
|
898
|
-
|
|
899
|
-
|
|
1035
|
+
const skillMdContent = this.platform === 'opencode'
|
|
1036
|
+
? `---
|
|
1037
|
+
name: ${skillName}
|
|
1038
|
+
description: "${normalizedDescription}"
|
|
1039
|
+
---
|
|
1040
|
+
|
|
1041
|
+
${content}
|
|
1042
|
+
`
|
|
1043
|
+
: `---
|
|
1044
|
+
name: "${skillName}"
|
|
1045
|
+
description: "${normalizedDescription}"
|
|
900
1046
|
---
|
|
901
1047
|
|
|
902
1048
|
${content}
|
|
@@ -905,6 +1051,10 @@ ${content}
|
|
|
905
1051
|
// 写入文件
|
|
906
1052
|
fs.writeFileSync(path.join(dest, 'SKILL.md'), skillMdContent, 'utf-8');
|
|
907
1053
|
|
|
1054
|
+
if (this.platform === 'codex') {
|
|
1055
|
+
this.convertInstalledSkillToCodex(dest);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
908
1058
|
// 清除缓存,让列表刷新
|
|
909
1059
|
this.skillsCache = null;
|
|
910
1060
|
this.cacheTime = 0;
|
|
@@ -920,6 +1070,7 @@ ${content}
|
|
|
920
1070
|
*/
|
|
921
1071
|
createSkillWithFiles({ directory, files }) {
|
|
922
1072
|
const dest = path.join(this.installDir, directory);
|
|
1073
|
+
const normalizedDirectory = this.normalizeSkillDirectoryName(directory);
|
|
923
1074
|
|
|
924
1075
|
// 检查是否已存在
|
|
925
1076
|
if (fs.existsSync(dest)) {
|
|
@@ -934,6 +1085,21 @@ ${content}
|
|
|
934
1085
|
throw new Error('技能必须包含 SKILL.md 文件');
|
|
935
1086
|
}
|
|
936
1087
|
|
|
1088
|
+
if (this.platform === 'opencode') {
|
|
1089
|
+
if (!OPENCODE_SKILL_NAME_REGEX.test(normalizedDirectory)) {
|
|
1090
|
+
throw new Error('OpenCode skill 目录名必须是小写字母/数字,并使用单个连字符连接');
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const skillMdFile = files.find(f => f.path === 'SKILL.md' || f.path.endsWith('/SKILL.md'));
|
|
1094
|
+
const skillMdContent = skillMdFile
|
|
1095
|
+
? (skillMdFile.isBase64 ? Buffer.from(skillMdFile.content, 'base64').toString('utf-8') : skillMdFile.content)
|
|
1096
|
+
: '';
|
|
1097
|
+
const validationError = this.validateOpenCodeSkillContent(skillMdContent, normalizedDirectory);
|
|
1098
|
+
if (validationError) {
|
|
1099
|
+
throw new Error(`OpenCode skill 格式不符合要求: ${validationError}`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
937
1103
|
// 创建目录
|
|
938
1104
|
fs.mkdirSync(dest, { recursive: true });
|
|
939
1105
|
|
|
@@ -956,6 +1122,10 @@ ${content}
|
|
|
956
1122
|
}
|
|
957
1123
|
}
|
|
958
1124
|
|
|
1125
|
+
if (this.platform === 'codex') {
|
|
1126
|
+
this.convertInstalledSkillToCodex(dest);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
959
1129
|
// 清除缓存
|
|
960
1130
|
this.skillsCache = null;
|
|
961
1131
|
this.cacheTime = 0;
|
|
@@ -1060,11 +1230,25 @@ ${content}
|
|
|
1060
1230
|
*/
|
|
1061
1231
|
addSkillFiles(directory, files) {
|
|
1062
1232
|
const skillPath = path.join(this.installDir, directory);
|
|
1233
|
+
const normalizedDirectory = this.normalizeSkillDirectoryName(directory);
|
|
1063
1234
|
|
|
1064
1235
|
if (!fs.existsSync(skillPath)) {
|
|
1065
1236
|
throw new Error(`技能 "${directory}" 不存在`);
|
|
1066
1237
|
}
|
|
1067
1238
|
|
|
1239
|
+
if (this.platform === 'opencode') {
|
|
1240
|
+
const incomingSkillMd = files.find(f => f.path === 'SKILL.md' || f.path.endsWith('/SKILL.md'));
|
|
1241
|
+
if (incomingSkillMd) {
|
|
1242
|
+
const content = incomingSkillMd.isBase64
|
|
1243
|
+
? Buffer.from(incomingSkillMd.content, 'base64').toString('utf-8')
|
|
1244
|
+
: incomingSkillMd.content;
|
|
1245
|
+
const validationError = this.validateOpenCodeSkillContent(content, normalizedDirectory);
|
|
1246
|
+
if (validationError) {
|
|
1247
|
+
throw new Error(`OpenCode skill 格式不符合要求: ${validationError}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1068
1252
|
const added = [];
|
|
1069
1253
|
for (const file of files) {
|
|
1070
1254
|
const filePath = path.join(skillPath, file.path);
|
|
@@ -1137,6 +1321,7 @@ ${content}
|
|
|
1137
1321
|
*/
|
|
1138
1322
|
updateSkillFile(directory, filePath, content, isBase64 = false) {
|
|
1139
1323
|
const skillPath = path.join(this.installDir, directory);
|
|
1324
|
+
const normalizedDirectory = this.normalizeSkillDirectoryName(directory);
|
|
1140
1325
|
|
|
1141
1326
|
if (!fs.existsSync(skillPath)) {
|
|
1142
1327
|
throw new Error(`技能 "${directory}" 不存在`);
|
|
@@ -1148,6 +1333,14 @@ ${content}
|
|
|
1148
1333
|
throw new Error(`文件 "${filePath}" 不存在`);
|
|
1149
1334
|
}
|
|
1150
1335
|
|
|
1336
|
+
if (this.platform === 'opencode' && /(^|\/)SKILL\.md$/i.test(filePath)) {
|
|
1337
|
+
const textContent = isBase64 ? Buffer.from(content, 'base64').toString('utf-8') : content;
|
|
1338
|
+
const validationError = this.validateOpenCodeSkillContent(textContent, normalizedDirectory);
|
|
1339
|
+
if (validationError) {
|
|
1340
|
+
throw new Error(`OpenCode skill 格式不符合要求: ${validationError}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1151
1344
|
if (isBase64) {
|
|
1152
1345
|
fs.writeFileSync(fullPath, Buffer.from(content, 'base64'));
|
|
1153
1346
|
} else {
|
|
@@ -1205,48 +1398,108 @@ ${content}
|
|
|
1205
1398
|
};
|
|
1206
1399
|
}
|
|
1207
1400
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1401
|
+
const normalizeRepoPath = (input = '') =>
|
|
1402
|
+
String(input)
|
|
1403
|
+
.replace(/\\/g, '/')
|
|
1404
|
+
.replace(/^\/+/, '')
|
|
1405
|
+
.replace(/\/+$/, '');
|
|
1210
1406
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
owner: cachedSkill.repoOwner,
|
|
1216
|
-
name: cachedSkill.repoName,
|
|
1217
|
-
branch: cachedSkill.repoBranch || 'main'
|
|
1218
|
-
};
|
|
1407
|
+
const parseRemoteSkillContent = (content, repo) => {
|
|
1408
|
+
const metadata = this.parseSkillMd(content);
|
|
1409
|
+
const bodyMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
|
|
1410
|
+
const body = bodyMatch ? bodyMatch[1].trim() : content;
|
|
1219
1411
|
|
|
1220
|
-
|
|
1412
|
+
return {
|
|
1413
|
+
directory,
|
|
1414
|
+
name: metadata.name || directory,
|
|
1415
|
+
description: metadata.description || '',
|
|
1416
|
+
content: body,
|
|
1417
|
+
fullContent: content,
|
|
1418
|
+
installed: false,
|
|
1419
|
+
source: 'github',
|
|
1420
|
+
repoOwner: repo.owner,
|
|
1421
|
+
repoName: repo.name
|
|
1422
|
+
};
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
const tryLoadRemoteDetailFromRepo = async (repo, extraCandidateDirs = []) => {
|
|
1426
|
+
try {
|
|
1221
1427
|
const treeUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/git/trees/${repo.branch}?recursive=1`;
|
|
1222
1428
|
const tree = await this.fetchGitHubApi(treeUrl);
|
|
1429
|
+
if (!tree?.tree) return null;
|
|
1223
1430
|
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
);
|
|
1431
|
+
const normalizedDirectory = normalizeRepoPath(directory);
|
|
1432
|
+
const candidateDirs = new Set();
|
|
1433
|
+
candidateDirs.add(normalizedDirectory);
|
|
1227
1434
|
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1435
|
+
for (const candidate of extraCandidateDirs) {
|
|
1436
|
+
const normalized = normalizeRepoPath(candidate);
|
|
1437
|
+
if (normalized) candidateDirs.add(normalized);
|
|
1438
|
+
}
|
|
1231
1439
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1440
|
+
if (repo.directory) {
|
|
1441
|
+
candidateDirs.add(normalizeRepoPath(`${repo.directory}/${normalizedDirectory}`));
|
|
1442
|
+
}
|
|
1234
1443
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1444
|
+
let skillFile = null;
|
|
1445
|
+
for (const candidateDir of candidateDirs) {
|
|
1446
|
+
if (!candidateDir) continue;
|
|
1447
|
+
skillFile = tree.tree.find(item =>
|
|
1448
|
+
item.type === 'blob' && item.path === `${candidateDir}/SKILL.md`
|
|
1449
|
+
);
|
|
1450
|
+
if (skillFile) break;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (!skillFile) {
|
|
1454
|
+
const targetBaseName = normalizedDirectory.split('/').pop();
|
|
1455
|
+
skillFile = tree.tree.find(item => {
|
|
1456
|
+
if (item.type !== 'blob' || !item.path.endsWith('/SKILL.md')) return false;
|
|
1457
|
+
const parts = item.path.split('/');
|
|
1458
|
+
const parentDir = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
1459
|
+
return parentDir === targetBaseName;
|
|
1460
|
+
});
|
|
1246
1461
|
}
|
|
1462
|
+
|
|
1463
|
+
if (!skillFile) return null;
|
|
1464
|
+
|
|
1465
|
+
const content = await this.fetchBlobContent(skillFile.sha, repo, skillFile.path);
|
|
1466
|
+
return parseRemoteSkillContent(content, repo);
|
|
1247
1467
|
} catch (err) {
|
|
1248
1468
|
console.warn('[SkillService] Fetch remote skill detail error:', err.message);
|
|
1469
|
+
return null;
|
|
1249
1470
|
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
// 先尝试使用缓存中的 repo 信息(最快)
|
|
1474
|
+
const cachedSkill = this.skillsCache?.find(s => s.directory === directory);
|
|
1475
|
+
if (cachedSkill && cachedSkill.repoOwner && cachedSkill.repoName) {
|
|
1476
|
+
const cachedRepo = {
|
|
1477
|
+
owner: cachedSkill.repoOwner,
|
|
1478
|
+
name: cachedSkill.repoName,
|
|
1479
|
+
branch: cachedSkill.repoBranch || 'main',
|
|
1480
|
+
directory: cachedSkill.repoDirectory || ''
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
const detail = await tryLoadRemoteDetailFromRepo(cachedRepo, [
|
|
1484
|
+
cachedSkill.fullDirectory || '',
|
|
1485
|
+
cachedSkill.repoDirectory ? `${cachedSkill.repoDirectory}/${directory}` : ''
|
|
1486
|
+
]);
|
|
1487
|
+
if (detail) return detail;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// 缓存缺失或过期时,回退到遍历仓库配置,避免详情页报错
|
|
1491
|
+
const repos = this.loadRepos().filter(repo => repo.enabled !== false);
|
|
1492
|
+
for (const repo of repos) {
|
|
1493
|
+
const detail = await tryLoadRemoteDetailFromRepo(
|
|
1494
|
+
{
|
|
1495
|
+
owner: repo.owner,
|
|
1496
|
+
name: repo.name,
|
|
1497
|
+
branch: repo.branch || 'main',
|
|
1498
|
+
directory: repo.directory || ''
|
|
1499
|
+
},
|
|
1500
|
+
[repo.directory ? `${repo.directory}/${directory}` : '']
|
|
1501
|
+
);
|
|
1502
|
+
if (detail) return detail;
|
|
1250
1503
|
}
|
|
1251
1504
|
|
|
1252
1505
|
throw new Error('技能不存在或无法获取');
|
|
@@ -1264,5 +1517,6 @@ ${content}
|
|
|
1264
1517
|
|
|
1265
1518
|
module.exports = {
|
|
1266
1519
|
SkillService,
|
|
1267
|
-
DEFAULT_REPOS
|
|
1520
|
+
DEFAULT_REPOS: DEFAULT_REPOS_BY_PLATFORM.claude,
|
|
1521
|
+
DEFAULT_REPOS_BY_PLATFORM
|
|
1268
1522
|
};
|