coding-tool-x 3.2.2 → 3.3.1

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 (30) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/web/assets/{Analytics-COVBIlMT.js → Analytics-BskCbia_.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-CwCbgetE.js → ConfigTemplates-B4X3rgfY.js} +1 -1
  4. package/dist/web/assets/{Home-CgMMTGxS.js → Home-DHYMMKOU.js} +1 -1
  5. package/dist/web/assets/{PluginManager-DQ4B002M.js → PluginManager-D_LoULGH.js} +1 -1
  6. package/dist/web/assets/{ProjectList-BT99XzrL.js → ProjectList-DiV4Qwa1.js} +1 -1
  7. package/dist/web/assets/{SessionList-ButOecT4.js → SessionList-B24o0wiX.js} +1 -1
  8. package/dist/web/assets/{SkillManager-e2C5kuhp.js → SkillManager-B9Rnuaig.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-Dh5Rzjkr.js → WorkspaceManager-BkL2l5J9.js} +1 -1
  10. package/dist/web/assets/icons-B29onFfZ.js +1 -0
  11. package/dist/web/assets/index-C5j22icm.css +1 -0
  12. package/dist/web/assets/index-ZttxvTKw.js +2 -0
  13. package/dist/web/assets/{naive-ui-DlpKk-8M.js → naive-ui-CxpuzdjU.js} +1 -1
  14. package/dist/web/index.html +4 -4
  15. package/package.json +1 -1
  16. package/src/server/api/opencode-channels.js +30 -2
  17. package/src/server/opencode-proxy-server.js +16 -116
  18. package/src/server/proxy-server.js +2 -10
  19. package/src/server/services/channels.js +7 -5
  20. package/src/server/services/codex-channels.js +7 -5
  21. package/src/server/services/codex-settings-manager.js +13 -0
  22. package/src/server/services/config-templates-service.js +28 -22
  23. package/src/server/services/gemini-channels.js +7 -5
  24. package/src/server/services/mcp-service.js +22 -1
  25. package/src/server/services/request-logger.js +190 -0
  26. package/src/server/services/speed-test.js +17 -108
  27. package/src/utils/port-helper.js +26 -5
  28. package/dist/web/assets/icons-DRrXwWZi.js +0 -1
  29. package/dist/web/assets/index-CwGg4bbn.css +0 -1
  30. package/dist/web/assets/index-j56-PHWL.js +0 -2
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-j56-PHWL.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-ZttxvTKw.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-C9MYpaSi.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-DET08QYg.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendors-DMjSfzlv.js">
12
- <link rel="modulepreload" crossorigin href="/assets/naive-ui-DlpKk-8M.js">
13
- <link rel="modulepreload" crossorigin href="/assets/icons-DRrXwWZi.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/naive-ui-CxpuzdjU.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/icons-B29onFfZ.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-CwGg4bbn.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-C5j22icm.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.2.2",
3
+ "version": "3.3.1",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -67,7 +67,8 @@ module.exports = (config) => {
67
67
  const value = String(channel?.gatewaySourceType || '').trim().toLowerCase();
68
68
  if (value === 'claude') return 'claude';
69
69
  if (value === 'gemini') return 'gemini';
70
- return 'codex';
70
+ if (value === 'codex') return 'codex';
71
+ return 'openai_compatible';
71
72
  }
72
73
 
73
74
  function mapGatewaySourceTypeToSpeedTestType(channel) {
@@ -130,6 +131,32 @@ module.exports = (config) => {
130
131
  }
131
132
  });
132
133
 
