coding-tool-x 3.8.8 → 3.8.10

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 (134) hide show
  1. package/README.md +56 -388
  2. package/dist/web/assets/AgentsDrawer-Bam7kBjG.js +1 -0
  3. package/dist/web/assets/Analytics-D5dUWXEa.js +1 -0
  4. package/dist/web/assets/AssetPathField-CLqalYv-.js +1 -0
  5. package/dist/web/assets/{ChatHistoryDrawer-CrfS0KYV.js → ChatHistoryDrawer-C1At65O3.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-ClMsJuUu.js → CollapseItem-CVWojShK.js} +1 -1
  7. package/dist/web/assets/CommandsDrawer-BEoypGBf.js +1 -0
  8. package/dist/web/assets/ConfigExportDrawer-DBvFDDWK.js +1 -0
  9. package/dist/web/assets/ConfigTemplates-BpaWG-iC.js +1 -0
  10. package/dist/web/assets/{ConfigTemplatesDrawer-BqM32Im7.js → ConfigTemplatesDrawer-C2lvyKMf.js} +1 -1
  11. package/dist/web/assets/ConfigTemplatesPanel-CorOY7kY.js +1 -0
  12. package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
  13. package/dist/web/assets/FavoritesDrawer-uW88N6te.js +1 -0
  14. package/dist/web/assets/GatewayConvertDrawer-i5Wt9Dd9.js +1 -0
  15. package/dist/web/assets/Home-BIxWvm29.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-BMMUufSc.js → MarkdownEditor-Ul0bSGj-.js} +1 -1
  17. package/dist/web/assets/McpDrawer-Cc2xq77I.js +1 -0
  18. package/dist/web/assets/McpFormDrawer-DLHX_nrI.js +1 -0
  19. package/dist/web/assets/{PluginManager-C4kJlpVX.js → PluginManager-BGQ0xF2F.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-05R4k15x.js → PluginsDrawer-STwHEqiF.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-1zuNP7da.js +1 -0
  22. package/dist/web/assets/ProjectList-VbIYSHbH.css +1 -0
  23. package/dist/web/assets/ProjectList-_pe2nk9c.js +1 -0
  24. package/dist/web/assets/PromptsDrawer-CJN1xZk7.js +1 -0
  25. package/dist/web/assets/RecentSessionsDrawer-DX1HI8Nm.js +1 -0
  26. package/dist/web/assets/SessionCard-Op6PHvi0.js +1 -0
  27. package/dist/web/assets/SessionList-Da_TAEYr.js +1 -0
  28. package/dist/web/assets/SettingsDrawer-DKLd4Sjy.js +1 -0
  29. package/dist/web/assets/{SkillManager-D9jkNuRM.js → SkillManager-PcAa5EbF.js} +1 -1
  30. package/dist/web/assets/{SkillsDrawer-0Z4IefxC.js → SkillsDrawer-DPdU4p6C.js} +1 -1
  31. package/dist/web/assets/SkillsPanel-B9qzRqjd.js +1 -0
  32. package/dist/web/assets/Upload-e1M9Yp5z.js +1 -0
  33. package/dist/web/assets/{WorkspaceDrawer-eujtWpoq.js → WorkspaceDrawer-oS6mB1VZ.js} +1 -1
  34. package/dist/web/assets/{WorkspaceManager-CQ-8Ej68.js → WorkspaceManager-6VvRWe3r.js} +1 -1
  35. package/dist/web/assets/WorkspacePanel-CYq0Z8iY.js +1 -0
  36. package/dist/web/assets/client-BgcNg4Bt.js +1 -0
  37. package/dist/web/assets/headers-DbowkmZJ.js +1 -0
  38. package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
  39. package/dist/web/assets/index-BlUXw84U.js +2 -0
  40. package/dist/web/assets/index-Bn8BUnwP.css +1 -0
  41. package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
  42. package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
  43. package/dist/web/assets/platforms-mqqe_82d.js +1 -0
  44. package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
  45. package/dist/web/assets/sessions-53-Fr68H.js +1 -0
  46. package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
  47. package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
  48. package/dist/web/assets/{useResponsiveDrawer-DZd5rNk5.js → useResponsiveDrawer-ChHZrTfl.js} +1 -1
  49. package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
  50. package/dist/web/index.html +8 -8
  51. package/package.json +4 -2
  52. package/src/commands/daemon.js +27 -4
  53. package/src/commands/export-config.js +17 -211
  54. package/src/config/model-metadata.json +180 -16
  55. package/src/platforms/channel-auth-registry.js +36 -6
  56. package/src/platforms/channel-auth-service.js +13 -3
  57. package/src/platforms/commands-service.js +28 -0
  58. package/src/platforms/driver-registry.js +1 -0
  59. package/src/platforms/drivers/claude/channels-implementation.js +79 -90
  60. package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
  61. package/src/platforms/drivers/codex/channels-implementation.js +34 -49
  62. package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
  63. package/src/platforms/drivers/dsh/api-operations.js +1 -1
  64. package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
  65. package/src/platforms/drivers/dsh/channels.js +0 -18
  66. package/src/platforms/drivers/dsh/common.js +7 -0
  67. package/src/platforms/drivers/dsh/native-config.js +7 -2
  68. package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
  69. package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
  70. package/src/platforms/drivers/generic-native-config.js +20 -0
  71. package/src/platforms/drivers/native-config-snapshot.js +30 -4
  72. package/src/platforms/drivers/omp/channels-implementation.js +51 -17
  73. package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
  74. package/src/platforms/drivers/omp/prompts.js +58 -2
  75. package/src/platforms/drivers/omp/proxy-implementation.js +25 -1
  76. package/src/platforms/drivers/opencode/channels-implementation.js +31 -22
  77. package/src/platforms/drivers/opencode/channels.js +6 -1
  78. package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
  79. package/src/platforms/manifest-schema.js +35 -2
  80. package/src/platforms/manifests/claude.json +0 -8
  81. package/src/platforms/manifests/codex.json +0 -8
  82. package/src/platforms/manifests/dsh.json +6 -9
  83. package/src/platforms/manifests/gemini.json +0 -8
  84. package/src/platforms/manifests/omp.json +2 -9
  85. package/src/platforms/manifests/opencode.json +28 -8
  86. package/src/platforms/native-oauth-adapters.js +351 -51
  87. package/src/platforms/oauth-credentials-service.js +344 -13
  88. package/src/server/index.js +37 -1
  89. package/src/server/services/bounded-content.js +60 -0
  90. package/src/server/services/channel-balance.js +10 -4
  91. package/src/server/services/config-export-service.js +2 -2
  92. package/src/server/services/config-templates-service.js +5 -1
  93. package/src/server/services/plugins-service.js +162 -85
  94. package/src/server/services/session-history-maintenance-worker.js +71 -0
  95. package/src/server/services/session-history-maintenance.js +166 -0
  96. package/src/server/services/skill-service.js +23 -3
  97. package/src/server/services/speed-test.js +0 -72
  98. package/src/shared/channel-driver.js +16 -0
  99. package/src/shared/driver-factories/api.js +1 -0
  100. package/src/utils/native-config-patcher.js +474 -0
  101. package/dist/web/assets/AgentsDrawer-DRQuwi0m.js +0 -1
  102. package/dist/web/assets/Analytics-Cf5KwEdj.js +0 -1
  103. package/dist/web/assets/AssetPathField-DyniM0pF.js +0 -1
  104. package/dist/web/assets/CommandsDrawer-CzHelf56.js +0 -1
  105. package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +0 -1
  106. package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +0 -1
  107. package/dist/web/assets/ConfigTemplatesPanel-lTdutC8H.js +0 -1
  108. package/dist/web/assets/FavoritesDrawer-DDyWUBJR.js +0 -1
  109. package/dist/web/assets/GatewayConvertDrawer-Rigtt_5J.js +0 -1
  110. package/dist/web/assets/Home-tCSKmSHV.js +0 -1
  111. package/dist/web/assets/McpDrawer-CmE-d7VW.js +0 -1
  112. package/dist/web/assets/McpFormDrawer-ByvS18od.js +0 -1
  113. package/dist/web/assets/PluginsPanel-BCoEs7Og.js +0 -1
  114. package/dist/web/assets/ProjectList-BJt1OIaI.js +0 -1
  115. package/dist/web/assets/ProjectList-DG3yu8xZ.css +0 -1
  116. package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +0 -1
  117. package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +0 -1
  118. package/dist/web/assets/SessionCard-BUgjHQwX.js +0 -1
  119. package/dist/web/assets/SessionList-B-bXZRFk.js +0 -1
  120. package/dist/web/assets/SettingsDrawer-BITzjQLl.js +0 -1
  121. package/dist/web/assets/SkillsPanel-BKIVOj7n.js +0 -1
  122. package/dist/web/assets/SpeedTestDrawer-Bx1bYf5i.js +0 -1
  123. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
  124. package/dist/web/assets/Upload-CzC4rGI4.js +0 -1
  125. package/dist/web/assets/WorkspacePanel-Cql5qE2R.js +0 -1
  126. package/dist/web/assets/client-UDwXBP9i.js +0 -1
  127. package/dist/web/assets/headers-BqDmkMjy.js +0 -1
  128. package/dist/web/assets/index-Cmmwj3Fx.js +0 -2
  129. package/dist/web/assets/index-Cnbe696C.css +0 -1
  130. package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
  131. package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
  132. package/dist/web/assets/project-config-B2baPzYL.js +0 -1
  133. package/dist/web/assets/sessions-DqlbocLJ.js +0 -1
  134. package/dist/web/assets/use-message-DqCwefG6.js +0 -1
@@ -3,8 +3,9 @@ const path = require('path');
3
3
  const BaseChannelService = require('../../../shared/base-channel-service');
4
4
  const { isProxyConfig } = require('./native-config-implementation');
5
5
  const { PATHS, NATIVE_PATHS } = require('../../../config/paths');
6
- const { clearNativeOAuth, readNativeOAuth, clearClaudeChannelConfig } = require('../../native-oauth-adapters');
6
+ const { readNativeOAuth, clearClaudeChannelConfig } = require('../../native-oauth-adapters');
7
7
  const { isWindowsLikePlatform } = require('../../../utils/home-dir');
8
+ const { updateJsoncFile } = require('../../../utils/native-config-patcher');
8
9
  const { normalizeGatewaySourceType } = require('../../../shared/proxy-utils');
9
10
  const {
10
11
  createSkippedResult,
@@ -96,62 +97,46 @@ function buildApiKeyHelperCommand(value) {
96
97
 
97
98
  // ── Claude 原生设置写入 ──
98
99
 
99
- function updateClaudeSettingsWithModelConfig(channel) {
100
- clearNativeOAuth('claude');
100
+ function updateClaudeSettingsWithModelConfig(channel, managedChannels = []) {
101
101
  const settingsPath = getClaudeSettingsPath();
102
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
103
-
104
- let settings = {};
105
- if (fs.existsSync(settingsPath)) {
106
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
107
- }
108
-
109
- if (!settings.env) {
110
- settings.env = {};
111
- }
112
-
113
102
  const { baseUrl, apiKey, modelConfig, presetId, proxyUrl } = channel;
114
-
115
- settings.env.ANTHROPIC_BASE_URL = baseUrl;
116
- settings.env.ANTHROPIC_API_KEY = apiKey;
117
- delete settings.env.ANTHROPIC_AUTH_TOKEN;
118
- delete settings.env.CLAUDE_CODE_OAUTH_TOKEN;
119
-
120
- if (presetId && presetId !== 'official' && modelConfig) {
121
- if (modelConfig.model) {
122
- settings.env.ANTHROPIC_MODEL = modelConfig.model;
123
- }
124
- if (modelConfig.haikuModel) {
125
- settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = modelConfig.haikuModel;
126
- }
127
- if (modelConfig.sonnetModel) {
128
- settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = modelConfig.sonnetModel;
129
- }
130
- if (modelConfig.opusModel) {
131
- settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = modelConfig.opusModel;
103
+ const managedProxyUrls = new Set([
104
+ ...managedChannels.map(item => item?.proxyUrl),
105
+ ...service.loadChannels().channels.map(item => item?.proxyUrl)
106
+ ].map(value => String(value || '').trim()).filter(Boolean));
107
+ updateJsoncFile(settingsPath, (settings) => {
108
+ if (!settings.env || typeof settings.env !== 'object' || Array.isArray(settings.env)) settings.env = {};
109
+ settings.env.ANTHROPIC_BASE_URL = baseUrl;
110
+ settings.env.ANTHROPIC_API_KEY = apiKey;
111
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
112
+ delete settings.env.CLAUDE_CODE_OAUTH_TOKEN;
113
+
114
+ const modelKeys = {
115
+ ANTHROPIC_MODEL: modelConfig?.model,
116
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: modelConfig?.haikuModel,
117
+ ANTHROPIC_DEFAULT_SONNET_MODEL: modelConfig?.sonnetModel,
118
+ ANTHROPIC_DEFAULT_OPUS_MODEL: modelConfig?.opusModel
119
+ };
120
+ if (presetId && presetId !== 'official' && modelConfig) {
121
+ Object.entries(modelKeys).forEach(([key, value]) => {
122
+ if (value) settings.env[key] = value;
123
+ else delete settings.env[key];
124
+ });
125
+ } else {
126
+ Object.keys(modelKeys).forEach(key => delete settings.env[key]);
132
127
  }
133
- } else {
134
- delete settings.env.ANTHROPIC_MODEL;
135
- delete settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
136
- delete settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
137
- delete settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
138
- }
139
-
140
- if (proxyUrl) {
141
- settings.env.HTTPS_PROXY = proxyUrl;
142
- settings.env.HTTP_PROXY = proxyUrl;
143
- } else {
144
- delete settings.env.HTTPS_PROXY;
145
- delete settings.env.HTTP_PROXY;
146
- delete settings.env.NO_PROXY;
147
- }
148
128
 
149
- if (settings.env && Object.keys(settings.env).length === 0) {
150
- delete settings.env;
151
- }
152
-
153
- settings.apiKeyHelper = buildApiKeyHelperCommand(apiKey);
154
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
129
+ if (proxyUrl) {
130
+ settings.env.HTTPS_PROXY = proxyUrl;
131
+ settings.env.HTTP_PROXY = proxyUrl;
132
+ } else {
133
+ ['HTTPS_PROXY', 'HTTP_PROXY'].forEach((key) => {
134
+ if (managedProxyUrls.has(String(settings.env[key] || '').trim())) delete settings.env[key];
135
+ });
136
+ }
137
+ if (Object.keys(settings.env).length === 0) delete settings.env;
138
+ settings.apiKeyHelper = buildApiKeyHelperCommand(apiKey);
139
+ });
155
140
  }
156
141
 
157
142
  function createClaudeOAuthError(message, code, statusCode) {
@@ -213,35 +198,20 @@ function validateClaudeOAuthMutation(channel, operation) {
213
198
 
214
199
  function updateClaudeSettings(baseUrl, apiKey) {
215
200
  const settingsPath = getClaudeSettingsPath();
216
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
217
-
218
- let settings = {};
219
- if (fs.existsSync(settingsPath)) {
220
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
221
- }
222
-
223
- if (!settings.env) {
224
- settings.env = {};
225
- }
226
-
227
- const useAuthToken = settings.env.ANTHROPIC_AUTH_TOKEN !== undefined;
228
- const useApiKey = settings.env.ANTHROPIC_API_KEY !== undefined;
229
-
230
- settings.env.ANTHROPIC_BASE_URL = baseUrl;
231
-
232
- if (useAuthToken || (!useAuthToken && !useApiKey)) {
233
- settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
234
- delete settings.env.ANTHROPIC_API_KEY;
235
- } else {
236
- settings.env.ANTHROPIC_API_KEY = apiKey;
237
- }
238
-
239
- if (settings.env && Object.keys(settings.env).length === 0) {
240
- delete settings.env;
241
- }
242
-
243
- settings.apiKeyHelper = buildApiKeyHelperCommand(apiKey);
244
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
201
+ updateJsoncFile(settingsPath, (settings) => {
202
+ if (!settings.env || typeof settings.env !== 'object' || Array.isArray(settings.env)) settings.env = {};
203
+ const useAuthToken = settings.env.ANTHROPIC_AUTH_TOKEN !== undefined;
204
+ const useApiKey = settings.env.ANTHROPIC_API_KEY !== undefined;
205
+ settings.env.ANTHROPIC_BASE_URL = baseUrl;
206
+ if (useAuthToken || (!useAuthToken && !useApiKey)) {
207
+ settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
208
+ delete settings.env.ANTHROPIC_API_KEY;
209
+ } else {
210
+ settings.env.ANTHROPIC_API_KEY = apiKey;
211
+ }
212
+ if (Object.keys(settings.env).length === 0) delete settings.env;
213
+ settings.apiKeyHelper = buildApiKeyHelperCommand(apiKey);
214
+ });
245
215
  }
246
216
 
247
217
  function resolveCurrentManagedChannel(channels = []) {
@@ -334,9 +304,9 @@ class ClaudeChannelService extends BaseChannelService {
334
304
  this._cacheInitialized = true;
335
305
  }
336
306
 
337
- _onAfterCreate(channel, _allChannels) {
307
+ _onAfterCreate(channel, allChannels) {
338
308
  if (!isClaudeProxyRunning() && channel.enabled !== false && !isOpenAiCompatibleGateway(channel)) {
339
- this._applyToNativeSettings(channel);
309
+ this._applyToNativeSettings(channel, allChannels);
340
310
  }
341
311
  }
342
312
 
@@ -347,7 +317,7 @@ class ClaudeChannelService extends BaseChannelService {
347
317
 
348
318
  if (oldChannel.enabled === false && nextChannel.enabled !== false) {
349
319
  if (!isOpenAiCompatibleGateway(nextChannel)) {
350
- this._applyToNativeSettings(nextChannel);
320
+ this._applyToNativeSettings(nextChannel, [...allChannels, oldChannel]);
351
321
  }
352
322
  return;
353
323
  }
@@ -358,19 +328,38 @@ class ClaudeChannelService extends BaseChannelService {
358
328
  && activeChannel?.id === nextChannel.id
359
329
  && !isOpenAiCompatibleGateway(nextChannel)
360
330
  ) {
361
- this._applyToNativeSettings(nextChannel);
331
+ this._applyToNativeSettings(nextChannel, [...allChannels, oldChannel]);
332
+ return;
333
+ }
334
+ if (oldChannel.enabled !== false && nextChannel.enabled === false) {
335
+ if (activeChannel && !isOpenAiCompatibleGateway(activeChannel)) {
336
+ this._applyToNativeSettings(activeChannel, [...allChannels, oldChannel]);
337
+ } else if (!activeChannel) {
338
+ clearClaudeChannelConfig([...allChannels, oldChannel].map(item => item?.proxyUrl));
339
+ }
362
340
  }
363
341
  }
364
342
 
365
- _onAfterDelete(_channel, allChannels) {
343
+ _onAfterDelete(channel, allChannels) {
366
344
  if (isClaudeProxyRunning()) {
367
345
  return;
368
346
  }
369
347
 
370
348
  const activeChannel = resolveCurrentManagedChannel(allChannels);
371
349
  if (activeChannel && !isOpenAiCompatibleGateway(activeChannel)) {
372
- this._applyToNativeSettings(activeChannel);
350
+ this._applyToNativeSettings(activeChannel, [...allChannels, channel]);
351
+ } else if (!activeChannel) {
352
+ clearClaudeChannelConfig([...allChannels, channel].map(item => item?.proxyUrl));
353
+ }
354
+ }
355
+
356
+ disableAllChannels() {
357
+ const channels = this.loadChannels().channels;
358
+ const result = super.disableAllChannels();
359
+ if (!isClaudeProxyRunning()) {
360
+ clearClaudeChannelConfig(channels.map(item => item?.proxyUrl));
373
361
  }
362
+ return result;
374
363
  }
375
364
 
376
365
  applyChannelToSettings(channelId) {
@@ -388,12 +377,12 @@ class ClaudeChannelService extends BaseChannelService {
388
377
  return super.applyChannelToSettings(channelId);
389
378
  }
390
379
 
391
- _applyToNativeSettings(channel) {
380
+ _applyToNativeSettings(channel, managedChannels = []) {
392
381
  if (channel.authMode === 'oauth') {
393
382
  applyNativeClaudeOAuth(channel);
394
383
  return;
395
384
  }
396
- updateClaudeSettingsWithModelConfig(channel);
385
+ updateClaudeSettingsWithModelConfig(channel, managedChannels);
397
386
  }
398
387
 
399
388
  getEffectiveApiKey(channel) {
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const { NATIVE_PATHS } = require('../../../config/paths');
3
3
  const { isWindowsLikePlatform } = require('../../../utils/home-dir');
4
+ const { readJsoncFile, writeJsoncFile } = require('../../../utils/native-config-patcher');
4
5
  const DEFAULT_NATIVE_PATHS = NATIVE_PATHS.claude;
5
6
  let nativePaths = DEFAULT_NATIVE_PATHS;
6
7
 
@@ -40,8 +41,7 @@ function hasBackup() {
40
41
  // 读取配置文件
41
42
  function readSettings() {
42
43
  try {
43
- const content = fs.readFileSync(getSettingsPath(), 'utf8');
44
- return JSON.parse(content);
44
+ return readJsoncFile(getSettingsPath());
45
45
  } catch (err) {
46
46
  throw new Error('Failed to read settings.json: ' + err.message);
47
47
  }
@@ -50,8 +50,7 @@ function readSettings() {
50
50
  // 写入配置文件
51
51
  function writeSettings(settings) {
52
52
  try {
53
- const content = JSON.stringify(settings, null, 2);
54
- fs.writeFileSync(getSettingsPath(), content, 'utf8');
53
+ writeJsoncFile(getSettingsPath(), settings);
55
54
  } catch (err) {
56
55
  throw new Error('Failed to write settings.json: ' + err.message);
57
56
  }
@@ -2,8 +2,8 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const toml = require('toml');
5
- const tomlStringify = require('@iarna/toml').stringify;
6
5
  const { PATHS } = require('../../../config/paths');
6
+ const { writeTomlFile } = require('../../../utils/native-config-patcher');
7
7
  const { getCodexDir } = require('./config');
8
8
  const { isProxyConfig, readConfig } = require('./native-config-implementation');
9
9
  const { syncCodexUserEnvironment } = require('./env-manager');
@@ -175,21 +175,24 @@ function syncAllChannelEnvVars() {
175
175
  }
176
176
  }
177
177
 
178
- function writeAnnotatedCodexConfig(configPath, config, comments = []) {
179
- let tomlContent = tomlStringify(config);
180
- if (comments.length > 0) {
181
- tomlContent = comments.join('\n') + '\n\n' + tomlContent;
182
- }
183
- fs.writeFileSync(configPath, tomlContent, 'utf8');
178
+ function writeAnnotatedCodexConfig(configPath, config) {
179
+ writeTomlFile(configPath, config, { atomic: true });
184
180
  }
185
181
 
186
- function pruneManagedProviders(existingProviders, currentProviderKey, allChannels) {
187
- const knownKeys = new Set(allChannels.map(ch => ch.providerKey).filter(Boolean));
188
- for (const key of Object.keys(existingProviders)) {
189
- if (key === 'cc-proxy' || (key !== currentProviderKey && knownKeys.has(key))) {
190
- delete existingProviders[key];
191
- }
182
+ function mergeCodexProviderConfig(existing = {}, channel = {}) {
183
+ const provider = {
184
+ ...existing,
185
+ name: channel.name,
186
+ base_url: channel.baseUrl,
187
+ wire_api: channel.wireApi || 'responses',
188
+ env_key: CODEX_MANAGED_ENV_KEY,
189
+ requires_openai_auth: channelRequiresOpenaiAuth(channel)
190
+ };
191
+ delete provider.query_params;
192
+ if (channel.queryParams && Object.keys(channel.queryParams).length > 0) {
193
+ provider.query_params = channel.queryParams;
192
194
  }
195
+ return provider;
193
196
  }
194
197
 
195
198
  function writeCodexConfigForMultiChannel(channels) {
@@ -201,8 +204,7 @@ function writeCodexConfigForMultiChannel(channels) {
201
204
  try {
202
205
  config = toml.parse(fs.readFileSync(configPath, 'utf8'));
203
206
  } catch (err) {
204
- console.warn('[Codex Channels] Failed to parse existing config.toml:', err.message);
205
- config = {};
207
+ throw new Error(`Failed to parse existing config.toml: ${err.message}`);
206
208
  }
207
209
  }
208
210
 
@@ -229,26 +231,17 @@ function writeCodexConfigForMultiChannel(channels) {
229
231
  config.model_provider = primary.providerKey;
230
232
 
231
233
  for (const ch of enabledChannels) {
232
- config.model_providers[ch.providerKey] = {
233
- name: ch.name,
234
- base_url: ch.baseUrl,
235
- wire_api: ch.wireApi || 'responses',
236
- env_key: CODEX_MANAGED_ENV_KEY,
237
- requires_openai_auth: channelRequiresOpenaiAuth(ch)
238
- };
239
- if (ch.queryParams && Object.keys(ch.queryParams).length > 0) {
240
- config.model_providers[ch.providerKey].query_params = ch.queryParams;
241
- }
234
+ config.model_providers[ch.providerKey] = mergeCodexProviderConfig(
235
+ config.model_providers[ch.providerKey],
236
+ ch
237
+ );
242
238
  }
243
239
  } else {
244
240
  clearManagedCodexConfig();
245
241
  return;
246
242
  }
247
243
 
248
- writeAnnotatedCodexConfig(configPath, config, [
249
- '# Codex Configuration',
250
- '# Managed by Coding-Tool (multi-channel)'
251
- ]);
244
+ writeAnnotatedCodexConfig(configPath, config);
252
245
  }
253
246
 
254
247
  // ── CodexChannelService ──
@@ -315,6 +308,12 @@ class CodexChannelService extends BaseChannelService {
315
308
  this._applyToNativeSettings(_next);
316
309
  return;
317
310
  }
311
+ if (_old.enabled !== false && _next.enabled === false) {
312
+ const activeChannel = resolveCurrentManagedChannel(allChannels);
313
+ if (activeChannel) this._applyToNativeSettings(activeChannel);
314
+ else clearManagedCodexConfig();
315
+ return;
316
+ }
318
317
  const activeChannel = resolveCurrentManagedChannel(allChannels);
319
318
  if (_next.enabled !== false && activeChannel?.id === _next.id) {
320
319
  this._applyToNativeSettings(_next);
@@ -347,7 +346,7 @@ class CodexChannelService extends BaseChannelService {
347
346
  try {
348
347
  config = toml.parse(fs.readFileSync(configPath, 'utf8'));
349
348
  } catch (err) {
350
- config = {};
349
+ throw new Error(`Failed to parse existing config.toml: ${err.message}`);
351
350
  }
352
351
  }
353
352
 
@@ -356,26 +355,12 @@ class CodexChannelService extends BaseChannelService {
356
355
  if (!config.model_providers || typeof config.model_providers !== 'object') {
357
356
  config.model_providers = {};
358
357
  }
359
- const data = this.loadChannels();
360
- pruneManagedProviders(config.model_providers, channel.providerKey, data.channels);
361
-
362
- config.model_providers[channel.providerKey] = {
363
- name: channel.name,
364
- base_url: channel.baseUrl,
365
- wire_api: channel.wireApi || 'responses',
366
- env_key: CODEX_MANAGED_ENV_KEY,
367
- requires_openai_auth: channelRequiresOpenaiAuth(channel)
368
- };
369
-
370
- if (channel.queryParams && Object.keys(channel.queryParams).length > 0) {
371
- config.model_providers[channel.providerKey].query_params = channel.queryParams;
372
- }
358
+ config.model_providers[channel.providerKey] = mergeCodexProviderConfig(
359
+ config.model_providers[channel.providerKey],
360
+ channel
361
+ );
373
362
 
374
- writeAnnotatedCodexConfig(configPath, config, [
375
- '# Codex Configuration',
376
- '# Managed by Coding-Tool',
377
- `# Current provider: ${channel.name}`
378
- ]);
363
+ writeAnnotatedCodexConfig(configPath, config);
379
364
  console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
380
365
  syncAllChannelEnvVars();
381
366
  }
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const toml = require('toml');
4
- const tomlStringify = require('@iarna/toml').stringify;
4
+ const { writeTomlFile } = require('../../../utils/native-config-patcher');
5
5
  const { NATIVE_PATHS } = require('../../../config/paths');
6
6
  const { syncCodexUserEnvironment } = require('./env-manager');
7
7
  const DEFAULT_NATIVE_PATHS = NATIVE_PATHS.codex;
@@ -83,53 +83,10 @@ function readConfig() {
83
83
  }
84
84
  }
85
85
 
86
- // 将配置对象转换为 TOML 字符串
87
- function configToToml(config) {
88
- let content = `# Codex Configuration
89
- # Managed by Coding-Tool (Proxy Mode)
90
-
91
- `;
92
-
93
- // 写入顶级字段
94
- for (const [key, value] of Object.entries(config)) {
95
- if (key === 'model_providers') continue; // 稍后处理
96
- if (typeof value === 'string') {
97
- content += `${key} = "${value}"\n`;
98
- } else if (typeof value === 'boolean') {
99
- content += `${key} = ${value}\n`;
100
- } else if (typeof value === 'number') {
101
- content += `${key} = ${value}\n`;
102
- }
103
- }
104
-
105
- content += '\n';
106
-
107
- // 写入 model_providers
108
- if (config.model_providers) {
109
- for (const [providerKey, providerConfig] of Object.entries(config.model_providers)) {
110
- content += `[model_providers.${providerKey}]\n`;
111
- for (const [key, value] of Object.entries(providerConfig)) {
112
- if (typeof value === 'string') {
113
- content += `${key} = "${value}"\n`;
114
- } else if (typeof value === 'boolean') {
115
- content += `${key} = ${value}\n`;
116
- } else if (typeof value === 'number') {
117
- content += `${key} = ${value}\n`;
118
- }
119
- }
120
- content += '\n';
121
- }
122
- }
123
-
124
- return content;
125
- }
126
-
127
86
  // 写入 config.toml
128
87
  function writeConfig(config) {
129
88
  try {
130
- const safeConfig = JSON.parse(JSON.stringify(config || {}));
131
- const content = tomlStringify(safeConfig);
132
- fs.writeFileSync(getConfigPath(), content, 'utf8');
89
+ writeTomlFile(getConfigPath(), config || {}, { atomic: true });
133
90
  } catch (err) {
134
91
  throw new Error('Failed to write config.toml: ' + err.message);
135
92
  }
@@ -254,12 +211,7 @@ function setProxyConfig(proxyPort) {
254
211
  }
255
212
 
256
213
  // 读取当前配置(不存在时使用空配置)
257
- let config;
258
- try {
259
- config = readConfig();
260
- } catch (err) {
261
- config = {};
262
- }
214
+ const config = configExists() ? readConfig() : {};
263
215
 
264
216
  // 设置 model_provider 为 proxy
265
217
  config.model_provider = 'cc-proxy';
@@ -186,7 +186,7 @@ function createDriver(context = {}) {
186
186
 
187
187
  // Keep the aggregate DSH API driver compatible with the manifest consistency
188
188
  // contract. Actual descriptor requests are still dispatched by capability.
189
- for (const operation of ['list', 'current', 'enabled', 'create', 'update', 'remove', 'applyToSettings', 'sync', 'order', 'models', 'probeModels', 'speedTest', 'speedTestAll']) {
189
+ for (const operation of ['list', 'current', 'enabled', 'create', 'update', 'remove', 'applyToSettings', 'sync', 'order', 'models', 'probeModels', 'speedTest']) {
190
190
  driver[operation] = (...args) => channels[operation](...args);
191
191
  }
192
192
  for (const operation of ['status', 'start', 'stop']) {
@@ -4,11 +4,12 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getPlatformStatePath: resolveStatePath } = require('../../../config/paths');
6
6
  const BaseChannelService = require('../../../shared/base-channel-service');
7
+ const { writeYamlFile } = require('../../../utils/native-config-patcher');
7
8
  const {
8
9
  resolvePaths,
9
10
  readYamlFile,
10
11
  writeAtomic,
11
- dumpYaml,
12
+ DSH_YAML_SOURCE_TAGS,
12
13
  isObject
13
14
  } = require('./common');
14
15
 
@@ -202,7 +203,7 @@ class DshChannelService extends BaseChannelService {
202
203
  }
203
204
 
204
205
  _activeChannel(channels = []) {
205
- return channels.find(channel => channel.enabled !== false) || channels[0] || null;
206
+ return channels.find(channel => channel.enabled !== false) || null;
206
207
  }
207
208
 
208
209
  _onAfterCreate(channel) {
@@ -217,9 +218,8 @@ class DshChannelService extends BaseChannelService {
217
218
  this._removeNativeProvider(oldChannel.providerKey);
218
219
  }
219
220
  const active = this._activeChannel(allChannels);
220
- if (active?.id === nextChannel.id || (oldChannel.enabled !== false && nextChannel.enabled === false)) {
221
- if (active) this._applyToNativeSettings(active);
222
- }
221
+ if (active) this._applyToNativeSettings(active);
222
+ else this._removeNativeProvider(nextChannel.providerKey);
223
223
  }
224
224
 
225
225
  _onAfterDelete(channel, allChannels) {
@@ -234,7 +234,11 @@ class DshChannelService extends BaseChannelService {
234
234
  }
235
235
 
236
236
  _writeNativeSettings(settings) {
237
- writeAtomic(this.nativePaths.settings, dumpYaml(settings), 0o600);
237
+ writeYamlFile(this.nativePaths.settings, settings, {
238
+ atomic: true,
239
+ mode: 0o600,
240
+ customTags: DSH_YAML_SOURCE_TAGS
241
+ });
238
242
  }
239
243
 
240
244
  _removeNativeProvider(providerKey) {
@@ -246,6 +250,15 @@ class DshChannelService extends BaseChannelService {
246
250
  if (!Object.prototype.hasOwnProperty.call(providers, key)) return;
247
251
  delete providers[key];
248
252
  settings['llm-pi-ai'] = { ...llm, providers };
253
+ const selected = isObject(settings['agent-default-model'])
254
+ ? { ...settings['agent-default-model'] }
255
+ : null;
256
+ if (selected?.provider === key) {
257
+ delete selected.provider;
258
+ delete selected.model;
259
+ if (Object.keys(selected).length > 0) settings['agent-default-model'] = selected;
260
+ else delete settings['agent-default-model'];
261
+ }
249
262
  this._writeNativeSettings(settings);
250
263
  }
251
264
 
@@ -255,7 +268,11 @@ class DshChannelService extends BaseChannelService {
255
268
  if (!isObject(credentials.refs)) credentials.refs = {};
256
269
  credentials.refs[channel.apiKeyEnv] = channel.apiKey || '';
257
270
  if (!isObject(credentials.records)) credentials.records = {};
258
- writeAtomic(this.nativePaths.credentials, dumpYaml(credentials), 0o600);
271
+ writeYamlFile(this.nativePaths.credentials, credentials, {
272
+ atomic: true,
273
+ mode: 0o600,
274
+ customTags: DSH_YAML_SOURCE_TAGS
275
+ });
259
276
  }
260
277
 
261
278
  _providerEntry(channel, baseUrl = channel.baseUrl, models = channel.models) {
@@ -273,7 +290,10 @@ class DshChannelService extends BaseChannelService {
273
290
  const settings = this._nativeSettings();
274
291
  const llm = isObject(settings['llm-pi-ai']) ? { ...settings['llm-pi-ai'] } : {};
275
292
  const providers = isObject(llm.providers) ? { ...llm.providers } : {};
276
- providers[channel.providerKey] = this._providerEntry(channel);
293
+ providers[channel.providerKey] = {
294
+ ...(isObject(providers[channel.providerKey]) ? providers[channel.providerKey] : {}),
295
+ ...this._providerEntry(channel)
296
+ };
277
297
  settings['llm-pi-ai'] = { ...llm, providers };
278
298
  settings['agent-default-model'] = {
279
299
  ...(isObject(settings['agent-default-model']) ? settings['agent-default-model'] : {}),
@@ -149,24 +149,6 @@ function createDriver(context = {}) {
149
149
  { authSourceType: 'dsh' }
150
150
  );
151
151
  }),
152
- speedTestAll: request => withAsyncResult(request, 'speedTestAll', async () => {
153
- const { testMultipleChannels } = require('../../../server/services/speed-test');
154
- const body = bodyOf(request);
155
- const channels = implementation.getServiceInstance().getEnabledChannels();
156
- const groups = new Map();
157
- for (const channel of channels) {
158
- const type = channelTypeForSpeed(channel);
159
- const group = groups.get(type) || [];
160
- group.push(channel);
161
- groups.set(type, group);
162
- }
163
- const groupedResults = await Promise.all([...groups.entries()].map(([type, group]) => (
164
- testMultipleChannels(group, body.timeout, type, body.concurrency)
165
- )));
166
- const resultById = new Map(groupedResults.flat().map(result => [result.channelId, result]));
167
- const results = channels.map(channel => resultById.get(channel.id)).filter(Boolean);
168
- return { results };
169
- }),
170
152
  saveOrder: request => withResult(request, 'order', () => {
171
153
  const body = bodyOf(request);
172
154
  implementation.getServiceInstance().saveChannelOrder(body.order || body.ids || []);
@@ -23,6 +23,12 @@ const DSH_YAML_SCHEMA = yaml.DEFAULT_SCHEMA.extend([
23
23
  })
24
24
  ]);
25
25
 
26
+ const DSH_YAML_SOURCE_TAGS = [{
27
+ tag: 'tag:yaml.org,2002:js',
28
+ resolve: value => `!!js ${value}`,
29
+ stringify: item => String(item.value).replace(/^!!js\s*/, '')
30
+ }];
31
+
26
32
  function clone(value) {
27
33
  if (value === undefined) return undefined;
28
34
  return JSON.parse(JSON.stringify(value));
@@ -548,6 +554,7 @@ module.exports = {
548
554
  resolvePaths,
549
555
  isObject,
550
556
  readYamlFile,
557
+ DSH_YAML_SOURCE_TAGS,
551
558
  readJsonFile,
552
559
  fileRevision,
553
560
  writeAtomic,
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const { resolvePaths, readYamlFile, writeAtomic, dumpYaml, mergeSettings, fileRevision, credentialMetadata, redactSecrets, listProfiles, listProfilePlugins, listProfileMcp, listProfilePrompts } = require('./common');
4
+ const { writeYamlFile } = require('../../../utils/native-config-patcher');
5
+ const { resolvePaths, readYamlFile, mergeSettings, fileRevision, credentialMetadata, redactSecrets, listProfiles, listProfilePlugins, listProfileMcp, listProfilePrompts, DSH_YAML_SOURCE_TAGS } = require('./common');
5
6
  const { resolveSkillRoots } = require('./resources');
6
7
 
7
8
  function createDriver(context = {}) {
@@ -35,7 +36,11 @@ function createDriver(context = {}) {
35
36
  }
36
37
  const current = readYamlFile(paths.settings, {});
37
38
  const next = mergeSettings(current, body.namespace, body.patch, body.replace === true);
38
- writeAtomic(paths.settings, dumpYaml(next), 0o600);
39
+ writeYamlFile(paths.settings, next, {
40
+ atomic: true,
41
+ mode: 0o600,
42
+ customTags: DSH_YAML_SOURCE_TAGS
43
+ });
39
44
  return this.getConfig();
40
45
  },
41
46
  getConfigCapabilities() {