@adversity/coding-tool-x 3.1.1 → 3.1.2
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/CHANGELOG.md +24 -0
- package/dist/web/assets/{ConfigTemplates-ZrK_s7ma.js → ConfigTemplates-DvcbKKdS.js} +1 -1
- package/dist/web/assets/Home-BJKPCBuk.css +1 -0
- package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
- package/dist/web/assets/{PluginManager-BD7QUZbU.js → PluginManager-jy_4GVxI.js} +1 -1
- package/dist/web/assets/{ProjectList-DRb1DuHV.js → ProjectList-Df1-NcNr.js} +1 -1
- package/dist/web/assets/{SessionList-lZ0LKzfT.js → SessionList-UWcZtC2r.js} +1 -1
- package/dist/web/assets/{SkillManager-C1xG5B4Q.js → SkillManager-IRdseMKB.js} +1 -1
- package/dist/web/assets/{Terminal-DksBo_lM.js → Terminal-BasTyDut.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Burx7XOo.js → WorkspaceManager-D-D2kK1V.js} +1 -1
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/config/default.js +2 -0
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/server/api/opencode-channels.js +84 -6
- package/src/server/api/opencode-proxy.js +41 -32
- package/src/server/api/opencode-sessions.js +4 -62
- package/src/server/api/settings.js +111 -0
- package/src/server/codex-proxy-server.js +6 -4
- package/src/server/gemini-proxy-server.js +6 -4
- package/src/server/index.js +13 -4
- package/src/server/opencode-proxy-server.js +1197 -86
- package/src/server/proxy-server.js +6 -4
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/env-checker.js +24 -1
- package/src/server/services/env-manager.js +29 -1
- package/src/server/services/opencode-channels.js +3 -1
- package/src/server/services/opencode-sessions.js +486 -218
- package/src/server/services/opencode-settings-manager.js +172 -36
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/websocket-server.js +24 -5
- package/dist/web/assets/Home-B8YfhZ3c.js +0 -1
- package/dist/web/assets/Home-Di2qsylF.css +0 -1
- package/dist/web/assets/index-Ufv5rCa5.css +0 -1
- package/dist/web/assets/index-lAkrRC3h.js +0 -2
|
@@ -16,10 +16,16 @@ const {
|
|
|
16
16
|
sanitizeBatchConcurrency,
|
|
17
17
|
runWithConcurrencyLimit
|
|
18
18
|
} = require('../services/speed-test');
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
clearOpenCodeRedirectCache,
|
|
21
|
+
collectProxyModelList,
|
|
22
|
+
getOpenCodeProxyStatus
|
|
23
|
+
} = require('../opencode-proxy-server');
|
|
24
|
+
const { setProxyConfig } = require('../services/opencode-settings-manager');
|
|
20
25
|
const {
|
|
21
26
|
fetchModelsFromProvider,
|
|
22
|
-
probeModelAvailability
|
|
27
|
+
probeModelAvailability,
|
|
28
|
+
clearCache
|
|
23
29
|
} = require('../services/model-detector');
|
|
24
30
|
|
|
25
31
|
module.exports = (config) => {
|
|
@@ -79,6 +85,71 @@ module.exports = (config) => {
|
|
|
79
85
|
return presetId === 'entry_claude' || presetId === 'entry_codex' || presetId === 'entry_gemini';
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
function refreshEditedChannelModelCache(channelId) {
|
|
89
|
+
if (!channelId) return;
|
|
90
|
+
clearCache(channelId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function syncOpenCodeProxyConfigByCache() {
|
|
94
|
+
const proxyStatus = getOpenCodeProxyStatus();
|
|
95
|
+
if (!proxyStatus?.running || !Number.isFinite(proxyStatus?.port)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const channels = getChannels().channels || [];
|
|
100
|
+
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
101
|
+
|
|
102
|
+
// Collect per-channel model lists for per-channel provider generation
|
|
103
|
+
let detectedModels = [];
|
|
104
|
+
try {
|
|
105
|
+
detectedModels = await collectProxyModelList(enabledChannels, { useCacheOnly: true }) || [];
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.warn('[OpenCode Channels API] Failed to collect cached models while syncing proxy config:', error.message);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const channelPayloads = enabledChannels.map((ch) => {
|
|
111
|
+
let models;
|
|
112
|
+
if (Array.isArray(ch.allowedModels) && ch.allowedModels.length > 0) {
|
|
113
|
+
// User explicitly selected models for this channel
|
|
114
|
+
models = ch.allowedModels;
|
|
115
|
+
} else {
|
|
116
|
+
// Fall back to configured + detected models
|
|
117
|
+
models = uniqueModels([
|
|
118
|
+
ch.model,
|
|
119
|
+
ch.speedTestModel,
|
|
120
|
+
...(Array.isArray(ch.modelRedirects)
|
|
121
|
+
? ch.modelRedirects.flatMap(r => [r?.from, r?.to])
|
|
122
|
+
: []),
|
|
123
|
+
...detectedModels
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
name: ch.name,
|
|
128
|
+
providerKey: ch.providerKey || ch.name,
|
|
129
|
+
model: ch.model || null,
|
|
130
|
+
models
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const currentChannel = enabledChannels[0];
|
|
135
|
+
const activeModel = currentChannel?.model || currentChannel?.speedTestModel || null;
|
|
136
|
+
setProxyConfig(proxyStatus.port, { channels: channelPayloads, model: activeModel });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function refreshEditedChannelAndSyncProxy(channelId) {
|
|
140
|
+
try {
|
|
141
|
+
await refreshEditedChannelModelCache(channelId);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.warn('[OpenCode Channels API] Refresh edited channel model cache failed:', error.message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await syncOpenCodeProxyConfigByCache();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.warn('[OpenCode Channels API] Sync proxy config after channel edit failed:', error.message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
82
153
|
/**
|
|
83
154
|
* GET /api/opencode/channels
|
|
84
155
|
* 获取所有 OpenCode 渠道
|
|
@@ -218,7 +289,7 @@ module.exports = (config) => {
|
|
|
218
289
|
* POST /api/opencode/channels
|
|
219
290
|
* 创建新渠道
|
|
220
291
|
*/
|
|
221
|
-
router.post('/', (req, res) => {
|
|
292
|
+
router.post('/', async (req, res) => {
|
|
222
293
|
try {
|
|
223
294
|
const {
|
|
224
295
|
name,
|
|
@@ -233,7 +304,8 @@ module.exports = (config) => {
|
|
|
233
304
|
modelRedirects,
|
|
234
305
|
speedTestModel,
|
|
235
306
|
presetId,
|
|
236
|
-
websiteUrl
|
|
307
|
+
websiteUrl,
|
|
308
|
+
allowedModels
|
|
237
309
|
} = req.body;
|
|
238
310
|
|
|
239
311
|
if (!name || !baseUrl) {
|
|
@@ -254,9 +326,12 @@ module.exports = (config) => {
|
|
|
254
326
|
modelRedirects: modelRedirects || [],
|
|
255
327
|
speedTestModel: speedTestModel || null,
|
|
256
328
|
presetId,
|
|
257
|
-
websiteUrl
|
|
329
|
+
websiteUrl,
|
|
330
|
+
allowedModels: allowedModels || []
|
|
258
331
|
});
|
|
259
332
|
|
|
333
|
+
clearOpenCodeRedirectCache(channel.id);
|
|
334
|
+
await refreshEditedChannelAndSyncProxy(channel.id);
|
|
260
335
|
res.json(channel);
|
|
261
336
|
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
262
337
|
} catch (err) {
|
|
@@ -269,13 +344,14 @@ module.exports = (config) => {
|
|
|
269
344
|
* PUT /api/opencode/channels/:channelId
|
|
270
345
|
* 更新渠道
|
|
271
346
|
*/
|
|
272
|
-
router.put('/:channelId', (req, res) => {
|
|
347
|
+
router.put('/:channelId', async (req, res) => {
|
|
273
348
|
try {
|
|
274
349
|
const { channelId } = req.params;
|
|
275
350
|
const updates = req.body;
|
|
276
351
|
|
|
277
352
|
const channel = updateChannel(channelId, updates);
|
|
278
353
|
clearOpenCodeRedirectCache(channelId);
|
|
354
|
+
await refreshEditedChannelAndSyncProxy(channelId);
|
|
279
355
|
res.json(channel);
|
|
280
356
|
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
281
357
|
} catch (err) {
|
|
@@ -292,6 +368,8 @@ module.exports = (config) => {
|
|
|
292
368
|
try {
|
|
293
369
|
const { channelId } = req.params;
|
|
294
370
|
const result = await deleteChannel(channelId);
|
|
371
|
+
clearOpenCodeRedirectCache(channelId);
|
|
372
|
+
await refreshEditedChannelAndSyncProxy(channelId);
|
|
295
373
|
res.json(result);
|
|
296
374
|
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
297
375
|
} catch (err) {
|
|
@@ -19,16 +19,6 @@ const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
|
|
22
|
-
function pushUniqueModel(allModels, seen, modelId) {
|
|
23
|
-
if (typeof modelId !== 'string') return;
|
|
24
|
-
const trimmed = modelId.trim();
|
|
25
|
-
if (!trimmed) return;
|
|
26
|
-
const key = trimmed.toLowerCase();
|
|
27
|
-
if (seen.has(key)) return;
|
|
28
|
-
seen.add(key);
|
|
29
|
-
allModels.push(trimmed);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
22
|
function sanitizeChannel(channel) {
|
|
33
23
|
if (!channel) return null;
|
|
34
24
|
return {
|
|
@@ -111,35 +101,54 @@ router.post('/start', async (req, res) => {
|
|
|
111
101
|
}
|
|
112
102
|
|
|
113
103
|
// 4. 设置代理配置(写入 OpenCode 配置文件)
|
|
114
|
-
//
|
|
115
|
-
const allModels = [];
|
|
116
|
-
const seen = new Set();
|
|
117
|
-
enabledChannels.forEach((ch) => {
|
|
118
|
-
const candidates = [
|
|
119
|
-
ch.model,
|
|
120
|
-
ch.speedTestModel
|
|
121
|
-
];
|
|
122
|
-
if (ch.modelConfig && typeof ch.modelConfig === 'object') {
|
|
123
|
-
candidates.push(ch.modelConfig.model, ch.modelConfig.opusModel, ch.modelConfig.sonnetModel, ch.modelConfig.haikuModel);
|
|
124
|
-
}
|
|
125
|
-
if (Array.isArray(ch.modelRedirects)) {
|
|
126
|
-
ch.modelRedirects.forEach(r => { candidates.push(r && r.from); candidates.push(r && r.to); });
|
|
127
|
-
}
|
|
128
|
-
candidates.forEach((m) => pushUniqueModel(allModels, seen, m));
|
|
129
|
-
});
|
|
104
|
+
// 收集每个渠道的模型列表,生成 per-channel provider 配置
|
|
130
105
|
|
|
131
106
|
// 若渠道未显式填写模型,回退使用代理聚合模型(含 /v1/models 与模型探测结果)。
|
|
107
|
+
let detectedModels = [];
|
|
132
108
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
109
|
+
detectedModels = await collectProxyModelList(enabledChannels, {
|
|
110
|
+
useCacheOnly: true
|
|
111
|
+
}) || [];
|
|
137
112
|
} catch (error) {
|
|
138
113
|
console.warn('[OpenCode Proxy] Failed to collect proxy models before writing config:', error.message);
|
|
139
114
|
}
|
|
140
115
|
|
|
141
|
-
const
|
|
142
|
-
|
|
116
|
+
const channelPayloads = enabledChannels.map((ch) => {
|
|
117
|
+
let models;
|
|
118
|
+
if (Array.isArray(ch.allowedModels) && ch.allowedModels.length > 0) {
|
|
119
|
+
models = ch.allowedModels;
|
|
120
|
+
} else {
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const collected = [];
|
|
123
|
+
const add = (m) => {
|
|
124
|
+
if (typeof m !== 'string') return;
|
|
125
|
+
const t = m.trim();
|
|
126
|
+
if (!t) return;
|
|
127
|
+
const k = t.toLowerCase();
|
|
128
|
+
if (seen.has(k)) return;
|
|
129
|
+
seen.add(k);
|
|
130
|
+
collected.push(t);
|
|
131
|
+
};
|
|
132
|
+
[ch.model, ch.speedTestModel].forEach(add);
|
|
133
|
+
if (ch.modelConfig && typeof ch.modelConfig === 'object') {
|
|
134
|
+
[ch.modelConfig.model, ch.modelConfig.opusModel, ch.modelConfig.sonnetModel, ch.modelConfig.haikuModel].forEach(add);
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(ch.modelRedirects)) {
|
|
137
|
+
ch.modelRedirects.forEach(r => { add(r && r.from); add(r && r.to); });
|
|
138
|
+
}
|
|
139
|
+
detectedModels.forEach(add);
|
|
140
|
+
models = collected;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
name: ch.name,
|
|
144
|
+
providerKey: ch.providerKey || ch.name,
|
|
145
|
+
model: ch.model || null,
|
|
146
|
+
models
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const activeModel = currentChannel.model || currentChannel.speedTestModel || null;
|
|
151
|
+
setProxyConfig(proxyResult.port, { channels: channelPayloads, model: activeModel });
|
|
143
152
|
|
|
144
153
|
// 5. 广播状态更新
|
|
145
154
|
const { broadcastProxyState } = require('../websocket-server');
|
|
@@ -4,6 +4,7 @@ const {
|
|
|
4
4
|
getProjects,
|
|
5
5
|
getSessionsByProject,
|
|
6
6
|
getSessionById,
|
|
7
|
+
getSessionMessages,
|
|
7
8
|
getRecentSessions,
|
|
8
9
|
searchSessions,
|
|
9
10
|
deleteSession,
|
|
@@ -14,8 +15,6 @@ const {
|
|
|
14
15
|
const { loadAliases } = require('../services/alias');
|
|
15
16
|
const { getTerminalLaunchCommand } = require('../services/terminal-config');
|
|
16
17
|
const { broadcastLog } = require('../websocket-server');
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
const path = require('path');
|
|
19
18
|
const os = require('os');
|
|
20
19
|
|
|
21
20
|
function isNotFoundError(error) {
|
|
@@ -156,7 +155,6 @@ module.exports = (config) => {
|
|
|
156
155
|
/**
|
|
157
156
|
* GET /api/opencode/sessions/:projectName/:sessionId/messages
|
|
158
157
|
* 获取会话的消息列表
|
|
159
|
-
* Note: OpenCode 的消息存储在单独的 message 目录,暂时返回基本信息
|
|
160
158
|
*/
|
|
161
159
|
router.get('/:projectName/:sessionId/messages', (req, res) => {
|
|
162
160
|
try {
|
|
@@ -167,66 +165,10 @@ module.exports = (config) => {
|
|
|
167
165
|
const { sessionId } = req.params;
|
|
168
166
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
169
167
|
const session = getSessionById(sessionId);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const messagesDir = path.join(
|
|
173
|
-
os.homedir(), '.local', 'share', 'opencode', 'storage', 'message', sessionId
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const convertedMessages = [];
|
|
177
|
-
|
|
178
|
-
if (fs.existsSync(messagesDir)) {
|
|
179
|
-
const files = fs.readdirSync(messagesDir)
|
|
180
|
-
.filter(f => f.endsWith('.json'))
|
|
181
|
-
.sort();
|
|
182
|
-
|
|
183
|
-
for (const file of files) {
|
|
184
|
-
try {
|
|
185
|
-
const content = fs.readFileSync(path.join(messagesDir, file), 'utf8');
|
|
186
|
-
const msg = JSON.parse(content);
|
|
187
|
-
|
|
188
|
-
if (msg.role === 'user') {
|
|
189
|
-
// 提取用户消息内容
|
|
190
|
-
let textContent = '';
|
|
191
|
-
if (Array.isArray(msg.content)) {
|
|
192
|
-
textContent = msg.content
|
|
193
|
-
.filter(c => c.type === 'text')
|
|
194
|
-
.map(c => c.text || '')
|
|
195
|
-
.join('\n');
|
|
196
|
-
} else if (typeof msg.content === 'string') {
|
|
197
|
-
textContent = msg.content;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
convertedMessages.push({
|
|
201
|
-
type: 'user',
|
|
202
|
-
content: textContent || '[空消息]',
|
|
203
|
-
timestamp: msg.time?.created ? new Date(msg.time.created).toISOString() : null,
|
|
204
|
-
model: null
|
|
205
|
-
});
|
|
206
|
-
} else if (msg.role === 'assistant') {
|
|
207
|
-
// 提取助手消息内容
|
|
208
|
-
let textContent = '';
|
|
209
|
-
if (Array.isArray(msg.content)) {
|
|
210
|
-
textContent = msg.content
|
|
211
|
-
.filter(c => c.type === 'text')
|
|
212
|
-
.map(c => c.text || '')
|
|
213
|
-
.join('\n');
|
|
214
|
-
} else if (typeof msg.content === 'string') {
|
|
215
|
-
textContent = msg.content;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
convertedMessages.push({
|
|
219
|
-
type: 'assistant',
|
|
220
|
-
content: textContent || '[空消息]',
|
|
221
|
-
timestamp: msg.time?.created ? new Date(msg.time.created).toISOString() : null,
|
|
222
|
-
model: msg.model || 'opencode'
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
} catch (parseErr) {
|
|
226
|
-
// 忽略解析错误
|
|
227
|
-
}
|
|
228
|
-
}
|
|
168
|
+
if (!session) {
|
|
169
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
229
170
|
}
|
|
171
|
+
const convertedMessages = getSessionMessages(sessionId);
|
|
230
172
|
|
|
231
173
|
// 分页处理
|
|
232
174
|
const pageNum = parseInt(page);
|
|
@@ -2,6 +2,8 @@ const express = require('express');
|
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const { detectAvailableTerminals } = require('../services/terminal-detector');
|
|
4
4
|
const { loadTerminalConfig, saveTerminalConfig, getSelectedTerminal } = require('../services/terminal-config');
|
|
5
|
+
const { MODEL_METADATA, resolveModelMetadata, METADATA_LAST_UPDATED } = require('../../config/model-metadata');
|
|
6
|
+
const { loadConfig, saveConfig } = require('../../config/loader');
|
|
5
7
|
|
|
6
8
|
// GET /api/settings/terminals - 获取可用终端列表
|
|
7
9
|
router.get('/terminals', (req, res) => {
|
|
@@ -58,4 +60,113 @@ router.post('/terminal-config', (req, res) => {
|
|
|
58
60
|
}
|
|
59
61
|
});
|
|
60
62
|
|
|
63
|
+
// GET /api/settings/model-metadata - 获取内置模型元数据表(limit + pricing)
|
|
64
|
+
router.get('/model-metadata', (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
// Return built-in defaults merged with any user overrides
|
|
67
|
+
const config = loadConfig();
|
|
68
|
+
const overrides = config.modelMetadataOverrides || {};
|
|
69
|
+
|
|
70
|
+
// Build merged table: built-in + user overrides
|
|
71
|
+
const merged = {};
|
|
72
|
+
for (const [id, meta] of Object.entries(MODEL_METADATA)) {
|
|
73
|
+
merged[id] = overrides[id]
|
|
74
|
+
? {
|
|
75
|
+
limit: { ...meta.limit, ...(overrides[id].limit || {}) },
|
|
76
|
+
pricing: { ...meta.pricing, ...(overrides[id].pricing || {}) }
|
|
77
|
+
}
|
|
78
|
+
: meta;
|
|
79
|
+
}
|
|
80
|
+
// Also include any user-added custom models from overrides
|
|
81
|
+
for (const [id, meta] of Object.entries(overrides)) {
|
|
82
|
+
if (!merged[id]) {
|
|
83
|
+
merged[id] = meta;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
res.json({
|
|
88
|
+
models: merged,
|
|
89
|
+
overrides,
|
|
90
|
+
lastUpdated: METADATA_LAST_UPDATED
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Error getting model metadata:', error);
|
|
94
|
+
res.status(500).json({ error: error.message });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// POST /api/settings/model-metadata - 保存模型元数据覆盖项
|
|
99
|
+
// Body: { overrides: { [modelId]: { limit?: {...}, pricing?: {...} } } }
|
|
100
|
+
router.post('/model-metadata', (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const { overrides } = req.body;
|
|
103
|
+
if (!overrides || typeof overrides !== 'object') {
|
|
104
|
+
return res.status(400).json({ error: 'overrides must be an object' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate each override entry
|
|
108
|
+
for (const [modelId, meta] of Object.entries(overrides)) {
|
|
109
|
+
if (typeof modelId !== 'string' || !modelId.trim()) {
|
|
110
|
+
return res.status(400).json({ error: `Invalid model ID: "${modelId}"` });
|
|
111
|
+
}
|
|
112
|
+
if (meta.limit !== undefined) {
|
|
113
|
+
if (typeof meta.limit !== 'object') {
|
|
114
|
+
return res.status(400).json({ error: `${modelId}: limit must be an object` });
|
|
115
|
+
}
|
|
116
|
+
if (meta.limit.context !== undefined && (typeof meta.limit.context !== 'number' || meta.limit.context <= 0)) {
|
|
117
|
+
return res.status(400).json({ error: `${modelId}: limit.context must be a positive number` });
|
|
118
|
+
}
|
|
119
|
+
if (meta.limit.output !== undefined && (typeof meta.limit.output !== 'number' || meta.limit.output <= 0)) {
|
|
120
|
+
return res.status(400).json({ error: `${modelId}: limit.output must be a positive number` });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (meta.pricing !== undefined) {
|
|
124
|
+
if (typeof meta.pricing !== 'object') {
|
|
125
|
+
return res.status(400).json({ error: `${modelId}: pricing must be an object` });
|
|
126
|
+
}
|
|
127
|
+
for (const field of ['input', 'output']) {
|
|
128
|
+
if (meta.pricing[field] !== undefined && (typeof meta.pricing[field] !== 'number' || meta.pricing[field] < 0)) {
|
|
129
|
+
return res.status(400).json({ error: `${modelId}: pricing.${field} must be a non-negative number` });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const config = loadConfig();
|
|
136
|
+
const newConfig = {
|
|
137
|
+
...config,
|
|
138
|
+
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
139
|
+
modelMetadataOverrides: overrides
|
|
140
|
+
};
|
|
141
|
+
saveConfig(newConfig);
|
|
142
|
+
|
|
143
|
+
res.json({ success: true, overrides });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Error saving model metadata:', error);
|
|
146
|
+
res.status(500).json({ error: error.message });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// DELETE /api/settings/model-metadata/:modelId - 删除单个模型覆盖项(恢复内置默认值)
|
|
151
|
+
router.delete('/model-metadata/:modelId', (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const modelId = decodeURIComponent(req.params.modelId);
|
|
154
|
+
const config = loadConfig();
|
|
155
|
+
const overrides = { ...(config.modelMetadataOverrides || {}) };
|
|
156
|
+
delete overrides[modelId];
|
|
157
|
+
|
|
158
|
+
const newConfig = {
|
|
159
|
+
...config,
|
|
160
|
+
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
161
|
+
modelMetadataOverrides: overrides
|
|
162
|
+
};
|
|
163
|
+
saveConfig(newConfig);
|
|
164
|
+
|
|
165
|
+
res.json({ success: true, modelId });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Error deleting model metadata override:', error);
|
|
168
|
+
res.status(500).json({ error: error.message });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
61
172
|
module.exports = router;
|
|
@@ -10,6 +10,7 @@ const DEFAULT_CONFIG = require('../config/default');
|
|
|
10
10
|
const { resolvePricing } = require('./utils/pricing');
|
|
11
11
|
const { recordRequest: recordCodexRequest } = require('./services/codex-statistics-service');
|
|
12
12
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
13
|
+
const { createDecodedStream } = require('./services/response-decoder');
|
|
13
14
|
const { getEnabledChannels, writeCodexConfigForMultiChannel, getEffectiveApiKey } = require('./services/codex-channels');
|
|
14
15
|
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
15
16
|
|
|
@@ -402,14 +403,15 @@ async function startCodexProxyServer(options = {}) {
|
|
|
402
403
|
totalTokens: 0,
|
|
403
404
|
model: ''
|
|
404
405
|
};
|
|
406
|
+
const parsedStream = createDecodedStream(proxyRes);
|
|
405
407
|
|
|
406
|
-
|
|
408
|
+
parsedStream.on('data', (chunk) => {
|
|
407
409
|
// 如果响应已关闭,停止处理
|
|
408
410
|
if (isResponseClosed) {
|
|
409
411
|
return;
|
|
410
412
|
}
|
|
411
413
|
|
|
412
|
-
buffer += chunk.toString();
|
|
414
|
+
buffer += chunk.toString('utf8');
|
|
413
415
|
|
|
414
416
|
// 检查是否是 SSE 流
|
|
415
417
|
if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
|
|
@@ -475,7 +477,7 @@ async function startCodexProxyServer(options = {}) {
|
|
|
475
477
|
}
|
|
476
478
|
});
|
|
477
479
|
|
|
478
|
-
|
|
480
|
+
parsedStream.on('end', () => {
|
|
479
481
|
// 如果不是流式响应,尝试从完整响应中解析
|
|
480
482
|
if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
|
|
481
483
|
try {
|
|
@@ -558,7 +560,7 @@ async function startCodexProxyServer(options = {}) {
|
|
|
558
560
|
}
|
|
559
561
|
});
|
|
560
562
|
|
|
561
|
-
|
|
563
|
+
parsedStream.on('error', (err) => {
|
|
562
564
|
// 忽略代理响应错误(可能是网络问题)
|
|
563
565
|
if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
|
|
564
566
|
console.error('Proxy response error:', err);
|
|
@@ -10,6 +10,7 @@ const DEFAULT_CONFIG = require('../config/default');
|
|
|
10
10
|
const { resolvePricing } = require('./utils/pricing');
|
|
11
11
|
const { recordRequest: recordGeminiRequest } = require('./services/gemini-statistics-service');
|
|
12
12
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
13
|
+
const { createDecodedStream } = require('./services/response-decoder');
|
|
13
14
|
const { getEffectiveApiKey } = require('./services/gemini-channels');
|
|
14
15
|
|
|
15
16
|
let proxyServer = null;
|
|
@@ -289,14 +290,15 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
289
290
|
totalTokens: 0,
|
|
290
291
|
model: ''
|
|
291
292
|
};
|
|
293
|
+
const parsedStream = createDecodedStream(proxyRes);
|
|
292
294
|
|
|
293
|
-
|
|
295
|
+
parsedStream.on('data', (chunk) => {
|
|
294
296
|
// 如果响应已关闭,停止处理
|
|
295
297
|
if (isResponseClosed) {
|
|
296
298
|
return;
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
buffer += chunk.toString();
|
|
301
|
+
buffer += chunk.toString('utf8');
|
|
300
302
|
|
|
301
303
|
// 检查是否是 SSE 流
|
|
302
304
|
if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
|
|
@@ -360,7 +362,7 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
360
362
|
}
|
|
361
363
|
});
|
|
362
364
|
|
|
363
|
-
|
|
365
|
+
parsedStream.on('end', () => {
|
|
364
366
|
// 如果不是流式响应,尝试从完整响应中解析
|
|
365
367
|
if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
|
|
366
368
|
try {
|
|
@@ -467,7 +469,7 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
467
469
|
}
|
|
468
470
|
});
|
|
469
471
|
|
|
470
|
-
|
|
472
|
+
parsedStream.on('error', (err) => {
|
|
471
473
|
// 忽略代理响应错误(可能是网络问题)
|
|
472
474
|
if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
|
|
473
475
|
console.error('Proxy response error:', err);
|
package/src/server/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const { setProxyConfig: setOpenCodeProxyConfig } = require('./services/opencode-
|
|
|
15
15
|
const { startProxyServer } = require('./proxy-server');
|
|
16
16
|
const { startCodexProxyServer } = require('./codex-proxy-server');
|
|
17
17
|
const { startGeminiProxyServer } = require('./gemini-proxy-server');
|
|
18
|
-
const { startOpenCodeProxyServer } = require('./opencode-proxy-server');
|
|
18
|
+
const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
|
|
19
19
|
const { createRemoteMutationGuard, createRemoteRouteGuard } = require('./services/network-access');
|
|
20
20
|
|
|
21
21
|
function isInteractivePortConflictMode(options = {}) {
|
|
@@ -269,8 +269,8 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
269
269
|
// 自动恢复代理状态
|
|
270
270
|
autoRestoreProxies();
|
|
271
271
|
|
|
272
|
-
//
|
|
273
|
-
performStartupHealthCheck();
|
|
272
|
+
// 延迟执行健康检查,避免阻塞启动
|
|
273
|
+
setTimeout(() => performStartupHealthCheck(), 2000);
|
|
274
274
|
|
|
275
275
|
return server;
|
|
276
276
|
}
|
|
@@ -349,7 +349,7 @@ function autoRestoreProxies() {
|
|
|
349
349
|
console.log(chalk.cyan('\n🔄 检测到 OpenCode 代理状态文件,正在自动启动...'));
|
|
350
350
|
const opencodeProxyPort = config.ports?.opencodeProxy || 20091;
|
|
351
351
|
startOpenCodeProxyServer(opencodeProxyPort)
|
|
352
|
-
.then((result) => {
|
|
352
|
+
.then(async (result) => {
|
|
353
353
|
if (result.success) {
|
|
354
354
|
console.log(chalk.green(`✅ OpenCode 代理已自动启动,端口: ${result.port}`));
|
|
355
355
|
try {
|
|
@@ -365,6 +365,15 @@ function autoRestoreProxies() {
|
|
|
365
365
|
}
|
|
366
366
|
});
|
|
367
367
|
});
|
|
368
|
+
const detectedModels = await collectProxyModelList(enabledChs, { useCacheOnly: true });
|
|
369
|
+
if (Array.isArray(detectedModels)) {
|
|
370
|
+
detectedModels.forEach((m) => {
|
|
371
|
+
if (typeof m === 'string' && m.trim() && !seen.has(m.trim().toLowerCase())) {
|
|
372
|
+
seen.add(m.trim().toLowerCase());
|
|
373
|
+
allModels.push(m.trim());
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
368
377
|
const firstChannel = enabledChs[0];
|
|
369
378
|
const activeModel = firstChannel && (firstChannel.model || firstChannel.speedTestModel) || null;
|
|
370
379
|
const cfgResult = setOpenCodeProxyConfig(result.port, { model: activeModel, models: allModels });
|