134
+ /**
135
+ * POST /api/opencode/channels/probe-models
136
+ * 用临时配置(新建渠道时)获取模型列表,无需 channelId
137
+ */
138
+ router.post('/probe-models', async (req, res) => {
139
+ try {
140
+ const { baseUrl, apiKey, gatewaySourceType } = req.body;
141
+ if (!baseUrl) {
142
+ return res.status(400).json({ error: 'baseUrl is required' });
143
+ }
144
+ const tempChannel = { baseUrl, apiKey: apiKey || '', gatewaySourceType: gatewaySourceType || 'codex' };
145
+ const gst = resolveGatewaySourceType(tempChannel);
146
+ const listResult = await fetchModelsFromProvider(tempChannel, gst, { useV1ModelsEndpoint: true, forceRefresh: true });
147
+ const listedModels = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
148
+ res.json({
149
+ models: listedModels,
150
+ supported: listedModels.length > 0,
151
+ error: listedModels.length > 0 ? null : (listResult.error || '未返回可用模型列表'),
152
+ errorHint: listedModels.length > 0 ? null : (listResult.errorHint || '请手动填写模型名称')
153
+ });
154
+ } catch (error) {
155
+ console.error('[OpenCode Channels API] Error probing models:', error);
156
+ res.status(500).json({ error: 'Failed to probe models' });
157
+ }
158
+ });
159
+
133
160
  /**
134
161
  * GET /api/opencode/channels/:channelId/models
135
162
  * 获取渠道可用模型列表
@@ -144,9 +171,10 @@ module.exports = (config) => {
144
171
  return res.status(404).json({ error: 'Channel not found' });
145
172
  }
146
173
 
174
+ const forceRefresh = req.query.forceRefresh === 'true';
147
175
  const gatewaySourceType = resolveGatewaySourceType(channel);
148
176
  const preferredModels = collectChannelPreferredModels(channel);
149
- const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
177
+ const listResult = await fetchModelsFromProvider(channel, gatewaySourceType, { useV1ModelsEndpoint: true, forceRefresh });
150
178
  const listedModels = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
151
179
  const shouldProbeByDefault = !!listResult.disabledByConfig;
152
180
  let result;
@@ -18,7 +18,7 @@ const { resolvePricing } = require('./utils/pricing');
18
18
  const { recordRequest: recordOpenCodeRequest } = require('./services/opencode-statistics-service');
19
19
  const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
20
20
  const { getEnabledChannels, getEffectiveApiKey } = require('./services/opencode-channels');
21
- const { persistProxyRequestSnapshot } = require('./services/request-logger');
21
+ const { persistProxyRequestSnapshot, loadClaudeRequestTemplate } = require('./services/request-logger');
22
22
  const { probeModelAvailability, fetchModelsFromProvider } = require('./services/model-detector');
23
23
  const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
24
24
 
@@ -279,69 +279,24 @@ function resolveClaudeAccountIdFromUserId(userId = '') {
279
279
  }
280
280
 
281
281
  function resolveClaudeAccountIdFromLogs() {
282
- const logsPath = path.join(os.homedir(), '.cc-tool', 'claude-requests.jsonl');
283
- if (!fs.existsSync(logsPath)) return '';
284
-
285
282
  try {
286
- const content = fs.readFileSync(logsPath, 'utf8');
287
- const lines = content.trim().split('\n');
288
- const accountIdCount = new Map();
289
-
290
- for (let index = lines.length - 1; index >= 0; index -= 1) {
291
- const line = lines[index].trim();
292
- if (!line) continue;
293
- try {
294
- const parsed = JSON.parse(line);
295
- const userId = parsed?.request?.body?.metadata?.user_id;
296
- const accountId = resolveClaudeAccountIdFromUserId(userId);
297
- if (accountId) {
298
- accountIdCount.set(accountId, (accountIdCount.get(accountId) || 0) + 1);
299
- }
300
- } catch {
301
- // ignore malformed line
302
- }
303
- }
304
-
305
- const ranked = Array.from(accountIdCount.entries())
306
- .filter(([accountId]) => accountId !== '0'.repeat(64))
307
- .sort((left, right) => right[1] - left[1]);
308
-
309
- if (ranked.length > 0) {
310
- return ranked[0][0];
311
- }
283
+ const template = loadClaudeRequestTemplate();
284
+ const userId = normalizeSessionKeyValue(template?.userId || '');
285
+ const accountId = resolveClaudeAccountIdFromUserId(userId);
286
+ return (accountId && accountId !== '0'.repeat(64)) ? accountId : '';
312
287
  } catch {
313
- // ignore read error
288
+ return '';
314
289
  }
315
-
316
- return '';
317
290
  }
318
291
 
319
292
  function resolveClaudeUserIdFromLogs() {
320
- const logsPath = path.join(os.homedir(), '.cc-tool', 'claude-requests.jsonl');
321
- if (!fs.existsSync(logsPath)) return '';
322
-
323
293
  try {
324
- const content = fs.readFileSync(logsPath, 'utf8');
325
- const lines = content.trim().split('\n');
326
- const userIdCount = new Map();
327
-
328
- for (let index = lines.length - 1; index >= 0; index -= 1) {
329
- const line = lines[index].trim();
330
- if (!line) continue;
331
- try {
332
- const parsed = JSON.parse(line);
333
- const userId = normalizeSessionKeyValue(parsed?.request?.body?.metadata?.user_id);
334
- if (!CLAUDE_USER_ID_FULL_RE.test(userId)) continue;
335
- const accountId = resolveClaudeAccountIdFromUserId(userId);
336
- if (!accountId || accountId === '0'.repeat(64)) continue;
337
- userIdCount.set(userId, (userIdCount.get(userId) || 0) + 1);
338
- } catch {
339
- // ignore malformed line
340
- }
341
- }
342
-
343
- const ranked = Array.from(userIdCount.entries()).sort((left, right) => right[1] - left[1]);
344
- return ranked.length > 0 ? ranked[0][0] : '';
294
+ const template = loadClaudeRequestTemplate();
295
+ const userId = normalizeSessionKeyValue(template?.userId || '');
296
+ if (!CLAUDE_USER_ID_FULL_RE.test(userId)) return '';
297
+ const accountId = resolveClaudeAccountIdFromUserId(userId);
298
+ if (!accountId || accountId === '0'.repeat(64)) return '';
299
+ return userId;
345
300
  } catch {
346
301
  return '';
347
302
  }
@@ -540,17 +495,6 @@ function buildClaudeBetaHeader(options = {}) {
540
495
  return betaFlags.join(',');
541
496
  }
542
497
 
543
- function buildDefaultClaudeCodeTools() {
544
- return DEFAULT_CLAUDE_CODE_TOOL_NAMES.map(name => ({
545
- name,
546
- description: `${name} tool`,
547
- input_schema: {
548
- type: 'object',
549
- properties: {},
550
- additionalProperties: true
551
- }
552
- }));
553
- }
554
498
 
555
499
  function hasExpectedClaudeToolSet(tools = []) {
556
500
  if (!Array.isArray(tools) || tools.length < DEFAULT_CLAUDE_CODE_TOOL_NAMES.length) {
@@ -582,52 +526,12 @@ function cloneJson(value) {
582
526
  }
583
527
  }
584
528
 
585
- function loadClaudeRequestTemplateFromLogs() {
586
- const logsPath = path.join(os.homedir(), '.cc-tool', 'claude-requests.jsonl');
587
- if (!fs.existsSync(logsPath)) return null;
588
-
589
- try {
590
- const content = fs.readFileSync(logsPath, 'utf8');
591
- const lines = content.trim().split('\n');
592
- for (let index = lines.length - 1; index >= 0; index -= 1) {
593
- const line = lines[index].trim();
594
- if (!line) continue;
595
- try {
596
- const parsed = JSON.parse(line);
597
- const body = parsed?.request?.body;
598
- if (!body || typeof body !== 'object') continue;
599
-
600
- const userId = normalizeSessionKeyValue(body?.metadata?.user_id);
601
- const accountId = resolveClaudeAccountIdFromUserId(userId);
602
- if (!CLAUDE_USER_ID_FULL_RE.test(userId) || !accountId || accountId === '0'.repeat(64)) continue;
603
-
604
- const tools = Array.isArray(body.tools) ? body.tools : [];
605
- const system = Array.isArray(body.system) ? body.system : [];
606
- if (!hasExpectedClaudeToolSet(tools)) continue;
607
- if (extractClaudeSystemCharCount(system) < CLAUDE_TEMPLATE_SYSTEM_MIN_CHARS) continue;
608
-
609
- return {
610
- userId,
611
- tools: cloneJson(tools),
612
- system: cloneJson(system)
613
- };
614
- } catch {
615
- // ignore malformed line
616
- }
617
- }
618
- } catch {
619
- return null;
620
- }
621
-
622
- return null;
623
- }
624
-
625
529
  function resolveClaudeRequestTemplate() {
626
530
  const now = Date.now();
627
531
  if (cachedClaudeRequestTemplate && now - cachedClaudeRequestTemplateAt < CLAUDE_TEMPLATE_CACHE_TTL_MS) {
628
532
  return cachedClaudeRequestTemplate;
629
533
  }
630
- cachedClaudeRequestTemplate = loadClaudeRequestTemplateFromLogs();
534
+ cachedClaudeRequestTemplate = loadClaudeRequestTemplate();
631
535
  cachedClaudeRequestTemplateAt = now;
632
536
  return cachedClaudeRequestTemplate;
633
537
  }
@@ -1060,14 +964,10 @@ function convertOpenCodePayloadToClaude(pathname, payload = {}, fallbackModel =
1060
964
 
1061
965
  const template = resolveClaudeRequestTemplate();
1062
966
 
1063
- converted.system = buildClaudeSystemBlocks(normalized.system, template?.system || []);
967
+ converted.system = buildClaudeSystemBlocks(normalized.system, template.system);
1064
968
 
1065
969
  const tools = normalizeOpenAiToolsToClaude(payload.tools || []);
1066
- if (tools.length > 0) {
1067
- converted.tools = tools;
1068
- } else {
1069
- converted.tools = template?.tools || buildDefaultClaudeCodeTools();
1070
- }
970
+ converted.tools = tools.length > 0 ? tools : template.tools;
1071
971
 
1072
972
  const toolChoice = normalizeToolChoiceToClaude(payload.tool_choice);
1073
973
  if (toolChoice) {
@@ -1083,7 +983,7 @@ function convertOpenCodePayloadToClaude(pathname, payload = {}, fallbackModel =
1083
983
  }
1084
984
 
1085
985
  // 某些 Claude relay 会校验 metadata.user_id 以识别 Claude Code 请求
1086
- converted.metadata = normalizeClaudeMetadata(payload.metadata, options.sessionUserId || template?.userId || '');
986
+ converted.metadata = normalizeClaudeMetadata(payload.metadata, options.sessionUserId || template.userId || '');
1087
987
 
1088
988
  return converted;
1089
989
  }
@@ -17,7 +17,7 @@ const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRunt
17
17
  const { createDecodedStream } = require('./services/response-decoder');
18
18
  const eventBus = require('../plugins/event-bus');
19
19
  const { getEffectiveApiKey } = require('./services/channels');
20
- const { persistProxyRequestSnapshot } = require('./services/request-logger');
20
+ const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('./services/request-logger');
21
21
 
22
22
  let proxyServer = null;
23
23
  let proxyApp = null;
@@ -328,15 +328,6 @@ async function startProxyServer(options = {}) {
328
328
  second: '2-digit'
329
329
  });
330
330
  const requestSnapshot = serializeFullClaudeRequest(req);
331
- broadcastLog({
332
- type: 'action',
333
- action: 'claude_request_received',
334
- message: '收到 Claude Code 请求',
335
- time,
336
- channel: channel.name,
337
- source: 'claude',
338
- requestSummary: buildClaudeRequestSummary(req, sessionId)
339
- });
340
331
  persistClaudeRequestSnapshot({
341
332
  timestamp: Date.now(),
342
333
  source: 'claude',
@@ -344,6 +335,7 @@ async function startProxyServer(options = {}) {
344
335
  sessionId: sessionId || null,
345
336
  request: requestSnapshot
346
337
  });
338
+ persistClaudeRequestTemplate(req.body);
347
339
 
348
340
  // 应用模型重定向(当 proxy 开启时)
349
341
  if (req.body && req.body.model) {
@@ -278,8 +278,9 @@ function updateChannel(id, updates) {
278
278
  const proxyStatus = getProxyStatus();
279
279
  const isProxyRunning = proxyStatus.running;
280
280
 
281
- // Single-channel enforcement when proxy is OFF: enabling a channel disables all others
282
- if (!isProxyRunning && nextChannel.enabled && !oldChannel.enabled) {
281
+ // Single-channel enforcement: enabling a channel disables all others
282
+ // (applies regardless of proxy state — user intent is to switch to this channel)
283
+ if (nextChannel.enabled && !oldChannel.enabled) {
283
284
  data.channels.forEach((ch, i) => {
284
285
  if (i !== index && ch.enabled) {
285
286
  ch.enabled = false;
@@ -298,9 +299,10 @@ function updateChannel(id, updates) {
298
299
 
299
300
  saveChannels(data);
300
301
 
301
- // Sync settings.json when proxy is OFF and the channel is (or just became) enabled
302
- if (!isProxyRunning && nextChannel.enabled) {
303
- console.log(`[Settings-sync] Proxy is OFF and channel "${nextChannel.name}" is enabled, syncing settings.json...`);
302
+ // Sync settings.json whenever a channel becomes enabled (proxy OFF: immediate switch;
303
+ // proxy ON: pre-configures for when proxy stops)
304
+ if (nextChannel.enabled) {
305
+ console.log(`[Settings-sync] Channel "${nextChannel.name}" enabled, syncing settings.json...`);
304
306
  updateClaudeSettingsWithModelConfig(nextChannel);
305
307
  }
306
308
 
@@ -252,8 +252,9 @@ function updateChannel(channelId, updates) {
252
252
  const proxyStatus = getCodexProxyStatus();
253
253
  const isProxyRunning = proxyStatus.running;
254
254
 
255
- // Single-channel enforcement when proxy is OFF: enabling a channel disables all others
256
- if (!isProxyRunning && newChannel.enabled && !oldChannel.enabled) {
255
+ // Single-channel enforcement: enabling a channel disables all others
256
+ // (applies regardless of proxy state — user intent is to switch to this channel)
257
+ if (newChannel.enabled && !oldChannel.enabled) {
257
258
  data.channels.forEach((ch, i) => {
258
259
  if (i !== index && ch.enabled) {
259
260
  ch.enabled = false;
@@ -272,9 +273,10 @@ function updateChannel(channelId, updates) {
272
273
 
273
274
  saveChannels(data);
274
275
 
275
- // Sync config.toml when proxy is OFF and the channel is (or just became) enabled
276
- if (!isProxyRunning && newChannel.enabled) {
277
- console.log(`[Codex Settings-sync] Proxy is OFF and channel "${newChannel.name}" is enabled, syncing config.toml...`);
276
+ // Sync config.toml whenever a channel becomes enabled (proxy OFF: immediate switch;
277
+ // proxy ON: pre-configures for when proxy stops)
278
+ if (newChannel.enabled) {
279
+ console.log(`[Codex Settings-sync] Channel "${newChannel.name}" enabled, syncing config.toml...`);
278
280
  applyChannelToSettings(channelId);
279
281
  }
280
282
 
@@ -336,6 +336,9 @@ function restoreSettings() {
336
336
  // 清理 shell 配置文件中的环境变量(可选,不影响恢复结果)
337
337
  removeEnvFromShell('CC_PROXY_KEY');
338
338
 
339
+ // 同步删除当前进程的环境变量,使恢复立即生效(无需新开终端)
340
+ delete process.env.CC_PROXY_KEY;
341
+
339
342
  console.log('Codex settings restored from backup');
340
343
  return { success: true };
341
344
  } catch (err) {
@@ -419,6 +422,9 @@ function injectEnvToShell(envName, envValue) {
419
422
  writeFileAtomic(configPath, nextContent);
420
423
  }
421
424
 
425
+ // 同步更新当前进程的环境变量,使变更立即生效(无需新开终端)
426
+ process.env[normalizedEnvName] = String(envValue ?? '');
427
+
422
428
  return { success: true, path: configPath, isFirstTime: !existed };
423
429
  } catch (err) {
424
430
  // 不抛出错误,只是警告,因为这不是致命问题
@@ -484,6 +490,10 @@ function removeEnvFromShell(envName) {
484
490
  const normalized = compactBlankLines(cleanedLines);
485
491
  const nextContent = normalized.length > 0 ? `${normalized.join('\n')}\n` : '';
486
492
  writeFileAtomic(configPath, nextContent);
493
+
494
+ // 同步删除当前进程的环境变量,使变更立即生效(无需新开终端)
495
+ delete process.env[normalizedEnvName];
496
+
487
497
  return { success: true };
488
498
  } catch (err) {
489
499
  console.warn(`[Codex] Failed to remove env from shell config: ${err.message}`);
@@ -527,6 +537,9 @@ function setProxyConfig(proxyPort) {
527
537
  // 注入环境变量到 shell 配置文件(解决某些系统环境变量优先级问题)
528
538
  const shellInjectResult = injectEnvToShell('CC_PROXY_KEY', 'PROXY_KEY');
529
539
 
540
+ // 同步更新当前进程的环境变量,使代理立即生效(无需新开终端)
541
+ process.env.CC_PROXY_KEY = 'PROXY_KEY';
542
+
530
543
  // 获取 shell 配置文件路径用于提示信息
531
544
  const shellConfigPath = shellInjectResult.path || getShellConfigPath();
532
545
  const sourceCommand = getShellReloadCommand(shellConfigPath);
@@ -14,6 +14,7 @@ const { SkillService } = require('./skill-service');
14
14
  const { PluginsService } = require('./plugins-service');
15
15
  const { convertCommandToCodex } = require('./format-converter');
16
16
  const mcpService = require('./mcp-service');
17
+ const promptsService = require('./prompts-service');
17
18
  const pluginsService = new PluginsService();
18
19
 
19
20
  // 配置模板文件路径
@@ -368,13 +369,10 @@ function readCurrentConfig(targetDir) {
368
369
  * 返回用户级的 agents, commands, plugins + MCP 服务器列表
369
370
  */
