coding-tool-x 3.5.8 → 3.5.10

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 (91) 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-BzoNzfbi.js → Analytics-DPb1zErn.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-O4ikBt1o.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
  7. package/dist/web/assets/{Home-BQjsnblU.js → Home-BKYHKSax.js} +1 -1
  8. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  9. package/dist/web/assets/{PluginManager-DS_DJnVc.js → PluginManager-jgeQOE24.js} +1 -1
  10. package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
  13. package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
  14. package/dist/web/assets/{SkillManager-DpNE02r0.js → SkillManager-CtEz5gGp.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-DMY7_SHh.js → WorkspaceManager-D3B0Y5XE.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-BnyNm74G.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-proxy.js +4 -2
  37. package/src/server/api/codex-sessions.js +88 -9
  38. package/src/server/api/commands.js +2 -38
  39. package/src/server/api/dashboard.js +1 -25
  40. package/src/server/api/gemini-proxy.js +4 -2
  41. package/src/server/api/gemini-sessions.js +86 -9
  42. package/src/server/api/hooks.js +1 -17
  43. package/src/server/api/opencode-proxy.js +7 -2
  44. package/src/server/api/opencode-sessions.js +82 -7
  45. package/src/server/api/plugins.js +34 -128
  46. package/src/server/api/proxy.js +5 -3
  47. package/src/server/api/sessions.js +180 -24
  48. package/src/server/api/workspaces.js +0 -14
  49. package/src/server/index.js +9 -31
  50. package/src/server/services/agents-service.js +62 -269
  51. package/src/server/services/base/base-channel-service.js +3 -1
  52. package/src/server/services/channels.js +5 -0
  53. package/src/server/services/codex-channels.js +2 -0
  54. package/src/server/services/codex-sessions.js +78 -108
  55. package/src/server/services/commands-service.js +81 -281
  56. package/src/server/services/config-export-service.js +7 -7
  57. package/src/server/services/config-registry-service.js +9 -8
  58. package/src/server/services/config-sync-manager.js +41 -61
  59. package/src/server/services/config-sync-service.js +3 -3
  60. package/src/server/services/gemini-channels.js +17 -6
  61. package/src/server/services/gemini-config.js +4 -3
  62. package/src/server/services/gemini-sessions.js +51 -26
  63. package/src/server/services/gemini-settings-manager.js +3 -2
  64. package/src/server/services/mcp-service.js +14 -9
  65. package/src/server/services/native-oauth-adapters.js +3 -3
  66. package/src/server/services/network-access.js +2 -61
  67. package/src/server/services/notification-hooks.js +19 -184
  68. package/src/server/services/opencode-channels.js +9 -0
  69. package/src/server/services/opencode-sessions.js +4 -4
  70. package/src/server/services/opencode-settings-manager.js +3 -3
  71. package/src/server/services/plugins-service.js +62 -996
  72. package/src/server/services/prompts-service.js +9 -5
  73. package/src/server/services/sessions.js +128 -99
  74. package/src/server/services/skill-service.js +18 -155
  75. package/src/server/websocket-server.js +4 -35
  76. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  77. package/dist/web/assets/Home-qzk118Of.css +0 -1
  78. package/dist/web/assets/ProjectList-CqYDtsHx.js +0 -1
  79. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  80. package/dist/web/assets/SessionList-CfPtcq6Y.css +0 -1
  81. package/dist/web/assets/SessionList-DMlLtMCz.js +0 -1
  82. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  83. package/dist/web/assets/index-Clf0l3wc.js +0 -2
  84. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  85. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  86. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  87. package/docs/Caddyfile.example +0 -19
  88. package/docs/reverse-proxy-https.md +0 -57
  89. package/src/server/services/https-cert.js +0 -171
  90. package/src/server/services/session-launch-command.js +0 -81
  91. package/src/server/services/web-ui-runtime.js +0 -54
@@ -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
 
@@ -516,11 +597,9 @@ module.exports = (config) => {
516
597
  timestamp: Date.now()
517
598
  });
518
599
 
519
- const { command, copyCommand } = buildLaunchCommand({
520
- cwd,
521
- executable: 'codex',
522
- args: ['resume', sessionId]
523
- });
600
+ const command = `codex resume ${sessionId}`;
601
+ const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
602
+ const copyCommand = `cd ${quotedCwd} && ${command}`;
524
603
 
