coding-tool-x 3.9.1 → 3.9.3-260928

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 (79) hide show
  1. package/README.md +1 -1
  2. package/dist/web/assets/{AgentsDrawer-CobzHziM.css → AgentsDrawer-Dubq1kQI.css} +1 -1
  3. package/dist/web/assets/AgentsDrawer-x29vGIIi.js +1 -0
  4. package/dist/web/assets/{Analytics-DTIvvMXu.js → Analytics-BJbR6XJO.js} +1 -1
  5. package/dist/web/assets/{AssetPathField-jv-lopQ3.js → AssetPathField-2W-16zgR.js} +1 -1
  6. package/dist/web/assets/{ChatHistoryDrawer-T4OVnJzM.js → ChatHistoryDrawer-C1URzQ_4.js} +1 -1
  7. package/dist/web/assets/{CollapseItem-P6BvSxEu.js → CollapseItem-DjkOkbwE.js} +1 -1
  8. package/dist/web/assets/{CommandsDrawer-DtFJRtow.js → CommandsDrawer-DcnfrHC4.js} +1 -1
  9. package/dist/web/assets/{ConfigExportDrawer-Di7KXhVS.js → ConfigExportDrawer-CKwG_RGJ.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplates-VdKE5Kp0.js → ConfigTemplates-C1tNpfzi.js} +1 -1
  11. package/dist/web/assets/{ConfigTemplatesDrawer-bQvPnUQ0.js → ConfigTemplatesDrawer-gZNPbcUR.js} +1 -1
  12. package/dist/web/assets/{ConfigTemplatesPanel-ChdyQ3C6.js → ConfigTemplatesPanel-C-Fy53Qo.js} +1 -1
  13. package/dist/web/assets/{FavoritesDrawer-A25XfI7C.js → FavoritesDrawer-Bp8FYHPk.js} +1 -1
  14. package/dist/web/assets/GatewayConvertDrawer-CvUkvNQy.css +1 -0
  15. package/dist/web/assets/GatewayConvertDrawer-j5IJzb2Y.js +1 -0
  16. package/dist/web/assets/Home-2MAvBnml.js +1 -0
  17. package/dist/web/assets/Home-BRNWE0Qy.css +1 -0
  18. package/dist/web/assets/{MarkdownEditor-BXwTqbMo.js → MarkdownEditor-BA_PcLed.js} +1 -1
  19. package/dist/web/assets/{McpDrawer-DIaiFq4R.js → McpDrawer-DAep5PP_.js} +1 -1
  20. package/dist/web/assets/{McpFormDrawer-CQgTYAux.js → McpFormDrawer-BRm-hzv1.js} +1 -1
  21. package/dist/web/assets/{PluginManager-BkGrr21J.js → PluginManager-C2yY8-FP.js} +1 -1
  22. package/dist/web/assets/{PluginsDrawer-Bq1JkM5r.js → PluginsDrawer-CxKnWzTO.js} +1 -1
  23. package/dist/web/assets/{PluginsPanel-CzGMK-Ap.css → PluginsPanel-DsmiW9TU.css} +1 -1
  24. package/dist/web/assets/PluginsPanel-DtbXSoXu.js +1 -0
  25. package/dist/web/assets/{ProjectList-QIC7-9ic.js → ProjectList-Cb25pa2s.js} +1 -1
  26. package/dist/web/assets/{PromptsDrawer-BLeUYZWW.js → PromptsDrawer-D39OJVRP.js} +1 -1
  27. package/dist/web/assets/{RecentSessionsDrawer-yOpScIwI.js → RecentSessionsDrawer-DkCjc4kS.js} +1 -1
  28. package/dist/web/assets/{SessionCard-DeKRHAzu.js → SessionCard-5Mwig8zy.js} +1 -1
  29. package/dist/web/assets/{SessionList-B0fPT_VB.js → SessionList-DVkIlraF.js} +1 -1
  30. package/dist/web/assets/{SettingsDrawer-DpZoDj3M.js → SettingsDrawer-Cr9qZDgO.js} +1 -1
  31. package/dist/web/assets/{SkillManager-BpPqV2o4.js → SkillManager-D935sPSV.js} +1 -1
  32. package/dist/web/assets/{SkillsDrawer-BgMlJbgS.js → SkillsDrawer-D1_3j74b.js} +1 -1
  33. package/dist/web/assets/{SkillsPanel-CK-pSSHU.js → SkillsPanel-6SehnNG8.js} +1 -1
  34. package/dist/web/assets/{Upload-DtuQ7ady.js → Upload-38eKB6wv.js} +1 -1
  35. package/dist/web/assets/{WorkspaceDrawer-Bu5mOGsF.js → WorkspaceDrawer-Bkfmlr5M.js} +1 -1
  36. package/dist/web/assets/{WorkspaceManager-D1mhnNV-.js → WorkspaceManager-DFVQ5YoE.js} +1 -1
  37. package/dist/web/assets/{WorkspacePanel-BIrkMC_5.js → WorkspacePanel-CEnogXEv.js} +1 -1
  38. package/dist/web/assets/client-oU6RT-6I.js +1 -0
  39. package/dist/web/assets/dsh-agent-presets-DjJRVMT6.js +1 -0
  40. package/dist/web/assets/index-CCATqHAD.js +2 -0
  41. package/dist/web/assets/index-p-yggtLj.css +1 -0
  42. package/dist/web/assets/{platforms-DrzAtnjY.js → platforms-DILqyP72.js} +1 -1
  43. package/dist/web/assets/platforms-JV36QBCw.js +1 -0
  44. package/dist/web/assets/{project-config-BVt5s-pH.js → project-config-CzFKBw_m.js} +1 -1
  45. package/dist/web/assets/{sessions-CaWUaysM.js → sessions-WXODMvLY.js} +1 -1
  46. package/dist/web/assets/{sessions-53-Fr68H.js → sessions-wvj5oaYr.js} +1 -1
  47. package/dist/web/assets/{useResponsiveDrawer-C-UacYVP.js → useResponsiveDrawer-BWRBXImg.js} +1 -1
  48. package/dist/web/assets/{workspaces-DQuZd4qw.js → workspaces-D2yLnT-B.js} +1 -1
  49. package/dist/web/index.html +5 -5
  50. package/package.json +4 -4
  51. package/src/config/model-metadata.json +143 -7
  52. package/src/platforms/drivers/dsh/api-operations.js +60 -2
  53. package/src/platforms/drivers/dsh/common.js +222 -11
  54. package/src/platforms/drivers/dsh/native-config.js +2 -2
  55. package/src/platforms/drivers/dsh/native-logs.js +16 -1
  56. package/src/platforms/drivers/dsh/plugin-manager.js +64 -6
  57. package/src/platforms/drivers/omp/gateway.js +27 -1
  58. package/src/platforms/drivers/opencode/conversion.js +60 -8
  59. package/src/platforms/manifest-schema.js +4 -0
  60. package/src/platforms/manifests/dsh.json +29 -4
  61. package/src/platforms/manifests/omp.json +2 -1
  62. package/src/platforms/registry.js +1 -0
  63. package/src/server/api/convert.js +26 -76
  64. package/src/server/api/platforms.js +1 -0
  65. package/src/server/services/model-detector.js +13 -22
  66. package/src/server/services/native-log-observer.js +1 -0
  67. package/src/server/services/plugins-service.js +13 -14
  68. package/src/server/services/request-logger.js +95 -0
  69. package/src/server/services/usage-log-utils.js +2 -1
  70. package/dist/web/assets/AgentsDrawer-MeGfH4I0.js +0 -1
  71. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +0 -1
  72. package/dist/web/assets/GatewayConvertDrawer-TSJPvY4f.js +0 -1
  73. package/dist/web/assets/Home-CSuFuJ9Y.css +0 -1
  74. package/dist/web/assets/Home-CzB85lKE.js +0 -1
  75. package/dist/web/assets/PluginsPanel-Clz1r67v.js +0 -1
  76. package/dist/web/assets/client-BgcNg4Bt.js +0 -1
  77. package/dist/web/assets/index-B86H0_5y.css +0 -1
  78. package/dist/web/assets/index-Bw6YXbBA.js +0 -2
  79. package/dist/web/assets/platforms-mqqe_82d.js +0 -1