370
371
  function getAvailableConfigs() {
371
- const agentServices = ['claude', 'codex', 'opencode'].map(platform => new AgentsService(platform));
372
- const commandServices = ['claude', 'opencode'].map(platform => new CommandsService(platform));
373
- const skillServices = ['claude', 'codex', 'gemini', 'opencode'].map(platform => new SkillService(platform));
374
-
372
+ const agentServices = [new AgentsService('claude')];
373
+ const commandServices = [new CommandsService('claude')];
375
374
  const agentMap = new Map();
376
375
  const commandMap = new Map();
377
- const skillMap = new Map();
378
376
 
379
377
  for (const service of agentServices) {
380
378
  const { agents } = service.listAgents();
@@ -398,14 +396,18 @@ function getAvailableConfigs() {
398
396
  }
399
397
  }
400
398
 
401
- for (const service of skillServices) {
402
- const installedSkills = service.getInstalledSkills();
403
- for (const skill of installedSkills || []) {
404
- const key = skill.directory || skill.name;
405
- if (!skillMap.has(key)) {
406
- skillMap.set(key, skill);
407
- }
408
- }
399
+ // 按平台分别获取 skills(每个平台有独立的安装目录)
400
+ const skillsByPlatform = {};
401
+ for (const platform of ['claude', 'codex', 'gemini', 'opencode']) {
402
+ const service = new SkillService(platform);
403
+ skillsByPlatform[platform] = service.getInstalledSkills().map(skill => ({
404
+ directory: skill.directory,
405
+ name: skill.name || skill.directory,
406
+ description: skill.description || '',
407
+ repoOwner: skill.repoOwner || null,
408
+ repoName: skill.repoName || null,
409
+ repoBranch: skill.repoBranch || null
410
+ }));
409
411
  }
410
412
 
411
413
  // 获取已安装的插件和市场插件
@@ -426,15 +428,18 @@ function getAvailableConfigs() {
426
428
  description: p.description
427
429
  }));
