coding-tool-x 3.3.7 → 3.3.8

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 (48) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-DLpoDZ2M.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-D_hRb55W.js} +1 -1
  4. package/dist/web/assets/Home-BMoFdAwy.css +1 -0
  5. package/dist/web/assets/Home-DNwp-0J-.js +1 -0
  6. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-JXsyym1s.js} +1 -1
  7. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DZWSeb-q.js} +1 -1
  8. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-Cs624DR3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-bEliz7qz.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-J3RecFGn.js} +1 -1
  11. package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
  12. package/dist/web/assets/index-BXeSvAwU.js +2 -0
  13. package/dist/web/assets/index-DWAC3Tdv.css +1 -0
  14. package/dist/web/index.html +3 -3
  15. package/package.json +3 -2
  16. package/src/commands/toggle-proxy.js +100 -5
  17. package/src/config/paths.js +102 -19
  18. package/src/server/api/channels.js +9 -0
  19. package/src/server/api/codex-channels.js +9 -0
  20. package/src/server/api/codex-proxy.js +22 -11
  21. package/src/server/api/gemini-proxy.js +22 -11
  22. package/src/server/api/oauth-credentials.js +163 -0
  23. package/src/server/api/opencode-proxy.js +22 -10
  24. package/src/server/api/plugins.js +3 -1
  25. package/src/server/api/proxy.js +39 -44
  26. package/src/server/api/skills.js +91 -13
  27. package/src/server/codex-proxy-server.js +1 -11
  28. package/src/server/index.js +1 -0
  29. package/src/server/services/channels.js +18 -22
  30. package/src/server/services/codex-channels.js +124 -175
  31. package/src/server/services/codex-config.js +2 -5
  32. package/src/server/services/codex-settings-manager.js +12 -348
  33. package/src/server/services/config-export-service.js +23 -2
  34. package/src/server/services/gemini-channels.js +11 -9
  35. package/src/server/services/mcp-service.js +33 -16
  36. package/src/server/services/native-keychain.js +243 -0
  37. package/src/server/services/native-oauth-adapters.js +890 -0
  38. package/src/server/services/oauth-credentials-service.js +786 -0
  39. package/src/server/services/oauth-utils.js +49 -0
  40. package/src/server/services/opencode-channels.js +13 -9
  41. package/src/server/services/opencode-settings-manager.js +169 -16
  42. package/src/server/services/plugins-service.js +22 -1
  43. package/src/server/services/settings-manager.js +13 -0
  44. package/src/server/services/skill-service.js +712 -332
  45. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  46. package/dist/web/assets/Home-obifg_9E.js +0 -1
  47. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  48. package/dist/web/assets/index-eEmjZKWP.css +0 -1
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
- const { resolvePreferredHomeDir } = require('../utils/home-dir');
6
+ const { resolvePreferredHomeDir, isWindowsLikePlatform } = require('../utils/home-dir');
7
7
 
8
8
  const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
9
9
 
@@ -63,6 +63,71 @@ function ensureStorageDirMigrated() {
63
63
  return CC_TOOL_BASE_DIR;
64
64
  }
65
65
 
