coding-tool-x 3.5.5 → 3.5.6

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 (32) hide show
  1. package/README.md +8 -4
  2. package/dist/web/assets/{Analytics-gvYu5sCM.js → Analytics-CRNCHeui.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-CPlH8Ehd.js → ConfigTemplates-C0erJdo2.js} +1 -1
  4. package/dist/web/assets/{Home-B-qbu3uk.js → Home-CL5z6Q4d.js} +1 -1
  5. package/dist/web/assets/{PluginManager-B2tQ_YUq.js → PluginManager-hDx0XMO_.js} +1 -1
  6. package/dist/web/assets/{ProjectList-kDadoXXs.js → ProjectList-BNsz96av.js} +1 -1
  7. package/dist/web/assets/{SessionList-eLgITwTV.js → SessionList-CG1UhFo3.js} +1 -1
  8. package/dist/web/assets/{SkillManager-B7zEB5Op.js → SkillManager-D6Vwpajh.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-C-RzB3ud.js → WorkspaceManager-C3TjeOPy.js} +1 -1
  10. package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
  11. package/dist/web/assets/index-GuER-BmS.js +2 -0
  12. package/dist/web/assets/{index-BHeh2z0i.css → index-VGAxnLqi.css} +1 -1
  13. package/dist/web/index.html +3 -3
  14. package/package.json +1 -1
  15. package/src/commands/stats.js +41 -4
  16. package/src/index.js +1 -0
  17. package/src/server/api/codex-sessions.js +6 -3
  18. package/src/server/api/dashboard.js +25 -1
  19. package/src/server/api/gemini-sessions.js +6 -3
  20. package/src/server/api/hooks.js +17 -1
  21. package/src/server/api/opencode-sessions.js +6 -3
  22. package/src/server/api/plugins.js +24 -33
  23. package/src/server/api/sessions.js +6 -3
  24. package/src/server/index.js +6 -4
  25. package/src/server/services/codex-sessions.js +107 -9
  26. package/src/server/services/network-access.js +14 -0
  27. package/src/server/services/notification-hooks.js +175 -16
  28. package/src/server/services/plugins-service.js +502 -44
  29. package/src/server/services/session-launch-command.js +81 -0
  30. package/src/server/services/sessions.js +103 -33
  31. package/src/server/websocket-server.js +25 -1
  32. package/dist/web/assets/index-DG00t-zy.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-DG00t-zy.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-GuER-BmS.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-aWwwFAao.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendors-Fza9uSYn.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-BaTCPPL5.js">
13
- <link rel="modulepreload" crossorigin href="/assets/icons-DlxD2wZJ.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/icons-CQuif85v.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-BHeh2z0i.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-VGAxnLqi.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.5.5",
3
+ "version": "3.5.6",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -9,6 +9,9 @@ const TOOL_ENDPOINTS = {
9
9
  gemini: '/api/gemini/statistics',
10
10
  opencode: '/api/opencode/statistics'
11
11
  };
12
+ const SHARED_TOOL_TYPE_BY_CLI = {
13
+ claude: 'claude-code'
14
+ };
12
15
 
