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.
- package/README.md +1 -1
- package/dist/web/assets/{AgentsDrawer-CobzHziM.css → AgentsDrawer-Dubq1kQI.css} +1 -1
- package/dist/web/assets/AgentsDrawer-x29vGIIi.js +1 -0
- package/dist/web/assets/{Analytics-DTIvvMXu.js → Analytics-BJbR6XJO.js} +1 -1
- package/dist/web/assets/{AssetPathField-jv-lopQ3.js → AssetPathField-2W-16zgR.js} +1 -1
- package/dist/web/assets/{ChatHistoryDrawer-T4OVnJzM.js → ChatHistoryDrawer-C1URzQ_4.js} +1 -1
- package/dist/web/assets/{CollapseItem-P6BvSxEu.js → CollapseItem-DjkOkbwE.js} +1 -1
- package/dist/web/assets/{CommandsDrawer-DtFJRtow.js → CommandsDrawer-DcnfrHC4.js} +1 -1
- package/dist/web/assets/{ConfigExportDrawer-Di7KXhVS.js → ConfigExportDrawer-CKwG_RGJ.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-VdKE5Kp0.js → ConfigTemplates-C1tNpfzi.js} +1 -1
- package/dist/web/assets/{ConfigTemplatesDrawer-bQvPnUQ0.js → ConfigTemplatesDrawer-gZNPbcUR.js} +1 -1
- package/dist/web/assets/{ConfigTemplatesPanel-ChdyQ3C6.js → ConfigTemplatesPanel-C-Fy53Qo.js} +1 -1
- package/dist/web/assets/{FavoritesDrawer-A25XfI7C.js → FavoritesDrawer-Bp8FYHPk.js} +1 -1
- package/dist/web/assets/GatewayConvertDrawer-CvUkvNQy.css +1 -0
- package/dist/web/assets/GatewayConvertDrawer-j5IJzb2Y.js +1 -0
- package/dist/web/assets/Home-2MAvBnml.js +1 -0
- package/dist/web/assets/Home-BRNWE0Qy.css +1 -0
- package/dist/web/assets/{MarkdownEditor-BXwTqbMo.js → MarkdownEditor-BA_PcLed.js} +1 -1
- package/dist/web/assets/{McpDrawer-DIaiFq4R.js → McpDrawer-DAep5PP_.js} +1 -1
- package/dist/web/assets/{McpFormDrawer-CQgTYAux.js → McpFormDrawer-BRm-hzv1.js} +1 -1
- package/dist/web/assets/{PluginManager-BkGrr21J.js → PluginManager-C2yY8-FP.js} +1 -1
- package/dist/web/assets/{PluginsDrawer-Bq1JkM5r.js → PluginsDrawer-CxKnWzTO.js} +1 -1
- package/dist/web/assets/{PluginsPanel-CzGMK-Ap.css → PluginsPanel-DsmiW9TU.css} +1 -1
- package/dist/web/assets/PluginsPanel-DtbXSoXu.js +1 -0
- package/dist/web/assets/{ProjectList-QIC7-9ic.js → ProjectList-Cb25pa2s.js} +1 -1
- package/dist/web/assets/{PromptsDrawer-BLeUYZWW.js → PromptsDrawer-D39OJVRP.js} +1 -1
- package/dist/web/assets/{RecentSessionsDrawer-yOpScIwI.js → RecentSessionsDrawer-DkCjc4kS.js} +1 -1
- package/dist/web/assets/{SessionCard-DeKRHAzu.js → SessionCard-5Mwig8zy.js} +1 -1
- package/dist/web/assets/{SessionList-B0fPT_VB.js → SessionList-DVkIlraF.js} +1 -1
- package/dist/web/assets/{SettingsDrawer-DpZoDj3M.js → SettingsDrawer-Cr9qZDgO.js} +1 -1
- package/dist/web/assets/{SkillManager-BpPqV2o4.js → SkillManager-D935sPSV.js} +1 -1
- package/dist/web/assets/{SkillsDrawer-BgMlJbgS.js → SkillsDrawer-D1_3j74b.js} +1 -1
- package/dist/web/assets/{SkillsPanel-CK-pSSHU.js → SkillsPanel-6SehnNG8.js} +1 -1
- package/dist/web/assets/{Upload-DtuQ7ady.js → Upload-38eKB6wv.js} +1 -1
- package/dist/web/assets/{WorkspaceDrawer-Bu5mOGsF.js → WorkspaceDrawer-Bkfmlr5M.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-D1mhnNV-.js → WorkspaceManager-DFVQ5YoE.js} +1 -1
- package/dist/web/assets/{WorkspacePanel-BIrkMC_5.js → WorkspacePanel-CEnogXEv.js} +1 -1
- package/dist/web/assets/client-oU6RT-6I.js +1 -0
- package/dist/web/assets/dsh-agent-presets-DjJRVMT6.js +1 -0
- package/dist/web/assets/index-CCATqHAD.js +2 -0
- package/dist/web/assets/index-p-yggtLj.css +1 -0
- package/dist/web/assets/{platforms-DrzAtnjY.js → platforms-DILqyP72.js} +1 -1
- package/dist/web/assets/platforms-JV36QBCw.js +1 -0
- package/dist/web/assets/{project-config-BVt5s-pH.js → project-config-CzFKBw_m.js} +1 -1
- package/dist/web/assets/{sessions-CaWUaysM.js → sessions-WXODMvLY.js} +1 -1
- package/dist/web/assets/{sessions-53-Fr68H.js → sessions-wvj5oaYr.js} +1 -1
- package/dist/web/assets/{useResponsiveDrawer-C-UacYVP.js → useResponsiveDrawer-BWRBXImg.js} +1 -1
- package/dist/web/assets/{workspaces-DQuZd4qw.js → workspaces-D2yLnT-B.js} +1 -1
- package/dist/web/index.html +5 -5
- package/package.json +4 -4
- package/src/config/model-metadata.json +143 -7
- package/src/platforms/drivers/dsh/api-operations.js +60 -2
- package/src/platforms/drivers/dsh/common.js +222 -11
- package/src/platforms/drivers/dsh/native-config.js +2 -2
- package/src/platforms/drivers/dsh/native-logs.js +16 -1
- package/src/platforms/drivers/dsh/plugin-manager.js +64 -6
- package/src/platforms/drivers/omp/gateway.js +27 -1
- package/src/platforms/drivers/opencode/conversion.js +60 -8
- package/src/platforms/manifest-schema.js +4 -0
- package/src/platforms/manifests/dsh.json +29 -4
- package/src/platforms/manifests/omp.json +2 -1
- package/src/platforms/registry.js +1 -0
- package/src/server/api/convert.js +26 -76
- package/src/server/api/platforms.js +1 -0
- package/src/server/services/model-detector.js +13 -22
- package/src/server/services/native-log-observer.js +1 -0
- package/src/server/services/plugins-service.js +13 -14
- package/src/server/services/request-logger.js +95 -0
- package/src/server/services/usage-log-utils.js +2 -1
- package/dist/web/assets/AgentsDrawer-MeGfH4I0.js +0 -1
- package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +0 -1
- package/dist/web/assets/GatewayConvertDrawer-TSJPvY4f.js +0 -1
- package/dist/web/assets/Home-CSuFuJ9Y.css +0 -1
- package/dist/web/assets/Home-CzB85lKE.js +0 -1
- package/dist/web/assets/PluginsPanel-Clz1r67v.js +0 -1
- package/dist/web/assets/client-BgcNg4Bt.js +0 -1
- package/dist/web/assets/index-B86H0_5y.css +0 -1
- package/dist/web/assets/index-Bw6YXbBA.js +0 -2
- 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
|
|
16
|
-
defaultTargetApi:
|
|
17
|
-
endpoints:
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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":
|
|
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":
|
|
42
|
+
"repositories": false,
|
|
42
43
|
"market": false,
|
|
43
|
-
"install":
|
|
44
|
-
"uninstall":
|
|
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",
|
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
*
|
|
33
|
-
* GET /api/convert/
|
|
20
|
+
* Read conversion capabilities for a target CLI.
|
|
21
|
+
* GET /api/convert/:target/formats
|
|
34
22
|
*/
|
|
35
|
-
router.get('/
|
|
36
|
-
const driver = getConversionDriver();
|
|
37
|
-
const formats =
|
|
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:
|
|
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
|
-
*
|
|
96
|
-
* POST /api/convert
|
|
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('
|
|
48
|
+
router.post('/:target', (req, res) => {
|
|
100
49
|
try {
|
|
101
50
|
const { sourceType, payload, options = {} } = req.body || {};
|
|
102
|
-
const driver = getConversionDriver();
|
|
103
|
-
const
|
|
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(
|
|
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
|
|
@@ -330,32 +330,29 @@ function buildRequestHeaders(channelType, channel) {
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
function buildOpenAiCompatibleUrl(baseUrl, endpoint) {
|
|
333
|
-
const
|
|
334
|
-
if (!
|
|
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 (
|
|
351
|
-
|
|
352
|
-
}
|
|
347
|
+
if (basePath.endsWith(normalizedEndpoint)) return parsed.toString();
|
|
348
|
+
if (basePath.endsWith(endpointWithoutV1)) return parsed.toString();
|
|
353
349
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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;
|