525
604
  res.json({
526
605
  success: true,
@@ -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,
@@ -14,7 +14,6 @@ const {
14
14
  } = require('../services/opencode-sessions');
15
15
  const { loadAliases } = require('../services/alias');
16
16
  const { broadcastLog } = require('../websocket-server');
17
- const { buildLaunchCommand } = require('../services/session-launch-command');
18
17
  const { HOME_DIR } = require('../../config/paths');
19
18
 
20
19
  function isNotFoundError(error) {
@@ -152,6 +151,73 @@ module.exports = (config) => {
152
151
  }
153
152
  });
154
153
 
154
+ /**
155
+ * GET /api/opencode/sessions/:projectName/:sessionId/status
156
+ * 获取会话轻量状态(用于实时刷新)
157
+ */
158
+ router.get('/:projectName/:sessionId/status', (req, res) => {
159
+ try {
160
+ if (!isOpenCodeInstalled()) {
161
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
162
+ }
163
+
164
+ const { sessionId } = req.params;
165
+ const session = getSessionById(sessionId);
166
+ if (!session) {
167
+ return res.status(404).json({ error: 'Session not found' });
168
+ }
169
+
170
+ res.json({
171
+ sessionId,
172
+ lastModified: session.mtime || null,
173
+ size: session.size || 0
174
+ });
175
+ } catch (err) {
176
+ console.error('[OpenCode API] Failed to get session status:', err);
177
+ res.status(500).json({ error: err.message });
178
+ }
179
+ });
180
+
181
+ /**
182
+ * GET /api/opencode/sessions/:projectName/:sessionId/outline
183
+ * 获取用户消息目录
184
+ */
185
+ router.get('/:projectName/:sessionId/outline', (req, res) => {
186
+ try {
187
+ if (!isOpenCodeInstalled()) {
188
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
189
+ }
190
+
191
+ const { sessionId } = req.params;
192
+ const session = getSessionById(sessionId);
193
+ if (!session) {
194
+ return res.status(404).json({ error: 'Session not found' });
195
+ }
196
+
197
+ let userMessageNumber = 0;
198
+ const items = [];
199
+ for (const msg of getSessionMessages(sessionId)) {
200
+ if (msg.type !== 'user') continue;
201
+ const preview = typeof msg.content === 'string' ? msg.content.trim() : '';
202
+ if (!preview) continue;
203
+ userMessageNumber += 1;
204
+ items.push({
205
+ userMessageNumber,
206
+ preview: preview.length > 42 ? `${preview.slice(0, 42)}...` : preview,
207
+ timestamp: msg.timestamp || null
208
+ });
209
+ }
210
+
211
+ res.json({
212
+ sessionId,
213
+ items
214
+ });
215
+ } catch (err) {
216
+ console.error('[OpenCode API] Failed to get session outline:', err);
217
+ res.status(500).json({ error: err.message });
218
+ }
219
+ });
220
+
155
221
  /**
156
222
  * GET /api/opencode/sessions/:projectName/:sessionId/messages
157
223
  * 获取会话的消息列表
@@ -168,7 +234,18 @@ module.exports = (config) => {
168
234
  if (!session) {
169
235
  return res.status(404).json({ error: 'Session not found' });
170
236
  }
171
- const convertedMessages = getSessionMessages(sessionId);
237
+ const baseMessages = getSessionMessages(sessionId);
238
+ let userMessageNumber = 0;
239
+ const convertedMessages = baseMessages.map((message) => {
240
+ if (message.type !== 'user') {
241
+ return message;
242
+ }
243
+ userMessageNumber += 1;
244
+ return {
245
+ ...message,
246
+ userMessageNumber
247
+ };
248
+ });
172
249
 
173
250
  // 分页处理
174
251
  const pageNum = parseInt(page);
@@ -351,11 +428,9 @@ module.exports = (config) => {
351
428
  const projects = getProjects();
352
429
  const project = projects.find(p => p.name === projectName);
353
430
  const cwd = session.directory || project?.fullPath || HOME_DIR;
354
- const { command, copyCommand } = buildLaunchCommand({
355
- cwd,
356
- executable: 'opencode',
357
- args: ['-r', sessionId]
358
- });
431
+ const command = `opencode -r ${sessionId}`;
432
+ const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
433
+ const copyCommand = `cd ${quotedCwd} && ${command}`;
359
434
 
360
435
  broadcastLog({
361
436
  type: 'action',