@@ -2,22 +2,69 @@
2
2
 
3
3
  function createDriver(context = {}) {
4
4
  const converter = require('./gateway-converter');
5
+ const {
6
+ loadLatestProxyRequestHeaders,
7
+ loadLatestProxyRequestSnapshot
8
+ } = require('../../../server/services/request-logger');
9
+ const target = context.platform || 'opencode';
10
+
11
+ function getOmpTargetContext() {
12
+ const snapshot = loadLatestProxyRequestSnapshot('omp');
13
+ const providerApi = snapshot?.route?.providerApi;
14
+ const targetApi = providerApi === 'openai-completions'
15
+ ? 'chat.completions'
16
+ : ['openai-responses', 'openai-codex-responses', 'azure-openai-responses'].includes(providerApi)
17
+ ? 'responses'
18
+ : null;
19
+ return { snapshot, providerApi, targetApi };
20
+ }
21
+
22
+ function convertPayload({ sourceType, payload, options = {} } = {}) {
23
+ const ompContext = target === 'omp' ? getOmpTargetContext() : null;
24
+ if (ompContext && !ompContext.targetApi) {
25
+ throw new Error('The latest OMP request log does not identify a supported OpenAI-compatible provider API. Send an OMP request through an OpenAI-compatible route, then retry.');
26
+ }
27
+ const requestedTargetApi = options.targetApi || options.api;
28
+ if (ompContext && requestedTargetApi && requestedTargetApi !== ompContext.targetApi) {
29
+ throw new Error(`The latest OMP request uses ${ompContext.providerApi}; conversion only supports ${ompContext.targetApi}.`);
30
+ }
31
+ const result = converter.convertToOpenCodePayload({
32
+ sourceType,
33
+ payload,
34
+ options: ompContext ? { ...options, targetApi: ompContext.targetApi } : options
35
+ });
36
+ return {
37
+ ...result,
38
+ target,
39
+ ...(ompContext?.snapshot?.route?.path ? { endpoint: ompContext.snapshot.route.path } : {}),
40
+ requestHeaders: loadLatestProxyRequestHeaders(target)
41
+ };
42
+ }
43
+
5
44
  return {
6
45
  platform: context.platform,
7
46
  capability: 'conversion',
8
47
  formats() {
48
+ const ompContext = target === 'omp' ? getOmpTargetContext() : null;
49
+ const targetApis = target === 'omp'
50
+ ? (ompContext.targetApi ? [ompContext.targetApi] : [])
51
+ : converter.SUPPORTED_TARGET_APIS;
9
52
  return {
10
53
  sourceTypes: converter.SUPPORTED_SOURCE_TYPES,
11
54
  formats: converter.SUPPORTED_SOURCE_TYPES.map(type => ({
12
55
  id: type,
13
56
  name: type === 'claude' ? 'Claude Code' : type === 'codex' ? 'Codex' : 'Gemini'
14
57
  })),
15
- targetApis: converter.SUPPORTED_TARGET_APIS,
16
- defaultTargetApi: 'responses',
17
- endpoints: {
18
- responses: '/v1/responses',
19
- 'chat.completions': '/v1/chat/completions'
20
- }
58
+ targetApis,
59
+ defaultTargetApi: targetApis[0] || null,
60
+ endpoints: ompContext
61
+ ? (ompContext.targetApi && ompContext.snapshot?.route?.path
62
+ ? { [ompContext.targetApi]: ompContext.snapshot.route.path }
63
+ : {})
64
+ : {
65
+ responses: '/v1/responses',
66
+ 'chat.completions': '/v1/chat/completions'
67
+ }
21
68
  };
22
69
  },
23
70
  getFormats() {
@@ -30,10 +77,15 @@ function createDriver(context = {}) {
30
77
  codex: converter.convertCodexToOpenCodePayload,
31
78
  gemini: converter.convertGeminiToOpenCodePayload
32
79
  };
33
- return (handlers[sourceType] || converter.convertToOpenCodePayload)({ sourceType, payload, options });
80
+ if (target === 'omp') return convertPayload({ sourceType, payload, options });
81
+ return {
82
+ ...(handlers[sourceType] || converter.convertToOpenCodePayload)({ sourceType, payload, options }),
83
+ target,
84
+ requestHeaders: loadLatestProxyRequestHeaders(target)
85
+ };
34
86
  },
35
87
  convert({ sourceType, payload, options = {} } = {}) {
36
- return converter.convertToOpenCodePayload({ sourceType, payload, options });
88
+ return convertPayload({ sourceType, payload, options });
37
89
  }
38
90
  };
39
91
  }
