coding-tool-x 3.3.7 → 3.3.9

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 (89) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +253 -326
  3. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
  5. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  6. package/dist/web/assets/Home-D7KX7iF8.js +1 -0
  7. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
  8. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
  9. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
  10. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
  12. package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
  13. package/dist/web/assets/index-CL-qpoJ_.js +2 -0
  14. package/dist/web/assets/index-D_5dRFOL.css +1 -0
  15. package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
  16. package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
  17. package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
  18. package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
  19. package/dist/web/index.html +7 -7
  20. package/docs/home.png +0 -0
  21. package/package.json +14 -5
  22. package/src/commands/daemon.js +3 -2
  23. package/src/commands/security.js +1 -2
  24. package/src/commands/toggle-proxy.js +100 -5
  25. package/src/config/paths.js +718 -90
  26. package/src/server/api/agents.js +1 -1
  27. package/src/server/api/channels.js +9 -0
  28. package/src/server/api/claude-hooks.js +13 -8
  29. package/src/server/api/codex-channels.js +9 -0
  30. package/src/server/api/codex-proxy.js +27 -15
  31. package/src/server/api/gemini-proxy.js +22 -11
  32. package/src/server/api/hooks.js +45 -0
  33. package/src/server/api/oauth-credentials.js +163 -0
  34. package/src/server/api/opencode-proxy.js +22 -10
  35. package/src/server/api/plugins.js +2 -1
  36. package/src/server/api/proxy.js +39 -44
  37. package/src/server/api/skills.js +91 -13
  38. package/src/server/api/ui-config.js +5 -0
  39. package/src/server/codex-proxy-server.js +90 -70
  40. package/src/server/gemini-proxy-server.js +107 -88
  41. package/src/server/index.js +2 -0
  42. package/src/server/opencode-proxy-server.js +381 -225
  43. package/src/server/proxy-server.js +86 -60
  44. package/src/server/services/alias.js +3 -3
  45. package/src/server/services/channels.js +21 -24
  46. package/src/server/services/codex-channels.js +158 -255
  47. package/src/server/services/codex-config.js +2 -5
  48. package/src/server/services/codex-env-manager.js +423 -0
  49. package/src/server/services/codex-settings-manager.js +21 -357
  50. package/src/server/services/codex-statistics-service.js +3 -27
  51. package/src/server/services/config-export-service.js +43 -9
  52. package/src/server/services/config-registry-service.js +3 -2
  53. package/src/server/services/config-sync-manager.js +1 -1
  54. package/src/server/services/favorites.js +4 -3
  55. package/src/server/services/gemini-channels.js +14 -12
  56. package/src/server/services/gemini-statistics-service.js +3 -25
  57. package/src/server/services/mcp-service.js +35 -19
  58. package/src/server/services/model-detector.js +4 -3
  59. package/src/server/services/native-keychain.js +243 -0
  60. package/src/server/services/native-oauth-adapters.js +891 -0
  61. package/src/server/services/network-access.js +39 -1
  62. package/src/server/services/notification-hooks.js +951 -0
  63. package/src/server/services/oauth-credentials-service.js +786 -0
  64. package/src/server/services/oauth-utils.js +49 -0
  65. package/src/server/services/opencode-channels.js +19 -15
  66. package/src/server/services/opencode-sessions.js +2 -2
  67. package/src/server/services/opencode-settings-manager.js +169 -16
  68. package/src/server/services/opencode-statistics-service.js +3 -27
  69. package/src/server/services/plugins-service.js +115 -15
  70. package/src/server/services/prompts-service.js +2 -3
  71. package/src/server/services/proxy-log-helper.js +242 -0
  72. package/src/server/services/proxy-runtime.js +6 -4
  73. package/src/server/services/repo-scanner-base.js +12 -4
  74. package/src/server/services/request-logger.js +7 -7
  75. package/src/server/services/security-config.js +4 -4
  76. package/src/server/services/session-cache.js +2 -2
  77. package/src/server/services/sessions.js +2 -2
  78. package/src/server/services/settings-manager.js +13 -0
  79. package/src/server/services/skill-service.js +867 -368
  80. package/src/server/services/statistics-service.js +5 -5
  81. package/src/server/services/ui-config.js +4 -3
  82. package/src/server/services/workspace-service.js +1 -1
  83. package/src/server/websocket-server.js +5 -4
  84. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  85. package/dist/web/assets/Home-obifg_9E.js +0 -1
  86. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  87. package/dist/web/assets/index-eEmjZKWP.css +0 -1
  88. package/docs/bannel.png +0 -0
  89. package/docs/model-redirection.md +0 -251