13
16
  /**
14
17
  * HTTP 请求辅助函数
@@ -154,6 +157,34 @@ function extractSummary(stats) {
154
157
  return summary;
155
158
  }
156
159
 
160
+ function extractToolSummary(stats, toolType) {
161
+ const sharedToolType = SHARED_TOOL_TYPE_BY_CLI[toolType];
162
+ if (!sharedToolType) {
163
+ return extractSummary(stats);
164
+ }
165
+
166
+ // `/api/statistics/*` is shared across tools, so Claude must be scoped explicitly.
167
+ const toolStats = stats?.byToolType?.[sharedToolType] || {};
168
+ return extractSummary({
169
+ summary: {
170
+ requests: normalizeNumber(toolStats.requests),
171
+ tokens: normalizeNumber(toolStats.tokens?.total),
172
+ cost: normalizeNumber(toolStats.cost),
173
+ inputTokens: normalizeNumber(toolStats.tokens?.input),
174
+ outputTokens: normalizeNumber(toolStats.tokens?.output),
175
+ cacheCreation: normalizeNumber(toolStats.tokens?.cacheCreation),
176
+ cacheRead: normalizeNumber(toolStats.tokens?.cacheRead),
177
+ reasoningTokens: normalizeNumber(toolStats.tokens?.reasoning),
178
+ cachedTokens: normalizeNumber(toolStats.tokens?.cached)
179
+ },
180
+ global: {
181
+ totalRequests: normalizeNumber(toolStats.requests),
182
+ totalTokens: normalizeNumber(toolStats.tokens?.total),
183
+ totalCost: normalizeNumber(toolStats.cost)
184
+ }
185
+ });
186
+ }
187
+
157
188
  function mergeSummaries(target, source) {
158
189
  target.requests += normalizeNumber(source.requests);
159
190
  target.tokens += normalizeNumber(source.tokens);
@@ -181,12 +212,12 @@ async function fetchToolStats(toolType, timeRange) {
181
212
 
182
213
  if (timeRange === 'today') {
183
214
  const response = await httpRequest('GET', `${endpointBase}/today`);
184
- return extractSummary(response.data);
215
+ return extractToolSummary(response.data, toolType);
185
216
  }
186
217
 
187
218
  if (timeRange === 'all') {
188
219
  const response = await httpRequest('GET', `${endpointBase}/summary`);
189
- return extractSummary(response.data);
220
+ return extractToolSummary(response.data, toolType);
190
221
  }
191
222
 
192
223
  const days = getRangeDays(timeRange);
@@ -194,7 +225,7 @@ async function fetchToolStats(toolType, timeRange) {
194
225
  for (let i = 0; i < days; i++) {
195
226
  const date = getDateString(i);
196
227
  const response = await httpRequest('GET', `${endpointBase}/daily/${date}`);
197
- const dailySummary = extractSummary(response.data);
228
+ const dailySummary = extractToolSummary(response.data, toolType);
198
229
  mergeSummaries(merged, dailySummary);
199
230
  }
200
231
  return merged;
@@ -394,5 +425,11 @@ async function handleStatsExport(type = null, format = 'json') {
394
425
 
395
426
  module.exports = {
396
427
  handleStats,
397
- handleStatsExport
428
+ handleStatsExport,
429
+ _test: {
430
+ extractSummary,
431
+ extractToolSummary,
432
+ fetchToolStats,
433
+ fetchOverallStats
434
+ }
398
435
  };
package/src/index.js CHANGED
@@ -39,6 +39,7 @@ function showHelp() {
39
39
 
40
40
  console.log(chalk.yellow('[START] 服务管理:'));
41
41
  console.log(' ctx start 启动所有服务(后台运行)');
42
+ console.log(' ctx start --host 启动所有服务(后台运行,允许 LAN 访问)');
42
43
  console.log(' ctx stop 停止所有服务');
43
44
  console.log(' ctx restart 重启所有服务');
44
45
  console.log(' ctx status 查看服务状态\n');
@@ -11,6 +11,7 @@ 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');
14
15
 
15
16
  const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
16
17
 
@@ -515,9 +516,11 @@ module.exports = (config) => {
515
516
  timestamp: Date.now()
516
517
  });
517
518
 
518
- const command = `codex resume ${sessionId}`;
519
- const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
520
- const copyCommand = `cd ${quotedCwd} && ${command}`;
519
+ const { command, copyCommand } = buildLaunchCommand({
520
+ cwd,
521
+ executable: 'codex',
522
+ args: ['resume', sessionId]
523
+ });
521
524
 
522
525
  res.json({
523
526
  success: true,
@@ -26,6 +26,30 @@ 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
+
29
53
  /**
30
54
  * GET /api/dashboard/init
31
55
  * 聚合首页所需的所有数据,一次请求返回
@@ -123,7 +147,7 @@ router.get('/init', async (req, res) => {
123
147
  opencode: opencodeCounts || { projectCount: 0, sessionCount: 0 }
124
148
  },
125
149
  todayStats: {
126
- claude: formatStats(claudeTodayStats),
150
+ claude: formatStats(extractToolScopedStats(claudeTodayStats, 'claude-code')),
127
151
  codex: formatStats(codexTodayStats),
128
152
  gemini: formatStats(geminiTodayStats),
129
153
  opencode: formatStats(opencodeTodayStats)
@@ -13,6 +13,7 @@ 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
 
17
18
  module.exports = (config) => {
18
19
  /**
@@ -407,9 +408,11 @@ module.exports = (config) => {
407
408
  const resumeIndex = sessionIndex + 1;
408
409
 
409
410
  // 构建 Gemini CLI 命令(使用 --resume <index> 恢复特定会话)
410
- const command = `gemini --resume ${resumeIndex}`;
411
- const quotedCwd = `"${String(projectPath).replace(/"/g, '\\"')}"`;
412
- const copyCommand = `cd ${quotedCwd} && ${command}`;
411
+ const { command, copyCommand } = buildLaunchCommand({
412
+ cwd: projectPath,
413
+ executable: 'gemini',
414
+ args: ['--resume', String(resumeIndex)]
415
+ });
413
416
 
414
417
  res.json({
415
418
  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 } = require('../services/network-access');
4
+ const { createSameOriginGuard, isLoopbackRequest } = require('../services/network-access');
5
5
 
6
6
  router.use(createSameOriginGuard({
7
7
  message: '禁止跨站访问通知配置接口'
@@ -42,4 +42,20 @@ 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
+
45
61
  module.exports = router;
@@ -14,6 +14,7 @@ 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');
17
18
  const { HOME_DIR } = require('../../config/paths');
18
19
 
19
20
  function isNotFoundError(error) {
@@ -350,9 +351,11 @@ module.exports = (config) => {
350
351
  const projects = getProjects();
351
352
  const project = projects.find(p => p.name === projectName);
352
353
  const cwd = session.directory || project?.fullPath || HOME_DIR;
353
- const command = `opencode -r ${sessionId}`;
354
- const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
355
- const copyCommand = `cd ${quotedCwd} && ${command}`;
354
+ const { command, copyCommand } = buildLaunchCommand({
355
+ cwd,
356
+ executable: 'opencode',
357
+ args: ['-r', sessionId]
358
+ });
356
359
 
357
360
  broadcastLog({
358
361
  type: 'action',
@@ -45,6 +45,24 @@ 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
+
48
66
  function sanitizeRepo(repo = {}) {
49
67
  const token = String(repo.token || '').trim();
50
68
  const sanitized = {
@@ -431,36 +449,7 @@ router.get('/:name/readme', async (req, res) => {
431
449
  try {
432
450
  const { platform, service } = getPluginsService(req);
433
451
  const { name } = req.params;
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
- };
452
+ const pluginInfo = extractPluginQueryInfo(name, req.query);
464
453
 
465
454
  const readme = await service.getPluginReadme(pluginInfo);
466
455
 
@@ -483,12 +472,14 @@ router.get('/:name/readme', async (req, res) => {
483
472
  * 获取单个插件详情
484
473
  * GET /api/plugins/:name
485
474
  */
