@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/web/assets/{ConfigTemplates-ZrK_s7ma.js → ConfigTemplates-DvcbKKdS.js} +1 -1
  3. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  4. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  5. package/dist/web/assets/{PluginManager-BD7QUZbU.js → PluginManager-jy_4GVxI.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DRb1DuHV.js → ProjectList-Df1-NcNr.js} +1 -1
  7. package/dist/web/assets/{SessionList-lZ0LKzfT.js → SessionList-UWcZtC2r.js} +1 -1
  8. package/dist/web/assets/{SkillManager-C1xG5B4Q.js → SkillManager-IRdseMKB.js} +1 -1
  9. package/dist/web/assets/{Terminal-DksBo_lM.js → Terminal-BasTyDut.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Burx7XOo.js → WorkspaceManager-D-D2kK1V.js} +1 -1
  11. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  12. package/dist/web/assets/index-CryrSLv8.js +2 -0
  13. package/dist/web/index.html +2 -2
  14. package/package.json +1 -1
  15. package/src/config/default.js +2 -0
  16. package/src/config/model-metadata.js +415 -0
  17. package/src/config/model-pricing.js +23 -93
  18. package/src/server/api/opencode-channels.js +84 -6
  19. package/src/server/api/opencode-proxy.js +41 -32
  20. package/src/server/api/opencode-sessions.js +4 -62
  21. package/src/server/api/settings.js +111 -0
  22. package/src/server/codex-proxy-server.js +6 -4
  23. package/src/server/gemini-proxy-server.js +6 -4
  24. package/src/server/index.js +13 -4
  25. package/src/server/opencode-proxy-server.js +1197 -86
  26. package/src/server/proxy-server.js +6 -4
  27. package/src/server/services/codex-sessions.js +105 -6
  28. package/src/server/services/env-checker.js +24 -1
  29. package/src/server/services/env-manager.js +29 -1
  30. package/src/server/services/opencode-channels.js +3 -1
  31. package/src/server/services/opencode-sessions.js +486 -218
  32. package/src/server/services/opencode-settings-manager.js +172 -36
  33. package/src/server/services/response-decoder.js +21 -0
  34. package/src/server/websocket-server.js +24 -5
  35. package/dist/web/assets/Home-B8YfhZ3c.js +0 -1
  36. package/dist/web/assets/Home-Di2qsylF.css +0 -1
  37. package/dist/web/assets/index-Ufv5rCa5.css +0 -1
  38. 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 { clearOpenCodeRedirectCache } = require('../opencode-proxy-server');
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
- const detectedModels = await collectProxyModelList(enabledChannels, { forceRefresh: false });
134
- if (Array.isArray(detectedModels)) {
135
- detectedModels.forEach(modelId => pushUniqueModel(allModels, seen, modelId));
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 activeModel = currentChannel.model || currentChannel.speedTestModel || allModels[0] || null;
142
- setProxyConfig(proxyResult.port, { model: activeModel, models: allModels });
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
- proxyRes.on('data', (chunk) => {
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
- proxyRes.on('end', () => {
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
- proxyRes.on('error', (err) => {
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
- proxyRes.on('data', (chunk) => {
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
- proxyRes.on('end', () => {
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
- proxyRes.on('error', (err) => {
472
+ parsedStream.on('error', (err) => {
471
473
  // 忽略代理响应错误(可能是网络问题)
472
474
  if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
473
475
  console.error('Proxy response error:', err);
@@ -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 });