coding-tool-x 3.5.9 → 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-C5W3axXs.js → Analytics-DPb1zErn.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
  7. package/dist/web/assets/{Home-C9TQNB6f.js → Home-BKYHKSax.js} +1 -1
  8. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  9. package/dist/web/assets/{PluginManager-9B_brLWT.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-vST8DRRg.js → SkillManager-CtEz5gGp.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-ov1KgRXR.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 +89 -12
  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 +83 -10
  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 +181 -27
  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 +6 -16
  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 +161 -183
  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-Bjt6mrsV.js +0 -1
  79. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  80. package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
  81. package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
  82. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  83. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  84. package/dist/web/assets/index-Duc7QP4e.js +0 -2
  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 -58
  91. package/src/server/services/web-ui-runtime.js +0 -54
@@ -45,24 +45,6 @@ function extractRepoPayload(source = {}) {
45
45
  };
46
46
  }
47
47
 
48
- function extractPluginQueryInfo(name, query = {}) {
49
- return {
50
- name,
51
- repoId: query.repoId || '',
52
- repoProvider: query.repoProvider || '',
53
- repoHost: query.repoHost || '',
54
- repoOwner: query.repoOwner || '',
55
- repoName: query.repoName || '',
56
- repoBranch: query.repoBranch || '',
57
- directory: query.directory || '',
58
- source: query.source || '',
59
- repoUrl: query.repoUrl || '',
60
- repoProjectPath: query.repoProjectPath || '',
61
- repoLocalPath: query.repoLocalPath || '',
62
- installPath: query.installPath || ''
63
- };
64
- }
65
-
66
48
  function sanitizeRepo(repo = {}) {
67
49
  const token = String(repo.token || '').trim();
68
50
  const sanitized = {
@@ -88,10 +70,7 @@ function sanitizeRepos(service, repos = []) {
88
70
  router.get('/', (req, res) => {
89
71
  try {
90
72
  const { platform, service } = getPluginsService(req);
91
- const forceRefresh = req.query.refresh === '1';
92
- const result = service.listPlugins({
93
- syncManagedLocalPlugins: forceRefresh
94
- });
73
+ const result = service.listPlugins();
95
74
 
96
75
  res.json({
97
76
  success: true,
@@ -192,46 +171,6 @@ router.post('/install', async (req, res) => {
192
171
  }
193
172
  });
194
173
 
195
- /**
196
- * 安装本地托管插件
197
- * POST /api/plugins/install-local
198
- * Body: { name }
199
- */
200
- router.post('/install-local', (req, res) => {
201
- try {
202
- const { platform, service } = getPluginsService(req);
203
- const { name } = req.body;
204
-
205
- if (!name) {
206
- return res.status(400).json({
207
- success: false,
208
- message: 'name is required'
209
- });
210
- }
211
-
212
- const result = service.installLocalPlugin(name);
213
- if (!result.success) {
214
- return res.status(400).json({
215
- success: false,
216
- message: result.error || result.message || 'Install failed'
217
- });
218
- }
219
-
220
- res.json({
221
- success: true,
222
- platform,
223
- plugin: result.plugin,
224
- message: result.message || `Plugin "${result.plugin?.name || name}" installed successfully`
225
- });
226
- } catch (err) {
227
- console.error('[Plugins API] Install local plugin error:', err);
228
- res.status(500).json({
229
- success: false,
230
- message: err.message
231
- });
232
- }
233
- });
234
-
235
174
  // ==================== 仓库管理 API ====================
236
175
 
237
176
  /**
@@ -483,66 +422,6 @@ router.post('/sync', async (req, res) => {
483
422
  }
484
423
  });
485
424
 
486
- /**
487
- * 获取插件文件列表
488
- * GET /api/plugins/:name/files
489
- */
490
- router.get('/:name/files', async (req, res) => {
491
- try {
492
- const { platform, service } = getPluginsService(req);
493
- const { name } = req.params;
494
- const pluginInfo = extractPluginQueryInfo(name, req.query);
495
- const result = await service.getPluginFiles(pluginInfo);
496
-
497
- res.json({
498
- success: true,
499
- platform,
500
- ...result
501
- });
502
- } catch (err) {
503
- console.error('[Plugins API] Get plugin files error:', err);
504
- res.status(500).json({
505
- success: false,
506
- message: err.message,
507
- files: []
508
- });
509
- }
510
- });
511
-
512
- /**
513
- * 获取插件单个文件内容
514
- * GET /api/plugins/:name/file/*
515
- */
516
- router.get('/:name/file/*', async (req, res) => {
517
- try {
518
- const { platform, service } = getPluginsService(req);
519
- const { name } = req.params;
520
- const filePath = req.params[0];
521
-
522
- if (!filePath) {
523
- return res.status(400).json({
524
- success: false,
525
- message: '请指定文件路径'
526
- });
527
- }
528
-
529
- const pluginInfo = extractPluginQueryInfo(name, req.query);
530
- const result = await service.getPluginFileContent(pluginInfo, filePath);
531
-
532
- res.json({
533
- success: true,
534
- platform,
535
- ...result
536
- });
537
- } catch (err) {
538
- console.error('[Plugins API] Get plugin file content error:', err);
539
- res.status(500).json({
540
- success: false,
541
- message: err.message
542
- });
543
- }
544
- });
545
-
546
425
  /**
547
426
  * 获取插件 README
548
427
  * GET /api/plugins/:name/readme
@@ -552,7 +431,36 @@ router.get('/:name/readme', async (req, res) => {
552
431
  try {
553
432
  const { platform, service } = getPluginsService(req);
554
433
  const { name } = req.params;
555
- const pluginInfo = extractPluginQueryInfo(name, req.query);
434
+ const {
435
+ repoId,
436
+ repoProvider,
437
+ repoHost,
438
+ repoOwner,
439
+ repoName,
440
+ repoBranch,
441
+ directory,
442
+ source,
443
+ repoUrl,
444
+ repoProjectPath,
445
+ repoLocalPath,
446
+ installPath
447
+ } = req.query;
448
+
449
+ const pluginInfo = {
450
+ name,
451
+ repoId,
452
+ repoProvider,
453
+ repoHost,
454
+ repoOwner,
455
+ repoName,
456
+ repoBranch,
457
+ directory,
458
+ source,
459
+ repoUrl,
460
+ repoProjectPath,
461
+ repoLocalPath,
462
+ installPath
463
+ };
556
464
 
557
465
  const readme = await service.getPluginReadme(pluginInfo);
558
466
 
@@ -575,14 +483,12 @@ router.get('/:name/readme', async (req, res) => {
575
483
  * 获取单个插件详情
576
484
  * GET /api/plugins/:name
577
485
  */
578
- router.get('/:name', async (req, res) => {
486
+ router.get('/:name', (req, res) => {
579
487
  try {
580
488
  const { platform, service } = getPluginsService(req);
581
489
  const { name } = req.params;
582
- const pluginInfo = extractPluginQueryInfo(name, req.query);
583
- const plugin = typeof service.getPluginDetail === 'function'
584
- ? await service.getPluginDetail(pluginInfo)
585
- : service.getPlugin(name);
490
+
491
+ const plugin = service.getPlugin(name);
586
492
 
587
493
  if (!plugin) {
588
494
  return res.status(404).json({
@@ -11,7 +11,7 @@ const {
11
11
  hasBackup,
12
12
  readSettings
13
13
  } = require('../services/settings-manager');
14
- const { getAllChannels, extractApiKeyFromHelper } = require('../services/channels');
14
+ const { getAllChannels, markChannelAsRecentlyUsed, extractApiKeyFromHelper } = require('../services/channels');
15
15
  const { clearNativeOAuth } = require('../services/native-oauth-adapters');
16
16
  const { clearAllLogs } = require('../websocket-server');
17
17
  const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('../../config/paths');
@@ -194,13 +194,15 @@ router.post('/start', async (req, res) => {
194
194
  }
195
195
 
196
196
  // 2. 从 settings.json 找到当前使用的渠道
197
- const currentChannel = findActiveChannelFromSettings();
197
+ let currentChannel = findActiveChannelFromSettings();
198
198
  if (!currentChannel) {
199
199
  return res.status(400).json({
200
200
  error: '无法从 settings.json 识别当前渠道。请先激活一个渠道。'
201
201
  });
202
202
  }
203
203
 
204
+ currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
205
+
204
206
  // 3. 保存当前激活渠道ID(用于代理模式)
205
207
  saveActiveChannelId(currentChannel.id);
206
208
  console.log(`[OK] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
@@ -218,7 +220,7 @@ router.post('/start', async (req, res) => {
218
220
 
219
221
  const updatedStatus = getProxyStatus();
220
222
  const channels = getAllChannels();
221
- const activeChannel = channels.find(ch => ch.enabled !== false);
223
+ const activeChannel = channels.find(ch => ch.id === currentChannel.id) || currentChannel;
222
224
 
223
225
  // 6. 通过 WebSocket 推送代理状态更新
224
226
  const { broadcastProxyState } = require('../websocket-server');
@@ -6,10 +6,118 @@ const readline = require('readline');
6
6
  const { getSessionsForProject, deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
7
7
  const { loadAliases } = require('../services/alias');
8
8
  const { broadcastLog } = require('../websocket-server');
9
- const { buildLaunchCommand } = require('../services/session-launch-command');
10
- const { NATIVE_PATHS, joinNativeBasePath } = require('../../config/paths');
9
+ const { NATIVE_PATHS } = require('../../config/paths');
11
10
  const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
12
11
 
12
+ function resolveClaudeSessionFile(projectName, sessionId, fullPath) {
13
+ const possiblePaths = [
14
+ path.join(fullPath, '.claude', 'sessions', sessionId + '.jsonl'),
15
+ path.join(CLAUDE_PROJECTS_DIR, projectName, sessionId + '.jsonl')
16
+ ];
17
+
18
+ for (const testPath of possiblePaths) {
19
+ if (fs.existsSync(testPath)) {
20
+ return {
21
+ sessionFile: testPath,
22
+ triedPaths: possiblePaths
23
+ };
24
+ }
25
+ }
26
+
27
+ return {
28
+ sessionFile: null,
29
+ triedPaths: possiblePaths
30
+ };
31
+ }
32
+
33
+ function getClaudeUserText(content) {
34
+ if (typeof content === 'string') {
35
+ return content === 'Warmup' ? '' : content;
36
+ }
37
+
38
+ if (!Array.isArray(content)) {
39
+ return '';
40
+ }
41
+
42
+ const parts = [];
43
+ for (const item of content) {
44
+ if (item?.type === 'text' && item.text) {
45
+ parts.push(item.text);
46
+ } else if (item?.type === 'image') {
47
+ parts.push('[图片]');
48
+ }
49
+ }
50
+
51
+ return parts.join('\n\n').trim();
52
+ }
53
+
54
+ function buildOutlinePreview(text = '') {
55
+ const firstLine = String(text)
56
+ .split('\n')
57
+ .map(line => line.trim())
58
+ .find(Boolean) || '(空消息)';
59
+ return firstLine.length > 42 ? `${firstLine.slice(0, 42)}...` : firstLine;
60
+ }
61
+
62
+ async function readClaudeSessionOutline(sessionFile) {
63
+ const outline = [];
64
+ const stream = fs.createReadStream(sessionFile, { encoding: 'utf8' });
65
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
66
+ let userMessageNumber = 0;
67
+
68
+ try {
69
+ for await (const line of rl) {
70
+ if (!line.trim()) continue;
71
+
72
+ let json;
73
+ try {
74
+ json = JSON.parse(line);
75
+ } catch (err) {
76
+ continue;
77
+ }
78
+
79
+ if (json.type !== 'user') {
80
+ continue;
81
+ }
82
+
83
+ const userText = getClaudeUserText(json.message?.content);
84
+ if (!userText) {
85
+ continue;
86
+ }
87
+
88
+ userMessageNumber += 1;
89
+ outline.push({
90
+ userMessageNumber,
91
+ preview: buildOutlinePreview(userText),
92
+ timestamp: json.timestamp || null
93
+ });
94
+ }
95
+ } finally {
96
+ rl.close();
97
+ stream.destroy();
98
+ }
99
+
100
+ return outline;
101
+ }
102
+
103
+ function normalizeForkOptions(body = {}) {
104
+ const alias = typeof body.alias === 'string' ? body.alias.trim() : '';
105
+ const rawNumber = body.afterUserMessageNumber;
106
+ const hasExplicitNumber = rawNumber !== undefined && rawNumber !== null && rawNumber !== '';
107
+ const parsedNumber = hasExplicitNumber ? parseInt(rawNumber, 10) : null;
108
+
109
+ if (hasExplicitNumber && (!Number.isInteger(parsedNumber) || parsedNumber <= 0)) {
110
+ const error = new Error('afterUserMessageNumber must be a positive integer');
111
+ error.statusCode = 400;
112
+ throw error;
113
+ }
114
+
115
+ return {
116
+ afterUserMessageNumber: parsedNumber,
117
+ alias: alias || null
118
+ };
119
+ }
120
+
13
121
  module.exports = (config) => {
14
122
  // GET /api/sessions/search/global - Search sessions across all projects
15
123
  router.get('/search/global', async (req, res) => {
@@ -137,11 +245,11 @@ module.exports = (config) => {
137
245
  router.post('/:projectName/:sessionId/fork', (req, res) => {
138
246
  try {
139
247
  const { projectName, sessionId } = req.params;
140
- const result = forkSession(config, projectName, sessionId);
248
+ const result = forkSession(config, projectName, sessionId, normalizeForkOptions(req.body));
141
249
  res.json(result);
142
250
  } catch (error) {
143
251
  console.error('Error forking session:', error);
144
- res.status(500).json({ error: error.message });
252
+ res.status(error.statusCode || 500).json({ error: error.message });
145
253
  }
146
254
  });
147
255
 
@@ -171,13 +279,13 @@ module.exports = (config) => {
171
279
  const year = now.getFullYear();
172
280
  const month = String(now.getMonth() + 1).padStart(2, '0');
173
281
  const day = String(now.getDate()).padStart(2, '0');
174
- sessionDir = joinNativeBasePath(NATIVE_PATHS.codex.sessions, String(year), month, day);
175
- sessionFile = joinNativeBasePath(sessionDir, `${newSessionId}.jsonl`);
282
+ sessionDir = path.join(NATIVE_PATHS.codex.sessions, String(year), month, day);
283
+ sessionFile = path.join(sessionDir, `${newSessionId}.jsonl`);
176
284
  } else if (toolType === 'gemini') {
177
285
  // Gemini: ~/.gemini/tmp/{hash}/chats/{sessionId}.json
178
286
  const pathHash = crypto.createHash('sha256').update(fullPath).digest('hex');
179
- sessionDir = joinNativeBasePath(NATIVE_PATHS.gemini.tmp, pathHash, 'chats');
180
- sessionFile = joinNativeBasePath(sessionDir, `${newSessionId}.json`);
287
+ sessionDir = path.join(NATIVE_PATHS.gemini.tmp, pathHash, 'chats');
288
+ sessionFile = path.join(sessionDir, `${newSessionId}.json`);
181
289
  } else {
182
290
  return res.status(400).json({ error: 'Invalid toolType. Must be claude, codex, or gemini' });
183
291
  }
@@ -275,6 +383,57 @@ module.exports = (config) => {
275
383
  }
276
384
  });
277
385
 
386
+ // GET /api/sessions/:projectName/:sessionId/status - Lightweight status for live sync
387
+ router.get('/:projectName/:sessionId/status', async (req, res) => {
388
+ try {
389
+ const { projectName, sessionId } = req.params;
390
+ const { fullPath } = parseRealProjectPath(projectName);
391
+ const { sessionFile, triedPaths } = resolveClaudeSessionFile(projectName, sessionId, fullPath);
392
+
393
+ if (!sessionFile) {
394
+ return res.status(404).json({
395
+ error: `Session file not found: ${sessionId}`,
396
+ triedPaths
397
+ });
398
+ }
399
+
400
+ const stats = await fs.promises.stat(sessionFile);
401
+ res.json({
402
+ sessionId,
403
+ lastModified: stats.mtime.toISOString(),
404
+ size: stats.size
405
+ });
406
+ } catch (error) {
407
+ console.error('Error fetching session status:', error);
408
+ res.status(500).json({ error: error.message });
409
+ }
410
+ });
411
+
412
+ // GET /api/sessions/:projectName/:sessionId/outline - Lightweight user outline for TOC and forking
413
+ router.get('/:projectName/:sessionId/outline', async (req, res) => {
414
+ try {
415
+ const { projectName, sessionId } = req.params;
416
+ const { fullPath } = parseRealProjectPath(projectName);
417
+ const { sessionFile, triedPaths } = resolveClaudeSessionFile(projectName, sessionId, fullPath);
418
+
419
+ if (!sessionFile) {
420
+ return res.status(404).json({
421
+ error: `Session file not found: ${sessionId}`,
422
+ triedPaths
423
+ });
424
+ }
425
+
426
+ const outline = await readClaudeSessionOutline(sessionFile);
427
+ res.json({
428
+ sessionId,
429
+ items: outline
430
+ });
431
+ } catch (error) {
432
+ console.error('Error fetching session outline:', error);
433
+ res.status(500).json({ error: error.message });
434
+ }
435
+ });
436
+
278
437
  // GET /api/sessions/:projectName/:sessionId/messages - Get session messages with pagination
279
438
  router.get('/:projectName/:sessionId/messages', async (req, res) => {
280
439
  try {
@@ -291,20 +450,12 @@ module.exports = (config) => {
291
450
  console.log(`[Messages API] Parsed project path: ${fullPath}`);
292
451
 
293
452
  // Try to find session file
294
- let sessionFile = null;
295
- const possiblePaths = [
296
- path.join(fullPath, '.claude', 'sessions', sessionId + '.jsonl'),
297
- path.join(CLAUDE_PROJECTS_DIR, projectName, sessionId + '.jsonl')
298
- ];
453
+ const { sessionFile, triedPaths: possiblePaths } = resolveClaudeSessionFile(projectName, sessionId, fullPath);
299
454
 
300
455
  console.log(`[Messages API] Trying paths:`, possiblePaths);
301
456
 
302
- for (const testPath of possiblePaths) {
303
- if (fs.existsSync(testPath)) {
304
- sessionFile = testPath;
305
- console.log(`[Messages API] Found session file: ${sessionFile}`);
306
- break;
307
- }
457
+ if (sessionFile) {
458
+ console.log(`[Messages API] Found session file: ${sessionFile}`);
308
459
  }
309
460
 
310
461
  if (!sessionFile) {
@@ -331,6 +482,7 @@ module.exports = (config) => {
331
482
  const stream = fs.createReadStream(sessionFile, { encoding: 'utf8' });
332
483
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
333
484
  let lastAssistantModel = null;
485
+ let userMessageNumber = 0;
334
486
 
335
487
  try {
336
488
  for await (const line of rl) {
@@ -362,6 +514,7 @@ module.exports = (config) => {
362
514
  let deferredToolResultContent = '';
363
515
 
364
516
  if (json.type === 'user') {
517
+ const normalizedUserText = getClaudeUserText(json.message?.content);
365
518
  if (typeof json.message?.content === 'string') {
366
519
  message.content = json.message.content;
367
520
  } else if (Array.isArray(json.message?.content)) {
@@ -389,6 +542,11 @@ module.exports = (config) => {
389
542
  deferredToolResultContent = toolResultParts.join('\n\n');
390
543
  }
391
544
  }
545
+
546
+ if (normalizedUserText) {
547
+ userMessageNumber += 1;
548
+ message.userMessageNumber = userMessageNumber;
549
+ }
392
550
  } else if (json.type === 'assistant') {
393
551
  if (Array.isArray(json.message?.content)) {
394
552
  const parts = [];
@@ -472,7 +630,6 @@ module.exports = (config) => {
472
630
  router.post('/:projectName/:sessionId/launch', async (req, res) => {
473
631
  try {
474
632
  const { projectName, sessionId } = req.params;
475
- const fork = Boolean(req.body?.fork);
476
633
  const path = require('path');
477
634
  const fs = require('fs');
478
635
 
@@ -565,18 +722,16 @@ module.exports = (config) => {
565
722
  broadcastLog({
566
723
  type: 'action',
567
724
  action: 'launch_session',
568
- message: `${fork ? '复制会话 Fork 命令' : '复制会话启动命令'} ${alias || sessionId.substring(0, 8)} (claude)`,
725
+ message: `复制会话启动命令 ${alias || sessionId.substring(0, 8)} (claude)`,
569
726
  sessionId,
570
727
  alias: alias || null,
571
728
  tool: 'claude',
572
729
  timestamp: Date.now()
573
730
  });
574
731
 
575
- const { command, copyCommand } = buildLaunchCommand({
576
- cwd,
577
- executable: 'claude',
578
- args: fork ? ['-r', sessionId, '--fork-session'] : ['-r', sessionId]
579
- });
732
+ const command = `claude -r ${sessionId}`;
733
+ const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
734
+ const copyCommand = `cd ${quotedCwd} && ${command}`;
580
735
 
581
736
  res.json({
582
737
  success: true,
@@ -584,7 +739,6 @@ module.exports = (config) => {
584
739
  sessionFile,
585
740
  sessionId,
586
741
  tool: 'claude',
587
- fork,
588
742
  command,
589
743
  copyCommand
590
744
  });
@@ -226,13 +226,6 @@ router.post('/', (req, res) => {
226
226
  });
227
227
  }
228
228
 
229
- if (proj.createWorktree && (!proj.branch || !proj.branch.trim())) {
230
- return res.status(400).json({
231
- success: false,
232
- message: '创建 worktree 时必须指定分支名'
233
- });
234
- }
235
-
236
229
  const normalizedBranch = normalizeBranchName(proj.branch);
237
230
  if (normalizedBranch) {
238
231
  const branchValidation = validateBranchName(normalizedBranch);
@@ -347,13 +340,6 @@ router.post('/:id/projects', (req, res) => {
347
340
  });
348
341
  }
349
342
 
350
- if (createWorktree && !normalizedBranch) {
351
- return res.status(400).json({
352
- success: false,
353
- message: '创建 worktree 时必须指定分支名'
354
- });
355
- }
356
-
357
343
  if (normalizedBranch) {
358
344
  const branchValidation = validateBranchName(normalizedBranch);
359
345
  if (!branchValidation.valid) {
@@ -1,5 +1,4 @@
1
1
  const express = require('express');
2
- const https = require('https');
3
2
  const path = require('path');
4
3
  const chalk = require('chalk');
5
4
  const { loadConfig } = require('../config/loader');
@@ -22,10 +21,8 @@ const { startProxyServer } = require('./proxy-server');
22
21
  const { startCodexProxyServer } = require('./codex-proxy-server');
23
22
  const { startGeminiProxyServer } = require('./gemini-proxy-server');
24
23
  const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
25
- const { createRemoteMutationGuard, isRemoteMutationAllowed } = require('./services/network-access');
24
+ const { createRemoteMutationGuard } = require('./services/network-access');
26
25
  const { createApiRequestLogger } = require('./services/request-logger');
27
- const { getLocalHttpsCredentials } = require('./services/https-cert');
28
- const { getWebUiProtocol, isHttpsEnabled, getWebUiBaseUrl, getWebSocketBaseUrl } = require('./services/web-ui-runtime');
29
26
 
30
27
  function getInquirer() {
31
28
  return require('inquirer');
@@ -62,8 +59,6 @@ function printPortToolIssue(issue = getPortToolIssue()) {
62
59
  async function startServer(port, host = '127.0.0.1', options = {}) {
63
60
  ensureStorageDirMigrated();
64
61
  const config = loadConfig();
65
- const webUiProtocol = getWebUiProtocol({ https: options.https });
66
- const httpsEnabled = isHttpsEnabled({ protocol: webUiProtocol });
67
62
  // 使用配置的端口,如果没有传入参数
68
63
  if (!port) {
69
64
  port = config.ports?.webUI || 19999;
@@ -137,9 +132,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
137
132
 
138
133
  const app = express();
139
134
  const lanMode = host === '0.0.0.0';
140
- const allowRemoteMutation = lanMode
141
- ? isRemoteMutationAllowed(process.env.CC_TOOL_ALLOW_REMOTE_WRITE)
142
- : true;
135
+ const allowRemoteMutation = process.env.CC_TOOL_ALLOW_REMOTE_WRITE === 'true';
143
136
 
144
137
  // Middleware
145
138
  app.use(express.json({ limit: '100mb' }));
@@ -163,7 +156,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
163
156
  app.use('/api', createRemoteMutationGuard({
164
157
  enabled: true,
165
158
  allowRemoteMutation,
166
- message: '当前已禁用 LAN 远程写操作,可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 重新开启。'
159
+ message: '出于安全考虑,LAN 模式默认仅允许本机执行写操作。可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 覆盖。'
167
160
  }));
168
161
 
169
162
  }
@@ -252,18 +245,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
252
245
  }
253
246
 
254
247
  // Start server(确保监听成功后才返回,避免命令误报“已启动”)
255
- let httpsCertificate = null;
256
- const server = httpsEnabled
257
- ? (() => {
258
- httpsCertificate = getLocalHttpsCredentials();
259
- const httpsServer = https.createServer({
260
- key: httpsCertificate.key,
261
- cert: httpsCertificate.cert
262
- }, app);
263
- httpsServer.listen(port, host);
264
- return httpsServer;
265
- })()
266
- : app.listen(port, host);
248
+ const server = app.listen(port, host);
267
249
  await new Promise((resolve) => {
268
250
  const onListening = () => {
269
251
  server.off('error', onError);
@@ -290,22 +272,18 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
290
272
  console.log(`\n[START] Coding-Tool Web UI running at:`);
291
273
  if (host === '0.0.0.0') {
292
274
  console.log(chalk.yellow(` [WARN] 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
293
- console.log(` ${getWebUiBaseUrl(port, { protocol: webUiProtocol })}`);
294
- console.log(chalk.gray(` ${webUiProtocol}://<your-ip>:${port} (LAN 访问)`));
275
+ console.log(` http://localhost:${port}`);
276
+ console.log(chalk.gray(` http://<your-ip>:${port} (LAN 访问)`));
295
277
  } else {
296
- console.log(` ${getWebUiBaseUrl(port, { protocol: webUiProtocol })}`);
278
+ console.log(` http://localhost:${port}`);
297
279
  }
298
280
 
299
281
  // 附加 WebSocket 服务器到同一个端口
300
282
  attachWebSocketServer(server, { host });
301
- console.log(` ${getWebSocketBaseUrl(port, { protocol: webUiProtocol })}/ws\n`);
302
-
303
- if (httpsEnabled && httpsCertificate?.generated) {
304
- console.log(chalk.gray(` [LOCK] 已生成本地 HTTPS 证书: ${httpsCertificate.certPath}`));
305
- }
283
+ console.log(` ws://localhost:${port}/ws\n`);
306
284
 
307
285
  if (host === '0.0.0.0' && !allowRemoteMutation) {
308
- console.log(chalk.yellow(' [LOCK] 已禁用 LAN 远程写操作 (CC_TOOL_ALLOW_REMOTE_WRITE=false)'));
286
+ console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认禁用'));
309
287
  }
310
288
  // 自动恢复代理状态
311
289
  autoRestoreProxies();