coding-tool-x 3.4.4 → 3.4.6

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 (32) hide show
  1. package/dist/web/assets/{Analytics-_Byi9M6y.js → Analytics-0PgPv5qO.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DIwosdtG.js → ConfigTemplates-pBGoYbCP.js} +1 -1
  3. package/dist/web/assets/{Home-DdNMuQ9c.js → Home-BRN882om.js} +1 -1
  4. package/dist/web/assets/{PluginManager-iuY24cnW.js → PluginManager-am97Huts.js} +1 -1
  5. package/dist/web/assets/{ProjectList-DSkMulzL.js → ProjectList-CXS9KJN1.js} +1 -1
  6. package/dist/web/assets/{SessionList-B6pGquIr.js → SessionList-BZyrzH7J.js} +1 -1
  7. package/dist/web/assets/{SkillManager-CHtQX5r8.js → SkillManager-p1CI0tYa.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-gNPs-VaI.js → WorkspaceManager-CUPvLoba.js} +1 -1
  9. package/dist/web/assets/index-B4Wl3JfR.js +2 -0
  10. package/dist/web/assets/{index-pMqqe9ei.css → index-Bgt_oqoE.css} +1 -1
  11. package/dist/web/index.html +2 -2
  12. package/package.json +2 -2
  13. package/src/server/api/claude-hooks.js +1 -0
  14. package/src/server/api/codex-channels.js +26 -0
  15. package/src/server/api/oauth-credentials.js +23 -1
  16. package/src/server/api/opencode-proxy.js +0 -2
  17. package/src/server/api/plugins.js +161 -14
  18. package/src/server/api/skills.js +62 -7
  19. package/src/server/codex-proxy-server.js +10 -2
  20. package/src/server/gemini-proxy-server.js +10 -2
  21. package/src/server/opencode-proxy-server.js +10 -2
  22. package/src/server/proxy-server.js +10 -2
  23. package/src/server/services/codex-channels.js +64 -21
  24. package/src/server/services/codex-env-manager.js +44 -28
  25. package/src/server/services/native-oauth-adapters.js +94 -10
  26. package/src/server/services/oauth-credentials-service.js +44 -2
  27. package/src/server/services/opencode-channels.js +0 -2
  28. package/src/server/services/plugins-service.js +1060 -235
  29. package/src/server/services/proxy-runtime.js +129 -5
  30. package/src/server/services/server-shutdown.js +79 -0
  31. package/src/server/services/skill-service.js +142 -17
  32. package/dist/web/assets/index-DGjGCo37.js +0 -2