486
- router.get('/:name', (req, res) => {
475
+ router.get('/:name', async (req, res) => {
487
476
  try {
488
477
  const { platform, service } = getPluginsService(req);
489
478
  const { name } = req.params;
490
-
491
- const plugin = service.getPlugin(name);
479
+ const pluginInfo = extractPluginQueryInfo(name, req.query);
480
+ const plugin = typeof service.getPluginDetail === 'function'
481
+ ? await service.getPluginDetail(pluginInfo)
482
+ : service.getPlugin(name);
492
483
 
493
484
  if (!plugin) {
494
485
  return res.status(404).json({
@@ -6,6 +6,7 @@ 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');
9
10
  const { NATIVE_PATHS } = require('../../config/paths');
10
11
  const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
11
12
 
@@ -570,9 +571,11 @@ module.exports = (config) => {
570
571
  timestamp: Date.now()
571
572
  });
572
573
 
573
- const command = `claude -r ${sessionId}`;
574
- const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
575
- const copyCommand = `cd ${quotedCwd} && ${command}`;
574
+ const { command, copyCommand } = buildLaunchCommand({
575
+ cwd,
576
+ executable: 'claude',
577
+ args: ['-r', sessionId]
578
+ });
576
579
 
577
580
  res.json({
578
581
  success: true,
@@ -21,7 +21,7 @@ const { startProxyServer } = require('./proxy-server');
21
21
  const { startCodexProxyServer } = require('./codex-proxy-server');
22
22
  const { startGeminiProxyServer } = require('./gemini-proxy-server');
23
23
  const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
24
- const { createRemoteMutationGuard } = require('./services/network-access');
24
+ const { createRemoteMutationGuard, isRemoteMutationAllowed } = require('./services/network-access');
25
25
  const { createApiRequestLogger } = require('./services/request-logger');
26
26
 
27
27
  function getInquirer() {
@@ -132,7 +132,9 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
132
132
 
133
133
  const app = express();
134
134
  const lanMode = host === '0.0.0.0';
135
- const allowRemoteMutation = process.env.CC_TOOL_ALLOW_REMOTE_WRITE === 'true';
135
+ const allowRemoteMutation = lanMode
136
+ ? isRemoteMutationAllowed(process.env.CC_TOOL_ALLOW_REMOTE_WRITE)
137
+ : true;
136
138
 
137
139
  // Middleware
138
140
  app.use(express.json({ limit: '100mb' }));
@@ -156,7 +158,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
156
158
  app.use('/api', createRemoteMutationGuard({
157
159
  enabled: true,
158
160
  allowRemoteMutation,
159
- message: '出于安全考虑,LAN 模式默认仅允许本机执行写操作。可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 覆盖。'
161
+ message: '当前已禁用 LAN 远程写操作,可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 重新开启。'
160
162
  }));
161
163
 
162
164
  }
@@ -283,7 +285,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
283
285
  console.log(` ws://localhost:${port}/ws\n`);
284
286
 
285
287
  if (host === '0.0.0.0' && !allowRemoteMutation) {
286
- console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认禁用'));
288
+ console.log(chalk.yellow(' [LOCK] 已禁用 LAN 远程写操作 (CC_TOOL_ALLOW_REMOTE_WRITE=false)'));
287
289
  }
288
290
  // 自动恢复代理状态
289
291
  autoRestoreProxies();
@@ -10,6 +10,7 @@ const ALL_SESSIONS_CACHE_TTL_MS = 20 * 1000;
10
10
  const PROJECTS_CACHE_TTL_MS = 300 * 1000;
11
11
  const PROJECT_SESSIONS_CACHE_TTL_MS = 120 * 1000;
12
12
  const FAST_META_READ_BYTES = 64 * 1024;
13
+ const MAX_SESSION_META_SUMMARY_CACHE_ENTRIES = 5000;
13
14
  const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
14
15
 
15
16
  let countsCache = {
@@ -32,6 +33,8 @@ let allSessionsCache = {
32
33
  value: []
33
34
  };
34
35
 
36
+ let sessionMetaSummaryCache = new Map();
37
+
35
38
  const CODEX_PROJECTS_CACHE_KEY = `${CacheKeys.PROJECTS}codex`;
36
39
  const codexSessionCacheKeys = new Set();
37
40
 
@@ -39,6 +42,47 @@ function getCodexSessionsCacheKey(projectName) {
39
42
  return `${CacheKeys.SESSIONS}codex:${projectName}`;
40
43
  }
41
44
 
45
+ function getSessionMetaSummaryCacheKey(filePath, fileMeta = {}) {
46
+ return `${filePath}:${fileMeta.mtimeMs || 0}:${fileMeta.size || 0}`;
47
+ }
48
+
49
+ function getCachedSessionMetaSummary(filePath, fileMeta) {
50
+ if (!fileMeta) {
51
+ return null;
52
+ }
53
+ return sessionMetaSummaryCache.get(getSessionMetaSummaryCacheKey(filePath, fileMeta)) || null;
54
+ }
55
+
56
+ function setCachedSessionMetaSummary(filePath, fileMeta, summary) {
57
+ if (!fileMeta || !summary?.payload) {
58
+ return;
59
+ }
60
+
61
+ const cacheKey = getSessionMetaSummaryCacheKey(filePath, fileMeta);
62
+ sessionMetaSummaryCache.delete(cacheKey);
63
+ sessionMetaSummaryCache.set(cacheKey, summary);
64
+
65
+ while (sessionMetaSummaryCache.size > MAX_SESSION_META_SUMMARY_CACHE_ENTRIES) {
66
+ const oldestKey = sessionMetaSummaryCache.keys().next().value;
67
+ if (!oldestKey) {
68
+ break;
69
+ }
70
+ sessionMetaSummaryCache.delete(oldestKey);
71
+ }
72
+ }
73
+
74
+ function pruneSessionMetaSummaryCache(files = []) {
75
+ const activeKeys = new Set(
76
+ files.map(file => getSessionMetaSummaryCacheKey(file.filePath, file))
77
+ );
78
+
79
+ for (const cacheKey of sessionMetaSummaryCache.keys()) {
80
+ if (!activeKeys.has(cacheKey)) {
81
+ sessionMetaSummaryCache.delete(cacheKey);
82
+ }
83
+ }
84
+ }
85
+
42
86
  /**
43
87
  * 获取会话目录
44
88
  */
@@ -128,6 +172,7 @@ function scanSessionFiles() {
128
172
  expiresAt,
129
173
  value: new Map(parsed.map(file => [file.sessionId, file]))
130
174
  };
175
+ pruneSessionMetaSummaryCache(parsed);
131
176
 
132
177
  return parsed;
133
178
  }
@@ -145,7 +190,7 @@ function getAllSessions() {
145
190
  const files = scanSessionFiles();
146
191
 
147
192
  const parsed = files.map(file => {
148
- const fastSummary = readSessionMetaSummaryFast(file.filePath);
193
+ const fastSummary = readSessionMetaSummaryFast(file.filePath, file);
149
194
  let session = null;
150
195
 
151
196
  if (fastSummary && fastSummary.payload) {
@@ -216,7 +261,7 @@ function normalizeSession(codexSession) {
216
261
  filePath: filePath || '',
217
262
  gitBranch: meta.git?.branch || null,
218
263
  firstMessage: preview || null,
219
- forkedFrom: null, // Codex 不支持 fork
264
+ forkedFrom: null,
220
265
 
221
266
  // 额外的 Codex 特有字段(前端可能需要)
222
267
  source: 'codex'
@@ -655,9 +700,10 @@ function forkSession(sessionId) {
655
700
 
656
701
  const newFileName = `rollout-${timestamp}-${newSessionId}.jsonl`;
657
702
  const newFilePath = path.join(targetDir, newFileName);
703
+ const rewrittenContent = rewriteForkedCodexSessionContent(content, newSessionId, now.toISOString());
658
704
 
659
705
  // 写入新文件
660
- fs.writeFileSync(newFilePath, content, 'utf8');
706
+ fs.writeFileSync(newFilePath, rewrittenContent, 'utf8');
661
707
 
662
708
  // 保存 fork 关系(复用 Claude Code 的 fork 关系存储)
663
709
  const { getForkRelations, saveForkRelations } = require('./sessions');
@@ -674,6 +720,50 @@ function forkSession(sessionId) {
674
720
  };
675
721
  }
676
722
 
723
+ function rewriteForkedCodexSessionContent(content, newSessionId, nowIsoTimestamp) {
724
+ const lines = String(content || '').split('\n');
725
+
726
+ return lines.map((line) => {
727
+ if (!line.trim()) {
728
+ return line;
729
+ }
730
+
731
+ let parsed;
732
+ try {
733
+ parsed = JSON.parse(line);
734
+ } catch (err) {
735
+ return line;
736
+ }
737
+
738
+ if (parsed.type === 'session_meta' && parsed.payload && typeof parsed.payload === 'object') {
739
+ return JSON.stringify({
740
+ ...parsed,
741
+ timestamp: nowIsoTimestamp,
742
+ payload: {
743
+ ...parsed.payload,
744
+ id: newSessionId,
745
+ timestamp: nowIsoTimestamp
746
+ }
747
+ });
748
+ }
749
+
750
+ if (parsed.type === 'event' && parsed.event && typeof parsed.event === 'object' && parsed.event.type === 'session_start') {
751
+ const nextEvent = { ...parsed.event };
752
+ if (typeof nextEvent.session_id === 'string' && nextEvent.session_id.trim()) {
753
+ nextEvent.session_id = newSessionId;
754
+ }
755
+
756
+ return JSON.stringify({
757
+ ...parsed,
758
+ timestamp: nowIsoTimestamp,
759
+ event: nextEvent
760
+ });
761
+ }
762
+
763
+ return line;
764
+ }).join('\n');
765
+ }
766
+
677
767
  /**
678
768
  * 获取会话排序(按项目)
679
769
  * @param {string} projectName - 项目名称
@@ -803,7 +893,12 @@ function extractCodexPreviewFromResponseItem(payload = {}) {
803
893
  return text.substring(0, 100);
804
894
  }
805
895
 
806
- function readSessionMetaSummaryFast(filePath) {
896
+ function readSessionMetaSummaryFast(filePath, fileMeta = null) {
897
+ const cached = getCachedSessionMetaSummary(filePath, fileMeta);
898
+ if (cached) {
899
+ return cached;
900
+ }
901
+
807
902
  let fd;
808
903
  try {
809
904
  fd = fs.openSync(filePath, 'r');
@@ -841,7 +936,9 @@ function readSessionMetaSummaryFast(filePath) {
841
936
  }
842
937
 
843
938
  if (!payload) return null;
844
- return { payload, preview };
939
+ const summary = { payload, preview };
940
+ setCachedSessionMetaSummary(filePath, fileMeta, summary);
941
+ return summary;
845
942
  } catch (err) {
846
943
  return null;
847
944
  } finally {
@@ -855,8 +952,8 @@ function readSessionMetaSummaryFast(filePath) {
855
952
  }
856
953
  }
857
954
 
858
- function readSessionMetaPayloadFast(filePath) {
859
- const summary = readSessionMetaSummaryFast(filePath);
955
+ function readSessionMetaPayloadFast(filePath, fileMeta = null) {
956
+ const summary = readSessionMetaSummaryFast(filePath, fileMeta);
860
957
  return summary?.payload || null;
861
958
  }
862
959
 
@@ -881,7 +978,7 @@ function calculateProjectAndSessionCounts() {
881
978
 
882
979
  const projectNames = new Set();
883
980
  sessions.forEach((session) => {
884
- const payload = readSessionMetaPayloadFast(session.filePath);
981
+ const payload = readSessionMetaPayloadFast(session.filePath, session);
885
982
  const projectName = extractCodexProjectNameFromMeta(payload || {});
886
983
  if (projectName) {
887
984
  projectNames.add(projectName);
@@ -932,5 +1029,6 @@ module.exports = {
932
1029
  saveSessionOrder,
933
1030
  getProjectOrder,
934
1031
  saveProjectOrder,
935
- getProjectAndSessionCounts
1032
+ getProjectAndSessionCounts,
1033
+ rewriteForkedCodexSessionContent
936
1034
  };
@@ -50,6 +50,19 @@ function isSameOriginRequest(req) {
50
50
  }
51
51
  }
52
52
 
53
+ function isRemoteMutationAllowed(envValue) {
54
+ if (envValue === undefined || envValue === null) {
55
+ return true;
56
+ }
57
+
58
+ const normalized = String(envValue).trim().toLowerCase();
59
+ if (!normalized) {
60
+ return true;
61
+ }
62
+
63
+ return !['0', 'false', 'no', 'off'].includes(normalized);
64
+ }
65
+
53
66
  function createRemoteMutationGuard(options = {}) {
54
67
  const enabled = options.enabled === true;
55
68
  const allowRemoteMutation = options.allowRemoteMutation === true;
@@ -112,6 +125,7 @@ module.exports = {
112
125
  isLoopbackAddress,
113
126
  isLoopbackRequest,
114
127
  isSameOriginRequest,
128
+ isRemoteMutationAllowed,
115
129
  createRemoteMutationGuard,
116
130
  createRemoteRouteGuard,
117
131
  createSameOriginGuard