@@ -68,6 +68,7 @@ const CAPABILITY_OPERATIONS = Object.freeze({
68
68
  'listProfileCapabilities',
69
69
  'listProfileMcp',
70
70
  'listProfilePrompts',
71
+ 'listProfileAgentPresets',
71
72
  'installPlugin',
72
73
  'uninstallPlugin',
73
74
  'updatePlugin',
@@ -75,6 +76,8 @@ const CAPABILITY_OPERATIONS = Object.freeze({
75
76
  'deleteMcp',
76
77
  'upsertPrompt',
77
78
  'deletePrompt',
79
+ 'upsertAgentPreset',
80
+ 'deleteAgentPreset',
78
81
  'listSkills',
79
82
  'getSkill',
80
83
  'createSkill',
@@ -113,6 +116,7 @@ const schema = {
113
116
  portLabel: { type: 'string', minLength: 1 },
114
117
  apiBasePath: { type: 'string', minLength: 1 },
115
118
  logFile: { type: 'string', minLength: 1 },
119
+ logLayout: { enum: ['basic'] },
116
120
  logAliases: { type: 'array', items: { type: 'string', minLength: 1 } },
117
121
  logNote: { type: 'string' },
118
122
  logColor: { type: 'string', minLength: 1 },
@@ -5,6 +5,7 @@
5
5
  "command": "dsh",
6
6
  "iconToken": "terminal",
7
7
  "color": "#2563eb",
8
+ "logLayout": "basic",
8
9
  "defaultVisible": true,
9
10
  "defaultEnabled": false,
10
11
  "sortOrder": 50,
@@ -25,7 +26,7 @@
25
26
  "resourceTypes": {
26
27
  "skills": true,
27
28
  "commands": false,
28
- "agents": false,
29
+ "agents": true,
29
30
  "plugins": true,
30
31
  "mcp": true,
31
32
  "prompts": true
@@ -38,10 +39,10 @@
38
39
  "refresh": true
39
40
  },
40
41
  "plugins": {
41
- "repositories": true,
42
+ "repositories": false,
42
43
  "market": false,
43
- "install": false,
44
- "uninstall": false,
44
+ "install": true,
45
+ "uninstall": true,
45
46
  "import": false,
46
47
  "syncRepos": false
47
48
  }
@@ -477,6 +478,30 @@
477
478
  "request": "default",
478
479
  "response": "default"
479
480
  },
481
+ {
482
+ "method": "GET",
483
+ "path": "/profiles/:profileName/agent-presets",
484
+ "capability": "api",
485
+ "operation": "listProfileAgentPresets",
486
+ "request": "default",
487
+ "response": "default"
488
+ },
489
+ {
490
+ "method": "POST",
491
+ "path": "/profiles/:profileName/agent-presets",
492
+ "capability": "api",
493
+ "operation": "upsertAgentPreset",
494
+ "request": "default",
495
+ "response": "default"
496
+ },
497
+ {
498
+ "method": "DELETE",
499
+ "path": "/profiles/:profileName/agent-presets/:presetId",
500
+ "capability": "api",
501
+ "operation": "deleteAgentPreset",
502
+ "request": "default",
503
+ "response": "default"
504
+ },
480
505
  {
481
506
  "method": "GET",
482
507
  "path": "/skills",
@@ -484,6 +484,7 @@
484
484
  "hooks": "legacy:omp",
485
485
  "resourceSync": "legacy:omp",
486
486
  "mcp": "legacy:omp",
487
- "prompts": "legacy:omp"
487
+ "prompts": "legacy:omp",
488
+ "conversion": "opencode-conversion"
488
489
  }
489
490
  }
@@ -188,6 +188,7 @@ function createPlatformRegistry({
188
188
  portLabel: platform.portLabel || null,
189
189
  apiPrefix: platform.api?.prefix || null,
190
190
  modelCatalogKey: platform.modelConfig?.catalogKey || platform.key,
191
+ ...(platform.logLayout ? { logLayout: platform.logLayout } : {}),
191
192
  capabilities
192
193
  };
193
194
  const resourceTypes = publicResourceTypes(platform.resourceTypes);
@@ -1,40 +1,28 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const { getPlatformCatalog } = require('../services/platform-catalog');
4
+ const { createSameOriginGuard } = require('../services/network-access');
4
5
 
5
- /**
6
- * 获取支持的格式列表
7
- * GET /api/convert/formats
8
- */
9
- router.get('/formats', (req, res) => {
10
- const driver = getPlatformCatalog().driver('opencode', 'conversion');
6
+ router.use(createSameOriginGuard({
7
+ message: '禁止跨站访问请求转换接口'
8
+ }));
9
+
10
+ function getConversionDriver(target) {
11
+ return getPlatformCatalog().driver(target, 'conversion');
12
+ }
13
+
14
+ function getConversionFormats(driver) {
11
15
  const formats = driver?.formats?.() || driver?.getFormats?.();
12
- if (!formats) {
13
- return res.status(404).json({ success: false, error: 'Conversion is not supported' });
14
- }
15
- const sourceTypes = formats.sourceTypes || [];
16
- res.json({
17
- formats: sourceTypes.map(type => ({
18
- id: type,
19
- name: (formats.formats || []).find(item => item.id === type)?.name
20
- || String(type).replace(/[-_]/g, ' ').replace(/\b\w/g, char => char.toUpperCase()),
21
- description: `${type} session format`,
22
- extension: '.jsonl',
23
- icon: type
24
- })),
25
- conversions: sourceTypes.flatMap(sourceType => sourceTypes
26
- .filter(targetType => targetType !== sourceType)
27
- .map(targetType => ({ from: sourceType, to: targetType })))
28
- });
29
- });
16
+ return formats;
17
+ }
30
18
 
31
19
  /**
32
- * 获取 OpenCode 网关支持格式
33
- * GET /api/convert/opencode/formats
20
+ * Read conversion capabilities for a target CLI.
21
+ * GET /api/convert/:target/formats
34
22
  */
35
- router.get('/opencode/formats', (req, res) => {
36
- const driver = getConversionDriver();
37
- const formats = driver?.formats?.() || driver?.getFormats?.();
23
+ router.get('/:target/formats', (req, res) => {
24
+ const driver = getConversionDriver(req.params.target);
25
+ const formats = getConversionFormats(driver);
38
26
  if (!formats) {
39
27
  return res.status(404).json({ success: false, error: 'Conversion is not supported' });
40
28
  }
@@ -45,62 +33,24 @@ router.get('/opencode/formats', (req, res) => {
45
33
  name: (formats.formats || []).find(item => item.id === type)?.name
46
34
  || String(type).replace(/[-_]/g, ' ').replace(/\b\w/g, char => char.toUpperCase())
47
35
  })),
48
- target: 'opencode',
36
+ target: req.params.target,
49
37
  targetApis: formats.targetApis || [],
50
38
  defaultTargetApi: formats.defaultTargetApi,
51
- endpoints: formats.endpoints || {},
52
- sourceEndpoints: Object.fromEntries(sourceTypes.map(type => [type, `/api/convert/opencode/${type}`]))
39
+ endpoints: formats.endpoints || {}
53
40
  });
54
41
  });
55
42
 
56
- function getConversionDriver() {
57
- return getPlatformCatalog().driver('opencode', 'conversion');
58
- }
59
-
60
- function handleOpenCodeConvert(req, res, sourceType) {
61
- try {
62
- const { payload, options = {} } = req.body || {};
63
- const driver = getConversionDriver();
64
-
65
- if (!driver) {
66
- return res.status(404).json({ success: false, error: 'Conversion is not supported' });
67
- }
68
- if (!payload) {
69
- return res.status(400).json({
70
- success: false,
71
- error: 'Missing required parameters: payload'
72
- });
73
- }
74
- const result = driver.convertSource
75
- ? driver.convertSource(sourceType, payload, options)
76
- : driver.convert({ sourceType, payload, options });
77
- return res.json({
78
- success: true,
79
- ...result
80
- });
81
- } catch (error) {
82
- console.error(`[Convert API] OpenCode ${sourceType} convert error:`, error);
83
- return res.status(500).json({
84
- success: false,
85
- error: error.message
86
- });
87
- }
88
- }
89
-
90
- router.post('/opencode/:sourceType', (req, res) => {
91
- return handleOpenCodeConvert(req, res, req.params.sourceType);
92
- });
93
-
94
43
  /**
95
- * 在线转换为 OpenCode 可处理格式
96
- * POST /api/convert/opencode
44
+ * Online conversion to a CLI-compatible request format.
45
+ * POST /api/convert/:target
97
46
  * Body: { sourceType, payload, options? }
98
47
  */
99
- router.post('/opencode', (req, res) => {
48
+ router.post('/:target', (req, res) => {
100
49
  try {
101
50
  const { sourceType, payload, options = {} } = req.body || {};
102
- const driver = getConversionDriver();
103
- const sourceTypes = driver?.formats?.().sourceTypes || driver?.getFormats?.().sourceTypes || [];
51
+ const driver = getConversionDriver(req.params.target);
52
+ const formats = getConversionFormats(driver);
53
+ const sourceTypes = formats?.sourceTypes || [];
104
54
  const normalized = driver?.normalizeSourceType?.(sourceType) || sourceType;
105
55
 
106
56
  if (!driver) {
@@ -124,7 +74,7 @@ router.post('/opencode', (req, res) => {
124
74
  ...driver.convert({ sourceType: normalized, payload, options })
125
75
  });
126
76
  } catch (error) {
127
- console.error('[Convert API] OpenCode gateway convert error:', error);
77
+ console.error(`[Convert API] ${req.params.target} convert error:`, error);
128
78
  return res.status(500).json({
129
79
  success: false,
130
80
  error: error.message
@@ -18,6 +18,7 @@ const PUBLIC_FIELDS = Object.freeze([
18
18
  'portLabel',
19
19
  'apiPrefix',
20
20
  'modelCatalogKey',
21
+ 'logLayout',
21
22
  'promptLabel',
22
23
  'resourceTypes',
23
24
  'resourceActions'
@@ -330,32 +330,29 @@ function buildRequestHeaders(channelType, channel) {
330
330
  }
331
331
 
332
332
  function buildOpenAiCompatibleUrl(baseUrl, endpoint) {
333
- const trimmed = String(baseUrl || '').trim().replace(/\/+$/, '');
334
- if (!trimmed) {
333
+ const rawBaseUrl = String(baseUrl || '').trim();
334
+ if (!rawBaseUrl) {
335
335
  throw new Error('Invalid baseUrl');
336
336
  }
337
337
 
338
+ const parsed = new URL(rawBaseUrl);
339
+ const basePath = parsed.pathname.replace(/\/+$/, '');
338
340
  const normalizedEndpoint = String(endpoint || '').startsWith('/')
339
341
  ? String(endpoint)
340
342
  : `/${endpoint}`;
341
-
342
- if (trimmed.endsWith(normalizedEndpoint)) {
343
- return trimmed;
344
- }
345
-
346
343
  const endpointWithoutV1 = normalizedEndpoint.startsWith('/v1/')
347
344
  ? normalizedEndpoint.slice(3)
348
345
  : normalizedEndpoint;
349
346
 
350
- if (trimmed.endsWith(endpointWithoutV1)) {
351
- return trimmed;
352
- }
347
+ if (basePath.endsWith(normalizedEndpoint)) return parsed.toString();
348
+ if (basePath.endsWith(endpointWithoutV1)) return parsed.toString();
353
349
 
354
- if (trimmed.endsWith('/v1') && normalizedEndpoint.startsWith('/v1/')) {
355
- return `${trimmed}${normalizedEndpoint.slice(3)}`;
356
- }
357
-
358
- return `${trimmed}${normalizedEndpoint}`;
350
+ const suffix = /\/v1$/.test(basePath) && normalizedEndpoint.startsWith('/v1/')
351
+ ? normalizedEndpoint.slice(3)
352
+ : normalizedEndpoint;
353
+ parsed.pathname = `${basePath}${suffix}` || '/';
354
+ parsed.hash = '';
355
+ return parsed.toString();
359
356
  }
360
357
 
361
358
 
@@ -1044,14 +1041,8 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1044
1041
  resolve(result);
1045
1042
  };
1046
1043
  try {
1047
- const baseUrl = channel.baseUrl.trim().replace(/\/+$/, '');
1048
1044
  const endpoint = capability.modelListEndpoint; // e.g. '/v1/models'
1049
- // 避免路径重复:如果 baseUrl 已包含 /v1,则只拼接 /models
1050
- const requestUrl = baseUrl.endsWith('/v1') && endpoint.startsWith('/v1/')
1051
- ? `${baseUrl}${endpoint.slice(3)}`
1052
- : `${baseUrl}${endpoint}`;
1053
-
1054
- const parsedUrl = new URL(requestUrl);
1045
+ const parsedUrl = new URL(buildOpenAiCompatibleUrl(channel.baseUrl, endpoint));
1055
1046
  const isHttps = parsedUrl.protocol === 'https:';
1056
1047
  const httpModule = isHttps ? https : http;
1057
1048
 
@@ -140,6 +140,7 @@ function recordEvent(platform, event, runtime = getPlatformRuntime(), channelCac
140
140
  channel,
141
141
  model: event.model || '',
142
142
  tokens,
143
+ cacheUsageAvailable: event.cacheUsageAvailable,
143
144
  cost: Number(event.cost) || 0,
144
145
  timestamp,
145
146
  usageMissing: false
@@ -117,6 +117,18 @@ const PLATFORM_CAPABILITIES = {
117
117
  import: false,
118
118
  syncRepos: false
119
119
  },
120
+ dsh: {
121
+ platform: 'dsh',
122
+ supportsPlugins: true,
123
+ repositories: false,
124
+ market: false,
125
+ install: true,
126
+ uninstall: true,
127
+ toggle: false,
128
+ config: false,
129
+ import: false,
130
+ syncRepos: false
131
+ },
120
132
  omp: {
121
133
  platform: 'omp',
122
134
  supportsPlugins: true,
@@ -134,19 +146,6 @@ const PLATFORM_CAPABILITIES = {
134
146
  repositoryAuth: false,
135
147
  installMetadataMode: 'omp'
136
148
  },
137
- dsh: {
138
- platform: 'dsh',
139
- supportsPlugins: true,
140
- repositories: true,
141
- market: false,
142
- install: false,
143
- uninstall: false,
144
- toggle: false,
145
- config: false,
146
- import: false,
147
- syncRepos: false,
148
- disabledReason: 'DSH 插件由 profile 管理,当前面板仅提供只读查看'
149
- }
150
149
  };
151
150
 
152
151
  function cloneRepos(repos = []) {
@@ -1670,7 +1669,7 @@ class PluginsService {
1670
1669
  source: 'dsh-profile',
1671
1670
  pluginType: 'dsh-profile',
1672
1671
  enabled: true,
1673
- readonly: true,
1672
+ readonly: plugin.builtIn === true,
1674
1673
  description: plugin.description || '',
1675
1674
  directory: plugin.name || name
1676
1675
  });
@@ -21,6 +21,21 @@ const { PATHS } = pathsModule;
21
21
  const getPlatformStatePath = typeof pathsModule.getPlatformStatePath === 'function'
22
22
  ? pathsModule.getPlatformStatePath
23
23
  : (category, platform) => PATHS[category]?.[platform];
24
+ const latestProxyRequestSnapshots = new Map();
25
+
26
+ function compactProxyRequestSnapshot(payload = {}) {
27
+ const request = payload.request && typeof payload.request === 'object' ? payload.request : {};
28
+ const route = payload.route && typeof payload.route === 'object' ? payload.route : undefined;
29
+ return {
30
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
31
+ ...(payload.source ? { source: payload.source } : {}),
32
+ request: {
33
+ ...(request.method ? { method: request.method } : {}),
34
+ ...(request.headers && typeof request.headers === 'object' ? { headers: request.headers } : {})
35
+ },
36
+ ...(route ? { route: { ...route } } : {})
37
+ };
38
+ }
24
39
 
25
40
  function ensureDir(dir) {
26
41
  if (!fs.existsSync(dir)) {
@@ -57,6 +72,10 @@ function persistProxyRequestSnapshot(source, payload) {
57
72
  if (!isProxyRequestLoggingEnabled()) return;
58
73
 
59
74
  try {
75
+ latestProxyRequestSnapshots.set(
76
+ String(source || '').trim().toLowerCase(),
77
+ compactProxyRequestSnapshot(payload)
78
+ );
60
79
  const logPath = getPlatformStatePath('requestSnapshots', source);
61
80
  if (!logPath) return;
62
81
  ensureDir(path.dirname(logPath));
@@ -70,6 +89,80 @@ function persistProxyRequestSnapshot(source, payload) {
70
89
  }
71
90
  }
72
91
 
92
+ function loadLatestProxyRequestSnapshot(source) {
93
+ const normalizedSource = String(source || '').trim().toLowerCase();
94
+ if (latestProxyRequestSnapshots.has(normalizedSource)) {
95
+ return latestProxyRequestSnapshots.get(normalizedSource);
96
+ }
97
+ const logPath = getPlatformStatePath('requestSnapshots', normalizedSource);
98
+ if (!logPath) return {};
99
+
100
+ let fd;
101
+ try {
102
+ fd = fs.openSync(logPath, 'r');
103
+ const fileSize = fs.fstatSync(fd).size;
104
+ const chunkSize = 1024 * 1024;
105
+ let position = fileSize;
106
+ let parts = [];
107
+ let bytesScanned = 0;
108
+ while (position > 0) {
109
+ const bytesToRead = Math.min(position, chunkSize);
110
+ position -= bytesToRead;
111
+ bytesScanned += bytesToRead;
112
+ const chunk = Buffer.alloc(bytesToRead);
113
+ fs.readSync(fd, chunk, 0, bytesToRead, position);
114
+ let segmentEnd = chunk.length;
115
+ for (let index = chunk.length - 1; index >= 0; index -= 1) {
116
+ if (chunk[index] !== 10) continue;
117
+ if (segmentEnd > index + 1) parts.push(chunk.subarray(index + 1, segmentEnd));
118
+ const line = Buffer.concat(parts.reverse()).toString('utf8');
119
+ parts = [];
120
+ segmentEnd = index;
121
+ if (!line.trim()) continue;
122
+ try {
123
+ const entry = JSON.parse(line);
124
+ if (entry && typeof entry === 'object') {
125
+ const compact = compactProxyRequestSnapshot(entry);
126
+ latestProxyRequestSnapshots.set(normalizedSource, compact);
127
+ return compact;
128
+ }
129
+ } catch {
130
+ // Ignore malformed records and continue scanning older complete lines.
131
+ }
132
+ }
133
+ if (segmentEnd > 0) parts.push(chunk.subarray(0, segmentEnd));
134
+ if (bytesScanned > 128 * 1024 * 1024) break;
135
+ }
136
+ } catch {
137
+ // Request snapshots are optional and may not exist yet.
138
+ } finally {
139
+ if (fd !== undefined) fs.closeSync(fd);
140
+ }
141
+ return {};
142
+ }
143
+
144
+ function loadLatestProxyRequestHeaders(source) {
145
+ const headers = loadLatestProxyRequestSnapshot(source)?.request?.headers;
146
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return {};
147
+
148
+ const hopByHopHeaders = new Set([
149
+ 'connection',
150
+ 'content-length',
151
+ 'host',
152
+ 'keep-alive',
153
+ 'proxy-connection',
154
+ 'te',
155
+ 'trailer',
156
+ 'transfer-encoding',
157
+ 'upgrade'
158
+ ]);
159
+ const sensitiveHeaderName = /(auth|cookie|session|token|secret|credential|api[-_]?key|password|signature|\bjwt\b|capability)/i;
160
+
161
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => (
162
+ !hopByHopHeaders.has(name.toLowerCase()) && !sensitiveHeaderName.test(name)
163
+ )));
164
+ }
165
+
73
166
  /**
74
167
  * 创建 Express 中间件,用于记录 REST API 请求
75
168
  * 仅当 CC_TOOL_LOG_API_REQUESTS=true 时生效
@@ -317,6 +410,8 @@ module.exports = {
317
410
  isProxyRequestLoggingEnabled,
318
411
  isApiRequestLoggingEnabled,
319
412
  persistProxyRequestSnapshot,
413
+ loadLatestProxyRequestSnapshot,
414
+ loadLatestProxyRequestHeaders,
320
415
  persistClaudeRequestTemplate,
321
416
  loadClaudeRequestTemplate,
322
417
  createApiRequestLogger
@@ -73,7 +73,7 @@ function formatRealtimeTime(timestamp = Date.now()) {
73
73
  return new Date(timestamp).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
74
74
  }
75
75
 
76
- function buildSuccessLogPayload({ source, requestId, channel, model, originalModel, redirectedModel, tokens, cost = 0, timestamp = Date.now(), usageMissing = false }) {
76
+ function buildSuccessLogPayload({ source, requestId, channel, model, originalModel, redirectedModel, tokens, cacheUsageAvailable, cost = 0, timestamp = Date.now(), usageMissing = false }) {
77
77
  const normalized = normalizeUsageTokens(source, tokens);
78
78
  const payload = {
79
79
  type: 'log', status: 'success', id: requestId, time: formatRealtimeTime(timestamp), channel,
@@ -82,6 +82,7 @@ function buildSuccessLogPayload({ source, requestId, channel, model, originalMod
82
82
  reasoningTokens: normalized.reasoning, totalTokens: normalized.total, cost,
83
83
  source: normalizeToolSource(source), timestamp, usageMissing: Boolean(usageMissing)
84
84
  };
85
+ if (typeof cacheUsageAvailable === 'boolean') payload.cacheUsageAvailable = cacheUsageAvailable;
85
86
  if (originalModel) payload.originalModel = originalModel;
86
87
  if (redirectedModel) payload.redirectedModel = redirectedModel;
87
88
  return payload;