coding-tool-x 3.8.6 → 3.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/web/assets/{AgentsDrawer-Db9wAyaZ.js → AgentsDrawer-DRQuwi0m.js} +1 -1
  2. package/dist/web/assets/Analytics-Cf5KwEdj.js +1 -0
  3. package/dist/web/assets/AssetPathField-DyniM0pF.js +1 -0
  4. package/dist/web/assets/{ChatHistoryDrawer-CwzJHyNP.js → ChatHistoryDrawer-CrfS0KYV.js} +1 -1
  5. package/dist/web/assets/{CollapseItem-CTCfSiue.js → CollapseItem-ClMsJuUu.js} +1 -1
  6. package/dist/web/assets/{CommandsDrawer-vDghB1U9.js → CommandsDrawer-CzHelf56.js} +1 -1
  7. package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +1 -0
  8. package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +1 -0
  9. package/dist/web/assets/{ConfigTemplatesDrawer-CbsJp76X.js → ConfigTemplatesDrawer-BqM32Im7.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesPanel-liMSTh7s.js → ConfigTemplatesPanel-lTdutC8H.js} +1 -1
  11. package/dist/web/assets/{Divider-D28g1gwj.js → Divider-DsdbkHrG.js} +1 -1
  12. package/dist/web/assets/{FavoritesDrawer-D6QSiYaL.js → FavoritesDrawer-DDyWUBJR.js} +1 -1
  13. package/dist/web/assets/Forward-DLCL7aMY.js +1 -0
  14. package/dist/web/assets/{GatewayConvertDrawer-RvET6iNG.js → GatewayConvertDrawer-Rigtt_5J.js} +1 -1
  15. package/dist/web/assets/Home-tCSKmSHV.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-CYmLGYju.js → MarkdownEditor-BMMUufSc.js} +1 -1
  17. package/dist/web/assets/McpDrawer-CmE-d7VW.js +1 -0
  18. package/dist/web/assets/{McpFormDrawer-BcTlgSpN.js → McpFormDrawer-ByvS18od.js} +1 -1
  19. package/dist/web/assets/{PluginManager-Dh2o0xkZ.js → PluginManager-C4kJlpVX.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-deKQ5u3I.js → PluginsDrawer-05R4k15x.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-BCoEs7Og.js +1 -0
  22. package/dist/web/assets/PluginsPanel-CzGMK-Ap.css +1 -0
  23. package/dist/web/assets/ProjectList-BJt1OIaI.js +1 -0
  24. package/dist/web/assets/ProjectList-DG3yu8xZ.css +1 -0
  25. package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +1 -0
  26. package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +1 -0
  27. package/dist/web/assets/RecentSessionsDrawer-yrAD4p1A.css +1 -0
  28. package/dist/web/assets/SessionCard-BUgjHQwX.js +1 -0
  29. package/dist/web/assets/SessionList-B-bXZRFk.js +1 -0
  30. package/dist/web/assets/SessionList-qG_XpPBJ.css +1 -0
  31. package/dist/web/assets/{SettingsDrawer-u_vwUuPS.js → SettingsDrawer-BITzjQLl.js} +1 -1
  32. package/dist/web/assets/SkillManager-D9jkNuRM.js +1 -0
  33. package/dist/web/assets/{SkillsDrawer-CkF3BpyC.js → SkillsDrawer-0Z4IefxC.js} +1 -1
  34. package/dist/web/assets/SkillsPanel-BKIVOj7n.js +1 -0
  35. package/dist/web/assets/{SkillsPanel-B3Nm5j32.css → SkillsPanel-C4ke56nF.css} +1 -1
  36. package/dist/web/assets/{SpeedTestDrawer-BgfG1Ked.js → SpeedTestDrawer-Bx1bYf5i.js} +1 -1
  37. package/dist/web/assets/Upload-CzC4rGI4.js +1 -0
  38. package/dist/web/assets/{WorkspaceDrawer-Br5CeCzL.js → WorkspaceDrawer-eujtWpoq.js} +1 -1
  39. package/dist/web/assets/{WorkspaceManager-B2O8-qIw.js → WorkspaceManager-CQ-8Ej68.js} +1 -1
  40. package/dist/web/assets/{WorkspacePanel-5oDKit3c.js → WorkspacePanel-Cql5qE2R.js} +1 -1
  41. package/dist/web/assets/headers-BqDmkMjy.js +1 -0
  42. package/dist/web/assets/{icons-B2Vg-IOC.js → icons-DmFe4b5j.js} +1 -1
  43. package/dist/web/assets/index-Cmmwj3Fx.js +2 -0
  44. package/dist/web/assets/{index-Bi1MQHYX.css → index-Cnbe696C.css} +1 -1
  45. package/dist/web/assets/{light-Cp0t7v32.js → light-0eavxNvt.js} +1 -1
  46. package/dist/web/assets/platforms-BmzzwnSw.js +1 -0
  47. package/dist/web/assets/platforms-DWXgmy2w.js +1 -0
  48. package/dist/web/assets/session-visibility-BAmxh3cK.js +1 -0
  49. package/dist/web/assets/sessions-D4yQtfKb.js +1 -0
  50. package/dist/web/assets/sessions-DqlbocLJ.js +1 -0
  51. package/dist/web/assets/use-message-DqCwefG6.js +1 -0
  52. package/dist/web/assets/{useResponsiveDrawer-jbmqZuJs.js → useResponsiveDrawer-DZd5rNk5.js} +1 -1
  53. package/dist/web/index.html +7 -7
  54. package/package.json +1 -1
  55. package/src/commands/search.js +17 -2
  56. package/src/config/paths.js +2 -0
  57. package/src/platforms/driver-registry.js +9 -1
  58. package/src/platforms/drivers/claude/session-history-adapter.js +72 -1
  59. package/src/platforms/drivers/claude/sessions-implementation.js +49 -7
  60. package/src/platforms/drivers/claude/sessions.js +30 -6
  61. package/src/platforms/drivers/codex/parser.js +57 -1
  62. package/src/platforms/drivers/codex/session-history-adapter.js +8 -2
  63. package/src/platforms/drivers/codex/sessions-implementation.js +25 -3
  64. package/src/platforms/drivers/codex/sessions.js +18 -1
  65. package/src/platforms/drivers/dsh/api-operations.js +396 -0
  66. package/src/platforms/drivers/dsh/channels-implementation.js +458 -0
  67. package/src/platforms/drivers/dsh/channels.js +191 -0
  68. package/src/platforms/drivers/dsh/common.js +571 -0
  69. package/src/platforms/drivers/dsh/native-config.js +93 -0
  70. package/src/platforms/drivers/dsh/native-logs.js +116 -0
  71. package/src/platforms/drivers/dsh/plugin-manager.js +117 -0
  72. package/src/platforms/drivers/dsh/projects.js +23 -0
  73. package/src/platforms/drivers/dsh/proxy-implementation.js +260 -0
  74. package/src/platforms/drivers/dsh/proxy.js +23 -0
  75. package/src/platforms/drivers/dsh/resources.js +346 -0
  76. package/src/platforms/drivers/dsh/sessions.js +587 -0
  77. package/src/platforms/drivers/dsh/statistics-implementation.js +238 -0
  78. package/src/platforms/drivers/dsh/statistics.js +24 -0
  79. package/src/platforms/drivers/gemini/session-history-adapter.js +49 -1
  80. package/src/platforms/drivers/gemini/sessions-implementation.js +46 -1
  81. package/src/platforms/drivers/gemini/sessions.js +2 -1
  82. package/src/platforms/drivers/omp/session-history-adapter.js +42 -1
  83. package/src/platforms/drivers/omp/sessions-implementation.js +51 -3
  84. package/src/platforms/drivers/omp/sessions.js +33 -2
  85. package/src/platforms/drivers/opencode/sessions-implementation.js +89 -9
  86. package/src/platforms/drivers/opencode/sessions.js +37 -1
  87. package/src/platforms/manifest-schema.js +43 -3
  88. package/src/platforms/manifests/dsh.json +542 -0
  89. package/src/platforms/path-resolver.js +2 -0
  90. package/src/platforms/platform-path-context.js +12 -2
  91. package/src/platforms/registry.js +16 -1
  92. package/src/platforms/runtime.js +5 -0
  93. package/src/platforms/session-history-adapters.js +1 -0
  94. package/src/server/api/platform-api-config.js +23 -2
  95. package/src/server/api/platform-route-factory.js +9 -2
  96. package/src/server/api/platforms.js +19 -1
  97. package/src/server/api/plugins.js +108 -13
  98. package/src/server/services/config-registry-service.js +10 -1
  99. package/src/server/services/config-templates-service.js +1 -0
  100. package/src/server/services/plugins-service.js +474 -103
  101. package/src/server/services/session-history-index.js +900 -140
  102. package/src/server/services/session-history-worker.js +3 -1
  103. package/src/server/services/skill-service.js +214 -7
  104. package/src/server/services/usage-log-utils.js +1 -0
  105. package/src/server/services/workspace-service.js +39 -10
  106. package/src/shared/channel-driver.js +80 -5
  107. package/src/shared/driver-factories/api.js +22 -6
  108. package/src/shared/driver-factories/projects.js +8 -0
  109. package/dist/web/assets/Analytics-C4gMzv7g.js +0 -1
  110. package/dist/web/assets/AssetPathField-Bd_nMFu3.js +0 -1
  111. package/dist/web/assets/ConfigExportDrawer-C-XDvlHc.js +0 -1
  112. package/dist/web/assets/ConfigTemplates-CYY8XRED.js +0 -1
  113. package/dist/web/assets/Home-P8eseZcf.js +0 -1
  114. package/dist/web/assets/McpDrawer-Ch3bkKIq.js +0 -1
  115. package/dist/web/assets/PluginsPanel-BofsgQgj.js +0 -1
  116. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +0 -1
  117. package/dist/web/assets/ProjectList-677RLZ8O.css +0 -1
  118. package/dist/web/assets/ProjectList-DhSWJHTA.js +0 -1
  119. package/dist/web/assets/PromptsDrawer-Cfb2FeeU.js +0 -1
  120. package/dist/web/assets/RecentSessionsDrawer-3eXamJhr.js +0 -1
  121. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +0 -1
  122. package/dist/web/assets/SessionCard-CW0eHURI.js +0 -1
  123. package/dist/web/assets/SessionList-BpYRz1cX.css +0 -1
  124. package/dist/web/assets/SessionList-DQfyDGWU.js +0 -1
  125. package/dist/web/assets/SkillManager-DHMprJLA.js +0 -1
  126. package/dist/web/assets/SkillsPanel-DWE4pFGx.js +0 -1
  127. package/dist/web/assets/Upload-CNwaGOws.js +0 -1
  128. package/dist/web/assets/headers-cf-QapUq.js +0 -1
  129. package/dist/web/assets/index-BMAUPq5K.js +0 -2
  130. package/dist/web/assets/platforms-CBkB45JP.js +0 -1
  131. package/dist/web/assets/platforms-FTpjxAT6.js +0 -1
  132. package/dist/web/assets/sessions-Cmy31slD.js +0 -1
  133. package/dist/web/assets/sessions-yC_oVVh8.js +0 -1
  134. package/dist/web/assets/use-message-DoLM8C1C.js +0 -1
@@ -0,0 +1,458 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getPlatformStatePath: resolveStatePath } = require('../../../config/paths');
6
+ const BaseChannelService = require('../../../shared/base-channel-service');
7
+ const {
8
+ resolvePaths,
9
+ readYamlFile,
10
+ writeAtomic,
11
+ dumpYaml,
12
+ isObject
13
+ } = require('./common');
14
+
15
+ const PROVIDER_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
16
+ const API_KEY_ENV_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/;
17
+ const SUPPORTED_PROVIDER_APIS = new Set([
18
+ 'openai-completions',
19
+ 'openai-responses',
20
+ 'anthropic-messages'
21
+ ]);
22
+ const REDACTED_API_KEY = '[REDACTED]';
23
+ const getStatePath = typeof resolveStatePath === 'function'
24
+ ? resolveStatePath
25
+ : () => undefined;
26
+
27
+ let configuredState = {
28
+ channels: getStatePath('channels', 'dsh'),
29
+ activeChannel: getStatePath('activeChannel', 'dsh'),
30
+ native: null
31
+ };
32
+ let service;
33
+
34
+ function normalizeProviderKey(value, fallback = 'dsh-provider') {
35
+ const normalized = String(value || '')
36
+ .trim()
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9_-]+/g, '-')
39
+ .replace(/^-+|-+$/g, '')
40
+ .slice(0, 64);
41
+ return PROVIDER_KEY_PATTERN.test(normalized) ? normalized : fallback;
42
+ }
43
+
44
+ function normalizeApiKeyEnv(value, providerKey) {
45
+ const candidate = String(value || '').trim().toUpperCase().replace(/[^A-Z0-9_]/g, '_');
46
+ if (API_KEY_ENV_PATTERN.test(candidate)) return candidate;
47
+ return `CTX_DSH_${String(providerKey || 'PROVIDER').toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`.slice(0, 128);
48
+ }
49
+
50
+ function normalizeModels(models, fallbackModel) {
51
+ const values = Array.isArray(models) ? models : [];
52
+ const normalized = values.map(item => {
53
+ if (typeof item === 'string') return { id: item.trim() };
54
+ if (isObject(item)) return { ...item, id: String(item.id || item.name || '').trim() };
55
+ return null;
56
+ }).filter(item => item?.id);
57
+ const fallback = String(fallbackModel || '').trim();
58
+ if (normalized.length === 0 && fallback) normalized.push({ id: fallback });
59
+ const seen = new Set();
60
+ return normalized.filter(item => {
61
+ const key = item.id.toLowerCase();
62
+ if (seen.has(key)) return false;
63
+ seen.add(key);
64
+ return true;
65
+ });
66
+ }
67
+
68
+ function readActiveMarker() {
69
+ try {
70
+ if (!configuredState.activeChannel || !fs.existsSync(configuredState.activeChannel)) return null;
71
+ const value = JSON.parse(fs.readFileSync(configuredState.activeChannel, 'utf8'));
72
+ return isObject(value) ? value : null;
73
+ } catch (_) {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function writeActiveMarker(activeChannelId, port) {
79
+ if (!configuredState.activeChannel) return;
80
+ const marker = {
81
+ activeChannelId: activeChannelId || null,
82
+ provider: 'ctx-dsh-proxy',
83
+ port: Number(port) || null,
84
+ updatedAt: Date.now()
85
+ };
86
+ writeAtomic(configuredState.activeChannel, `${JSON.stringify(marker, null, 2)}\n`, 0o600);
87
+ }
88
+
89
+ function clearActiveMarker() {
90
+ try {
91
+ if (configuredState.activeChannel && fs.existsSync(configuredState.activeChannel)) {
92
+ fs.unlinkSync(configuredState.activeChannel);
93
+ }
94
+ } catch (_) {
95
+ // Marker cleanup must not hide a successful proxy shutdown.
96
+ }
97
+ }
98
+
99
+ function configure({ pathContext, paths, channelsFilePath } = {}) {
100
+ const custom = pathContext?.customized === true;
101
+ const native = pathContext?.native || paths || {};
102
+ const directTestState = !pathContext && paths?.dir
103
+ ? path.join(paths.dir, 'channels.json')
104
+ : null;
105
+ configuredState = {
106
+ channels: channelsFilePath
107
+ || pathContext?.state?.channels
108
+ || directTestState
109
+ || getStatePath('channels', 'dsh'),
110
+ activeChannel: custom
111
+ ? (pathContext?.state?.activeChannel || getStatePath('activeChannel', 'dsh'))
112
+ : (paths?.dir && !pathContext
113
+ ? path.join(paths.dir, 'active-channel.json')
114
+ : getStatePath('activeChannel', 'dsh')),
115
+ native: resolvePaths({ paths: native, pathContext })
116
+ };
117
+ if (service) {
118
+ service.channelsFilePath = configuredState.channels;
119
+ service.nativePaths = configuredState.native;
120
+ }
121
+ }
122
+
123
+ function isProxyModeEnabled() {
124
+ return Boolean(readActiveMarker());
125
+ }
126
+
127
+ class DshChannelService extends BaseChannelService {
128
+ constructor() {
129
+ super({
130
+ platform: 'dsh',
131
+ channelsFilePath: configuredState.channels,
132
+ defaultGatewaySource: 'openai_compatible',
133
+ isProxyRunning: isProxyModeEnabled
134
+ });
135
+ this.nativePaths = configuredState.native || resolvePaths({});
136
+ }
137
+
138
+ _applyDefaults(channel) {
139
+ const normalized = super._applyDefaults(channel);
140
+ normalized.name = String(normalized.name || normalized.baseUrl || normalized.providerKey || 'DSH 渠道').trim();
141
+ normalized.providerKey = normalizeProviderKey(normalized.providerKey || normalized.baseUrl || normalized.name || `dsh-${normalized.id || 'provider'}`);
142
+ normalized.providerApi = SUPPORTED_PROVIDER_APIS.has(String(normalized.providerApi || '').trim())
143
+ ? String(normalized.providerApi).trim()
144
+ : 'openai-completions';
145
+ normalized.baseUrl = String(normalized.baseUrl || '').trim().replace(/\/$/, '');
146
+ normalized.models = normalizeModels(normalized.models, normalized.model || 'deepseek-chat');
147
+ normalized.model = String(normalized.model || normalized.models[0]?.id || '').trim();
148
+ if (normalized.model && !normalized.models.some(model => model.id === normalized.model)) {
149
+ normalized.models = [{ id: normalized.model }, ...normalized.models];
150
+ }
151
+ normalized.apiKeyEnv = normalizeApiKeyEnv(normalized.apiKeyEnv, normalized.providerKey);
152
+ normalized.gatewaySourceType = 'openai_compatible';
153
+ return normalized;
154
+ }
155
+
156
+ _normalizeAuthFields(fields = {}, channels = [], existing = null) {
157
+ const next = { ...fields };
158
+ if (next.apiKey === REDACTED_API_KEY || next.apiKey === '已设置' || next.apiKey === '已隐藏') {
159
+ delete next.apiKey;
160
+ }
161
+ return super._normalizeAuthFields(next, channels, existing);
162
+ }
163
+
164
+ _validateUniqueness(channels, fields, excludeId) {
165
+ const hasProviderKeyMutation = Object.prototype.hasOwnProperty.call(fields || {}, 'providerKey');
166
+ if (excludeId && !hasProviderKeyMutation) return;
167
+ const providerKey = normalizeProviderKey(fields.providerKey || fields.name || fields.baseUrl);
168
+ const duplicate = channels.find(channel => (
169
+ channel.id !== excludeId && String(channel.providerKey || '').toLowerCase() === providerKey.toLowerCase()
170
+ ));
171
+ if (duplicate) {
172
+ const error = new Error(`DSH providerKey already exists: ${providerKey}`);
173
+ error.statusCode = 409;
174
+ error.code = 'dsh_provider_key_conflict';
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ _validateBeforeChannelMutation(channel, _allChannels, { operation }) {
180
+ if (!channel.baseUrl) throw new Error('DSH channel baseUrl is required');
181
+ try {
182
+ const parsed = new URL(channel.baseUrl);
183
+ if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('unsupported protocol');
184
+ } catch (_) {
185
+ throw new Error('DSH channel baseUrl must be a valid http(s) URL');
186
+ }
187
+ if (!PROVIDER_KEY_PATTERN.test(channel.providerKey)) {
188
+ throw new Error('DSH providerKey must contain only lowercase letters, digits, underscores, or hyphens');
189
+ }
190
+ if (!SUPPORTED_PROVIDER_APIS.has(channel.providerApi)) {
191
+ throw new Error(`Unsupported DSH provider api: ${channel.providerApi}`);
192
+ }
193
+ if (!channel.model || !channel.models.length) {
194
+ throw new Error('DSH channel requires at least one model');
195
+ }
196
+ if (channel.authMode === 'api_key' && operation === 'create' && !String(channel.apiKey || '').trim()) {
197
+ throw new Error('DSH API-key channel requires an apiKey');
198
+ }
199
+ if (!API_KEY_ENV_PATTERN.test(channel.apiKeyEnv)) {
200
+ throw new Error('DSH apiKeyEnv must be an uppercase environment-style name');
201
+ }
202
+ }
203
+
204
+ _activeChannel(channels = []) {
205
+ return channels.find(channel => channel.enabled !== false) || channels[0] || null;
206
+ }
207
+
208
+ _onAfterCreate(channel) {
209
+ if (!isProxyModeEnabled() && channel.enabled !== false) {
210
+ this._applyToNativeSettings(channel);
211
+ }
212
+ }
213
+
214
+ _onAfterUpdate(oldChannel, nextChannel, allChannels) {
215
+ if (isProxyModeEnabled()) return;
216
+ if (oldChannel.providerKey !== nextChannel.providerKey) {
217
+ this._removeNativeProvider(oldChannel.providerKey);
218
+ }
219
+ const active = this._activeChannel(allChannels);
220
+ if (active?.id === nextChannel.id || (oldChannel.enabled !== false && nextChannel.enabled === false)) {
221
+ if (active) this._applyToNativeSettings(active);
222
+ }
223
+ }
224
+
225
+ _onAfterDelete(channel, allChannels) {
226
+ if (isProxyModeEnabled()) return;
227
+ this._removeNativeProvider(channel.providerKey);
228
+ const active = this._activeChannel(allChannels);
229
+ if (active) this._applyToNativeSettings(active);
230
+ }
231
+
232
+ _nativeSettings() {
233
+ return readYamlFile(this.nativePaths.settings, {});
234
+ }
235
+
236
+ _writeNativeSettings(settings) {
237
+ writeAtomic(this.nativePaths.settings, dumpYaml(settings), 0o600);
238
+ }
239
+
240
+ _removeNativeProvider(providerKey) {
241
+ const key = String(providerKey || '').trim();
242
+ if (!key || key === 'ctx-dsh-proxy') return;
243
+ const settings = this._nativeSettings();
244
+ const llm = isObject(settings['llm-pi-ai']) ? { ...settings['llm-pi-ai'] } : {};
245
+ const providers = isObject(llm.providers) ? { ...llm.providers } : {};
246
+ if (!Object.prototype.hasOwnProperty.call(providers, key)) return;
247
+ delete providers[key];
248
+ settings['llm-pi-ai'] = { ...llm, providers };
249
+ this._writeNativeSettings(settings);
250
+ }
251
+
252
+ _writeCredentials(channel) {
253
+ if (channel.authMode !== 'api_key') return;
254
+ const credentials = readYamlFile(this.nativePaths.credentials, { version: 1, refs: {}, records: {} });
255
+ if (!isObject(credentials.refs)) credentials.refs = {};
256
+ credentials.refs[channel.apiKeyEnv] = channel.apiKey || '';
257
+ if (!isObject(credentials.records)) credentials.records = {};
258
+ writeAtomic(this.nativePaths.credentials, dumpYaml(credentials), 0o600);
259
+ }
260
+
261
+ _providerEntry(channel, baseUrl = channel.baseUrl, models = channel.models) {
262
+ const entry = {
263
+ displayName: channel.name,
264
+ api: channel.providerApi,
265
+ baseURL: baseUrl,
266
+ models: normalizeModels(models, channel.model)
267
+ };
268
+ if (channel.authMode === 'api_key') entry.apiKeyEnv = channel.apiKeyEnv;
269
+ return entry;
270
+ }
271
+
272
+ _applyToNativeSettings(channel) {
273
+ const settings = this._nativeSettings();
274
+ const llm = isObject(settings['llm-pi-ai']) ? { ...settings['llm-pi-ai'] } : {};
275
+ const providers = isObject(llm.providers) ? { ...llm.providers } : {};
276
+ providers[channel.providerKey] = this._providerEntry(channel);
277
+ settings['llm-pi-ai'] = { ...llm, providers };
278
+ settings['agent-default-model'] = {
279
+ ...(isObject(settings['agent-default-model']) ? settings['agent-default-model'] : {}),
280
+ provider: channel.providerKey,
281
+ model: channel.model
282
+ };
283
+ this._writeNativeSettings(settings);
284
+ this._writeCredentials(channel);
285
+ clearActiveMarker();
286
+ return channel;
287
+ }
288
+
289
+ applyChannelToSettings(channelId) {
290
+ if (isProxyModeEnabled()) {
291
+ const error = new Error('请先停止 DSH 动态代理,再写入单渠道配置');
292
+ error.statusCode = 409;
293
+ error.code = 'dsh_proxy_running';
294
+ throw error;
295
+ }
296
+ return super.applyChannelToSettings(channelId);
297
+ }
298
+
299
+ getCurrentChannel() {
300
+ const marker = readActiveMarker();
301
+ if (marker?.activeChannelId) {
302
+ const active = this.loadChannels().channels.find(item => item.id === marker.activeChannelId) || null;
303
+ if (active) {
304
+ return {
305
+ providerKey: 'ctx-dsh-proxy',
306
+ model: active.model || null,
307
+ proxy: true,
308
+ channel: active
309
+ };
310
+ }
311
+ }
312
+ const settings = this._nativeSettings();
313
+ const selected = isObject(settings['agent-default-model']) ? settings['agent-default-model'] : {};
314
+ const providerKey = String(selected.provider || '').trim();
315
+ const channel = this.loadChannels().channels.find(item => item.providerKey === providerKey) || null;
316
+ return {
317
+ providerKey: providerKey || null,
318
+ model: String(selected.model || '').trim() || null,
319
+ channel
320
+ };
321
+ }
322
+
323
+ syncCurrentChannel() {
324
+ const current = this.getCurrentChannel();
325
+ if (current.channel) return { added: 0, updated: 0, skipped: 1, current: current.channel };
326
+ if (!current.providerKey) return { added: 0, updated: 0, skipped: 0, warnings: ['DSH 当前配置没有 agent-default-model.provider'] };
327
+
328
+ const settings = this._nativeSettings();
329
+ const provider = settings['llm-pi-ai']?.providers?.[current.providerKey];
330
+ if (!isObject(provider) || !provider.baseURL) {
331
+ return { added: 0, updated: 0, skipped: 0, warnings: [`未找到可导入的 DSH provider: ${current.providerKey}`] };
332
+ }
333
+ const credentials = readYamlFile(this.nativePaths.credentials, {});
334
+ const apiKeyEnv = String(provider.apiKeyEnv || '').trim();
335
+ const apiKey = apiKeyEnv && isObject(credentials.refs) ? String(credentials.refs[apiKeyEnv] || '') : '';
336
+ const channel = this.createChannel({
337
+ name: provider.displayName || current.providerKey,
338
+ providerKey: current.providerKey,
339
+ providerApi: provider.api || 'openai-completions',
340
+ baseUrl: provider.baseURL,
341
+ model: current.model || provider.models?.[0]?.id || provider.models?.[0] || 'default',
342
+ models: provider.models,
343
+ apiKeyEnv: apiKeyEnv || undefined,
344
+ apiKey,
345
+ authMode: apiKey ? 'api_key' : 'none',
346
+ enabled: true
347
+ });
348
+ return { added: 1, updated: 0, skipped: 0, current: channel };
349
+ }
350
+
351
+ setProxyConfig(port, { activeChannelId } = {}) {
352
+ const channels = this.getEnabledChannels();
353
+ if (channels.length === 0) throw new Error('请先添加并启用至少一个 DSH 渠道');
354
+ const selected = channels.find(channel => channel.id === activeChannelId) || channels[0];
355
+ const settings = this._nativeSettings();
356
+ const llm = isObject(settings['llm-pi-ai']) ? { ...settings['llm-pi-ai'] } : {};
357
+ const providers = isObject(llm.providers) ? { ...llm.providers } : {};
358
+ const models = channels.flatMap(channel => channel.models.map(model => ({ id: model.id })));
359
+ providers['ctx-dsh-proxy'] = {
360
+ displayName: 'coding-tool-x DSH 动态代理',
361
+ api: 'openai-completions',
362
+ baseURL: `http://127.0.0.1:${Number(port)}/v1`,
363
+ models: models.length > 0 ? models : [{ id: selected.model }]
364
+ };
365
+ settings['llm-pi-ai'] = { ...llm, providers };
366
+ settings['agent-default-model'] = {
367
+ ...(isObject(settings['agent-default-model']) ? settings['agent-default-model'] : {}),
368
+ provider: 'ctx-dsh-proxy',
369
+ model: selected.model
370
+ };
371
+ this._writeNativeSettings(settings);
372
+ writeActiveMarker(selected.id, port);
373
+ return { channel: selected, port, provider: 'ctx-dsh-proxy' };
374
+ }
375
+
376
+ restoreNativeConfig() {
377
+ const marker = readActiveMarker();
378
+ const channels = this.loadChannels().channels;
379
+ const selected = channels.find(channel => channel.id === marker?.activeChannelId)
380
+ || channels.find(channel => channel.enabled !== false)
381
+ || channels[0];
382
+ if (!selected) {
383
+ clearActiveMarker();
384
+ return null;
385
+ }
386
+ const settings = this._nativeSettings();
387
+ const llm = isObject(settings['llm-pi-ai']) ? { ...settings['llm-pi-ai'] } : {};
388
+ const providers = isObject(llm.providers) ? { ...llm.providers } : {};
389
+ delete providers['ctx-dsh-proxy'];
390
+ settings['llm-pi-ai'] = { ...llm, providers };
391
+ this._writeNativeSettings(settings);
392
+ clearActiveMarker();
393
+ return this.applyChannelToSettings(selected.id);
394
+ }
395
+
396
+ getEffectiveApiKey(channel) {
397
+ const id = channel?.id;
398
+ const source = id ? this.loadChannels().channels.find(item => item.id === id) : channel;
399
+ return source?.authMode === 'none' ? null : String(source?.apiKey || '').trim() || null;
400
+ }
401
+ }
402
+
403
+ function getServiceInstance() {
404
+ if (!service) service = new DshChannelService();
405
+ service.channelsFilePath = configuredState.channels;
406
+ service.nativePaths = configuredState.native || service.nativePaths;
407
+ return service;
408
+ }
409
+
410
+ function toPublicChannel(channel) {
411
+ if (!channel || typeof channel !== 'object') return channel;
412
+ const { apiKey: _apiKey, ...publicChannel } = channel;
413
+ return {
414
+ ...publicChannel,
415
+ apiKey: channel.apiKey ? REDACTED_API_KEY : '',
416
+ apiKeyConfigured: Boolean(channel.apiKey),
417
+ models: normalizeModels(channel.models, channel.model)
418
+ };
419
+ }
420
+
421
+ function getChannels() {
422
+ return { channels: getServiceInstance().getChannels().channels.map(toPublicChannel) };
423
+ }
424
+
425
+ function getEnabledChannels() {
426
+ return getServiceInstance().getEnabledChannels().map(toPublicChannel);
427
+ }
428
+
429
+ function getCurrentChannel() {
430
+ const result = getServiceInstance().getCurrentChannel();
431
+ return { ...result, channel: toPublicChannel(result.channel) };
432
+ }
433
+
434
+ function syncCurrentDshChannel() {
435
+ const result = getServiceInstance().syncCurrentChannel();
436
+ return { ...result, current: toPublicChannel(result.current) };
437
+ }
438
+
439
+ module.exports = {
440
+ configure,
441
+ getServiceInstance,
442
+ getChannels,
443
+ getEnabledChannels,
444
+ getCurrentChannel,
445
+ syncCurrentDshChannel,
446
+ toPublicChannel,
447
+ getEffectiveApiKey: channel => getServiceInstance().getEffectiveApiKey(channel),
448
+ createChannel: fields => getServiceInstance().createChannel(fields),
449
+ updateChannel: (id, patch) => getServiceInstance().updateChannel(id, patch),
450
+ deleteChannel: id => getServiceInstance().deleteChannel(id),
451
+ applyChannelToSettings: id => getServiceInstance().applyChannelToSettings(id),
452
+ saveChannelOrder: order => getServiceInstance().saveChannelOrder(order),
453
+ setDshProxyConfig: (port, options) => getServiceInstance().setProxyConfig(port, options),
454
+ restoreDshNativeConfig: () => getServiceInstance().restoreNativeConfig(),
455
+ clearActiveMarker,
456
+ isProxyModeEnabled,
457
+ readActiveMarker
458
+ };
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ const implementation = require('./channels-implementation');
4
+
5
+ function resultFor(request, status, data, error) {
6
+ const route = request?.route || {};
7
+ const result = {
8
+ status,
9
+ platform: 'dsh',
10
+ capability: route.capability || 'channels',
11
+ operation: route.operation || 'channels'
12
+ };
13
+ if (status === 'ok') result.data = data;
14
+ if (error) {
15
+ result.error = error instanceof Error ? error.message : String(error);
16
+ result.code = error.code || status;
17
+ Object.defineProperty(result, 'cause', { value: error, enumerable: false });
18
+ }
19
+ return result;
20
+ }
21
+
22
+ function errorStatus(error) {
23
+ if (error?.statusCode === 409) return 'conflict';
24
+ if (error?.statusCode === 422) return 'unprocessable';
25
+ if (error?.statusCode === 400 || /required|invalid|unsupported/i.test(String(error?.message || ''))) return 'invalid';
26
+ return 'failed';
27
+ }
28
+
29
+ function withResult(request, operation, callback) {
30
+ try {
31
+ const value = callback();
32
+ return resultFor({ ...request, route: { ...(request?.route || {}), operation } }, 'ok', value);
33
+ } catch (error) {
34
+ return resultFor({ ...request, route: { ...(request?.route || {}), operation } }, errorStatus(error), undefined, error);
35
+ }
36
+ }
37
+
38
+ function withAsyncResult(request, operation, callback) {
39
+ try {
40
+ return Promise.resolve(callback())
41
+ .then(value => resultFor({ ...request, route: { ...(request?.route || {}), operation } }, 'ok', value))
42
+ .catch(error => resultFor({ ...request, route: { ...(request?.route || {}), operation } }, errorStatus(error), undefined, error));
43
+ } catch (error) {
44
+ return Promise.resolve(resultFor({ ...request, route: { ...(request?.route || {}), operation } }, errorStatus(error), undefined, error));
45
+ }
46
+ }
47
+
48
+ function bodyOf(request) {
49
+ if (request && Object.prototype.hasOwnProperty.call(request, 'body')) return request.body || {};
50
+ return request || {};
51
+ }
52
+
53
+ function findChannel(channelId) {
54
+ const data = implementation.getServiceInstance().getChannels();
55
+ const channels = Array.isArray(data) ? data : data?.channels;
56
+ return Array.isArray(channels)
57
+ ? channels.find(channel => String(channel?.id || '') === String(channelId))
58
+ : null;
59
+ }
60
+
61
+ function channelTypeForSpeed(channel) {
62
+ return channel?.providerApi === 'anthropic-messages' ? 'claude'
63
+ : channel?.providerApi === 'openai-responses' ? 'codex'
64
+ : 'openai_compatible';
65
+ }
66
+
67
+ function createDriver(context = {}) {
68
+ implementation.configure(context);
69
+ const driver = {
70
+ platform: 'dsh',
71
+ capability: 'channels',
72
+ getCliMetadata: () => ({
73
+ supportsCliCreate: false,
74
+ supportsCliToggle: true,
75
+ managedProviderConfig: true,
76
+ defaultPort: 20093,
77
+ createUnavailableMessage: '提示: DSH 渠道请通过 Web UI 或 API 添加。'
78
+ }),
79
+ list: request => withResult(request, 'list', () => implementation.getChannels()),
80
+ getChannels: () => implementation.getChannels().channels,
81
+ getEnabled: request => withResult(request, 'enabled', () => ({ channels: implementation.getEnabledChannels() })),
82
+ getEnabledChannels: () => implementation.getEnabledChannels(),
83
+ current: request => withResult(request, 'current', () => implementation.getCurrentChannel()),
84
+ create: request => withResult(request, 'create', () => {
85
+ const channel = implementation.createChannel(bodyOf(request));
86
+ return { channel: implementation.toPublicChannel(channel) };
87
+ }),
88
+ createChannel: fields => implementation.createChannel(fields),
89
+ update: request => withResult(request, 'update', () => {
90
+ const channelId = request?.params?.channelId || bodyOf(request).id;
91
+ if (!channelId) throw Object.assign(new Error('Channel id is required'), { statusCode: 400 });
92
+ const patch = { ...bodyOf(request) };
93
+ delete patch.id;
94
+ const channel = implementation.updateChannel(channelId, patch);
95
+ return { channel: implementation.toPublicChannel(channel) };
96
+ }),
97
+ updateChannel: (id, patch) => implementation.updateChannel(id, patch),
98
+ remove: request => withResult(request, 'remove', () => {
99
+ const channelId = request?.params?.channelId || (typeof request === 'string' ? request : bodyOf(request).id);
100
+ if (!channelId) throw Object.assign(new Error('Channel id is required'), { statusCode: 400 });
101
+ return implementation.deleteChannel(channelId);
102
+ }),
103
+ deleteChannel: id => implementation.deleteChannel(id),
104
+ applyToSettings: request => withResult(request, 'applyToSettings', () => {
105
+ const channelId = request?.params?.channelId || (typeof request === 'string' ? request : bodyOf(request).id);
106
+ if (!channelId) throw Object.assign(new Error('Channel id is required'), { statusCode: 400 });
107
+ return { channel: implementation.toPublicChannel(implementation.applyChannelToSettings(channelId)) };
108
+ }),
109
+ applyNativeConfig: input => {
110
+ const channelId = typeof input === 'string' ? input : input?.params?.channelId || input?.body?.id;
111
+ return implementation.applyChannelToSettings(channelId);
112
+ },
113
+ sync: request => withResult(request, 'sync', () => implementation.syncCurrentDshChannel()),
114
+ syncCurrent: request => withResult(request, 'sync', () => implementation.syncCurrentDshChannel()),
115
+ models: request => withAsyncResult(request, 'models', async () => {
116
+ const { fetchModelsFromProvider } = require('../../../server/services/model-detector');
117
+ const channelId = request?.params?.channelId;
118
+ if (!channelId) throw Object.assign(new Error('Channel id is required'), { statusCode: 400 });
119
+ const channel = findChannel(channelId);
120
+ if (!channel) throw Object.assign(new Error(`Channel not found: ${channelId}`), { statusCode: 400 });
121
+ return fetchModelsFromProvider(channel, channelTypeForSpeed(channel), {
122
+ forceRefresh: request?.query?.forceRefresh === 'true' || request?.query?.force === '1'
123
+ });
124
+ }),
125
+ probeModels: request => withAsyncResult(request, 'probeModels', async () => {
126
+ const { probeModelAvailability } = require('../../../server/services/model-detector');
127
+ const input = bodyOf(request);
128
+ const probe = await probeModelAvailability(input, channelTypeForSpeed(input), {
129
+ forceRefresh: input.forceRefresh === true || input.force === true,
130
+ preferredModels: Array.isArray(input.preferredModels) ? input.preferredModels : [],
131
+ stopOnFirstAvailable: input.stopOnFirstAvailable === true
132
+ });
133
+ return {
134
+ ...probe,
135
+ models: Array.isArray(probe?.availableModels) ? probe.availableModels : []
136
+ };
137
+ }),
138
+ speedTest: request => withAsyncResult(request, 'speedTest', async () => {
139
+ const { testChannelSpeed } = require('../../../server/services/speed-test');
140
+ const channelId = request?.params?.channelId;
141
+ if (!channelId) throw Object.assign(new Error('Channel id is required'), { statusCode: 400 });
142
+ const channel = findChannel(channelId);
143
+ if (!channel) throw Object.assign(new Error(`Channel not found: ${channelId}`), { statusCode: 400 });
144
+ const body = bodyOf(request);
145
+ return testChannelSpeed(
146
+ channel,
147
+ body.timeout,
148
+ channelTypeForSpeed(channel),
149
+ { authSourceType: 'dsh' }
150
+ );
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
+ saveOrder: request => withResult(request, 'order', () => {
171
+ const body = bodyOf(request);
172
+ implementation.getServiceInstance().saveChannelOrder(body.order || body.ids || []);
173
+ return implementation.getChannels();
174
+ }),
175
+ order: request => withResult(request, 'order', () => {
176
+ const body = bodyOf(request);
177
+ implementation.getServiceInstance().saveChannelOrder(body.order || body.ids || []);
178
+ return implementation.getChannels();
179
+ }),
180
+ saveChannelOrder: order => implementation.getServiceInstance().saveChannelOrder(order),
181
+ getEffectiveApiKey: channel => implementation.getEffectiveApiKey(channel),
182
+ getHealthPolicy: () => ({ freezeOnFailure: true }),
183
+ normalizeDashboardChannels: value => {
184
+ if (value && typeof value === 'object' && typeof value.status === 'string') return value;
185
+ return Array.isArray(value) ? { channels: value } : (value || { channels: [] });
186
+ }
187
+ };
188
+ return driver;
189
+ }
190
+
191
+ module.exports = { createDriver };