@@ -105,7 +105,7 @@ function getAllowedProjectRoots() {
105
105
 
106
106
  // 从工作区配置中扩展允许目录,避免误拦截外部磁盘/自定义根目录项目
107
107
  try {
108
- const workspaceConfigPath = path.join(PATHS.base, 'workspaces.json');
108
+ const workspaceConfigPath = PATHS.workspaces;
109
109
  if (fs.existsSync(workspaceConfigPath)) {
110
110
  const raw = fs.readFileSync(workspaceConfigPath, 'utf-8');
111
111
  const parsed = JSON.parse(raw || '{}');
@@ -1,5 +1,6 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
+ const fs = require('fs');
3
4
  const {
4
5
  getAllChannels,
5
6
  applyChannelToSettings,
@@ -18,6 +19,8 @@ const {
18
19
  sanitizeBatchConcurrency,
19
20
  runWithConcurrencyLimit
20
21
  } = require('../services/speed-test');
22
+ const { PATHS } = require('../../config/paths');
23
+ const { deleteBackup } = require('../services/settings-manager');
21
24
  const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
22
25
  const { broadcastLog, broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
23
26
  const { clearRedirectCache } = require('../proxy-server');
@@ -185,6 +188,12 @@ router.post('/:id/apply-to-settings', async (req, res) => {
185
188
  // Stop proxy and restore backup
186
189
  const { stopProxyServer } = require('../proxy-server');
187
190
  await stopProxyServer({ clearStartTime: false });
191
+ deleteBackup();
192
+ try {
193
+ fs.unlinkSync(PATHS.activeChannel.claude);
194
+ } catch {
195
+ // ignore missing active channel marker
196
+ }
188
197
 
189
198
  // Re-apply channel settings after proxy stop to prevent race condition
190
199
  // (stopProxyServer restores backup, then we overwrite it with current channel)
@@ -5,21 +5,26 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const https = require('https');
7
7
  const http = require('http');
8
+ const { PATHS, NATIVE_PATHS } = require('../../config/paths');
8
9
  const { resolvePreferredHomeDir, normalizeWindowsHomePath } = require('../../utils/home-dir');
10
+ const { createSameOriginGuard } = require('../services/network-access');
9
11
 
10
12
  // 检测操作系统
11
13
  const platform = os.platform(); // 'darwin' | 'win32' | 'linux'
14
+ router.use(createSameOriginGuard({
15
+ message: '禁止跨站访问 Claude Hooks 配置接口'
16
+ }));
12
17
 
13
18
  const HOME_DIR = resolvePreferredHomeDir(platform, process.env, os.homedir());
14
19
 
15
20
  // Claude settings.json 路径
16
- const CLAUDE_SETTINGS_PATH = path.join(HOME_DIR, '.claude', 'settings.json');
21
+ const CLAUDE_SETTINGS_PATH = NATIVE_PATHS.claude.settings;
17
22
 
18
23
  // UI 配置路径(记录用户是否主动关闭过、飞书配置等)
19
- const UI_CONFIG_PATH = path.join(HOME_DIR, '.cc-tool', 'ui-config.json');
24
+ const UI_CONFIG_PATH = PATHS.uiConfig;
20
25
 
21
26
  // 通知脚本路径(用于飞书通知)
22
- const NOTIFY_SCRIPT_PATH = path.join(HOME_DIR, '.cc-tool', 'notify-hook.js');
27
+ const NOTIFY_SCRIPT_PATH = PATHS.notifyHook;
23
28
 
24
29
  // 读取 Claude settings.json
25
30
  function readClaudeSettings() {
@@ -222,17 +227,17 @@ function shouldRepairStopHook(settings, expectedScriptPath = NOTIFY_SCRIPT_PATH,
222
227
  return false;
223
228
  }
224
229
 
225
- const markerType = parseNotifyTypeMarker(command);
226
- if (!markerType) {
227
- return false;
228
- }
229
-
230
230
  const normalizedCommand = normalizePathForCompare(command);
231
231
  const normalizedExpected = normalizePathForCompare(expectedScriptPath);
232
232
  if (!normalizedCommand.includes(normalizedExpected)) {
233
233
  return true;
234
234
  }
235
235
 
236
+ const markerType = parseNotifyTypeMarker(command);
237
+ if (!markerType) {
238
+ return true;
239
+ }
240
+
236
241
  return !fileExists(expectedScriptPath);
237
242
  }
238
243
 
@@ -1,5 +1,6 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
+ const fs = require('fs');
3
4
  const {
4
5
  getChannels,
5
6
  createChannel,
@@ -20,6 +21,8 @@ const {
20
21
  runWithConcurrencyLimit
21
22
  } = require('../services/speed-test');
22
23
  const { clearCodexRedirectCache } = require('../codex-proxy-server');
24
+ const { deleteBackup } = require('../services/codex-settings-manager');
25
+ const { PATHS } = require('../../config/paths');
23
26
  const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
24
27
  const CODEX_GATEWAY_SOURCE_TYPE = 'codex';
25
28
 
@@ -349,6 +352,12 @@ module.exports = (config) => {
349
352
  if (proxyStatus && proxyStatus.running) {
350
353
  console.log(`Codex proxy is running, stopping to apply channel settings: ${channel.name}`);
351
354
  await stopCodexProxyServer({ clearStartTime: false });
355
+ deleteBackup();
356
+ try {
357
+ fs.unlinkSync(PATHS.activeChannel.codex);
358
+ } catch {
359
+ // ignore missing active channel marker
360
+ }
352
361
 
353
362
  broadcastLog({
354
363
  type: 'action',
@@ -15,6 +15,7 @@ const {
15
15
  readConfig
16
16
  } = require('../services/codex-settings-manager');
17
17
  const { getChannels, getEnabledChannels } = require('../services/codex-channels');
18
+ const { clearNativeOAuth } = require('../services/native-oauth-adapters');
18
19
  const { clearAllLogs } = require('../websocket-server');
19
20
  const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
20
21
  const fs = require('fs');
@@ -42,6 +43,24 @@ function selectLatestEnabledChannel(channels) {
42
43
  }, enabledChannels[0]);
43
44
  }
44
45
 
46
+ function resolveActiveChannel(channels, activeChannelId = null) {
47
+ if (!Array.isArray(channels) || channels.length === 0) {
48
+ return null;
49
+ }
50
+
51
+ if (activeChannelId) {
52
+ const matched = channels.find(channel => channel.id === activeChannelId);
53
+ if (matched) {
54
+ return matched;
55
+ }
56
+ }
57
+
58
+ return selectLatestEnabledChannel(channels)
59
+ || channels.find(channel => channel.enabled !== false)
60
+ || channels[0]
61
+ || null;
62
+ }
63
+
45
64
  // 保存激活渠道ID
46
65
  function saveActiveChannelId(channelId) {
47
66
  ensureStorageDirMigrated();
@@ -82,8 +101,7 @@ router.get('/status', (req, res) => {
82
101
  try {
83
102
  const proxyStatus = getCodexProxyStatus();
84
103
  const { channels } = getChannels();
85
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
86
- const activeChannel = enabledChannels[0]; // 多渠道模式:第一个启用的渠道
104
+ const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
87
105
  const configStatus = {
88
106
  isProxyConfig: isProxyConfig(),
89
107
  configExists: configExists(),
@@ -143,6 +161,7 @@ router.post('/start', async (req, res) => {
143
161
  }
144
162
 
145
163
  // 5. 设置代理配置(备份并修改 config.toml 和 auth.json)
164
+ clearNativeOAuth('codex');
146
165
  const configResult = setProxyConfig(proxyResult.port);
147
166
 
148
167
  const updatedStatus = getCodexProxyStatus();
@@ -155,11 +174,12 @@ router.post('/start', async (req, res) => {
155
174
  let message = `Codex proxy started on port ${proxyResult.port}, active channel: ${currentChannel.name}`;
156
175
  let envHint = null;
157
176
 
158
- // 只有首次注入环境变量时才提示用户执行 source 命令
159
- if (configResult.envInjected && configResult.isFirstTime) {
177
+ if (configResult.envInjected && configResult.reloadRequired) {
160
178
  envHint = {
161
- command: configResult.sourceCommand,
162
- message: `首次启用需在 Codex 终端执行: ${configResult.sourceCommand}`
179
+ command: configResult.sourceCommand || null,
180
+ message: configResult.sourceCommand
181
+ ? `请在 Codex 终端执行: ${configResult.sourceCommand}`
182
+ : '请重新打开 Codex 终端以加载新的用户环境变量'
163
183
  };
164
184
  }
165
185
 
@@ -179,17 +199,9 @@ router.post('/start', async (req, res) => {
179
199
  // 停止代理
180
200
  router.post('/stop', async (req, res) => {
181
201
  try {
182
- // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
183
202
  const { channels } = getChannels();
184
203
  const activeChannelId = loadActiveChannelId();
185
- let activeChannel = selectLatestEnabledChannel(channels);
186
- if (!activeChannel && activeChannelId) {
187
- activeChannel = channels.find(ch => ch.id === activeChannelId);
188
- }
189
- if (!activeChannel) {
190
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
191
- activeChannel = enabledChannels[0] || channels[0] || null;
192
- }
204
+ const activeChannel = resolveActiveChannel(channels, activeChannelId);
193
205
 
194
206
  // 2. 停止代理服务器
195
207
  const proxyResult = await stopCodexProxyServer();
@@ -16,6 +16,7 @@ const {
16
16
  readEnv
17
17
  } = require('../services/gemini-settings-manager');
18
18
  const { getChannels, getEnabledChannels } = require('../services/gemini-channels');
19
+ const { clearNativeOAuth } = require('../services/native-oauth-adapters');
19
20
  const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
20
21
  const fs = require('fs');
21
22
  const path = require('path');
@@ -37,6 +38,24 @@ function selectLatestEnabledChannel(channels) {
37
38
  }, enabledChannels[0]);
38
39
  }
39
40
 
41
+ function resolveActiveChannel(channels, activeChannelId = null) {
42
+ if (!Array.isArray(channels) || channels.length === 0) {
43
+ return null;
44
+ }
45
+
46
+ if (activeChannelId) {
47
+ const matched = channels.find(channel => channel.id === activeChannelId);
48
+ if (matched) {
49
+ return matched;
50
+ }
51
+ }
52
+
53
+ return selectLatestEnabledChannel(channels)
54
+ || channels.find(channel => channel.enabled !== false)
55
+ || channels[0]
56
+ || null;
57
+ }
58
+
40
59
  // 保存激活渠道ID
41
60
  function saveActiveChannelId(channelId) {
42
61
  ensureStorageDirMigrated();
@@ -83,8 +102,7 @@ router.get('/status', (req, res) => {
83
102
  currentProxyPort: getCurrentProxyPort()
84
103
  };
85
104
  const { channels } = getChannels();
86
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
87
- const activeChannel = enabledChannels[0]; // 多渠道模式:第一个启用的渠道
105
+ const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
88
106
 
89
107
  res.json({
90
108
  proxy: proxyStatus,
@@ -141,6 +159,7 @@ router.post('/start', async (req, res) => {
141
159
  }
142
160
 
143
161
  // 5. 设置代理配置(备份并修改 .env 和 settings.json)
162
+ clearNativeOAuth('gemini');
144
163
  setProxyConfig(proxyResult.port);
145
164
 
146
165
  const { broadcastProxyState } = require('../websocket-server');
@@ -164,17 +183,9 @@ router.post('/start', async (req, res) => {
164
183
  // 停止代理
165
184
  router.post('/stop', async (req, res) => {
166
185
  try {
167
- // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
168
186
  const { channels } = getChannels();
169
187
  const activeChannelId = loadActiveChannelId();
170
- let activeChannel = selectLatestEnabledChannel(channels);
171
- if (!activeChannel && activeChannelId) {
172
- activeChannel = channels.find(ch => ch.id === activeChannelId);
173
- }
174
- if (!activeChannel) {
175
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
176
- activeChannel = enabledChannels[0] || channels[0] || null;
177
- }
188
+ const activeChannel = resolveActiveChannel(channels, activeChannelId);
178
189
 
179
190
  // 2. 停止代理服务器
180
191
  const proxyResult = await stopGeminiProxyServer();
@@ -0,0 +1,45 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const notificationHooks = require('../services/notification-hooks');
4
+ const { createSameOriginGuard } = require('../services/network-access');
5
+
6
+ router.use(createSameOriginGuard({
7
+ message: '禁止跨站访问通知配置接口'
8
+ }));
9
+
10
+ router.get('/', (req, res) => {
11
+ try {
12
+ res.json(notificationHooks.getNotificationSettings());
13
+ } catch (error) {
14
+ console.error('Error getting notification hook settings:', error);
15
+ res.status(error.statusCode || 500).json({ error: error.message });
16
+ }
17
+ });
18
+
19
+ router.post('/', (req, res) => {
20
+ try {
21
+ const result = notificationHooks.saveNotificationSettings(req.body || {});
22
+ res.json({
23
+ ...result,
24
+ message: '通知设置已保存'
25
+ });
26
+ } catch (error) {
27
+ console.error('Error saving notification hook settings:', error);
28
+ res.status(error.statusCode || 500).json({ error: error.message });
29
+ }
30
+ });
31
+
32
+ router.post('/test', async (req, res) => {
33
+ try {
34
+ await notificationHooks.testNotification(req.body || {});
35
+ res.json({
36
+ success: true,
37
+ message: req.body?.testFeishu ? '飞书测试通知已发送' : '系统测试通知已发送'
38
+ });
39
+ } catch (error) {
40
+ console.error('Error testing notification hook settings:', error);
41
+ res.status(error.statusCode || 500).json({ error: error.message });
42
+ }
43
+ });
44
+
45
+ module.exports = router;
@@ -0,0 +1,163 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ SUPPORTED_TOOLS,
5
+ getAllToolSummaries,
6
+ getToolSummary,
7
+ importCredential,
8
+ syncLocalCredential,
9
+ setDefaultCredential,
10
+ deleteCredential,
11
+ applyStoredCredential,
12
+ clearNativeOAuthState,
13
+ fetchCredentialUsage
14
+ } = require('../services/oauth-credentials-service');
15
+
16
+ function assertTool(tool) {
17
+ if (!SUPPORTED_TOOLS.includes(tool)) {
18
+ const error = new Error(`Unsupported OAuth tool: ${tool}`);
19
+ error.statusCode = 404;
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ function broadcastToolProxyState(tool) {
25
+ const { broadcastProxyState } = require('../websocket-server');
26
+
27
+ if (tool === 'claude') {
28
+ const { getProxyStatus } = require('../proxy-server');
29
+ const { getAllChannels } = require('../services/channels');
30
+ const channels = getAllChannels();
31
+ const activeChannel = channels.find(ch => ch.enabled !== false) || null;
32
+ broadcastProxyState('claude', getProxyStatus(), activeChannel, channels);
33
+ return;
34
+ }
35
+
36
+ if (tool === 'codex') {
37
+ const { getCodexProxyStatus } = require('../codex-proxy-server');
38
+ const { getChannels } = require('../services/codex-channels');
39
+ const channels = getChannels().channels || [];
40
+ const activeChannel = channels.find(ch => ch.enabled !== false) || null;
41
+ broadcastProxyState('codex', getCodexProxyStatus(), activeChannel, channels);
42
+ return;
43
+ }
44
+
45
+ if (tool === 'gemini') {
46
+ const { getGeminiProxyStatus } = require('../gemini-proxy-server');
47
+ const { getChannels } = require('../services/gemini-channels');
48
+ const channels = getChannels().channels || [];
49
+ const activeChannel = channels.find(ch => ch.enabled !== false) || null;
50
+ broadcastProxyState('gemini', getGeminiProxyStatus(), activeChannel, channels);
51
+ return;
52
+ }
53
+
54
+ if (tool === 'opencode') {
55
+ const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
56
+ const { getChannels } = require('../services/opencode-channels');
57
+ const channels = getChannels().channels || [];
58
+ const activeChannel = channels.find(ch => ch.enabled !== false) || null;
59
+ broadcastProxyState('opencode', getOpenCodeProxyStatus(), activeChannel, channels);
60
+ }
61
+ }
62
+
63
+ router.get('/', (req, res) => {
64
+ try {
65
+ res.json({ tools: getAllToolSummaries() });
66
+ } catch (error) {
67
+ res.status(500).json({ error: error.message });
68
+ }
69
+ });
70
+
71
+ router.get('/:tool', (req, res) => {
72
+ try {
73
+ const { tool } = req.params;
74
+ assertTool(tool);
75
+ res.json({ tool, summary: getToolSummary(tool) });
76
+ } catch (error) {
77
+ res.status(error.statusCode || 500).json({ error: error.message });
78
+ }
79
+ });
80
+
81
+ router.post('/:tool/import', (req, res) => {
82
+ try {
83
+ const { tool } = req.params;
84
+ assertTool(tool);
85
+ const credential = importCredential(tool, req.body || {});
86
+ res.json({ tool, credential, summary: getToolSummary(tool) });
87
+ } catch (error) {
88
+ res.status(error.statusCode || 500).json({ error: error.message });
89
+ }
90
+ });
91
+
92
+ router.post('/:tool/sync-local', (req, res) => {
93
+ try {
94
+ const { tool } = req.params;
95
+ assertTool(tool);
96
+ const result = syncLocalCredential(tool);
97
+ res.json({ tool, ...result });
98
+ } catch (error) {
99
+ res.status(error.statusCode || 500).json({ error: error.message });
100
+ }
101
+ });
102
+
103
+ router.post('/:tool/:credentialId/default', (req, res) => {
104
+ try {
105
+ const { tool, credentialId } = req.params;
106
+ assertTool(tool);
107
+ const summary = setDefaultCredential(tool, credentialId);
108
+ res.json({ tool, summary });
109
+ } catch (error) {
110
+ res.status(error.statusCode || 500).json({ error: error.message });
111
+ }
112
+ });
113
+
114
+ router.post('/:tool/:credentialId/apply', async (req, res) => {
115
+ try {
116
+ const { tool, credentialId } = req.params;
117
+ assertTool(tool);
118
+ const result = await applyStoredCredential(tool, credentialId);
119
+ broadcastToolProxyState(tool);
120
+ res.json({
121
+ tool,
122
+ ...result,
123
+ message: `${tool} 已切换到 OAuth 凭证控制`
124
+ });
125
+ } catch (error) {
126
+ res.status(error.statusCode || 500).json({ error: error.message });
127
+ }
128
+ });
129
+
130
+ router.post('/:tool/clear-native', (req, res) => {
131
+ try {
132
+ const { tool } = req.params;
133
+ assertTool(tool);
134
+ const nativeState = clearNativeOAuthState(tool);
135
+ res.json({ tool, nativeState });
136
+ } catch (error) {
137
+ res.status(error.statusCode || 500).json({ error: error.message });
138
+ }
139
+ });
140
+
141
+ router.get('/:tool/:credentialId/usage', async (req, res) => {
142
+ try {
143
+ const { tool, credentialId } = req.params;
144
+ assertTool(tool);
145
+ const result = await fetchCredentialUsage(tool, credentialId);
146
+ res.json({ tool, credentialId, usage: result });
147
+ } catch (error) {
148
+ res.status(error.statusCode || 500).json({ error: error.message });
149
+ }
150
+ });
151
+
152
+ router.delete('/:tool/:credentialId', (req, res) => {
153
+ try {
154
+ const { tool, credentialId } = req.params;
155
+ assertTool(tool);
156
+ const summary = deleteCredential(tool, credentialId);
157
+ res.json({ tool, summary });
158
+ } catch (error) {
159
+ res.status(error.statusCode || 500).json({ error: error.message });
160
+ }
161
+ });
162
+
163
+ module.exports = router;
@@ -16,6 +16,7 @@ const {
16
16
  getCurrentProxyPort
17
17
  } = require('../services/opencode-settings-manager');
18
18
  const { getChannels, getEnabledChannels, applyChannelToSettings } = require('../services/opencode-channels');
19
+ const { clearNativeOAuth } = require('../services/native-oauth-adapters');
19
20
  const { getSchedulerState } = require('../services/channel-scheduler');
20
21
  const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
21
22
  const fs = require('fs');
@@ -42,6 +43,24 @@ function selectLatestEnabledChannel(channels) {
42
43
  }, enabledChannels[0]);
43
44
  }
44
45
 
46
+ function resolveActiveChannel(channels, activeChannelId = null) {
47
+ if (!Array.isArray(channels) || channels.length === 0) {
48
+ return null;
49
+ }
50
+
51
+ if (activeChannelId) {
52
+ const matched = channels.find(channel => channel.id === activeChannelId);
53
+ if (matched) {
54
+ return matched;
55
+ }
56
+ }
57
+
58
+ return selectLatestEnabledChannel(channels)
59
+ || channels.find(channel => channel.enabled !== false)
60
+ || channels[0]
61
+ || null;
62
+ }
63
+
45
64
  // 保存激活渠道ID
46
65
  function saveActiveChannelId(channelId) {
47
66
  ensureStorageDirMigrated();
@@ -84,7 +103,7 @@ router.get('/status', (req, res) => {
84
103
  const proxyStatus = getOpenCodeProxyStatus();
85
104
  const { channels } = getChannels();
86
105
  const enabledChannels = channels.filter(ch => ch.enabled !== false);
87
- const activeChannel = enabledChannels[0];
106
+ const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
88
107
  const configStatus = {
89
108
  isProxyConfig: isProxyConfig(),
90
109
  configExists: configExists(),
@@ -176,6 +195,7 @@ router.post('/start', async (req, res) => {
176
195
  });
177
196
 
178
197
  const activeModel = currentChannel.model || currentChannel.speedTestModel || null;
198
+ clearNativeOAuth('opencode');
179
199
  setProxyConfig(proxyResult.port, { channels: channelPayloads, model: activeModel });
180
200
 
181
201
  // 5. 广播状态更新
@@ -199,17 +219,9 @@ router.post('/start', async (req, res) => {
199
219
  // 停止代理
200
220
  router.post('/stop', async (req, res) => {
201
221
  try {
202
- // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
203
222
  const { channels } = getChannels();
204
223
  const activeChannelId = loadActiveChannelId();
205
- let activeChannel = selectLatestEnabledChannel(channels);
206
- if (!activeChannel && activeChannelId) {
207
- activeChannel = channels.find(ch => ch.id === activeChannelId);
208
- }
209
- if (!activeChannel) {
210
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
211
- activeChannel = enabledChannels[0] || channels[0] || null;
212
- }
224
+ const activeChannel = resolveActiveChannel(channels, activeChannelId);
213
225
 
214
226
  // 2. 停止代理服务器
215
227
  const proxyResult = await stopOpenCodeProxyServer();
@@ -57,7 +57,8 @@ router.get('/', (req, res) => {
57
57
  router.get('/market', async (req, res) => {
58
58
  try {
59
59
  const { platform, service } = getPluginsService(req);
60
- const plugins = await service.getMarketPlugins();
60
+ const forceRefresh = req.query.refresh === '1';
61
+ const plugins = await service.getMarketPlugins(forceRefresh);
61
62
 
62
63
  res.json({
63
64
  success: true,