coding-tool-x 3.5.9 → 3.5.11

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 (92) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -25
  3. package/bin/ctx.js +1 -6
  4. package/dist/web/assets/{Analytics-C5W3axXs.js → Analytics-Bw-0r4Ys.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-C6QRIeDj.js} +1 -1
  7. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  8. package/dist/web/assets/{Home-C9TQNB6f.js → Home-B_Z9ythw.js} +1 -1
  9. package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-Ce-mws6Q.js} +1 -1
  10. package/dist/web/assets/ProjectList-9Z7FFCIT.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-CyETQcs8.js +1 -0
  13. package/dist/web/assets/SessionList-D12muXvx.css +1 -0
  14. package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-DSUcP1se.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-Cvbc11bJ.js} +1 -1
  16. package/dist/web/assets/icons-lKCTySl7.js +1 -0
  17. package/dist/web/assets/index-BS7aplYp.css +1 -0
  18. package/dist/web/assets/index-CTeg1xG-.js +2 -0
  19. package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
  20. package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
  21. package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
  22. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
  23. package/dist/web/index.html +8 -8
  24. package/package.json +1 -2
  25. package/src/commands/daemon.js +5 -33
  26. package/src/commands/export-config.js +6 -6
  27. package/src/commands/stats.js +4 -41
  28. package/src/commands/ui.js +3 -12
  29. package/src/config/channel-preset-websites.js +114 -0
  30. package/src/config/default.js +9 -5
  31. package/src/config/loader.js +2 -2
  32. package/src/config/model-metadata.json +16 -20
  33. package/src/config/paths.js +33 -166
  34. package/src/index.js +34 -125
  35. package/src/server/api/agents.js +2 -52
  36. package/src/server/api/codex-channels.js +4 -0
  37. package/src/server/api/codex-proxy.js +45 -2
  38. package/src/server/api/codex-sessions.js +89 -12
  39. package/src/server/api/commands.js +2 -38
  40. package/src/server/api/dashboard.js +1 -25
  41. package/src/server/api/gemini-proxy.js +4 -2
  42. package/src/server/api/gemini-sessions.js +86 -9
  43. package/src/server/api/hooks.js +1 -17
  44. package/src/server/api/opencode-proxy.js +7 -2
  45. package/src/server/api/opencode-sessions.js +83 -10
  46. package/src/server/api/plugins.js +34 -128
  47. package/src/server/api/proxy.js +5 -3
  48. package/src/server/api/sessions.js +181 -27
  49. package/src/server/api/workspaces.js +0 -14
  50. package/src/server/index.js +9 -31
  51. package/src/server/services/agents-service.js +62 -269
  52. package/src/server/services/base/base-channel-service.js +3 -1
  53. package/src/server/services/channels.js +5 -0
  54. package/src/server/services/codex-channels.js +2 -0
  55. package/src/server/services/codex-sessions.js +88 -108
  56. package/src/server/services/commands-service.js +81 -281
  57. package/src/server/services/config-export-service.js +7 -7
  58. package/src/server/services/config-registry-service.js +9 -8
  59. package/src/server/services/config-sync-manager.js +41 -61
  60. package/src/server/services/config-sync-service.js +3 -3
  61. package/src/server/services/gemini-channels.js +17 -6
  62. package/src/server/services/gemini-config.js +4 -3
  63. package/src/server/services/gemini-sessions.js +59 -26
  64. package/src/server/services/gemini-settings-manager.js +3 -2
  65. package/src/server/services/mcp-service.js +14 -9
  66. package/src/server/services/native-oauth-adapters.js +3 -3
  67. package/src/server/services/network-access.js +2 -61
  68. package/src/server/services/notification-hooks.js +19 -184
  69. package/src/server/services/opencode-channels.js +9 -0
  70. package/src/server/services/opencode-sessions.js +6 -16
  71. package/src/server/services/opencode-settings-manager.js +3 -3
  72. package/src/server/services/plugins-service.js +62 -996
  73. package/src/server/services/prompts-service.js +9 -5
  74. package/src/server/services/sessions.js +171 -183
  75. package/src/server/services/skill-service.js +18 -155
  76. package/src/server/websocket-server.js +4 -35
  77. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  78. package/dist/web/assets/Home-qzk118Of.css +0 -1
  79. package/dist/web/assets/ProjectList-Bjt6mrsV.js +0 -1
  80. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  81. package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
  82. package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
  83. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  84. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  85. package/dist/web/assets/index-Duc7QP4e.js +0 -2
  86. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  87. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  88. package/docs/Caddyfile.example +0 -19
  89. package/docs/reverse-proxy-https.md +0 -57
  90. package/src/server/services/https-cert.js +0 -171
  91. package/src/server/services/session-launch-command.js +0 -58
  92. package/src/server/services/web-ui-runtime.js +0 -54
