@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.
Files changed (133) hide show
  1. package/CHANGELOG.md +38 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +92 -13
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/ui.js +8 -1
  45. package/src/commands/update.js +97 -0
  46. package/src/commands/workspace.js +1 -1
  47. package/src/config/default.js +39 -2
  48. package/src/config/loader.js +74 -8
  49. package/src/config/paths.js +105 -33
  50. package/src/index.js +67 -4
  51. package/src/plugins/constants.js +3 -2
  52. package/src/plugins/plugin-api.js +1 -1
  53. package/src/reset-config.js +4 -2
  54. package/src/server/api/agents.js +57 -14
  55. package/src/server/api/channels.js +112 -33
  56. package/src/server/api/codex-channels.js +111 -18
  57. package/src/server/api/codex-proxy.js +14 -8
  58. package/src/server/api/commands.js +71 -18
  59. package/src/server/api/config-export.js +0 -6
  60. package/src/server/api/config-registry.js +11 -3
  61. package/src/server/api/config.js +376 -5
  62. package/src/server/api/convert.js +133 -0
  63. package/src/server/api/dashboard.js +22 -6
  64. package/src/server/api/gemini-channels.js +107 -18
  65. package/src/server/api/gemini-proxy.js +14 -8
  66. package/src/server/api/gemini-sessions.js +1 -1
  67. package/src/server/api/health-check.js +4 -3
  68. package/src/server/api/mcp.js +3 -3
  69. package/src/server/api/opencode-channels.js +419 -0
  70. package/src/server/api/opencode-projects.js +99 -0
  71. package/src/server/api/opencode-proxy.js +198 -0
  72. package/src/server/api/opencode-sessions.js +403 -0
  73. package/src/server/api/opencode-statistics.js +57 -0
  74. package/src/server/api/plugins.js +66 -19
  75. package/src/server/api/prompts.js +2 -2
  76. package/src/server/api/proxy.js +7 -4
  77. package/src/server/api/sessions.js +3 -0
  78. package/src/server/api/skills.js +69 -18
  79. package/src/server/api/workspaces.js +78 -6
  80. package/src/server/codex-proxy-server.js +32 -19
  81. package/src/server/dev-server.js +1 -1
  82. package/src/server/gemini-proxy-server.js +17 -3
  83. package/src/server/index.js +164 -48
  84. package/src/server/opencode-proxy-server.js +4375 -0
  85. package/src/server/proxy-server.js +30 -19
  86. package/src/server/services/agents-service.js +61 -24
  87. package/src/server/services/channel-scheduler.js +9 -5
  88. package/src/server/services/channels.js +70 -12
  89. package/src/server/services/codex-channels.js +61 -23
  90. package/src/server/services/codex-settings-manager.js +271 -49
  91. package/src/server/services/codex-statistics-service.js +2 -2
  92. package/src/server/services/commands-service.js +84 -25
  93. package/src/server/services/config-export-service.js +7 -45
  94. package/src/server/services/config-registry-service.js +63 -17
  95. package/src/server/services/config-sync-manager.js +160 -7
  96. package/src/server/services/config-templates-service.js +204 -51
  97. package/src/server/services/env-checker.js +26 -12
  98. package/src/server/services/env-manager.js +126 -18
  99. package/src/server/services/favorites.js +5 -3
  100. package/src/server/services/gemini-channels.js +37 -15
  101. package/src/server/services/gemini-statistics-service.js +2 -2
  102. package/src/server/services/mcp-service.js +350 -9
  103. package/src/server/services/model-detector.js +707 -221
  104. package/src/server/services/network-access.js +80 -0
  105. package/src/server/services/opencode-channels.js +206 -0
  106. package/src/server/services/opencode-gateway-converter.js +639 -0
  107. package/src/server/services/opencode-sessions.js +663 -0
  108. package/src/server/services/opencode-settings-manager.js +342 -0
  109. package/src/server/services/opencode-statistics-service.js +255 -0
  110. package/src/server/services/plugins-service.js +479 -22
  111. package/src/server/services/prompts-service.js +53 -11
  112. package/src/server/services/proxy-runtime.js +1 -1
  113. package/src/server/services/repo-scanner-base.js +1 -1
  114. package/src/server/services/security-config.js +1 -1
  115. package/src/server/services/session-cache.js +1 -1
  116. package/src/server/services/skill-service.js +300 -46
  117. package/src/server/services/speed-test.js +464 -186
  118. package/src/server/services/statistics-service.js +2 -2
  119. package/src/server/services/terminal-commands.js +10 -3
  120. package/src/server/services/terminal-config.js +1 -1
  121. package/src/server/services/ui-config.js +1 -1
  122. package/src/server/services/workspace-service.js +57 -100
  123. package/src/server/websocket-server.js +132 -3
  124. package/src/ui/menu.js +49 -40
  125. package/src/utils/port-helper.js +22 -8
  126. package/src/utils/session.js +5 -4
  127. package/dist/web/assets/icons-BxudHPiX.js +0 -1
  128. package/dist/web/assets/index-D2VfwJBa.js +0 -14
  129. package/dist/web/assets/index-oXBzu0bd.css +0 -41
  130. package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
  131. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  132. package/src/server/api/permissions.js +0 -385
  133. 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(), '.claude', 'cc-tool');
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
- if (!preset.apps) {
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 { apps, content } = preset;
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(), '.claude', 'cc-tool');
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(), '.claude', 'cc-tool');
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(), '.claude', 'cc-tool');
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 = {
@@ -11,7 +11,7 @@ let hasMessagesPersisted = {};
11
11
  let hasMessagesPersistTimer = null;
12
12
 
13
13
  function getCcToolDir() {
14
- return path.join(os.homedir(), '.claude', 'cc-tool');
14
+ return path.join(os.homedir(), '.cc-tool');
15
15
  }
16
16
 
17
17
  function ensureDirExists(dir) {
@@ -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
- // directory 字段支持指定仓库子目录
25
- const DEFAULT_REPOS = [
26
- { owner: 'anthropics', name: 'skills', branch: 'main', directory: '', enabled: true }
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.installDir = path.join(os.homedir(), '.claude', 'skills');
35
- this.configDir = path.join(os.homedir(), '.claude', 'cc-tool');
36
- this.reposConfigPath = path.join(this.configDir, 'skill-repos.json');
37
- this.cachePath = path.join(this.configDir, 'skills-cache.json');
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
- return data.repos || DEFAULT_REPOS;
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 DEFAULT_REPOS;
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
- name: "${name}"
899
- description: "${description}"
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
- const cachedSkill = this.skillsCache?.find(s => s.directory === directory);
1401
+ const normalizeRepoPath = (input = '') =>
1402
+ String(input)
1403
+ .replace(/\\/g, '/')
1404
+ .replace(/^\/+/, '')
1405
+ .replace(/\/+$/, '');
1210
1406
 
1211
- if (cachedSkill && cachedSkill.repoOwner && cachedSkill.repoName) {
1212
- // GitHub 获取内容
1213
- try {
1214
- const repo = {
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
- // 获取文件树找到 SKILL.md 的 SHA
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 skillFile = tree.tree?.find(item =>
1225
- item.type === 'blob' && item.path === `${directory}/SKILL.md`
1226
- );
1431
+ const normalizedDirectory = normalizeRepoPath(directory);
1432
+ const candidateDirs = new Set();
1433
+ candidateDirs.add(normalizedDirectory);
1227
1434
 
1228
- if (skillFile) {
1229
- const content = await this.fetchBlobContent(skillFile.sha, repo, skillFile.path);
1230
- const metadata = this.parseSkillMd(content);
1435
+ for (const candidate of extraCandidateDirs) {
1436
+ const normalized = normalizeRepoPath(candidate);
1437
+ if (normalized) candidateDirs.add(normalized);
1438
+ }
1231
1439
 
1232
- const bodyMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
1233
- const body = bodyMatch ? bodyMatch[1].trim() : content;
1440
+ if (repo.directory) {
1441
+ candidateDirs.add(normalizeRepoPath(`${repo.directory}/${normalizedDirectory}`));
1442
+ }
1234
1443
 
1235
- return {
1236
- directory,
1237
- name: metadata.name || directory,
1238
- description: metadata.description || '',
1239
- content: body,
1240
- fullContent: content,
1241
- installed: false,
1242
- source: 'github',
1243
- repoOwner: repo.owner,
1244
- repoName: repo.name
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
  };