428
430
 
431
+ // 获取 Prompts 预设(用于 CLAUDE.md 内容选择)
432
+ const { presets: promptPresets } = promptsService.getAllPresets();
433
+ const promptsList = Object.values(promptPresets).map(p => ({
434
+ id: p.id,
435
+ name: p.name,
436
+ description: p.description || '',
437
+ content: p.content || '',
438
+ isBuiltin: p.isBuiltin || false
439
+ }));
440
+
429
441
  return {
430
- skills: Array.from(skillMap.values()).map(skill => ({
431
- directory: skill.directory,
432
- name: skill.name || skill.directory,
433
- description: skill.description || '',
434
- repoOwner: skill.repoOwner || null,
435
- repoName: skill.repoName || null,
436
- repoBranch: skill.repoBranch || null
437
- })),
442
+ skillsByPlatform,
438
443
  agents: Array.from(agentMap.values()).map(a => ({
439
444
  fileName: a.fileName,
440
445
  name: a.name,
@@ -462,7 +467,8 @@ function getAvailableConfigs() {
462
467
  repoUrl: p.repoUrl || null
463
468
  })),
464
469
  mcpServers: mcpServerList,
465
- mcpPresets
470
+ mcpPresets,
471
+ prompts: promptsList
466
472
  };
467
473
  }