@@ -14,7 +14,7 @@ const {
14
14
  hasBackup,
15
15
  readConfig
16
16
  } = require('../services/codex-settings-manager');
17
- const { getChannels, getEnabledChannels } = require('../services/codex-channels');
17
+ const { getChannels, getEnabledChannels, markChannelAsRecentlyUsed } = require('../services/codex-channels');
18
18
  const { clearNativeOAuth } = require('../services/native-oauth-adapters');
19
19
  const { clearAllLogs } = require('../websocket-server');
20
20
  const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
@@ -61,6 +61,31 @@ function resolveActiveChannel(channels, activeChannelId = null) {
61
61
  || null;
62
62
  }
63
63
 
64
+ function recoverChannelAfterMissingUpdate(previousChannel) {
65
+ const { channels } = getChannels();
66
+ const allChannels = Array.isArray(channels) ? channels : [];
67
+ const enabledChannels = allChannels.filter(channel => channel.enabled !== false);
68
+
69
+ const previousProviderKey = String(previousChannel?.providerKey || '').trim();
70
+ if (previousProviderKey) {
71
+ const sameProvider = enabledChannels.find(channel => channel.providerKey === previousProviderKey);
72
+ if (sameProvider) {
73
+ return sameProvider;
74
+ }
75
+ }
76
+
77
+ if (allChannels.length > 0) {
78
+ return resolveActiveChannel(allChannels, null);
79
+ }
80
+
81
+ const refreshedEnabledChannels = getEnabledChannels();
82
+ if (Array.isArray(refreshedEnabledChannels) && refreshedEnabledChannels.length > 0) {
83
+ return refreshedEnabledChannels[0];
84
+ }
85
+
86
+ return null;
87
+ }
88
+
64
89
  // 保存激活渠道ID
65
90
  function saveActiveChannelId(channelId) {
66
91
  ensureStorageDirMigrated();
@@ -149,6 +174,24 @@ router.post('/start', async (req, res) => {
149
174
  });
150
175
  }
151
176
 
177
+ try {
178
+ currentChannel = markChannelAsRecentlyUsed(currentChannel.id) || currentChannel;
179
+ } catch (error) {
180
+ if (error?.message !== 'Channel not found') {
181
+ throw error;
182
+ }
183
+
184
+ const recoveredChannel = recoverChannelAfterMissingUpdate(currentChannel);
185
+ if (!recoveredChannel) {
186
+ return res.status(400).json({
187
+ error: 'No enabled Codex channel found. Please create and enable a channel first.'
188
+ });
189
+ }
190
+
191
+ currentChannel = recoveredChannel;
192
+ console.warn(`[Codex Proxy] Active channel disappeared before startup, falling back to ${currentChannel.name} (${currentChannel.id})`);
193
+ }
194
+
152
195
  // 3. 保存当前激活渠道ID(用于代理模式)
153
196
  saveActiveChannelId(currentChannel.id);