66
+ function resolveExistingEnvPath(envValue) {
67
+ if (typeof envValue !== 'string') {
68
+ return '';
69
+ }
70
+ const trimmed = envValue.trim();
71
+ return trimmed || '';
72
+ }
73
+
74
+ function pickExistingDir(candidates, fallback) {
75
+ for (const candidate of candidates) {
76
+ if (candidate && fs.existsSync(candidate)) {
77
+ return candidate;
78
+ }
79
+ }
80
+ return fallback;
81
+ }
82
+
83
+ function getClaudeConfigDir() {
84
+ return resolveExistingEnvPath(process.env.CLAUDE_CONFIG_DIR) || path.join(HOME_DIR, '.claude');
85
+ }
86
+
87
+ function getCodexDir() {
88
+ return resolveExistingEnvPath(process.env.CODEX_HOME) || path.join(HOME_DIR, '.codex');
89
+ }
90
+
91
+ function getGeminiDir() {
92
+ return path.join(HOME_DIR, '.gemini');
93
+ }
94
+
95
+ function getOpenCodeDataDir() {
96
+ if (isWindowsLikePlatform(process.platform, process.env)) {
97
+ const localAppData = resolveExistingEnvPath(process.env.LOCALAPPDATA);
98
+ return path.join(localAppData || path.join(HOME_DIR, 'AppData', 'Local'), 'opencode');
99
+ }
100
+
101
+ const xdgDataHome = resolveExistingEnvPath(process.env.XDG_DATA_HOME);
102
+ const preferredDir = path.join(xdgDataHome || path.join(HOME_DIR, '.local', 'share'), 'opencode');
103
+
104
+ if (process.platform === 'darwin') {
105
+ const legacyDarwinDir = path.join(HOME_DIR, 'Library', 'Application Support', 'opencode');
106
+ return pickExistingDir([preferredDir, legacyDarwinDir], preferredDir);
107
+ }
108
+
109
+ return preferredDir;
110
+ }
111
+
112
+ function getOpenCodeConfigDir() {
113
+ if (isWindowsLikePlatform(process.platform, process.env)) {
114
+ const appData = resolveExistingEnvPath(process.env.APPDATA);
115
+ return path.join(appData || path.join(HOME_DIR, 'AppData', 'Roaming'), 'opencode');
116
+ }
117
+
118
+ const xdgConfigHome = resolveExistingEnvPath(process.env.XDG_CONFIG_HOME);
119
+ const preferredDir = path.join(xdgConfigHome || path.join(HOME_DIR, '.config'), 'opencode');
120
+
121
+ if (process.platform === 'darwin') {
122
+ const xdgDataHome = resolveExistingEnvPath(process.env.XDG_DATA_HOME);
123
+ const legacyDataDir = path.join(xdgDataHome || path.join(HOME_DIR, '.local', 'share'), 'opencode');
124
+ const legacyDarwinDir = path.join(HOME_DIR, 'Library', 'Application Support', 'opencode');
125
+ return pickExistingDir([preferredDir, legacyDarwinDir, legacyDataDir], preferredDir);
126
+ }
127
+
128
+ return preferredDir;
129
+ }
130
+
66
131
  // 路径配置
