coding-tool-x 3.8.9 → 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 (124) 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-BTmQ1aQr.js → ChatHistoryDrawer-C1At65O3.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-CwqJk3bE.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-gyERg9iC.js → ConfigTemplates-BpaWG-iC.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesDrawer-B17RqFi4.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-9TTwTS-U.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-BuRcWuBj.js → PluginManager-BGQ0xF2F.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-_zZENaQn.js → PluginsDrawer-STwHEqiF.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-1zuNP7da.js +1 -0
  22. package/dist/web/assets/ProjectList-_pe2nk9c.js +1 -0
  23. package/dist/web/assets/PromptsDrawer-CJN1xZk7.js +1 -0
  24. package/dist/web/assets/RecentSessionsDrawer-DX1HI8Nm.js +1 -0
  25. package/dist/web/assets/SessionCard-Op6PHvi0.js +1 -0
  26. package/dist/web/assets/SessionList-Da_TAEYr.js +1 -0
  27. package/dist/web/assets/SettingsDrawer-DKLd4Sjy.js +1 -0
  28. package/dist/web/assets/{SkillManager-S698Hns8.js → SkillManager-PcAa5EbF.js} +1 -1
  29. package/dist/web/assets/{SkillsDrawer-Ms4lPCow.js → SkillsDrawer-DPdU4p6C.js} +1 -1
  30. package/dist/web/assets/SkillsPanel-B9qzRqjd.js +1 -0
  31. package/dist/web/assets/Upload-e1M9Yp5z.js +1 -0
  32. package/dist/web/assets/{WorkspaceDrawer-L7HJRc6u.js → WorkspaceDrawer-oS6mB1VZ.js} +1 -1
  33. package/dist/web/assets/{WorkspaceManager-DsoNg2z8.js → WorkspaceManager-6VvRWe3r.js} +1 -1
  34. package/dist/web/assets/WorkspacePanel-CYq0Z8iY.js +1 -0
  35. package/dist/web/assets/client-BgcNg4Bt.js +1 -0
  36. package/dist/web/assets/{headers-B5JWGn3v.js → headers-DbowkmZJ.js} +1 -1
  37. package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
  38. package/dist/web/assets/index-BlUXw84U.js +2 -0
  39. package/dist/web/assets/index-Bn8BUnwP.css +1 -0
  40. package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
  41. package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
  42. package/dist/web/assets/platforms-mqqe_82d.js +1 -0
  43. package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
  44. package/dist/web/assets/sessions-53-Fr68H.js +1 -0
  45. package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
  46. package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
  47. package/dist/web/assets/{useResponsiveDrawer-V_onl9NV.js → useResponsiveDrawer-ChHZrTfl.js} +1 -1
  48. package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
  49. package/dist/web/index.html +8 -8
  50. package/package.json +4 -2
  51. package/src/commands/export-config.js +17 -211
  52. package/src/config/model-metadata.json +180 -16
  53. package/src/platforms/channel-auth-service.js +1 -1
  54. package/src/platforms/commands-service.js +28 -0
  55. package/src/platforms/driver-registry.js +1 -0
  56. package/src/platforms/drivers/claude/channels-implementation.js +79 -90
  57. package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
  58. package/src/platforms/drivers/codex/channels-implementation.js +34 -49
  59. package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
  60. package/src/platforms/drivers/dsh/api-operations.js +1 -1
  61. package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
  62. package/src/platforms/drivers/dsh/channels.js +0 -18
  63. package/src/platforms/drivers/dsh/common.js +7 -0
  64. package/src/platforms/drivers/dsh/native-config.js +7 -2
  65. package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
  66. package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
  67. package/src/platforms/drivers/generic-native-config.js +20 -0
  68. package/src/platforms/drivers/native-config-snapshot.js +30 -4
  69. package/src/platforms/drivers/omp/channels-implementation.js +51 -17
  70. package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
  71. package/src/platforms/drivers/opencode/channels-implementation.js +6 -0
  72. package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
  73. package/src/platforms/manifest-schema.js +35 -2
  74. package/src/platforms/manifests/claude.json +0 -8
  75. package/src/platforms/manifests/codex.json +0 -8
  76. package/src/platforms/manifests/dsh.json +6 -9
  77. package/src/platforms/manifests/gemini.json +0 -8
  78. package/src/platforms/manifests/omp.json +0 -8
  79. package/src/platforms/manifests/opencode.json +0 -8
  80. package/src/platforms/native-oauth-adapters.js +33 -33
  81. package/src/server/index.js +2 -0
  82. package/src/server/services/bounded-content.js +60 -0
  83. package/src/server/services/channel-balance.js +10 -4
  84. package/src/server/services/config-export-service.js +2 -2
  85. package/src/server/services/config-templates-service.js +5 -1
  86. package/src/server/services/plugins-service.js +162 -85
  87. package/src/server/services/session-history-maintenance-worker.js +71 -0
  88. package/src/server/services/session-history-maintenance.js +166 -0
  89. package/src/server/services/skill-service.js +23 -3
  90. package/src/server/services/speed-test.js +0 -72
  91. package/src/shared/channel-driver.js +16 -0
  92. package/src/shared/driver-factories/api.js +1 -0
  93. package/src/utils/native-config-patcher.js +474 -0
  94. package/dist/web/assets/AgentsDrawer-Cj5CJ4d9.js +0 -1
  95. package/dist/web/assets/Analytics-1doS-8uC.js +0 -1
  96. package/dist/web/assets/AssetPathField-C9TOPl7o.js +0 -1
  97. package/dist/web/assets/CommandsDrawer-CaZWDI_G.js +0 -1
  98. package/dist/web/assets/ConfigExportDrawer-C3vSZEsS.js +0 -1
  99. package/dist/web/assets/ConfigTemplatesPanel-DKIC_FqG.js +0 -1
  100. package/dist/web/assets/FavoritesDrawer-DaziQmBp.js +0 -1
  101. package/dist/web/assets/GatewayConvertDrawer-OMgWUUSc.js +0 -1
  102. package/dist/web/assets/Home-DOSUTfrE.js +0 -1
  103. package/dist/web/assets/McpDrawer-I8rl8lGH.js +0 -1
  104. package/dist/web/assets/McpFormDrawer-DEP3C7ut.js +0 -1
  105. package/dist/web/assets/PluginsPanel-Bs5qZ-TK.js +0 -1
  106. package/dist/web/assets/ProjectList-BSxuL2td.js +0 -1
  107. package/dist/web/assets/PromptsDrawer-CoZ-ZtsY.js +0 -1
  108. package/dist/web/assets/RecentSessionsDrawer-B_17aJBG.js +0 -1
  109. package/dist/web/assets/SessionCard-BL_x0d3t.js +0 -1
  110. package/dist/web/assets/SessionList-DhRYZ6vl.js +0 -1
  111. package/dist/web/assets/SettingsDrawer-fYb0DU7X.js +0 -1
  112. package/dist/web/assets/SkillsPanel-rqnqAOGL.js +0 -1
  113. package/dist/web/assets/SpeedTestDrawer-Bbn38SfV.js +0 -1
  114. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
  115. package/dist/web/assets/Upload-CdyVHxCI.js +0 -1
  116. package/dist/web/assets/WorkspacePanel-Be8PVZah.js +0 -1
  117. package/dist/web/assets/client-UDwXBP9i.js +0 -1
  118. package/dist/web/assets/index-3Lp6z8TQ.css +0 -1
  119. package/dist/web/assets/index-BKlAWTH-.js +0 -2
  120. package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
  121. package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
  122. package/dist/web/assets/project-config-B2baPzYL.js +0 -1
  123. package/dist/web/assets/sessions-CC0j-KH_.js +0 -1
  124. package/dist/web/assets/use-message-DqCwefG6.js +0 -1