154
197
  console.log(`[Codex Proxy] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
@@ -166,7 +209,7 @@ router.post('/start', async (req, res) => {
166
209
 
167
210
  const updatedStatus = getCodexProxyStatus();
168
211
  const { channels: allChannels } = getChannels();
169
- const activeChannel = allChannels.filter(ch => ch.enabled !== false)[0];
212
+ const activeChannel = allChannels.find(ch => ch.id === currentChannel.id) || currentChannel;
170
213
  const { broadcastProxyState } = require('../websocket-server');
171
214
  broadcastProxyState('codex', updatedStatus, activeChannel, allChannels);
172
215
 
@@ -11,7 +11,6 @@ const {
11
11
  } = require('../services/codex-sessions');
12
12
  const { isCodexInstalled } = require('../services/codex-config');
13
13
  const { loadAliases } = require('../services/alias');
14
- const { buildLaunchCommand } = require('../services/session-launch-command');
15
14
 
16
15
  const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
17
16
 
@@ -22,6 +21,24 @@ function logPerf(route, startMs, detail = '') {
22
21
  console.log(`[Codex Perf] ${route}: ${duration}ms${suffix}`);
23
22
  }
24
23
 
24
+ function normalizeForkOptions(body = {}) {
25
+ const alias = typeof body.alias === 'string' ? body.alias.trim() : '';
26
+ const rawNumber = body.afterUserMessageNumber;
27
+ const hasExplicitNumber = rawNumber !== undefined && rawNumber !== null && rawNumber !== '';
28
+ const parsedNumber = hasExplicitNumber ? parseInt(rawNumber, 10) : null;
29
+
30
+ if (hasExplicitNumber && (!Number.isInteger(parsedNumber) || parsedNumber <= 0)) {
31
+ const error = new Error('afterUserMessageNumber must be a positive integer');
32
+ error.statusCode = 400;
33
+ throw error;
34
+ }
35
+
36
+ return {
37
+ afterUserMessageNumber: parsedNumber,
38
+ alias: alias || null
39
+ };
40
+ }
41
+
25
42
  module.exports = (config) => {
26
43
  // ============================================
27
44
  // 静态路由必须放在参数路由之前
@@ -203,6 +220,67 @@ module.exports = (config) => {
203
220
  }
204
221
  });
205
222
 
223
+ router.get('/:projectName/:sessionId/status', (req, res) => {
224
+ try {
225
+ if (!isCodexInstalled()) {
226
+ return res.status(404).json({ error: 'Codex CLI not installed' });
227
+ }
228
+
229
+ const { sessionId } = req.params;
230
+ const session = getSessionById(sessionId);
231
+ if (!session || !session.filePath) {
232
+ return res.status(404).json({ error: 'Session not found' });
233
+ }
234
+
235
+ const fs = require('fs');
236
+ const stats = fs.statSync(session.filePath);
237
+ res.json({
238
+ sessionId,
239
+ lastModified: stats.mtime.toISOString(),
240
+ size: stats.size
241
+ });
242
+ } catch (err) {
243
+ console.error('[Codex API] Failed to get session status:', err);
244
+ res.status(500).json({ error: err.message });
245
+ }
246
+ });
247
+
248
+ router.get('/:projectName/:sessionId/outline', (req, res) => {
249
+ try {
250
+ if (!isCodexInstalled()) {
251
+ return res.status(404).json({ error: 'Codex CLI not installed' });
252
+ }
253
+
254
+ const { sessionId } = req.params;
255
+ const session = getSessionById(sessionId);
256
+ if (!session) {
257
+ return res.status(404).json({ error: 'Session not found' });
258
+ }
259
+
260
+ let userMessageNumber = 0;
261
+ const items = [];
262
+ for (const msg of session.messages || []) {
263
+ if (msg.role !== 'user') continue;
264
+ const preview = typeof msg.content === 'string' ? msg.content.trim() : '';
265
+ if (!preview) continue;
266
+ userMessageNumber += 1;
267
+ items.push({
268
+ userMessageNumber,
269
+ preview: preview.length > 42 ? `${preview.slice(0, 42)}...` : preview,
270
+ timestamp: msg.timestamp || null
271
+ });
272
+ }
273
+
274
+ res.json({
275
+ sessionId,
276
+ items
277
+ });
278
+ } catch (err) {
279
+ console.error('[Codex API] Failed to get session outline:', err);
280
+ res.status(500).json({ error: err.message });
281
+ }
282
+ });
283
+
206
284
  /**
207
285
  * GET /api/codex/sessions/:projectName/:sessionId/messages
208
286
  * 获取会话的消息列表
@@ -228,14 +306,17 @@ module.exports = (config) => {
228
306
  // 转换消息格式为前端期望的格式
229
307
  const convertedMessages = [];
230
308
 
309
+ let userMessageNumber = 0;
231
310
  for (const msg of session.messages) {
232
311
  // 用户消息
233
312
  if (msg.role === 'user') {
313
+ userMessageNumber += 1;
234
314
  convertedMessages.push({
235
315
  type: 'user',
236
316
  content: msg.content || '[空消息]',
237
317
  timestamp: msg.timestamp,
238
- model: null
318
+ model: null,
319
+ userMessageNumber
239
320
  });
240
321
  }
241
322
  // 助手消息(普通回复)
@@ -422,12 +503,12 @@ module.exports = (config) => {
422
503
  }
423
504
 
424
505
  const { sessionId } = req.params;
425
- const result = forkSession(sessionId);
506
+ const result = forkSession(sessionId, normalizeForkOptions(req.body));
426
507
 
427
508
  res.json(result);
428
509
  } catch (err) {
429
510
  console.error('[Codex API] Failed to fork session:', err);
430
- res.status(500).json({ error: err.message });
511
+ res.status(err.statusCode || 500).json({ error: err.message });
431
512
  }
432
513
  });
433
514
 
@@ -468,7 +549,6 @@ module.exports = (config) => {
468
549
  }
469
550
 
470
551
  const { sessionId } = req.params;
471
- const fork = Boolean(req.body?.fork);
472
552
  const fs = require('fs');
473
553
 
474
554
  // 获取会话详情
@@ -511,17 +591,15 @@ module.exports = (config) => {
511
591
  broadcastLog({
512
592
  type: 'action',
513
593
  action: 'launch_codex_session',
514
- message: `${fork ? '复制 Codex 会话 Fork 命令' : '复制 Codex 会话启动命令'} ${alias || sessionId.substring(0, 8)}`,
594
+ message: `复制 Codex 会话启动命令 ${alias || sessionId.substring(0, 8)}`,
515
595
  sessionId: sessionId,
516
596
  alias: alias || null,
517
597
  timestamp: Date.now()
518
598
  });
519
599
 
520
- const { command, copyCommand } = buildLaunchCommand({
521
- cwd,
522
- executable: 'codex',
523
- args: fork ? ['fork', sessionId] : ['resume', sessionId]
524
- });
600
+ const command = `codex resume ${sessionId}`;
601
+ const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
602
+ const copyCommand = `cd ${quotedCwd} && ${command}`;
525
603
 
526
604
  res.json({
527
605
  success: true,
@@ -529,7 +607,6 @@ module.exports = (config) => {
529
607
  sessionFile: session.filePath,
530
608
  sessionId,
531
609
  tool: 'codex',
532
- fork,
533
610
  command,
534
611
  copyCommand
535
612
  });
@@ -35,11 +35,8 @@ function getCommandsService(req) {
35
35
  router.get('/', (req, res) => {
36
36
  try {
37
37
  const { platform, service } = getCommandsService(req);
38
- const { projectPath, refresh } = req.query;
39
- const forceRefresh = refresh === '1';
40
- const result = service.listCommands(projectPath || null, {
41
- syncManagedLocalCommands: forceRefresh
42
- });
38
+ const { projectPath } = req.query;
39
+ const result = service.listCommands(projectPath || null);
43
40
 
44
41
  res.json({
45
42
  success: true,
@@ -449,39 +446,6 @@ router.post('/install', async (req, res) => {
449
446
  }
450
447
  });
451
448
 
452
- /**
453
- * 安装本地托管命令
454
- * POST /api/commands/install-local
455
- * Body: { path }
456
- */
457
- router.post('/install-local', (req, res) => {
458
- try {
459
- const { platform, service } = getCommandsService(req);
460
- const { path } = req.body;
461
-
462
- if (!path) {
463
- return res.status(400).json({
464
- success: false,
465
- message: 'Missing path'
466
- });
467
- }
468
-
469
- const result = service.installLocalCommand(path);
470
-
471
- res.json({
472
- success: true,
473
- platform,
474
- ...result
475
- });
476
- } catch (err) {
477
- console.error('[Commands API] Install local command error:', err);
478
- res.status(500).json({
479
- success: false,
480
- message: err.message
481
- });
482
- }
483
- });
484
-
485
449
  /**
486
450
  * 卸载命令
487
451
  * POST /api/commands/uninstall
@@ -26,30 +26,6 @@ const { getTodayStatistics: getCodexTodayStatistics } = require('../services/cod
26
26
  const { getTodayStatistics: getGeminiTodayStatistics } = require('../services/gemini-statistics-service');
27
27
  const { getTodayStatistics: getOpenCodeTodayStatistics } = require('../services/opencode-statistics-service');
28
28
 
29
- function filterEntriesByToolType(entries = {}, toolType) {
30
- return Object.fromEntries(
31
- Object.entries(entries || {}).filter(([, value]) => !toolType || !value?.toolType || value.toolType === toolType)
32
- );
33
- }
34
-
35
- function extractToolScopedStats(stats, toolType) {
36
- if (!stats || !toolType || !stats.byToolType) {
37
- return stats;
38
- }
39
-
40
- const toolScope = stats.byToolType[toolType] || {};
41
- return {
42
- ...stats,
43
- summary: {
44
- requests: toolScope.requests || 0,
45
- tokens: toolScope.tokens?.total || 0,
46
- cost: toolScope.cost || 0
47
- },
48
- byChannel: filterEntriesByToolType(stats.byChannel, toolType),
49
- byModel: filterEntriesByToolType(stats.byModel, toolType)
50
- };
51
- }
52
-
53
29
  /**
54
30
  * GET /api/dashboard/init
55
31
  * 聚合首页所需的所有数据,一次请求返回
@@ -147,7 +123,7 @@ router.get('/init', async (req, res) => {
147
123
  opencode: opencodeCounts || { projectCount: 0, sessionCount: 0 }
148
124
  },
149
125
  todayStats: {
150
- claude: formatStats(extractToolScopedStats(claudeTodayStats, 'claude-code')),
126
+ claude: formatStats(claudeTodayStats),
151
127
  codex: formatStats(codexTodayStats),
152
128
  gemini: formatStats(geminiTodayStats),
153
129
  opencode: formatStats(opencodeTodayStats)
@@ -15,7 +15,7 @@ const {
15
15
  hasBackup,
16
16
  readEnv
17
17
  } = require('../services/gemini-settings-manager');
18
- const { getChannels, getEnabledChannels } = require('../services/gemini-channels');
18
+ const { getChannels, getEnabledChannels, markChannelAsRecentlyUsed } = require('../services/gemini-channels');
19
19
  const { clearNativeOAuth } = require('../services/native-oauth-adapters');
20
20
  const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
21
21
  const fs = require('fs');
@@ -147,6 +147,8 @@ router.post('/start', async (req, res) => {
147
147
  });
148
148
  }
149
149
 
150
+ currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
151
+
150
152
  // 3. 保存当前激活渠道ID(用于代理模式)
151
153
  saveActiveChannelId(currentChannel.id);
152
154
  console.log(`[Gemini Proxy] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
@@ -165,7 +167,7 @@ router.post('/start', async (req, res) => {
165
167
  const { broadcastProxyState } = require('../websocket-server');
166
168
  const proxyStatus = getGeminiProxyStatus();
167
169
  const { channels: allChannels } = getChannels();
168
- const activeChannel = allChannels.filter(ch => ch.enabled !== false)[0];
170
+ const activeChannel = allChannels.find(ch => ch.id === currentChannel.id) || currentChannel;
169
171
  broadcastProxyState('gemini', proxyStatus, activeChannel, allChannels);
170
172
 
171
173
  res.json({
@@ -13,7 +13,24 @@ const {
13
13
  } = require('../services/gemini-sessions');
14
14
  const { isGeminiInstalled } = require('../services/gemini-config');
15
15
  const { loadAliases } = require('../services/alias');
16
- const { buildLaunchCommand } = require('../services/session-launch-command');
16
+
17
+ function normalizeForkOptions(body = {}) {
18
+ const alias = typeof body.alias === 'string' ? body.alias.trim() : '';
19
+ const rawNumber = body.afterUserMessageNumber;
20
+ const hasExplicitNumber = rawNumber !== undefined && rawNumber !== null && rawNumber !== '';
21
+ const parsedNumber = hasExplicitNumber ? parseInt(rawNumber, 10) : null;
22
+
23
+ if (hasExplicitNumber && (!Number.isInteger(parsedNumber) || parsedNumber <= 0)) {
24
+ const error = new Error('afterUserMessageNumber must be a positive integer');
25
+ error.statusCode = 400;
26
+ throw error;
27
+ }
28
+
29
+ return {
30
+ afterUserMessageNumber: parsedNumber,
31
+ alias: alias || null
32
+ };
33
+ }
17
34
 
18
35
  module.exports = (config) => {
19
36
  /**
@@ -146,6 +163,65 @@ module.exports = (config) => {
146
163
  }
147
164
  });
148
165
 
166
+ router.get('/:projectHash/:sessionId/status', (req, res) => {
167
+ try {
168
+ if (!isGeminiInstalled()) {
169
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
170
+ }
171
+
172
+ const { sessionId } = req.params;
173
+ const session = getSessionById(sessionId);
174
+ if (!session) {
175
+ return res.status(404).json({ error: 'Session not found' });
176
+ }
177
+
178
+ res.json({
179
+ sessionId,
180
+ lastModified: session.lastUpdated || session.mtime || null,
181
+ size: session.size || 0
182
+ });
183
+ } catch (err) {
184
+ console.error('[Gemini API] Failed to get session status:', err);
185
+ res.status(500).json({ error: err.message });
186
+ }
187
+ });
188
+
189
+ router.get('/:projectHash/:sessionId/outline', (req, res) => {
190
+ try {
191
+ if (!isGeminiInstalled()) {
192
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
193
+ }
194
+
195
+ const { sessionId } = req.params;
196
+ const session = getSessionById(sessionId);
197
+ if (!session) {
198
+ return res.status(404).json({ error: 'Session not found' });
199
+ }
200
+
201
+ let userMessageNumber = 0;
202
+ const items = [];
203
+ for (const msg of session.messages || []) {
204
+ if (msg.type !== 'user') continue;
205
+ const preview = typeof msg.content === 'string' ? msg.content.trim() : '';
206
+ if (!preview) continue;
207
+ userMessageNumber += 1;
208
+ items.push({
209
+ userMessageNumber,
210
+ preview: preview.length > 42 ? `${preview.slice(0, 42)}...` : preview,
211
+ timestamp: msg.timestamp || null
212
+ });
213
+ }
214
+
215
+ res.json({
216
+ sessionId,
217
+ items
218
+ });
219
+ } catch (err) {
220
+ console.error('[Gemini API] Failed to get session outline:', err);
221
+ res.status(500).json({ error: err.message });
222
+ }
223
+ });
224
+
149
225
  /**
150
226
  * GET /api/gemini/sessions/:projectHash/:sessionId/messages
151
227
  * 获取会话的消息列表
@@ -168,14 +244,17 @@ module.exports = (config) => {
168
244
  // 转换消息格式为前端期望的格式
169
245
  const convertedMessages = [];
170
246
 
247
+ let userMessageNumber = 0;
171
248
  for (const msg of session.messages || []) {
172
249
  // 用户消息
173
250
  if (msg.type === 'user') {
251
+ userMessageNumber += 1;
174
252
  convertedMessages.push({
175
253
  type: 'user',
176
254
  content: msg.content || '[空消息]',
177
255
  timestamp: msg.timestamp,
178
- model: null
256
+ model: null,
257
+ userMessageNumber
179
258
  });
180
259
  }
181
260
  // Gemini 助手消息(type 是 'gemini' 而不是 'assistant')
@@ -322,12 +401,12 @@ module.exports = (config) => {
322
401
  }
323
402
 
324
403
  const { sessionId } = req.params;
325
- const result = forkSession(sessionId);
404
+ const result = forkSession(sessionId, normalizeForkOptions(req.body));
326
405
 
327
406
  res.json(result);
328
407
  } catch (err) {
329
408
  console.error('[Gemini API] Failed to fork session:', err);
330
- res.status(500).json({ error: err.message });
409
+ res.status(err.statusCode || 500).json({ error: err.message });
331
410
  }
332
411
  });
333
412
 
@@ -408,11 +487,9 @@ module.exports = (config) => {
408
487
  const resumeIndex = sessionIndex + 1;
409
488
 
410
489
  // 构建 Gemini CLI 命令(使用 --resume <index> 恢复特定会话)
411
- const { command, copyCommand } = buildLaunchCommand({
412
- cwd: projectPath,
413
- executable: 'gemini',
414
- args: ['--resume', String(resumeIndex)]
415
- });
490
+ const command = `gemini --resume ${resumeIndex}`;
491
+ const quotedCwd = `"${String(projectPath).replace(/"/g, '\\"')}"`;
492
+ const copyCommand = `cd ${quotedCwd} && ${command}`;
416
493
 
417
494
  res.json({
418
495
  success: true,
@@ -1,7 +1,7 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const notificationHooks = require('../services/notification-hooks');
4
- const { createSameOriginGuard, isLoopbackRequest } = require('../services/network-access');
4
+ const { createSameOriginGuard } = require('../services/network-access');
5
5
 
6
6
  router.use(createSameOriginGuard({
7
7
  message: '禁止跨站访问通知配置接口'
@@ -42,20 +42,4 @@ router.post('/test', async (req, res) => {
42
42
  }
43
43
  });
44
44
 
45
- router.post('/browser-event', (req, res) => {
46
- try {
47
- if (!isLoopbackRequest(req)) {
48
- return res.status(403).json({
49
- error: '浏览器提醒事件仅允许本机触发'
50
- });
51
- }
52
-
53
- notificationHooks.emitBrowserNotificationEvent(req.body || {});
54
- res.json({ success: true });
55
- } catch (error) {
56
- console.error('Error emitting browser notification event:', error);
57
- res.status(error.statusCode || 500).json({ error: error.message });
58
- }
59
- });
60
-
61
45
  module.exports = router;
@@ -21,6 +21,7 @@ const {
21
21
  getChannels,
22
22
  getEnabledChannels,
23
23
  applyChannelToSettings,
24
+ markChannelAsRecentlyUsed,
24
25
  getEffectiveApiKeyCandidates
25
26
  } = require('../services/opencode-channels');
26
27
  const { getSchedulerState } = require('../services/channel-scheduler');
@@ -218,7 +219,9 @@ router.post('/start', async (req, res) => {
218
219
  }
219
220
 
220
221
  const { channels: allChannels } = getChannels();
221
- const currentChannel = findActiveChannelFromNativeConfig(allChannels) || enabledChannels[0];
222
+ let currentChannel = findActiveChannelFromNativeConfig(allChannels) || enabledChannels[0];
223
+
224
+ currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
222
225
 
223
226
  // 2. 保存当前激活渠道ID
224
227
  saveActiveChannelId(currentChannel.id);
@@ -285,7 +288,9 @@ router.post('/start', async (req, res) => {
285
288
  // 5. 广播状态更新
286
289
  const { broadcastProxyState } = require('../websocket-server');
287
290
  const updatedStatus = getOpenCodeProxyStatus();
288
- broadcastProxyState('opencode', updatedStatus, currentChannel, allChannels);
291
+ const { channels: latestChannels } = getChannels();
292
+ const activeChannel = latestChannels.find(channel => channel.id === currentChannel.id) || currentChannel;
293
+ broadcastProxyState('opencode', updatedStatus, activeChannel, latestChannels);
289
294
 
290
295
  res.json({
291
296
  success: true,