@adversity/coding-tool-x 3.1.0 → 3.1.2

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 (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.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 +81 -12
  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/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -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
  };