@@ -22,6 +22,7 @@ const { persistProxyRequestSnapshot, loadClaudeRequestTemplate } = require('./se
22
22
  const { probeModelAvailability, fetchModelsFromProvider } = require('./services/model-detector');
23
23
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
24
24
  const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
25
+ const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
25
26
 
26
27
  let proxyServer = null;
27
28
  let proxyApp = null;
@@ -4650,6 +4651,7 @@ async function startOpenCodeProxyServer(options = {}) {
4650
4651
 
4651
4652
  // 启动服务器
4652
4653
  proxyServer = http.createServer(proxyApp);
4654
+ attachServerShutdownHandling(proxyServer);
4653
4655
 
4654
4656
  return new Promise((resolve, reject) => {
4655
4657
  proxyServer.listen(port, '127.0.0.1', () => {
@@ -4692,8 +4694,13 @@ async function stopOpenCodeProxyServer(options = {}) {
4692
4694
 
4693
4695
  requestMetadata.clear();
4694
4696
 
4697
+ const shutdownTimer = expediteServerShutdown(proxyServer);
4698
+
4695
4699
  return new Promise((resolve) => {
4696
4700
  proxyServer.close(() => {
4701
+ if (shutdownTimer) {
4702
+ clearTimeout(shutdownTimer);
4703
+ }
4697
4704
  console.log('OpenCode proxy server stopped');
4698
4705
 
4699
4706
  // 清除代理启动时间(仅当明确要求时)
@@ -4713,8 +4720,9 @@ async function stopOpenCodeProxyServer(options = {}) {
4713
4720
  // 获取代理服务器状态
4714
4721
  function getOpenCodeProxyStatus() {
4715
4722
  const config = loadConfig();
4716
- const startTime = getProxyStartTime('opencode');
4717
- const runtime = getProxyRuntime('opencode');
4723
+ const allowRecovery = !!proxyServer;
4724
+ const startTime = getProxyStartTime('opencode', { allowRecovery });
4725
+ const runtime = getProxyRuntime('opencode', { allowRecovery });
4718
4726
 
4719
4727
  return {
4720
4728
  running: !!proxyServer,
@@ -20,6 +20,7 @@ const { getEffectiveApiKey } = require('./services/channels');
20
20
  const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('./services/request-logger');
21
21
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
22
22
  const { redirectModel } = require('./services/base/proxy-utils');
23
+ const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
23
24
 
24
25
  let proxyServer = null;
25
26
  let proxyApp = null;
@@ -541,6 +542,7 @@ async function startProxyServer(options = {}) {
541
542
  });
542
543
 
543
544
  proxyServer = http.createServer(proxyApp);
545
+ attachServerShutdownHandling(proxyServer);
544
546
 
545
547
  return new Promise((resolve, reject) => {
546
548
  proxyServer.listen(port, '127.0.0.1', () => {
@@ -580,8 +582,13 @@ async function stopProxyServer(options = {}) {
580
582
 
581
583
  requestMetadata.clear();
582
584
 
585
+ const shutdownTimer = expediteServerShutdown(proxyServer);
586
+
583
587
  return new Promise((resolve) => {
584
588
  proxyServer.close(() => {
589
+ if (shutdownTimer) {
590
+ clearTimeout(shutdownTimer);
591
+ }
585
592
  console.log('[OK] Proxy server stopped');
586
593
  if (clearStartTime) {
587
594
  clearProxyStartTime('claude');
@@ -599,8 +606,9 @@ async function stopProxyServer(options = {}) {
599
606
  // 获取代理服务器状态
600
607
  function getProxyStatus() {
601
608
  const config = loadConfig();
602
- const startTime = getProxyStartTime('claude');
603
- const runtime = getProxyRuntime('claude');
609
+ const allowRecovery = !!proxyServer;
610
+ const startTime = getProxyStartTime('claude', { allowRecovery });
611
+ const runtime = getProxyRuntime('claude', { allowRecovery });
604
612
 
605
613
  return {
606
614
  running: !!proxyServer,
@@ -5,27 +5,46 @@ const toml = require('toml');
5
5
  const tomlStringify = require('@iarna/toml').stringify;
6
6
  const { PATHS } = require('../../config/paths');
7
7
  const { getCodexDir } = require('./codex-config');
8
- const { isProxyConfig } = require('./codex-settings-manager');
8
+ const { isProxyConfig, readConfig } = require('./codex-settings-manager');
9
9
  const { clearNativeOAuth } = require('./native-oauth-adapters');
10
10
  const { syncCodexUserEnvironment } = require('./codex-env-manager');
11
11
  const BaseChannelService = require('./base/base-channel-service');
12
12
 
13
- const CODEX_PROXY_ENV_KEY = 'CC_PROXY_KEY';
13
+ const CODEX_MANAGED_ENV_KEY = 'CC_PROXY_KEY';
14
14
  const CODEX_PROXY_ENV_VALUE = 'PROXY_KEY';
15
15
 
16
16
  // ── Codex 特有工具函数 ──
17
17
 
18
- function buildManagedCodexEnvMap(channels = [], { includeProxyKey = false } = {}) {
19
- if (includeProxyKey) {
20
- return { [CODEX_PROXY_ENV_KEY]: CODEX_PROXY_ENV_VALUE };
18
+ function resolveCurrentManagedChannel(channels = []) {
19
+ const allChannels = Array.isArray(channels) ? channels : [];
20
+ let currentProvider = '';
21
+
22
+ try {
23
+ currentProvider = String(readConfig()?.model_provider || '').trim();
24
+ } catch (err) {
25
+ currentProvider = '';
21
26
  }
22
- const envMap = {};
23
- for (const ch of channels) {
24
- if (ch.enabled !== false && ch.envKey && ch.apiKey) {
25
- envMap[ch.envKey] = ch.apiKey;
27
+
28
+ if (currentProvider && currentProvider !== 'cc-proxy') {
29
+ const matched = allChannels.find(ch => ch.providerKey === currentProvider);
30
+ if (matched) {
31
+ return matched;
26
32
  }
27
33
  }
28
- return envMap;
34
+
35
+ return allChannels.find(ch => ch.enabled !== false) || null;
36
+ }
37
+
38
+ function buildManagedCodexEnvMap(channels = [], { includeProxyKey = false, activeChannel = null } = {}) {
39
+ if (includeProxyKey) {
40
+ return { [CODEX_MANAGED_ENV_KEY]: CODEX_PROXY_ENV_VALUE };
41
+ }
42
+
43
+ const targetChannel = activeChannel || resolveCurrentManagedChannel(channels);
44
+ if (targetChannel?.apiKey) {
45
+ return { [CODEX_MANAGED_ENV_KEY]: targetChannel.apiKey };
46
+ }
47
+ return {};
29
48
  }
30
49
 
31
50
  function syncAllChannelEnvVars() {
@@ -34,7 +53,8 @@ function syncAllChannelEnvVars() {
34
53
  const data = svc.loadChannels();
35
54
  const proxyRunning = isProxyConfig();
36
55
  const envMap = buildManagedCodexEnvMap(data.channels, {
37
- includeProxyKey: proxyRunning
56
+ includeProxyKey: proxyRunning,
57
+ activeChannel: proxyRunning ? null : resolveCurrentManagedChannel(data.channels)
38
58
  });
39
59
  syncCodexUserEnvironment(envMap, { replace: true });
40
60
  } catch (err) {
@@ -87,7 +107,7 @@ function writeCodexConfigForMultiChannel(channels) {
87
107
  name: ch.name,
88
108
  base_url: ch.baseUrl,
89
109
  wire_api: ch.wireApi || 'responses',
90
- env_key: ch.envKey,
110
+ env_key: CODEX_MANAGED_ENV_KEY,
91
111
  requires_openai_auth: ch.requiresOpenaiAuth !== false
92
112
  };
93
113
  if (ch.queryParams && Object.keys(ch.queryParams).length > 0) {
@@ -121,7 +141,7 @@ class CodexChannelService extends BaseChannelService {
121
141
  _applyDefaults(channel) {
122
142
  const ch = super._applyDefaults(channel);
123
143
  ch.providerKey = ch.providerKey || '';
124
- ch.envKey = ch.envKey || '';
144
+ ch.envKey = CODEX_MANAGED_ENV_KEY;
125
145
  ch.wireApi = ch.wireApi || 'responses';
126
146
  ch.model = ch.model || '';
127
147
  ch.speedTestModel = ch.speedTestModel || null;
@@ -143,19 +163,38 @@ class CodexChannelService extends BaseChannelService {
143
163
  }
144
164
 
145
165
  _onAfterCreate(_channel, _allChannels) {
166
+ if (_channel.enabled !== false && !isProxyConfig()) {
167
+ this._applyToNativeSettings(_channel);
168
+ return;
169
+ }
146
170
  syncAllChannelEnvVars();
147
- // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
148
171
  }
149
172
 
150
- _onAfterUpdate(_old, _next, _allChannels) {
173
+ _onAfterUpdate(_old, _next, allChannels) {
174
+ if (!isProxyConfig()) {
175
+ if (_old.enabled === false && _next.enabled !== false) {
176
+ this._applyToNativeSettings(_next);
177
+ return;
178
+ }
179
+ const activeChannel = resolveCurrentManagedChannel(allChannels);
180
+ if (_next.enabled !== false && activeChannel?.id === _next.id) {
181
+ this._applyToNativeSettings(_next);
182
+ return;
183
+ }
184
+ }
151
185
  syncAllChannelEnvVars();
152
- // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
153
186
  }
154
187
 
155
- _onAfterDelete(_channel, _allChannels) {
188
+ _onAfterDelete(_channel, allChannels) {
189
+ if (!isProxyConfig()) {
190
+ const activeChannel = resolveCurrentManagedChannel(allChannels);
191
+ if (activeChannel && activeChannel.enabled !== false) {
192
+ this._applyToNativeSettings(activeChannel);
193
+ return;
194
+ }
195
+ }
156
196
  clearNativeOAuth('codex');
157
197
  syncAllChannelEnvVars();
158
- // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
159
198
  }
160
199
 
161
200
  _applyToNativeSettings(channel) {
@@ -184,7 +223,7 @@ class CodexChannelService extends BaseChannelService {
184
223
  name: channel.name,
185
224
  base_url: channel.baseUrl,
186
225
  wire_api: channel.wireApi || 'responses',
187
- env_key: channel.envKey,
226
+ env_key: CODEX_MANAGED_ENV_KEY,
188
227
  requires_openai_auth: channel.requiresOpenaiAuth !== false
189
228
  };
190
229
 
@@ -215,10 +254,9 @@ const service = getServiceInstance();
215
254
  function getChannels() { return service.getChannels(); }
216
255
  function getEnabledChannels() { return service.getEnabledChannels(); }
217
256
  function createChannel(name, providerKey, baseUrl, apiKey, wireApi, extraConfig = {}) {
218
- const envKey = extraConfig.envKey || `${providerKey.toUpperCase()}_API_KEY`;
219
257
  return service.createChannel({
220
258
  name, providerKey, baseUrl, apiKey, wireApi,
221
- envKey,
259
+ envKey: CODEX_MANAGED_ENV_KEY,
222
260
  ...extraConfig,
223
261
  });
224
262
  }
@@ -251,4 +289,9 @@ module.exports = {
251
289
  applyChannelToSettings,
252
290
  getEffectiveApiKey,
253
291
  disableAllChannels,
292
+ _test: {
293
+ buildManagedCodexEnvMap,
294
+ CODEX_MANAGED_ENV_KEY,
295
+ resolveCurrentManagedChannel
296
+ }
254
297
  };
@@ -32,6 +32,40 @@ function powershellQuote(value) {
32
32
  return `'${String(value).replace(/'/g, "''")}'`;
33
33
  }
34
34
 
35
+ function buildWindowsSettingChangeScript() {
36
+ return [
37
+ 'Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @"',
38
+ '[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]',
39
+ 'public static extern IntPtr SendMessageTimeout(',
40
+ ' IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,',
41
+ ' uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);',
42
+ '"@',
43
+ '$HWND_BROADCAST = [IntPtr]0xffff',
44
+ '$WM_SETTINGCHANGE = 0x1a',
45
+ '$SMTO_ABORTIFHUNG = 0x0002',
46
+ '$result = [UIntPtr]::Zero',
47
+ '[Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE,',
48
+ ' [UIntPtr]::Zero, "Environment", $SMTO_ABORTIFHUNG, 5000, [ref]$result) | Out-Null'
49
+ ].join('\n');
50
+ }
51
+
52
+ function buildWindowsEnvBatchScript(operations = [], { includeSettingChangeBroadcast = true } = {}) {
53
+ const normalizedOperations = Array.isArray(operations) ? operations.filter(Boolean) : [];
54
+ const lines = normalizedOperations.map((operation) => {
55
+ const key = powershellQuote(operation.key || '');
56
+ if (operation.remove) {
57
+ return `[Environment]::SetEnvironmentVariable(${key}, $null, 'User')`;
58
+ }
59
+ return `[Environment]::SetEnvironmentVariable(${key}, ${powershellQuote(operation.value || '')}, 'User')`;
60
+ });
61
+
62
+ if (includeSettingChangeBroadcast && lines.length > 0) {
63
+ lines.push(buildWindowsSettingChangeScript());
64
+ }
65
+
66
+ return lines.join('\n');
67
+ }
68
+
35
69
  function buildHomeRelativeShellPath(filePath, homeDir) {
36
70
  const normalizedHome = path.resolve(homeDir);
37
71
  const normalizedFilePath = path.resolve(filePath);
@@ -341,48 +375,28 @@ function runLaunchctlCommand(args, execSync) {
341
375
  }
342
376
 
343
377
  function broadcastWindowsSettingChange(execSync) {
344
- const script = [
345
- 'Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @"',
346
- '[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]',
347
- 'public static extern IntPtr SendMessageTimeout(',
348
- ' IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,',
349
- ' uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);',
350
- '"@',
351
- '$HWND_BROADCAST = [IntPtr]0xffff',
352
- '$WM_SETTINGCHANGE = 0x1a',
353
- '$SMTO_ABORTIFHUNG = 0x0002',
354
- '$result = [UIntPtr]::Zero',
355
- '[Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE,',
356
- ' [UIntPtr]::Zero, "Environment", $SMTO_ABORTIFHUNG, 5000, [ref]$result) | Out-Null'
357
- ].join('\n');
358
- runWindowsEnvCommand(script, execSync);
378
+ runWindowsEnvCommand(buildWindowsSettingChangeScript(), execSync);
359
379
  }
360
380
 
361
381
  function syncWindowsEnvironment(nextValues, previousState, options) {
362
382
  const { stateFilePath, execSync } = options;
363
383
  const nextKeys = Object.keys(nextValues).sort();
364
384
  const previousValues = previousState.values || {};
365
- let changed = false;
385
+ const operations = [];
366
386
 
367
387
  for (const [key, value] of Object.entries(nextValues)) {
368
388
  if (previousValues[key] === value) continue;
369
- setWindowsUserEnv(key, value, execSync);
370
- changed = true;
389
+ operations.push({ key, value });
371
390
  }
372
391
 
373
392
  for (const key of Object.keys(previousValues)) {
374
393
  if (Object.prototype.hasOwnProperty.call(nextValues, key)) continue;
375
- removeWindowsUserEnv(key, execSync);
376
- changed = true;
394
+ operations.push({ key, remove: true });
377
395
  }
378
396
 
379
- // 广播 WM_SETTINGCHANGE,通知已打开的应用(如 VSCode)刷新环境变量
397
+ const changed = operations.length > 0;
380
398
  if (changed) {
381
- try {
382
- broadcastWindowsSettingChange(execSync);
383
- } catch {
384
- // 广播失败不影响主流程,环境变量已写入注册表
385
- }
399
+ runWindowsEnvCommand(buildWindowsEnvBatchScript(operations), execSync);
386
400
  }
387
401
 
388
402
  if (nextKeys.length > 0) {
@@ -427,14 +441,14 @@ function runWindowsEnvCommand(script, execSync) {
427
441
 
428
442
  function setWindowsUserEnv(key, value, execSync) {
429
443
  runWindowsEnvCommand(
430
- `[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, ${powershellQuote(value)}, 'User')`,
444
+ buildWindowsEnvBatchScript([{ key, value }], { includeSettingChangeBroadcast: false }),
431
445
  execSync
432
446
  );
433
447
  }
434
448
 
435
449
  function removeWindowsUserEnv(key, execSync) {
436
450
  runWindowsEnvCommand(
437
- `[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, $null, 'User')`,
451
+ buildWindowsEnvBatchScript([{ key, remove: true }], { includeSettingChangeBroadcast: false }),
438
452
  execSync
439
453
  );
440
454
  }
@@ -473,8 +487,10 @@ module.exports = {
473
487
  syncCodexUserEnvironment,
474
488
  _test: {
475
489
  broadcastWindowsSettingChange,
490
+ buildWindowsEnvBatchScript,
476
491
  buildHomeRelativeShellPath,
477
492
  buildNextEnvValues,
493
+ buildWindowsSettingChangeScript,
478
494
  buildSourceSnippet,
479
495
  getPosixProfileCandidates,
480
496
  readState,
@@ -219,7 +219,7 @@ function inspectClaudeState() {
219
219
 
220
220
  return {
221
221
  tool: 'claude',
222
- mode: proxyStatus.running ? 'proxy' : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle')),
222
+ mode: proxyStatus.running ? 'proxy' : (channelConfigured ? 'channel' : (nativeOAuth ? 'oauth' : 'idle')),
223
223
  proxyRunning: proxyStatus.running,
224
224
  oauthPresent: Boolean(nativeOAuth),
225
225
  channelConfigured,
@@ -395,7 +395,7 @@ function inspectCodexState() {
395
395
 
396
396
  return {
397
397
  tool: 'codex',
398
- mode: proxyStatus.running ? 'proxy' : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle')),
398
+ mode: proxyStatus.running ? 'proxy' : (channelConfigured ? 'channel' : (nativeOAuth ? 'oauth' : 'idle')),
399
399
  proxyRunning: proxyStatus.running,
400
400
  oauthPresent: Boolean(nativeOAuth),
401
401
  channelConfigured,
@@ -619,7 +619,7 @@ function inspectGeminiState() {
619
619
 
620
620
  return {
621
621
  tool: 'gemini',
622
- mode: proxyStatus.running ? 'proxy' : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle')),
622
+ mode: proxyStatus.running ? 'proxy' : (channelConfigured ? 'channel' : (nativeOAuth ? 'oauth' : 'idle')),
623
623
  proxyRunning: proxyStatus.running,
624
624
  oauthPresent: Boolean(nativeOAuth),
625
625
  channelConfigured,
@@ -737,10 +737,67 @@ function clearOpenCodeOAuth() {
737
737
  writeJsonFile(NATIVE_PATHS.opencode.auth, payload);
738
738
  }
739
739
 
740
- function applyOpenCodeOAuth(credential) {
741
- clearOpenCodeOAuth();
742
- opencodeSettingsManager.clearManagedChannelConfig();
740
+ function disableOpenCodeOAuthCredential(credential = {}) {
741
+ const providerId = String(credential.providerId || '').trim();
742
+ const accessToken = String(credential.accessToken || credential.primaryToken || '').trim();
743
+ const payload = readJsonFile(NATIVE_PATHS.opencode.auth, {});
744
+ if (!payload || typeof payload !== 'object') {
745
+ return;
746
+ }
747
+
748
+ Object.keys(payload).forEach((key) => {
749
+ const target = payload[key];
750
+ if (!target || target.type !== 'oauth') {
751
+ return;
752
+ }
753
+
754
+ const providerMatched = providerId && key === providerId;
755
+ const tokenMatched = accessToken && String(target.access || '').trim() === accessToken;
756
+ if (providerMatched || tokenMatched) {
757
+ delete payload[key];
758
+ }
759
+ });
760
+
761
+ if (Object.keys(payload).length === 0) {
762
+ removeFileIfExists(NATIVE_PATHS.opencode.auth);
763
+ return;
764
+ }
765
+
766
+ writeJsonFile(NATIVE_PATHS.opencode.auth, payload);
767
+ }
768
+
769
+ function isManagedOpenCodeProvider(provider) {
770
+ if (!provider || typeof provider !== 'object') {
771
+ return false;
772
+ }
773
+
774
+ if (provider.__ctx_managed__ === true) {
775
+ return true;
776
+ }
777
+
778
+ const apiKey = String(provider?.options?.apiKey || '').trim();
779
+ const baseUrl = String(provider?.options?.baseURL || '').trim();
780
+ return apiKey === 'PROXY_KEY' && (baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost'));
781
+ }
782
+
783
+ function clearOpenCodeManagedModelSelection(config) {
784
+ const modelRef = String(config?.model || '').trim();
785
+ if (!modelRef || !modelRef.includes('/')) {
786
+ return;
787
+ }
788
+
789
+ const providerId = modelRef.split('/')[0].trim();
790
+ if (!providerId) {
791
+ return;
792
+ }
793
+
794
+ const provider = config?.provider?.[providerId];
795
+ if (isManagedOpenCodeProvider(provider)) {
796
+ delete config.model;
797
+ }
798
+ }
743
799
 
800
+ function applyOpenCodeOAuth(credential) {
744
801
  const providerId = String(credential.providerId || 'openai').trim() || 'openai';
745
802
  const payload = readJsonFile(NATIVE_PATHS.opencode.auth, {});
746
803
  payload[providerId] = {
@@ -758,9 +815,12 @@ function applyOpenCodeOAuth(credential) {
758
815
  ? opencodeSettingsManager.readConfig(configPath)
759
816
  : {};
760
817
  config.provider = config.provider && typeof config.provider === 'object' ? config.provider : {};
761
- if (!config.provider[providerId]) {
762
- config.provider[providerId] = {};
763
- }
818
+ config.provider[providerId] = config.provider[providerId] && typeof config.provider[providerId] === 'object'
819
+ ? config.provider[providerId]
820
+ : {};
821
+ // Preserve existing ctx-managed API providers for OpenCode coexistence, but
822
+ // drop the active managed selection so OAuth-backed providers become available.
823
+ clearOpenCodeManagedModelSelection(config);
764
824
  opencodeSettingsManager.writeConfig(configPath, config);
765
825
 
766
826
  return { storage: 'auth-file' };
@@ -787,7 +847,11 @@ function inspectOpenCodeState() {
787
847
 
788
848
  return {
789
849
  tool: 'opencode',
790
- mode: proxyStatus.running ? 'proxy' : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle')),
850
+ mode: proxyStatus.running
851
+ ? 'proxy'
852
+ : (nativeOAuth && channelConfigured
853
+ ? 'mixed'
854
+ : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle'))),
791
855
  proxyRunning: proxyStatus.running,
792
856
  oauthPresent: Boolean(nativeOAuth),
793
857
  channelConfigured,
@@ -865,6 +929,25 @@ function clearNativeOAuth(tool) {
865
929
  }
866
930
  }
867
931
 
932
+ function disableNativeOAuthCredential(tool, credential = {}) {
933
+ switch (tool) {
934
+ case 'claude':
935
+ clearClaudeOAuth();
936
+ return;
937
+ case 'codex':
938
+ clearCodexOAuth();
939
+ return;
940
+ case 'gemini':
941
+ clearGeminiOAuth();
942
+ return;
943
+ case 'opencode':
944
+ disableOpenCodeOAuthCredential(credential);
945
+ return;
946
+ default:
947
+ throw new Error(`Unsupported OAuth tool: ${tool}`);
948
+ }
949
+ }
950
+
868
951
  function applyOAuthCredential(tool, credential) {
869
952
  switch (tool) {
870
953
  case 'claude':
@@ -887,5 +970,6 @@ module.exports = {
887
970
  readNativeOAuth,
888
971
  readAllNativeOAuth,
889
972
  clearNativeOAuth,
973
+ disableNativeOAuthCredential,
890
974
  applyOAuthCredential
891
975
  };
@@ -12,6 +12,7 @@ const {
12
12
  inspectTool,
13
13
  readAllNativeOAuth,
14
14
  clearNativeOAuth,
15
+ disableNativeOAuthCredential,
15
16
  applyOAuthCredential
16
17
  } = require('./native-oauth-adapters');
17
18
  const { maskToken, decodeJwtPayload, removeFileIfExists } = require('./oauth-utils');
@@ -300,6 +301,23 @@ function sanitizeCredential(entry, defaultCredentialId) {
300
301
  };
301
302
  }
302
303
 
304
+ function sanitizeNativeCredential(entry = {}) {
305
+ const primaryToken = entry.primaryToken
306
+ || entry.accessToken
307
+ || entry.token
308
+ || '';
309
+
310
+ return {
311
+ providerId: entry.providerId || '',
312
+ accountId: entry.accountId || '',
313
+ accountEmail: entry.accountEmail || '',
314
+ expiresAt: entry.expiresAt || null,
315
+ lastRefresh: entry.lastRefresh || null,
316
+ storage: entry.storage || '',
317
+ tokenPreview: maskToken(primaryToken)
318
+ };
319
+ }
320
+
303
321
  function sanitizeToolSummary(tool, toolStore) {
304
322
  const credentials = (toolStore.credentials || [])
305
323
  .map((entry) => sanitizeCredential(entry, toolStore.defaultCredentialId))
@@ -309,11 +327,16 @@ function sanitizeToolSummary(tool, toolStore) {
309
327
  if (aTime !== bTime) return bTime - aTime;
310
328
  return (b.createdAt || 0) - (a.createdAt || 0);
311
329
  });
330
+ const nativeState = inspectTool(tool);
331
+ const nativeCredentials = readAllNativeOAuth(tool).map((entry) => sanitizeNativeCredential(entry));
312
332
  return {
313
333
  tool,
314
334
  defaultCredentialId: toolStore.defaultCredentialId || null,
315
335
  credentials,
316
- nativeState: inspectTool(tool)
336
+ nativeState: {
337
+ ...nativeState,
338
+ nativeCredentials
339
+ }
317
340
  };
318
341
  }
319
342
 
@@ -612,7 +635,9 @@ async function applyStoredCredential(tool, credentialId) {
612
635
  const entry = findStoredCredential(tool, credentialId);
613
636
  const proxyStopped = await stopProxyIfRunning(tool);
614
637
  cleanupManagedArtifacts(tool);
615
- disableAllChannelsForTool(tool);
638
+ if (tool !== 'opencode') {
639
+ disableAllChannelsForTool(tool);
640
+ }
616
641
  applyOAuthCredential(tool, entry.secrets);
617
642
 
618
643
  // 记录最近使用时间
@@ -631,6 +656,22 @@ async function applyStoredCredential(tool, credentialId) {
631
656
  };
632
657
  }
633
658
 
659
+ function disableStoredCredential(tool, credentialId) {
660
+ assertSupportedTool(tool);
661
+ const entry = findStoredCredential(tool, credentialId);
662
+ disableNativeOAuthCredential(tool, {
663
+ ...(entry.secrets || {}),
664
+ providerId: entry.providerId || entry.secrets?.providerId || '',
665
+ accountId: entry.accountId || entry.secrets?.accountId || ''
666
+ });
667
+
668
+ return {
669
+ credential: sanitizeCredential(entry, readStore().tools[tool]?.defaultCredentialId || null),
670
+ toolSummary: getToolSummary(tool),
671
+ nativeState: inspectTool(tool)
672
+ };
673
+ }
674
+
634
675
  function clearNativeOAuthState(tool) {
635
676
  assertSupportedTool(tool);
636
677
  clearNativeOAuth(tool);
@@ -791,6 +832,7 @@ module.exports = {
791
832
  setDefaultCredential,
792
833
  deleteCredential,
793
834
  applyStoredCredential,
835
+ disableStoredCredential,
794
836
  clearNativeOAuthState,
795
837
  fetchCredentialUsage
796
838
  };
@@ -2,7 +2,6 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { PATHS } = require('../../config/paths');
5
- const { clearNativeOAuth } = require('./native-oauth-adapters');
6
5
  const { setChannelConfig } = require('./opencode-settings-manager');
7
6
  const { normalizeGatewaySourceType } = require('./base/proxy-utils');
8
7
 
@@ -251,7 +250,6 @@ function applyChannelToSettings(channelId) {
251
250
  });
252
251
  saveChannels(data);
253
252
 
254
- clearNativeOAuth('opencode');
255
253
  setChannelConfig(channel);
256
254
 
257
255
  return channel;