@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
@@ -8,7 +8,24 @@ const express = require('express');
8
8
  const { PluginsService } = require('../services/plugins-service');
9
9
 
10
10
  const router = express.Router();
11
- const pluginsService = new PluginsService();
11
+ const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
12
+ const pluginServices = new Map();
13
+
14
+ function resolvePlatform(rawPlatform) {
15
+ return SUPPORTED_PLATFORMS.includes(rawPlatform) ? rawPlatform : 'claude';
16
+ }
17
+
18
+ function getPlatform(req) {
19
+ return resolvePlatform(req.query?.platform || req.body?.platform);
20
+ }
21
+
22
+ function getPluginsService(req) {
23
+ const platform = getPlatform(req);
24
+ if (!pluginServices.has(platform)) {
25
+ pluginServices.set(platform, new PluginsService(platform));
26
+ }
27
+ return { platform, service: pluginServices.get(platform) };
28
+ }
12
29
 
13
30
  /**
14
31
  * 获取插件列表
@@ -16,10 +33,12 @@ const pluginsService = new PluginsService();
16
33
  */
17
34
  router.get('/', (req, res) => {
18
35
  try {
19
- const result = pluginsService.listPlugins();
36
+ const { platform, service } = getPluginsService(req);
37
+ const result = service.listPlugins();
20
38
 
21
39
  res.json({
22
40
  success: true,
41
+ platform,
23
42
  ...result
24
43
  });
25
44
  } catch (err) {
@@ -37,10 +56,12 @@ router.get('/', (req, res) => {
37
56
  */
38
57
  router.get('/market', async (req, res) => {
39
58
  try {
40
- const plugins = await pluginsService.getMarketPlugins();
59
+ const { platform, service } = getPluginsService(req);
60
+ const plugins = await service.getMarketPlugins();
41
61
 
42
62
  res.json({
43
63
  success: true,
64
+ platform,
44
65
  plugins
45
66
  });
46
67
  } catch (err) {
@@ -55,26 +76,29 @@ router.get('/market', async (req, res) => {
55
76
  /**
56
77
  * 安装插件
57
78
  * POST /api/plugins/install
58
- * Body: { directory, repo: { owner, name, branch } }
79
+ * Body: { directory, repo: { owner, name, branch } } or { source }
59
80
  */
60
81
  router.post('/install', async (req, res) => {
61
82
  try {
62
- const { directory, repo, gitUrl } = req.body;
83
+ const { platform, service } = getPluginsService(req);
84
+ const { directory, repo, gitUrl, source } = req.body;
63
85
 
64
86
  // Support both new format (directory + repo) and legacy format (gitUrl)
65
87
  let installUrl;
66
- if (directory && repo) {
88
+ if (source) {
89
+ installUrl = source;
90
+ } else if (directory && repo) {
67
91
  installUrl = `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch || 'main'}/${directory}`;
68
92
  } else if (gitUrl) {
69
93
  installUrl = gitUrl;
70
94
  } else {
71
95
  return res.status(400).json({
72
96
  success: false,
73
- message: 'Either (directory + repo) or gitUrl is required'
97
+ message: 'Either source, (directory + repo), or gitUrl is required'
74
98
  });
75
99
  }
76
100
 
77
- const result = await pluginsService.installPlugin(installUrl);
101
+ const result = await service.installPlugin(installUrl);
78
102
 
79
103
  if (!result.success) {
80
104
  return res.status(400).json({
@@ -85,6 +109,7 @@ router.post('/install', async (req, res) => {
85
109
 
86
110
  res.json({
87
111
  success: true,
112
+ platform,
88
113
  plugin: result.plugin,
89
114
  message: `Plugin "${result.plugin.name}" installed successfully`
90
115
  });
@@ -105,9 +130,11 @@ router.post('/install', async (req, res) => {
105
130
  */
106
131
  router.get('/repos', (req, res) => {
107
132
  try {
108
- const repos = pluginsService.getRepos();
133
+ const { platform, service } = getPluginsService(req);
134
+ const repos = service.getRepos();
109
135
  res.json({
110
136
  success: true,
137
+ platform,
111
138
  repos
112
139
  });
113
140
  } catch (err) {
@@ -126,6 +153,7 @@ router.get('/repos', (req, res) => {
126
153
  */
127
154
  router.post('/repos', (req, res) => {
128
155
  try {
156
+ const { platform, service } = getPluginsService(req);
129
157
  const repo = req.body;
130
158
 
131
159
  if (!repo || !repo.url) {
@@ -135,10 +163,11 @@ router.post('/repos', (req, res) => {
135
163
  });
136
164
  }
137
165
 
138
- const repos = pluginsService.addRepo(repo);
166
+ const repos = service.addRepo(repo);
139
167
 
140
168
  res.json({
141
169
  success: true,
170
+ platform,
142
171
  repos,
143
172
  message: 'Repository added successfully'
144
173
  });
@@ -157,12 +186,14 @@ router.post('/repos', (req, res) => {
157
186
  */
158
187
  router.delete('/repos/:owner/:name', (req, res) => {
159
188
  try {
189
+ const { platform, service } = getPluginsService(req);
160
190
  const { owner, name } = req.params;
161
191
 
162
- const repos = pluginsService.removeRepo(owner, name);
192
+ const repos = service.removeRepo(owner, name);
163
193
 
164
194
  res.json({
165
195
  success: true,
196
+ platform,
166
197
  repos,
167
198
  message: 'Repository removed successfully'
168
199
  });
@@ -182,6 +213,7 @@ router.delete('/repos/:owner/:name', (req, res) => {
182
213
  */
183
214
  router.put('/repos/:owner/:name/toggle', (req, res) => {
184
215
  try {
216
+ const { platform, service } = getPluginsService(req);
185
217
  const { owner, name } = req.params;
186
218
  const { enabled } = req.body;
187
219
 
@@ -192,10 +224,11 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
192
224
  });
193
225
  }
194
226
 
195
- const repos = pluginsService.toggleRepo(owner, name, enabled);
227
+ const repos = service.toggleRepo(owner, name, enabled);
196
228
 
197
229
  res.json({
198
230
  success: true,
231
+ platform,
199
232
  repos,
200
233
  message: `Repository ${enabled ? 'enabled' : 'disabled'} successfully`
201
234
  });
@@ -214,10 +247,12 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
214
247
  */
215
248
  router.post('/repos/sync', async (req, res) => {
216
249
  try {
217
- const result = await pluginsService.syncRepos();
250
+ const { platform, service } = getPluginsService(req);
251
+ const result = await service.syncRepos();
218
252
 
219
253
  res.json({
220
254
  success: true,
255
+ platform,
221
256
  ...result,
222
257
  message: 'Repositories synced successfully'
223
258
  });
@@ -236,10 +271,12 @@ router.post('/repos/sync', async (req, res) => {
236
271
  */
237
272
  router.post('/sync', async (req, res) => {
238
273
  try {
239
- const result = await pluginsService.syncPlugins();
274
+ const { platform, service } = getPluginsService(req);
275
+ const result = await service.syncPlugins();
240
276
 
241
277
  res.json({
242
278
  success: true,
279
+ platform,
243
280
  ...result,
244
281
  message: 'Plugins synced successfully'
245
282
  });
@@ -259,6 +296,7 @@ router.post('/sync', async (req, res) => {
259
296
  */
260
297
  router.get('/:name/readme', async (req, res) => {
261
298
  try {
299
+ const { platform, service } = getPluginsService(req);
262
300
  const { name } = req.params;
263
301
  const { repoOwner, repoName, repoBranch, directory, source, repoUrl } = req.query;
264
302
 
@@ -272,10 +310,11 @@ router.get('/:name/readme', async (req, res) => {
272
310
  repoUrl
273
311
  };
274
312
 
275
- const readme = await pluginsService.getPluginReadme(pluginInfo);
313
+ const readme = await service.getPluginReadme(pluginInfo);
276
314
 
277
315
  res.json({
278
316
  success: true,
317
+ platform,
279
318
  readme
280
319
  });
281
320
  } catch (err) {
@@ -294,9 +333,10 @@ router.get('/:name/readme', async (req, res) => {
294
333
  */
295
334
  router.get('/:name', (req, res) => {
296
335
  try {
336
+ const { platform, service } = getPluginsService(req);
297
337
  const { name } = req.params;
298
338
 
299
- const plugin = pluginsService.getPlugin(name);
339
+ const plugin = service.getPlugin(name);
300
340
 
301
341
  if (!plugin) {
302
342
  return res.status(404).json({
@@ -307,6 +347,7 @@ router.get('/:name', (req, res) => {
307
347
 
308
348
  res.json({
309
349
  success: true,
350
+ platform,
310
351
  plugin
311
352
  });
312
353
  } catch (err) {
@@ -324,9 +365,10 @@ router.get('/:name', (req, res) => {
324
365
  */
325
366
  router.delete('/:name', (req, res) => {
326
367
  try {
368
+ const { platform, service } = getPluginsService(req);
327
369
  const { name } = req.params;
328
370
 
329
- const result = pluginsService.uninstallPlugin(name);
371
+ const result = service.uninstallPlugin(name);
330
372
 
331
373
  if (!result.success) {
332
374
  return res.status(400).json({
@@ -337,6 +379,7 @@ router.delete('/:name', (req, res) => {
337
379
 
338
380
  res.json({
339
381
  success: true,
382
+ platform,
340
383
  message: result.message
341
384
  });
342
385
  } catch (err) {
@@ -355,6 +398,7 @@ router.delete('/:name', (req, res) => {
355
398
  */
356
399
  router.put('/:name/toggle', (req, res) => {
357
400
  try {
401
+ const { platform, service } = getPluginsService(req);
358
402
  const { name } = req.params;
359
403
  const { enabled } = req.body;
360
404
 
@@ -365,10 +409,11 @@ router.put('/:name/toggle', (req, res) => {
365
409
  });
366
410
  }
367
411
 
368
- const plugin = pluginsService.togglePlugin(name, enabled);
412
+ const plugin = service.togglePlugin(name, enabled);
369
413
 
370
414
  res.json({
371
415
  success: true,
416
+ platform,
372
417
  plugin,
373
418
  message: `Plugin "${name}" ${enabled ? 'enabled' : 'disabled'} successfully`
374
419
  });
@@ -388,6 +433,7 @@ router.put('/:name/toggle', (req, res) => {
388
433
  */
389
434
  router.put('/:name/config', (req, res) => {
390
435
  try {
436
+ const { platform, service } = getPluginsService(req);
391
437
  const { name } = req.params;
392
438
  const { config } = req.body;
393
439
 
@@ -398,10 +444,11 @@ router.put('/:name/config', (req, res) => {
398
444
  });
399
445
  }
400
446
 
401
- const result = pluginsService.updatePluginConfig(name, config);
447
+ const result = service.updatePluginConfig(name, config);
402
448
 
403
449
  res.json({
404
450
  success: true,
451
+ platform,
405
452
  message: result.message
406
453
  });
407
454
  } catch (err) {
@@ -203,7 +203,7 @@ router.get('/platform/:platform', (req, res) => {
203
203
  try {
204
204
  const { platform } = req.params;
205
205
 
206
- if (!['claude', 'codex', 'gemini'].includes(platform)) {
206
+ if (!['claude', 'codex', 'gemini', 'opencode'].includes(platform)) {
207
207
  return res.status(400).json({
208
208
  success: false,
209
209
  error: `无效的平台: ${platform}`
@@ -234,7 +234,7 @@ router.post('/import/:platform', (req, res) => {
234
234
  const { platform } = req.params;
235
235
  const { name } = req.body;
236
236
 
237
- if (!['claude', 'codex', 'gemini'].includes(platform)) {
237
+ if (!['claude', 'codex', 'gemini', 'opencode'].includes(platform)) {
238
238
  return res.status(400).json({
239
239
  success: false,
240
240
  error: `无效的平台: ${platform}`
@@ -12,6 +12,7 @@ const {
12
12
  } = require('../services/settings-manager');
13
13
  const { getAllChannels } = require('../services/channels');
14
14
  const { clearAllLogs } = require('../websocket-server');
15
+ const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
15
16
  const fs = require('fs');
16
17
  const path = require('path');
17
18
  const os = require('os');
@@ -28,18 +29,20 @@ function sanitizeChannelForResponse(channel) {
28
29
 
29
30
  // 保存激活渠道ID
30
31
  function saveActiveChannelId(channelId) {
31
- const dir = path.join(os.homedir(), '.claude', 'cc-tool');
32
+ ensureStorageDirMigrated();
33
+ const filePath = PATHS.activeChannel.claude;
34
+ const dir = path.dirname(filePath);
32
35
  if (!fs.existsSync(dir)) {
33
36
  fs.mkdirSync(dir, { recursive: true });
34
37
  }
35
- const filePath = path.join(dir, 'active-channel.json');
36
38
  fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
37
39
  }
38
40
 
39
41
 
40
42
  // 加载上次激活的渠道ID
41
43
  function loadActiveChannelId() {
42
- const filePath = path.join(os.homedir(), '.claude', 'cc-tool', 'active-channel.json');
44
+ ensureStorageDirMigrated();
45
+ const filePath = PATHS.activeChannel.claude;
43
46
  try {
44
47
  if (fs.existsSync(filePath)) {
45
48
  const content = fs.readFileSync(filePath, 'utf8');
@@ -246,7 +249,7 @@ router.post('/stop', async (req, res) => {
246
249
  }
247
250
  }
248
251
 
249
- const activeChannelPath = path.join(os.homedir(), '.claude', 'cc-tool', 'active-channel.json');
252
+ const activeChannelPath = PATHS.activeChannel.claude;
250
253
  if (fs.existsSync(activeChannelPath)) {
251
254
  fs.unlinkSync(activeChannelPath);
252
255
  console.log('✅ Removed active-channel.json');
@@ -189,6 +189,9 @@ module.exports = (config) => {
189
189
  try {
190
190
  const { projectName } = req.params;
191
191
  const { order } = req.body;
192
+ if (!Array.isArray(order)) {
193
+ return res.status(400).json({ error: 'order must be an array' });
194
+ }
192
195
  saveSessionOrder(projectName, order);
193
196
  res.json({ success: true });
194
197
  } catch (error) {
@@ -2,6 +2,8 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const { detectAvailableTerminals } = require('../services/terminal-detector');
4
4
  const { loadTerminalConfig, saveTerminalConfig, getSelectedTerminal } = require('../services/terminal-config');
5
+ const { MODEL_METADATA, resolveModelMetadata, METADATA_LAST_UPDATED } = require('../../config/model-metadata');
6
+ const { loadConfig, saveConfig } = require('../../config/loader');
5
7
 
6
8
  // GET /api/settings/terminals - 获取可用终端列表
7
9
  router.get('/terminals', (req, res) => {
@@ -58,4 +60,113 @@ router.post('/terminal-config', (req, res) => {
58
60
  }
59
61
  });
60
62
 
63
+ // GET /api/settings/model-metadata - 获取内置模型元数据表(limit + pricing)
64
+ router.get('/model-metadata', (req, res) => {
65
+ try {
66
+ // Return built-in defaults merged with any user overrides
67
+ const config = loadConfig();
68
+ const overrides = config.modelMetadataOverrides || {};
69
+
70
+ // Build merged table: built-in + user overrides
71
+ const merged = {};
72
+ for (const [id, meta] of Object.entries(MODEL_METADATA)) {
73
+ merged[id] = overrides[id]
74
+ ? {
75
+ limit: { ...meta.limit, ...(overrides[id].limit || {}) },
76
+ pricing: { ...meta.pricing, ...(overrides[id].pricing || {}) }
77
+ }
78
+ : meta;
79
+ }
80
+ // Also include any user-added custom models from overrides
81
+ for (const [id, meta] of Object.entries(overrides)) {
82
+ if (!merged[id]) {
83
+ merged[id] = meta;
84
+ }
85
+ }
86
+
87
+ res.json({
88
+ models: merged,
89
+ overrides,
90
+ lastUpdated: METADATA_LAST_UPDATED
91
+ });
92
+ } catch (error) {
93
+ console.error('Error getting model metadata:', error);
94
+ res.status(500).json({ error: error.message });
95
+ }
96
+ });
97
+
98
+ // POST /api/settings/model-metadata - 保存模型元数据覆盖项
99
+ // Body: { overrides: { [modelId]: { limit?: {...}, pricing?: {...} } } }
100
+ router.post('/model-metadata', (req, res) => {
101
+ try {
102
+ const { overrides } = req.body;
103
+ if (!overrides || typeof overrides !== 'object') {
104
+ return res.status(400).json({ error: 'overrides must be an object' });
105
+ }
106
+
107
+ // Validate each override entry
108
+ for (const [modelId, meta] of Object.entries(overrides)) {
109
+ if (typeof modelId !== 'string' || !modelId.trim()) {
110
+ return res.status(400).json({ error: `Invalid model ID: "${modelId}"` });
111
+ }
112
+ if (meta.limit !== undefined) {
113
+ if (typeof meta.limit !== 'object') {
114
+ return res.status(400).json({ error: `${modelId}: limit must be an object` });
115
+ }
116
+ if (meta.limit.context !== undefined && (typeof meta.limit.context !== 'number' || meta.limit.context <= 0)) {
117
+ return res.status(400).json({ error: `${modelId}: limit.context must be a positive number` });
118
+ }
119
+ if (meta.limit.output !== undefined && (typeof meta.limit.output !== 'number' || meta.limit.output <= 0)) {
120
+ return res.status(400).json({ error: `${modelId}: limit.output must be a positive number` });
121
+ }
122
+ }
123
+ if (meta.pricing !== undefined) {
124
+ if (typeof meta.pricing !== 'object') {
125
+ return res.status(400).json({ error: `${modelId}: pricing must be an object` });
126
+ }
127
+ for (const field of ['input', 'output']) {
128
+ if (meta.pricing[field] !== undefined && (typeof meta.pricing[field] !== 'number' || meta.pricing[field] < 0)) {
129
+ return res.status(400).json({ error: `${modelId}: pricing.${field} must be a non-negative number` });
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ const config = loadConfig();
136
+ const newConfig = {
137
+ ...config,
138
+ projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
139
+ modelMetadataOverrides: overrides
140
+ };
141
+ saveConfig(newConfig);
142
+
143
+ res.json({ success: true, overrides });
144
+ } catch (error) {
145
+ console.error('Error saving model metadata:', error);
146
+ res.status(500).json({ error: error.message });
147
+ }
148
+ });
149
+
150
+ // DELETE /api/settings/model-metadata/:modelId - 删除单个模型覆盖项(恢复内置默认值)
151
+ router.delete('/model-metadata/:modelId', (req, res) => {
152
+ try {
153
+ const modelId = decodeURIComponent(req.params.modelId);
154
+ const config = loadConfig();
155
+ const overrides = { ...(config.modelMetadataOverrides || {}) };
156
+ delete overrides[modelId];
157
+
158
+ const newConfig = {
159
+ ...config,
160
+ projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
161
+ modelMetadataOverrides: overrides
162
+ };
163
+ saveConfig(newConfig);
164
+
165
+ res.json({ success: true, modelId });
166
+ } catch (error) {
167
+ console.error('Error deleting model metadata override:', error);
168
+ res.status(500).json({ error: error.message });
169
+ }
170
+ });
171
+
61
172
  module.exports = router;