@@ -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() {
@@ -3,8 +3,9 @@ const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { PATHS, NATIVE_PATHS } = require('../../../config/paths');
5
5
  const { resolveChannelWebsiteUrl } = require('../../../config/channel-preset-websites');
6
- const { clearNativeOAuth, readNativeOAuth, clearGeminiChannelConfig } = require('../../native-oauth-adapters');
6
+ const { readNativeOAuth, clearGeminiChannelConfig } = require('../../native-oauth-adapters');
7
7
  const { normalizeGatewaySourceType } = require('../../../shared/proxy-utils');
8
+ const { readJsoncFile, updateJsoncFile, updateEnvFile } = require('../../../utils/native-config-patcher');
8
9
  const {
9
10
  createSkippedResult,
10
11
  isLocalProxyBaseUrl,
@@ -84,12 +85,16 @@ function applyNativeGeminiOAuth(channel) {
84
85
  clearGeminiChannelConfig();
85
86
 
86
87
  const settingsPath = configuredNative.settings;
87
- const settings = readGeminiSettings();
88
- settings.security = settings.security || {};
89
- settings.security.auth = settings.security.auth || {};
90
- settings.security.auth.selectedType = 'oauth-personal';
91
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
92
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
88
+ updateJsoncFile(settingsPath, (settings) => {
89
+ settings.security = settings.security && typeof settings.security === 'object' && !Array.isArray(settings.security)
90
+ ? settings.security
91
+ : {};
92
+ settings.security.auth = settings.security.auth && typeof settings.security.auth === 'object'
93
+ && !Array.isArray(settings.security.auth)
94
+ ? settings.security.auth
95
+ : {};
96
+ settings.security.auth.selectedType = 'oauth-personal';
97
+ });
93
98
  console.log(`[Gemini Channels] Applied native OAuth channel ${channel.name}`);
94
99
  return channel;
95
100
  }
@@ -168,14 +173,11 @@ function readExistingGeminiEnv() {
168
173
 
169
174
  function writeGeminiEnv(env = {}) {
170
175
  const envPath = path.join(getGeminiDir(), '.env');
171
- const content = Object.entries(env)
172
- .map(([key, value]) => `${key}=${value}`)
173
- .join('\n');
174
-
175
- fs.writeFileSync(envPath, content ? `${content}\n` : '', 'utf8');
176
- if (process.platform !== 'win32') {
177
- fs.chmodSync(envPath, 0o600);
178
- }
176
+ const managedKeys = ['GOOGLE_GEMINI_BASE_URL', 'GEMINI_API_KEY', 'GEMINI_MODEL'];
177
+ updateEnvFile(envPath, Object.fromEntries(managedKeys.map(key => [
178
+ key,
179
+ Object.prototype.hasOwnProperty.call(env, key) ? env[key] : undefined
180
+ ])), { atomic: true, mode: 0o600 });
179
181
  }
180
182
 
181
183
  // 检查是否在代理模式
@@ -515,8 +517,6 @@ function applyChannelToSettings(channelId, channels = null) {
515
517
  return applyNativeGeminiOAuth(channel);
516
518
  }
517
519
 
518
- clearNativeOAuth('gemini');
519
-
520
520
  const geminiDir = getGeminiDir();
521
521
 
522
522
  if (!fs.existsSync(geminiDir)) {
@@ -532,22 +532,16 @@ function applyChannelToSettings(channelId, channels = null) {
532
532
 
533
533
  // 确保 settings.json 存在并配置正确的认证模式
534
534
  const settingsPath = path.join(geminiDir, 'settings.json');
535
- let settings = {};
536
-
537
- if (fs.existsSync(settingsPath)) {
538
- try {
539
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
540
- } catch (err) {
541
- console.warn('[Gemini Channels] Failed to read settings.json, creating new');
542
- }
543
- }
544
-
545
- // 设置认证模式为 gemini-api-key(第三方 API)
546
- settings.security = settings.security || {};
547
- settings.security.auth = settings.security.auth || {};
548
- settings.security.auth.selectedType = 'gemini-api-key';
549
-
550
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
535
+ updateJsoncFile(settingsPath, (settings) => {
536
+ settings.security = settings.security && typeof settings.security === 'object' && !Array.isArray(settings.security)
537
+ ? settings.security
538
+ : {};
539
+ settings.security.auth = settings.security.auth && typeof settings.security.auth === 'object'
540
+ && !Array.isArray(settings.security.auth)
541
+ ? settings.security.auth
542
+ : {};
543
+ settings.security.auth.selectedType = 'gemini-api-key';
544
+ });
551
545
 
552
546
  console.log(`[Gemini Channels] Applied channel ${channel.name} to .env`);
553
547
  return channel;
@@ -600,6 +594,17 @@ function writeGeminiConfigForMultiChannel(allChannels) {
600
594
  delete env.GEMINI_API_KEY;
601
595
  delete env.GEMINI_MODEL;
602
596
  writeGeminiEnv(env);
597
+ const settingsPath = path.join(geminiDir, 'settings.json');
598
+ if (fs.existsSync(settingsPath)) {
599
+ updateJsoncFile(settingsPath, (settings) => {
600
+ const auth = settings.security?.auth;
601
+ if (auth?.selectedType === 'gemini-api-key') {
602
+ delete auth.selectedType;
603
+ if (Object.keys(auth).length === 0) delete settings.security.auth;
604
+ if (Object.keys(settings.security).length === 0) delete settings.security;
605
+ }
606
+ });
607
+ }
603
608
  return;
604
609
  }
605
610
 
@@ -611,22 +616,13 @@ function writeGeminiConfigForMultiChannel(allChannels) {
611
616
 
612
617
  // 确保 settings.json 存在并配置正确的认证模式
613
618
  const settingsPath = path.join(geminiDir, 'settings.json');
614
- let settings = {};
615
-
616
- if (fs.existsSync(settingsPath)) {
617
- try {
618
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
619
- } catch (err) {
620
- console.warn('[Gemini Channels] Failed to read settings.json, creating new');
621
- }
622
- }
623
-
624
- // 设置认证模式为 gemini-api-key(第三方 API)
625
- settings.security = settings.security || {};
626
- settings.security.auth = settings.security.auth || {};
627
- settings.security.auth.selectedType = 'gemini-api-key';
628
-
629
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
619
+ updateJsoncFile(settingsPath, (settings) => {
620
+ settings.security = settings.security && typeof settings.security === 'object' ? settings.security : {};
621
+ settings.security.auth = settings.security.auth && typeof settings.security.auth === 'object'
622
+ ? settings.security.auth
623
+ : {};
624
+ settings.security.auth.selectedType = 'gemini-api-key';
625
+ });
630
626
  }
631
627
 
632
628
  // 获取所有启用的渠道(供调度器使用)
@@ -682,7 +678,7 @@ function disableAllChannels() {
682
678
  data.channels.forEach(ch => { ch.enabled = false; });
683
679
  saveChannels(data);
684
680
  if (!getGeminiProxyState().running) {
685
- clearGeminiChannelConfig();
681
+ writeGeminiConfigForMultiChannel(data.channels);
686
682
  }
687
683
  }
688
684
 
@@ -691,11 +687,7 @@ function readGeminiSettings() {
691
687
  if (!settingsPath || !fs.existsSync(settingsPath)) {
692
688
  return {};
693
689
  }
694
- try {
695
- return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
696
- } catch {
697
- return {};
698
- }
690
+ return readJsoncFile(settingsPath);
699
691
  }
700
692
 
701
693
  function normalizeGeminiChannel(channel = {}) {
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { NATIVE_PATHS } = require('../../../config/paths');
4
+ const { readJsoncFile, writeJsoncFile, updateEnvFile } = require('../../../utils/native-config-patcher');
4
5
  const DEFAULT_NATIVE_PATHS = NATIVE_PATHS.gemini;
5
6
  let nativePaths = DEFAULT_NATIVE_PATHS;
6
7
 
@@ -64,25 +65,14 @@ function readEnv() {
64
65
  }
65
66
  }
66
67
 
67
- // 将环境对象转换为 .env 字符串
68
- function envToString(env) {
69
- let content = '';
70
- for (const [key, value] of Object.entries(env)) {
71
- content += `${key}=${value}\n`;
72
- }
73
- return content;
74
- }
75
-
76
68
  // 写入 .env
77
69
  function writeEnv(env) {
78
70
  try {
79
- const content = envToString(env);
80
- fs.writeFileSync(getEnvPath(), content, 'utf8');
81
-
82
- // 设置文件权限为 600 (仅所有者可读写)
83
- if (process.platform !== 'win32') {
84
- fs.chmodSync(getEnvPath(), 0o600);
85
- }
71
+ const managedKeys = ['GOOGLE_GEMINI_BASE_URL', 'GEMINI_API_KEY', 'GEMINI_MODEL'];
72
+ updateEnvFile(getEnvPath(), Object.fromEntries(managedKeys.map(key => [
73
+ key,
74
+ Object.prototype.hasOwnProperty.call(env || {}, key) ? env[key] : undefined
75
+ ])), { atomic: true, mode: 0o600 });
86
76
  } catch (err) {
87
77
  throw new Error('Failed to write .env: ' + err.message);
88
78
  }
@@ -94,8 +84,7 @@ function readSettings() {
94
84
  if (!settingsExists()) {
95
85
  return {};
96
86
  }
97
- const content = fs.readFileSync(getSettingsPath(), 'utf8');
98
- return JSON.parse(content);
87
+ return readJsoncFile(getSettingsPath());
99
88
  } catch (err) {
100
89
  throw new Error('Failed to read settings.json: ' + err.message);
101
90
  }
@@ -104,8 +93,7 @@ function readSettings() {
104
93
  // 写入 settings.json
105
94
  function writeSettings(settings) {
106
95
  try {
107
- const content = JSON.stringify(settings, null, 2);
108
- fs.writeFileSync(getSettingsPath(), content, 'utf8');
96
+ writeJsoncFile(getSettingsPath(), settings);
109
97
  } catch (err) {
110
98
  throw new Error('Failed to write settings.json: ' + err.message);
111
99
  }
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const { createNativeSnapshotMethods } = require('./native-config-snapshot');
4
+
5
+ function createDriver(context = {}) {
6
+ return {
7
+ platform: context.platform,
8
+ capability: 'nativeConfig',
9
+ ...context,
10
+ ...createNativeSnapshotMethods({}, {
11
+ platform: context.platform,
12
+ runtime: context.runtime,
13
+ manifest: context.manifest,
14
+ pathContext: context.pathContext,
15
+ paths: context.paths
16
+ })
17
+ };
18
+ }
19
+
20
+ module.exports = { createDriver };