coding-tool-x 3.8.7 → 3.8.9

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 (143) hide show
  1. package/README.md +2 -2
  2. package/dist/web/assets/{AgentsDrawer-C7ePByZx.js → AgentsDrawer-Cj5CJ4d9.js} +1 -1
  3. package/dist/web/assets/Analytics-1doS-8uC.js +1 -0
  4. package/dist/web/assets/AssetPathField-C9TOPl7o.js +1 -0
  5. package/dist/web/assets/{ChatHistoryDrawer-Ccl4rhdz.js → ChatHistoryDrawer-BTmQ1aQr.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-MxG_UD4o.js → CollapseItem-CwqJk3bE.js} +1 -1
  7. package/dist/web/assets/{CommandsDrawer-DWbaxHRZ.js → CommandsDrawer-CaZWDI_G.js} +1 -1
  8. package/dist/web/assets/ConfigExportDrawer-C3vSZEsS.js +1 -0
  9. package/dist/web/assets/{ConfigTemplates-AiRPv2cd.js → ConfigTemplates-gyERg9iC.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesDrawer-CKMFsfEF.js → ConfigTemplatesDrawer-B17RqFi4.js} +1 -1
  11. package/dist/web/assets/{ConfigTemplatesPanel-Cvbnkvpt.js → ConfigTemplatesPanel-DKIC_FqG.js} +1 -1
  12. package/dist/web/assets/{Divider-D28g1gwj.js → Divider-DsdbkHrG.js} +1 -1
  13. package/dist/web/assets/{FavoritesDrawer-DA-dR_vP.js → FavoritesDrawer-DaziQmBp.js} +1 -1
  14. package/dist/web/assets/Forward-DLCL7aMY.js +1 -0
  15. package/dist/web/assets/{GatewayConvertDrawer-402_raGb.js → GatewayConvertDrawer-OMgWUUSc.js} +1 -1
  16. package/dist/web/assets/Home-DOSUTfrE.js +1 -0
  17. package/dist/web/assets/{MarkdownEditor-BvkDECwH.js → MarkdownEditor-9TTwTS-U.js} +1 -1
  18. package/dist/web/assets/McpDrawer-I8rl8lGH.js +1 -0
  19. package/dist/web/assets/{McpFormDrawer-DPMnyt5L.js → McpFormDrawer-DEP3C7ut.js} +1 -1
  20. package/dist/web/assets/{PluginManager-FKVeugr4.js → PluginManager-BuRcWuBj.js} +1 -1
  21. package/dist/web/assets/{PluginsDrawer-MvM1R55d.js → PluginsDrawer-_zZENaQn.js} +1 -1
  22. package/dist/web/assets/PluginsPanel-Bs5qZ-TK.js +1 -0
  23. package/dist/web/assets/PluginsPanel-CzGMK-Ap.css +1 -0
  24. package/dist/web/assets/ProjectList-BSxuL2td.js +1 -0
  25. package/dist/web/assets/{ProjectList-677RLZ8O.css → ProjectList-VbIYSHbH.css} +1 -1
  26. package/dist/web/assets/PromptsDrawer-CoZ-ZtsY.js +1 -0
  27. package/dist/web/assets/RecentSessionsDrawer-B_17aJBG.js +1 -0
  28. package/dist/web/assets/RecentSessionsDrawer-yrAD4p1A.css +1 -0
  29. package/dist/web/assets/SessionCard-BL_x0d3t.js +1 -0
  30. package/dist/web/assets/SessionList-DhRYZ6vl.js +1 -0
  31. package/dist/web/assets/SessionList-qG_XpPBJ.css +1 -0
  32. package/dist/web/assets/{SettingsDrawer-lMjttbkD.js → SettingsDrawer-fYb0DU7X.js} +1 -1
  33. package/dist/web/assets/SkillManager-S698Hns8.js +1 -0
  34. package/dist/web/assets/{SkillsDrawer-DF6JsSyj.js → SkillsDrawer-Ms4lPCow.js} +1 -1
  35. package/dist/web/assets/{SkillsPanel-B3Nm5j32.css → SkillsPanel-C4ke56nF.css} +1 -1
  36. package/dist/web/assets/SkillsPanel-rqnqAOGL.js +1 -0
  37. package/dist/web/assets/{SpeedTestDrawer-BaZkZqPP.js → SpeedTestDrawer-Bbn38SfV.js} +1 -1
  38. package/dist/web/assets/Upload-CdyVHxCI.js +1 -0
  39. package/dist/web/assets/{WorkspaceDrawer-BFSwZ5yU.js → WorkspaceDrawer-L7HJRc6u.js} +1 -1
  40. package/dist/web/assets/{WorkspaceManager-BHL1uYct.js → WorkspaceManager-DsoNg2z8.js} +1 -1
  41. package/dist/web/assets/{WorkspacePanel-B5PooBtg.js → WorkspacePanel-Be8PVZah.js} +1 -1
  42. package/dist/web/assets/{headers-cf-QapUq.js → headers-B5JWGn3v.js} +1 -1
  43. package/dist/web/assets/{icons-B2Vg-IOC.js → icons-DmFe4b5j.js} +1 -1
  44. package/dist/web/assets/{index-BohDDAy_.css → index-3Lp6z8TQ.css} +1 -1
  45. package/dist/web/assets/index-BKlAWTH-.js +2 -0
  46. package/dist/web/assets/{light-Cp0t7v32.js → light-0eavxNvt.js} +1 -1
  47. package/dist/web/assets/platforms-BmzzwnSw.js +1 -0
  48. package/dist/web/assets/platforms-DWXgmy2w.js +1 -0
  49. package/dist/web/assets/session-visibility-BAmxh3cK.js +1 -0
  50. package/dist/web/assets/sessions-CC0j-KH_.js +1 -0
  51. package/dist/web/assets/sessions-D4yQtfKb.js +1 -0
  52. package/dist/web/assets/use-message-DqCwefG6.js +1 -0
  53. package/dist/web/assets/{useResponsiveDrawer-D-DDpbnI.js → useResponsiveDrawer-V_onl9NV.js} +1 -1
  54. package/dist/web/index.html +7 -7
  55. package/package.json +1 -1
  56. package/src/commands/daemon.js +27 -4
  57. package/src/commands/search.js +17 -2
  58. package/src/config/paths.js +2 -0
  59. package/src/platforms/channel-auth-registry.js +36 -6
  60. package/src/platforms/channel-auth-service.js +12 -2
  61. package/src/platforms/driver-registry.js +9 -1
  62. package/src/platforms/drivers/claude/session-history-adapter.js +72 -1
  63. package/src/platforms/drivers/claude/sessions-implementation.js +49 -7
  64. package/src/platforms/drivers/claude/sessions.js +30 -6
  65. package/src/platforms/drivers/codex/parser.js +57 -1
  66. package/src/platforms/drivers/codex/session-history-adapter.js +8 -2
  67. package/src/platforms/drivers/codex/sessions-implementation.js +25 -3
  68. package/src/platforms/drivers/codex/sessions.js +18 -1
  69. package/src/platforms/drivers/dsh/api-operations.js +396 -0
  70. package/src/platforms/drivers/dsh/channels-implementation.js +458 -0
  71. package/src/platforms/drivers/dsh/channels.js +191 -0
  72. package/src/platforms/drivers/dsh/common.js +571 -0
  73. package/src/platforms/drivers/dsh/native-config.js +93 -0
  74. package/src/platforms/drivers/dsh/native-logs.js +116 -0
  75. package/src/platforms/drivers/dsh/plugin-manager.js +117 -0
  76. package/src/platforms/drivers/dsh/projects.js +23 -0
  77. package/src/platforms/drivers/dsh/proxy-implementation.js +260 -0
  78. package/src/platforms/drivers/dsh/proxy.js +23 -0
  79. package/src/platforms/drivers/dsh/resources.js +346 -0
  80. package/src/platforms/drivers/dsh/sessions.js +587 -0
  81. package/src/platforms/drivers/dsh/statistics-implementation.js +238 -0
  82. package/src/platforms/drivers/dsh/statistics.js +24 -0
  83. package/src/platforms/drivers/gemini/session-history-adapter.js +49 -1
  84. package/src/platforms/drivers/gemini/sessions-implementation.js +46 -1
  85. package/src/platforms/drivers/gemini/sessions.js +2 -1
  86. package/src/platforms/drivers/omp/prompts.js +58 -2
  87. package/src/platforms/drivers/omp/proxy-implementation.js +25 -1
  88. package/src/platforms/drivers/omp/session-history-adapter.js +42 -1
  89. package/src/platforms/drivers/omp/sessions-implementation.js +51 -3
  90. package/src/platforms/drivers/omp/sessions.js +33 -2
  91. package/src/platforms/drivers/opencode/channels-implementation.js +25 -22
  92. package/src/platforms/drivers/opencode/channels.js +6 -1
  93. package/src/platforms/drivers/opencode/sessions-implementation.js +89 -9
  94. package/src/platforms/drivers/opencode/sessions.js +37 -1
  95. package/src/platforms/manifest-schema.js +43 -3
  96. package/src/platforms/manifests/dsh.json +542 -0
  97. package/src/platforms/manifests/omp.json +2 -1
  98. package/src/platforms/manifests/opencode.json +28 -0
  99. package/src/platforms/native-oauth-adapters.js +318 -18
  100. package/src/platforms/oauth-credentials-service.js +344 -13
  101. package/src/platforms/path-resolver.js +2 -0
  102. package/src/platforms/platform-path-context.js +12 -2
  103. package/src/platforms/registry.js +16 -1
  104. package/src/platforms/runtime.js +5 -0
  105. package/src/platforms/session-history-adapters.js +1 -0
  106. package/src/server/api/platform-api-config.js +23 -2
  107. package/src/server/api/platform-route-factory.js +9 -2
  108. package/src/server/api/platforms.js +19 -1
  109. package/src/server/api/plugins.js +108 -13
  110. package/src/server/index.js +35 -1
  111. package/src/server/services/config-registry-service.js +10 -1
  112. package/src/server/services/config-templates-service.js +1 -0
  113. package/src/server/services/plugins-service.js +474 -103
  114. package/src/server/services/session-history-index.js +900 -140
  115. package/src/server/services/session-history-worker.js +3 -1
  116. package/src/server/services/skill-service.js +214 -7
  117. package/src/server/services/usage-log-utils.js +1 -0
  118. package/src/server/services/workspace-service.js +39 -10
  119. package/src/shared/driver-factories/api.js +22 -6
  120. package/src/shared/driver-factories/projects.js +8 -0
  121. package/dist/web/assets/Analytics-BVG6ehFv.js +0 -1
  122. package/dist/web/assets/AssetPathField-BZI9NvEr.js +0 -1
  123. package/dist/web/assets/ConfigExportDrawer-DrawKwxE.js +0 -1
  124. package/dist/web/assets/Home-CTF8ZWKW.js +0 -1
  125. package/dist/web/assets/McpDrawer-DZGHkuDm.js +0 -1
  126. package/dist/web/assets/PluginsPanel-CKuBs38B.js +0 -1
  127. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +0 -1
  128. package/dist/web/assets/ProjectList-tsjwjXV1.js +0 -1
  129. package/dist/web/assets/PromptsDrawer-B-X9ci8n.js +0 -1
  130. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +0 -1
  131. package/dist/web/assets/RecentSessionsDrawer-wLktMF_c.js +0 -1
  132. package/dist/web/assets/SessionCard-DkJPy0TY.js +0 -1
  133. package/dist/web/assets/SessionList-BpYRz1cX.css +0 -1
  134. package/dist/web/assets/SessionList-Gzhh4FOZ.js +0 -1
  135. package/dist/web/assets/SkillManager-CzFxWxx-.js +0 -1
  136. package/dist/web/assets/SkillsPanel--lLEts9y.js +0 -1
  137. package/dist/web/assets/Upload-BEQ-3Kqq.js +0 -1
  138. package/dist/web/assets/index-BFInlZGI.js +0 -2
  139. package/dist/web/assets/platforms-CBkB45JP.js +0 -1
  140. package/dist/web/assets/platforms-FTpjxAT6.js +0 -1
  141. package/dist/web/assets/sessions-Cmy31slD.js +0 -1
  142. package/dist/web/assets/sessions-yC_oVVh8.js +0 -1
  143. package/dist/web/assets/use-message-DoLM8C1C.js +0 -1
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { resolvePaths } = require('./common');
6
+ const { decodeArtifact } = require('./sessions');
7
+ const {
8
+ normalizeCost,
9
+ normalizeUsage,
10
+ walkFiles,
11
+ readJsonLines,
12
+ createScannedFileCursor
13
+ } = require('../native-log-utils');
14
+ const { calculateUsageCost } = require('../../../server/services/usage-log-utils');
15
+
16
+ const SESSION_FILE_PATTERN = /^session(?:\.v\d+)?\.jsonl(?:\.zstd)?$/;
17
+
18
+ function firstObject(...candidates) {
19
+ return candidates.find(value => value && typeof value === 'object' && !Array.isArray(value)) || null;
20
+ }
21
+
22
+ function extractUsage(record = {}) {
23
+ const data = record.data || {};
24
+ const message = data.message || record.message || {};
25
+ const source = message.source || data.source || {};
26
+ return firstObject(
27
+ record.usage,
28
+ record.tokens,
29
+ data.usage,
30
+ data.tokens,
31
+ message.usage,
32
+ message.tokens,
33
+ source.usage,
34
+ source.tokens,
35
+ data.response?.usage,
36
+ data.result?.usage
37
+ );
38
+ }
39
+
40
+ function normalizeTimestamp(value) {
41
+ if (typeof value === 'number' && Number.isFinite(value)) {
42
+ if (value > 1e11) return value;
43
+ if (value > 1e9) return value * 1000;
44
+ }
45
+ if (typeof value === 'string' && value.trim()) {
46
+ const parsed = Date.parse(value);
47
+ if (Number.isFinite(parsed)) return parsed;
48
+ }
49
+ return Date.now();
50
+ }
51
+
52
+ function normalizeDshEvent(record = {}, filePath = '', index = 0) {
53
+ const usage = extractUsage(record);
54
+ if (!usage) return null;
55
+
56
+ const data = record.data || {};
57
+ const message = data.message || record.message || {};
58
+ const source = message.source || data.source || {};
59
+ const model = record.model || data.model || message.model || source.model || '';
60
+ const provider = record.provider || data.provider || message.provider || source.provider || '';
61
+ const tokens = normalizeUsage(usage);
62
+ const id = record.id
63
+ || data.id
64
+ || message.id
65
+ || `${path.basename(filePath)}:${record.seq ?? index}`;
66
+ const timestamp = normalizeTimestamp(record.timestamp || record.time || data.timestamp || message.timestamp);
67
+ const cost = normalizeCost(record.cost ?? data.cost ?? message.cost ?? usage.cost)
68
+ || calculateUsageCost('dsh', model, tokens);
69
+
70
+ return {
71
+ id: `dsh:${path.basename(filePath)}:${id}`,
72
+ source: 'dsh',
73
+ sessionId: path.basename(path.dirname(filePath)),
74
+ timestamp,
75
+ provider,
76
+ model,
77
+ tokens,
78
+ cost,
79
+ channelId: record.channelId || data.channelId || message.channelId || source.channelId,
80
+ channel: record.channel || data.channel || message.channel || provider
81
+ };
82
+ }
83
+
84
+ function resolveSessionRoot({ nativeRoot, pathContext, paths } = {}) {
85
+ if (paths?.sessions) return paths.sessions;
86
+ if (pathContext?.customized && pathContext.native?.sessions) return pathContext.native.sessions;
87
+ if (nativeRoot) return nativeRoot;
88
+ return resolvePaths({ pathContext }).sessions;
89
+ }
90
+
91
+ function createDriver({ nativeRoot, pathContext, paths, fsImpl = fs } = {}) {
92
+ const resolvedNativeRoot = resolveSessionRoot({ nativeRoot, pathContext, paths });
93
+
94
+ return {
95
+ platform: 'dsh',
96
+ capability: 'nativeLogs',
97
+ createNativeLogCursor({ fs: cursorFs = fsImpl, skipInitialParse = false } = {}) {
98
+ return createScannedFileCursor({
99
+ scanFiles: () => walkFiles(
100
+ resolvedNativeRoot,
101
+ name => SESSION_FILE_PATTERN.test(name),
102
+ cursorFs
103
+ ),
104
+ parseFile: filePath => filePath.endsWith('.zstd')
105
+ ? decodeArtifact(filePath).events
106
+ : readJsonLines(filePath, cursorFs),
107
+ normalizeEvent: normalizeDshEvent,
108
+ fsImpl: cursorFs,
109
+ skipInitialParse
110
+ });
111
+ },
112
+ createCursor(options) { return this.createNativeLogCursor(options); }
113
+ };
114
+ }
115
+
116
+ module.exports = { createDriver, extractUsage, normalizeDshEvent };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const { execFile } = require('child_process');
4
+ const path = require('path');
5
+ const { promisify } = require('util');
6
+ const { resolvePaths, listProfilePlugins, safeProfileName } = require('./common');
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ function redactCommandOutput(value) {
11
+ return String(value || '')
12
+ .replace(/((?:api[-_]?key|access[-_]?token|refresh[-_]?token|token|secret|password|authorization)\s*[=:]\s*)([^\s,;]+)/gi, '$1[REDACTED]')
13
+ .replace(/(https?:\/\/[^\s/@]+:)[^@\s]+@/gi, '$1[REDACTED]@');
14
+ }
15
+
16
+ function normalizeSpecs(value) {
17
+ const values = Array.isArray(value) ? value : [value];
18
+ const specs = values
19
+ .map(spec => String(spec || '').trim())
20
+ .filter(Boolean);
21
+ if (!specs.length) throw new Error('至少需要一个 DSH plugin package spec');
22
+ if (specs.some(spec => spec.includes('\0') || spec.startsWith('-'))) {
23
+ throw new Error('DSH plugin package spec 不能是选项或包含 NUL 字符');
24
+ }
25
+ return specs;
26
+ }
27
+
28
+ function resolveWorkingDirectory(paths, value) {
29
+ if (value === undefined || value === null || value === '') return paths.dir;
30
+ const cwd = path.resolve(String(value));
31
+ try {
32
+ if (!require('fs').statSync(cwd).isDirectory()) throw new Error('not a directory');
33
+ } catch (_) {
34
+ throw new Error(`DSH plugin working directory does not exist: ${cwd}`);
35
+ }
36
+ return cwd;
37
+ }
38
+
39
+ function commandFor(context = {}) {
40
+ return context.dshCommand
41
+ || context.dependencies?.dshCommand
42
+ || process.env.DSH_COMMAND
43
+ || 'dsh';
44
+ }
45
+
46
+ function commandEnvironment(paths, context = {}) {
47
+ const env = { ...process.env, ...(context.dshEnv || {}) };
48
+ env.DSH_HOME = paths.dir;
49
+ if (paths.installAnchor) env.DSH_INSTALL_ANCHOR = paths.installAnchor;
50
+ if (paths.installRoot) env.DSH_INSTALL_ROOT = paths.installRoot;
51
+ return env;
52
+ }
53
+
54
+ async function runDshPlugin(context = {}, profileName, args, options = {}) {
55
+ const paths = resolvePaths(context);
56
+ const profile = safeProfileName(profileName);
57
+ const command = commandFor(context);
58
+ const cwd = resolveWorkingDirectory(paths, options.cwd);
59
+ const commandArgs = ['plugin', '--profile', profile, ...args];
60
+ let result;
61
+ try {
62
+ result = await execFileAsync(command, commandArgs, {
63
+ cwd,
64
+ env: commandEnvironment(paths, context),
65
+ encoding: 'utf8',
66
+ maxBuffer: 4 * 1024 * 1024,
67
+ timeout: options.timeoutMs || 15 * 60 * 1000,
68
+ windowsHide: true
69
+ });
70
+ } catch (error) {
71
+ const status = error && typeof error.code === 'number' ? error.code : null;
72
+ const wrapped = new Error(redactCommandOutput(error?.stderr?.trim() || error?.message || 'DSH plugin command failed'));
73
+ wrapped.code = error?.code === 'ENOENT' ? 'DSH_COMMAND_NOT_FOUND' : 'DSH_PLUGIN_COMMAND_FAILED';
74
+ wrapped.statusCode = error?.code === 'ENOENT' ? 503 : 422;
75
+ wrapped.exitCode = status;
76
+ wrapped.stdout = redactCommandOutput(error?.stdout);
77
+ wrapped.stderr = redactCommandOutput(error?.stderr);
78
+ throw wrapped;
79
+ }
80
+ return {
81
+ command,
82
+ args: commandArgs,
83
+ cwd,
84
+ profile,
85
+ exitCode: 0,
86
+ stdout: redactCommandOutput(result.stdout),
87
+ stderr: redactCommandOutput(result.stderr),
88
+ plugins: listProfilePlugins(context, profile)
89
+ };
90
+ }
91
+
92
+ async function installPlugin(context = {}, request = {}) {
93
+ const body = request.body || {};
94
+ return runDshPlugin(context, request.params?.profileName, ['add', ...normalizeSpecs(body.specs || body.spec || body.package)], {
95
+ cwd: body.cwd
96
+ });
97
+ }
98
+
99
+ async function uninstallPlugin(context = {}, request = {}) {
100
+ const body = request.body || {};
101
+ const specs = normalizeSpecs(body.specs || body.spec || request.params?.pluginName);
102
+ return runDshPlugin(context, request.params?.profileName, ['remove', ...specs], { cwd: body.cwd });
103
+ }
104
+
105
+ async function updatePlugin(context = {}, request = {}) {
106
+ const body = request.body || {};
107
+ const specs = body.specs || body.spec || body.package;
108
+ const args = ['update', ...(specs === undefined ? [] : normalizeSpecs(specs))];
109
+ return runDshPlugin(context, request.params?.profileName, args, { cwd: body.cwd });
110
+ }
111
+
112
+ module.exports = {
113
+ runDshPlugin,
114
+ installPlugin,
115
+ uninstallPlugin,
116
+ updatePlugin
117
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { createDriver: createSessionsDriver } = require('./sessions');
4
+
5
+ function createDriver(context = {}) {
6
+ const sessions = createSessionsDriver(context);
7
+ const unsupported = operation => () => ({
8
+ status: 'unsupported',
9
+ platform: 'dsh',
10
+ capability: 'projects',
11
+ operation
12
+ });
13
+ return {
14
+ platform: 'dsh',
15
+ capability: 'projects',
16
+ listProjects: sessions.listProjects,
17
+ listProjectsPage: sessions.listProjectsPage,
18
+ saveProjectOrder: unsupported('saveProjectOrder'),
19
+ deleteProject: unsupported('deleteProject')
20
+ };
21
+ }
22
+
23
+ module.exports = { createDriver };
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const httpProxy = require('http-proxy');
5
+ const http = require('http');
6
+ const chalk = require('chalk');
7
+ const { broadcastSchedulerState } = require('../../../server/websocket-server');
8
+ const { allocateChannel, releaseChannel, getSchedulerState } = require('../../../server/services/channel-scheduler');
9
+ const { recordSuccess, recordFailure } = require('../../../server/services/channel-health');
10
+ const { loadConfig } = require('../../../config/loader');
11
+ const {
12
+ getEnabledChannels,
13
+ getEffectiveApiKey,
14
+ setDshProxyConfig,
15
+ restoreDshNativeConfig,
16
+ clearActiveMarker,
17
+ isProxyModeEnabled,
18
+ readActiveMarker
19
+ } = require('./channels-implementation');
20
+ const { persistProxyRequestSnapshot } = require('../../../server/services/request-logger');
21
+ const { redirectModel, resolveTargetUrl, isChatCompletionsPath, ensureOpenAiStreamUsage } = require('../../../shared/proxy-utils');
22
+ const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('../../../server/services/proxy-runtime');
23
+ const { attachServerShutdownHandling, expediteServerShutdown } = require('../../../server/services/server-shutdown');
24
+
25
+ let proxyServer = null;
26
+ let currentPort = null;
27
+ const requestMetadata = new Map();
28
+
29
+ const jsonBodyParser = express.json({
30
+ limit: '100mb',
31
+ verify: (req, _res, buffer) => {
32
+ req.rawBody = Buffer.from(buffer);
33
+ }
34
+ });
35
+
36
+ function shouldParseJson(req) {
37
+ return req.method === 'POST' && String(req.headers['content-type'] || '').includes('application/json');
38
+ }
39
+
40
+ function openAiChannels() {
41
+ return getEnabledChannels().filter(channel => channel.providerApi === 'openai-completions');
42
+ }
43
+
44
+ function releaseOnce(channel) {
45
+ let released = false;
46
+ return () => {
47
+ if (released) return;
48
+ released = true;
49
+ if (channel?.id) {
50
+ releaseChannel(channel.id, 'dsh');
51
+ broadcastSchedulerState('dsh', getSchedulerState('dsh'));
52
+ }
53
+ };
54
+ }
55
+
56
+ async function startDshProxyServer(options = {}) {
57
+ if (proxyServer) {
58
+ const activeChannelId = readActiveMarker()?.activeChannelId;
59
+ const activeChannel = activeChannelId
60
+ ? getEnabledChannels().find(channel => channel.id === activeChannelId) || null
61
+ : null;
62
+ return { success: true, port: currentPort, provider: 'ctx-dsh-proxy', activeChannel };
63
+ }
64
+
65
+ if (openAiChannels().length === 0) {
66
+ throw new Error('DSH 动态代理当前只支持 openai-completions;请先添加并启用至少一个兼容渠道');
67
+ }
68
+
69
+ const preserveStartTime = options.preserveStartTime === true;
70
+ const config = loadConfig();
71
+ const requestedPort = Number(options.port ?? config.ports?.dshProxy ?? 20093);
72
+ const app = express();
73
+ const proxy = httpProxy.createProxyServer({});
74
+
75
+ app.use((req, res, next) => shouldParseJson(req) ? jsonBodyParser(req, res, next) : next());
76
+ app.use(async (req, res) => {
77
+ let channel;
78
+ let release;
79
+ try {
80
+ channel = await allocateChannel({
81
+ source: 'dsh',
82
+ providerApi: 'openai-completions',
83
+ enableSessionBinding: false
84
+ });
85
+ req.selectedChannel = channel;
86
+ release = releaseOnce(channel);
87
+ res.on('close', release);
88
+ res.on('error', release);
89
+ broadcastSchedulerState('dsh', getSchedulerState('dsh'));
90
+
91
+ const apiKey = getEffectiveApiKey(channel);
92
+ if (channel.authMode !== 'none' && !apiKey) {
93
+ release();
94
+ return res.status(401).json({
95
+ error: { message: 'DSH channel API key is not configured', type: 'authentication_error' }
96
+ });
97
+ }
98
+ req.effectiveApiKey = apiKey;
99
+
100
+ if (req.body?.model) {
101
+ const originalModel = req.body.model;
102
+ const redirectedModel = redirectModel(originalModel, channel, { useTierFallback: false });
103
+ if (redirectedModel !== originalModel) req.body.model = redirectedModel;
104
+ }
105
+ if (shouldParseJson(req) && isChatCompletionsPath(req.url)) {
106
+ ensureOpenAiStreamUsage(req.body);
107
+ }
108
+ if (shouldParseJson(req) && req.body) {
109
+ req.rawBody = Buffer.from(JSON.stringify(req.body));
110
+ }
111
+
112
+ persistProxyRequestSnapshot('dsh', {
113
+ timestamp: Date.now(),
114
+ source: 'dsh',
115
+ channel: channel.name,
116
+ request: { method: req.method, url: req.url, headers: req.headers, body: req.body || null }
117
+ });
118
+
119
+ proxy.web(req, res, {
120
+ target: resolveTargetUrl(channel.baseUrl, req.url),
121
+ changeOrigin: true,
122
+ proxyTimeout: 120000,
123
+ timeout: 120000
124
+ }, error => {
125
+ release?.();
126
+ if (!error) return;
127
+ recordFailure(channel.id, 'dsh', error);
128
+ if (!res.headersSent) {
129
+ res.status(502).json({ error: { message: `Proxy error: ${error.message}`, type: 'proxy_error' } });
130
+ }
131
+ });
132
+ } catch (error) {
133
+ release?.();
134
+ if (!res.headersSent) {
135
+ res.status(503).json({ error: { message: error.message || 'No DSH channel available', type: 'channel_pool_exhausted' } });
136
+ }
137
+ }
138
+ });
139
+
140
+ proxy.on('proxyReq', (proxyReq, req) => {
141
+ const channel = req.selectedChannel;
142
+ if (!channel) return;
143
+ const metadata = {
144
+ channelId: channel.id,
145
+ channel: channel.name,
146
+ startTime: Date.now(),
147
+ requestModel: req.body?.model || ''
148
+ };
149
+ requestMetadata.set(req, metadata);
150
+ proxyReq.removeHeader('authorization');
151
+ if (req.effectiveApiKey) proxyReq.setHeader('authorization', `Bearer ${req.effectiveApiKey}`);
152
+ proxyReq.setHeader('content-type', 'application/json');
153
+ if (shouldParseJson(req) && (req.rawBody || req.body)) {
154
+ const body = req.rawBody
155
+ ? (Buffer.isBuffer(req.rawBody) ? req.rawBody : Buffer.from(req.rawBody))
156
+ : Buffer.from(JSON.stringify(req.body));
157
+ proxyReq.setHeader('Content-Length', body.length);
158
+ proxyReq.write(body);
159
+ proxyReq.end();
160
+ }
161
+ });
162
+
163
+ proxy.on('proxyRes', (proxyRes, req, res) => {
164
+ const metadata = requestMetadata.get(req);
165
+ if (!metadata) return;
166
+ if (Number(proxyRes.statusCode) >= 400) {
167
+ recordFailure(metadata.channelId, 'dsh', new Error(`DSH upstream HTTP ${proxyRes.statusCode}`));
168
+ } else {
169
+ recordSuccess(metadata.channelId, 'dsh');
170
+ }
171
+ requestMetadata.delete(req);
172
+ res?.on('close', () => requestMetadata.delete(req));
173
+ });
174
+
175
+ proxy.on('error', (error, req, res) => {
176
+ if (req?.selectedChannel) {
177
+ recordFailure(req.selectedChannel.id, 'dsh', error);
178
+ releaseChannel(req.selectedChannel.id, 'dsh');
179
+ broadcastSchedulerState('dsh', getSchedulerState('dsh'));
180
+ }
181
+ if (res && !res.headersSent) res.status(502).json({ error: { message: `Proxy error: ${error.message}`, type: 'proxy_error' } });
182
+ });
183
+
184
+ const server = http.createServer(app);
185
+ attachServerShutdownHandling(server);
186
+ proxyServer = server;
187
+ currentPort = requestedPort;
188
+
189
+ try {
190
+ await new Promise((resolve, reject) => {
191
+ server.once('error', reject);
192
+ server.listen(requestedPort, '127.0.0.1', resolve);
193
+ });
194
+ const port = server.address()?.port || requestedPort;
195
+ currentPort = port;
196
+ const active = openAiChannels()[0];
197
+ setDshProxyConfig(port, { activeChannelId: active?.id });
198
+ saveProxyStartTime('dsh', preserveStartTime);
199
+ console.log(`DSH proxy server started on http://127.0.0.1:${port}`);
200
+ return { success: true, port, provider: 'ctx-dsh-proxy', activeChannel: active };
201
+ } catch (error) {
202
+ proxyServer = null;
203
+ currentPort = null;
204
+ clearActiveMarker();
205
+ try { await new Promise(resolve => server.close(() => resolve())); } catch (_) {}
206
+ if (error.code === 'EADDRINUSE') {
207
+ console.error(chalk.red(`DSH proxy port ${requestedPort} is already in use`));
208
+ }
209
+ throw error;
210
+ }
211
+ }
212
+
213
+ async function stopDshProxyServer(options = {}) {
214
+ const clearStartTime = options.clearStartTime !== false;
215
+ const stoppedPort = currentPort;
216
+ requestMetadata.clear();
217
+
218
+ if (proxyServer) {
219
+ const shutdownTimer = expediteServerShutdown(proxyServer);
220
+ await new Promise(resolve => proxyServer.close(resolve));
221
+ if (shutdownTimer) clearTimeout(shutdownTimer);
222
+ proxyServer = null;
223
+ currentPort = null;
224
+ }
225
+
226
+ if (!isProxyModeEnabled()) {
227
+ if (clearStartTime) clearProxyStartTime('dsh');
228
+ return { success: true, port: stoppedPort, restored: null };
229
+ }
230
+
231
+ const restored = restoreDshNativeConfig();
232
+ if (clearStartTime) clearProxyStartTime('dsh');
233
+ return { success: true, port: stoppedPort, restored: restored ? restored.id || restored.name : null };
234
+ }
235
+
236
+ function getDshProxyStatus() {
237
+ const config = loadConfig();
238
+ const running = Boolean(proxyServer);
239
+ const activeChannelId = readActiveMarker()?.activeChannelId;
240
+ const activeChannel = activeChannelId
241
+ ? getEnabledChannels().find(channel => channel.id === activeChannelId) || null
242
+ : null;
243
+ return {
244
+ running,
245
+ port: currentPort,
246
+ defaultPort: config.ports?.dshProxy || 20093,
247
+ startTime: getProxyStartTime('dsh', { allowRecovery: running }),
248
+ runtime: getProxyRuntime('dsh', { allowRecovery: running }),
249
+ mode: 'managed-provider',
250
+ activeChannel,
251
+ enabledChannelsCount: getEnabledChannels().length,
252
+ supportedChannelsCount: openAiChannels().length
253
+ };
254
+ }
255
+
256
+ module.exports = {
257
+ startDshProxyServer,
258
+ stopDshProxyServer,
259
+ getDshProxyStatus
260
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { createProxyDriver } = require('../../../shared/proxy-driver');
4
+
5
+ function createDriver(context = {}) {
6
+ return createProxyDriver({
7
+ ...context,
8
+ platform: 'dsh',
9
+ servicePath: './dsh/proxy-implementation',
10
+ localServicePath: '../platforms/drivers/dsh/proxy-implementation',
11
+ exports: {
12
+ status: 'getDshProxyStatus',
13
+ start: 'startDshProxyServer',
14
+ stop: 'stopDshProxyServer'
15
+ },
16
+ cliMetadata: {
17
+ managedProviderConfig: true,
18
+ defaultPort: 20093
19
+ }
20
+ });
21
+ }
22
+
23
+ module.exports = { createDriver };