67
132
  const PATHS = {
68
133
  // 基础目录
@@ -126,6 +191,9 @@ const PATHS = {
126
191
  // UI 配置
127
192
  uiConfig: path.join(CC_TOOL_BASE_DIR, 'ui-config.json'),
128
193
 
194
+ // OAuth 凭证注册表
195
+ oauthCredentials: path.join(CC_TOOL_BASE_DIR, 'oauth-credentials.json'),
196
+
129
197
  // 飞书通知脚本
130
198
  notifyHook: path.join(CC_TOOL_BASE_DIR, 'notify-hook.js'),
131
199
 
@@ -151,35 +219,45 @@ const PATHS = {
151
219
  const NATIVE_PATHS = {
152
220
  // Claude Code 原生配置
153
221
  claude: {
154
- settings: path.join(HOME_DIR, '.claude', 'settings.json'),
155
- settingsBackup: path.join(HOME_DIR, '.claude', 'settings.json.cc-tool-backup'),
156
- projects: path.join(HOME_DIR, '.claude', 'projects')
222
+ dir: getClaudeConfigDir(),
223
+ settings: path.join(getClaudeConfigDir(), 'settings.json'),
224
+ settingsBackup: path.join(getClaudeConfigDir(), 'settings.json.cc-tool-backup'),
225
+ projects: path.join(getClaudeConfigDir(), 'projects'),
226
+ credentials: path.join(getClaudeConfigDir(), '.credentials.json')
157
227
  },
158
228
 
159
229
  // Codex 原生配置
160
230
  codex: {
161
- config: path.join(HOME_DIR, '.codex', 'config.toml'),
162
- configBackup: path.join(HOME_DIR, '.codex', 'config.toml.cc-tool-backup'),
163
- auth: path.join(HOME_DIR, '.codex', 'auth.json'),
164
- authBackup: path.join(HOME_DIR, '.codex', 'auth.json.cc-tool-backup'),
165
- sessions: path.join(HOME_DIR, '.codex', 'sessions')
231
+ dir: getCodexDir(),
232
+ config: path.join(getCodexDir(), 'config.toml'),
233
+ configBackup: path.join(getCodexDir(), 'config.toml.cc-tool-backup'),
234
+ auth: path.join(getCodexDir(), 'auth.json'),
235
+ authBackup: path.join(getCodexDir(), 'auth.json.cc-tool-backup'),
236
+ sessions: path.join(getCodexDir(), 'sessions')
166
237
  },
167
238
 
168
239
  // Gemini 原生配置
169
240
  gemini: {
170
- env: path.join(HOME_DIR, '.gemini', '.env'),
171
- envBackup: path.join(HOME_DIR, '.gemini', '.env.cc-tool-backup'),
172
- tmp: path.join(HOME_DIR, '.gemini', 'tmp')
241
+ dir: getGeminiDir(),
242
+ env: path.join(getGeminiDir(), '.env'),
243
+ envBackup: path.join(getGeminiDir(), '.env.cc-tool-backup'),
244
+ tmp: path.join(getGeminiDir(), 'tmp'),
245
+ settings: path.join(getGeminiDir(), 'settings.json'),
246
+ settingsBackup: path.join(getGeminiDir(), 'settings.json.cc-tool-backup'),
247
+ googleAccounts: path.join(getGeminiDir(), 'google_accounts.json'),
248
+ oauthCredentialsLegacy: path.join(getGeminiDir(), 'oauth_creds.json'),
249
+ oauthCredentialsEncrypted: path.join(getGeminiDir(), 'mcp-oauth-tokens-v2.json')
173
250
  },
174
251
 
175
252
  // OpenCode 原生配置
176
253
  opencode: {
177
- data: path.join(HOME_DIR, '.local', 'share', 'opencode'),
178
- config: path.join(HOME_DIR, '.config', 'opencode'),
179
- sessions: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'session'),
180
- projects: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'project'),
181
- messages: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'message'),
182
- log: path.join(HOME_DIR, '.local', 'share', 'opencode', 'log')
254
+ data: getOpenCodeDataDir(),
255
+ config: getOpenCodeConfigDir(),
256
+ sessions: path.join(getOpenCodeDataDir(), 'storage', 'session'),
257
+ projects: path.join(getOpenCodeDataDir(), 'storage', 'project'),
258
+ messages: path.join(getOpenCodeDataDir(), 'storage', 'message'),
259
+ log: path.join(getOpenCodeDataDir(), 'log'),
260
+ auth: path.join(getOpenCodeDataDir(), 'auth.json')
183
261
  }
184
262
  };
185
263
 
@@ -190,5 +268,10 @@ module.exports = {
190
268
  CTX_BASE_DIR,
191
269
  CC_TOOL_BASE_DIR,
192
270
  LEGACY_BASE_DIRS,
193
- ensureStorageDirMigrated
271
+ ensureStorageDirMigrated,
272
+ getClaudeConfigDir,
273
+ getCodexDir,
274
+ getGeminiDir,
275
+ getOpenCodeDataDir,
276
+ getOpenCodeConfigDir
194
277
  };
@@ -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)
@@ -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();
@@ -179,17 +198,9 @@ router.post('/start', async (req, res) => {
179
198
  // 停止代理
180
199
  router.post('/stop', async (req, res) => {
181
200
  try {
182
- // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
183
201
  const { channels } = getChannels();
184
202
  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
- }
203
+ const activeChannel = resolveActiveChannel(channels, activeChannelId);
193
204
 
194
205
  // 2. 停止代理服务器
195
206
  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,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,9 @@ 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
+ if (forceRefresh) service._marketCache = null;
62
+ const plugins = await service.getMarketPlugins(forceRefresh);
61
63
 
62
64
  res.json({
63
65
  success: true,