468
474
 
@@ -237,8 +237,9 @@ function updateChannel(channelId, updates) {
237
237
  const proxyStatus = getGeminiProxyStatus();
238
238
  const isProxyRunning = proxyStatus.running;
239
239
 
240
- // Single-channel enforcement when proxy is OFF: enabling a channel disables all others
241
- if (!isProxyRunning && nextChannel.enabled && !oldChannel.enabled) {
240
+ // Single-channel enforcement: enabling a channel disables all others
241
+ // (applies regardless of proxy state — user intent is to switch to this channel)
242
+ if (nextChannel.enabled && !oldChannel.enabled) {
242
243
  data.channels.forEach((ch, i) => {
243
244
  if (i !== index && ch.enabled) {
244
245
  ch.enabled = false;
@@ -257,9 +258,10 @@ function updateChannel(channelId, updates) {
257
258
 
258
259
  saveChannels(data);
259
260
 
260
- // Sync .env when proxy is OFF and the channel is (or just became) enabled
261
- if (!isProxyRunning && nextChannel.enabled) {
262
- console.log(`[Gemini Settings-sync] Proxy is OFF and channel "${nextChannel.name}" is enabled, syncing .env...`);
261
+ // Sync .env whenever a channel becomes enabled (proxy OFF: immediate switch;
262
+ // proxy ON: pre-configures for when proxy stops)
263
+ if (nextChannel.enabled) {
264
+ console.log(`[Gemini Settings-sync] Channel "${nextChannel.name}" enabled, syncing .env...`);
263
265
  applyChannelToSettings(channelId, data.channels);
264
266
  } else {
265
267
  // 更新 Gemini 配置文件 (full rewrite for non-active-channel changes)
@@ -1620,7 +1620,7 @@ function updateServerOrder(serverIds) {
1620
1620
 
1621
1621
  /**
1622
1622
  * 导出所有 MCP 配置
1623
- * @param {string} format - 导出格式: 'json' | 'claude' | 'codex' | 'opencode'
1623
+ * @param {string} format - 导出格式: 'json' | 'claude' | 'codex' | 'opencode' | 'gemini'
1624
1624
  */
1625
1625
  function exportServers(format = 'json') {
1626
1626
  const servers = getAllServers();
@@ -1632,6 +1632,8 @@ function exportServers(format = 'json') {
1632
1632
  return exportForCodex(servers);
1633
1633
  case 'opencode':
1634
1634
  return exportForOpenCode(servers);
1635
+ case 'gemini':
1636
+ return exportForGemini(servers);
1635
1637
  case 'json':
1636
1638
  default:
1637
1639
  return exportAsJson(servers);
@@ -1712,6 +1714,25 @@ function exportForOpenCode(servers) {
1712
1714
  };
1713
1715
  }
1714
1716
 
1717
+ /**
1718
+ * 导出为 Gemini 格式
1719
+ */
1720
+ function exportForGemini(servers) {
1721
+ const mcpServers = {};
1722
+
1723
+ for (const [id, server] of Object.entries(servers)) {
1724
+ if (server.apps?.gemini) {
1725
+ mcpServers[id] = extractServerSpec(server.server);
1726
+ }
1727
+ }
1728
+
1729
+ return {
1730
+ format: 'gemini',
1731
+ content: JSON.stringify({ mcpServers }, null, 2),
1732
+ filename: 'gemini-mcp-config.json'
1733
+ };
1734
+ }
1735
+
1715
1736
  module.exports = {
1716
1737
  getAllServers,
1717
1738
  getServer,