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.
- package/README.md +8 -4
- package/dist/web/assets/{Analytics-gvYu5sCM.js → Analytics-CRNCHeui.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CPlH8Ehd.js → ConfigTemplates-C0erJdo2.js} +1 -1
- package/dist/web/assets/{Home-B-qbu3uk.js → Home-CL5z6Q4d.js} +1 -1
- package/dist/web/assets/{PluginManager-B2tQ_YUq.js → PluginManager-hDx0XMO_.js} +1 -1
- package/dist/web/assets/{ProjectList-kDadoXXs.js → ProjectList-BNsz96av.js} +1 -1
- package/dist/web/assets/{SessionList-eLgITwTV.js → SessionList-CG1UhFo3.js} +1 -1
- package/dist/web/assets/{SkillManager-B7zEB5Op.js → SkillManager-D6Vwpajh.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-C-RzB3ud.js → WorkspaceManager-C3TjeOPy.js} +1 -1
- package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
- package/dist/web/assets/index-GuER-BmS.js +2 -0
- package/dist/web/assets/{index-BHeh2z0i.css → index-VGAxnLqi.css} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +41 -4
- package/src/index.js +1 -0
- package/src/server/api/codex-sessions.js +6 -3
- package/src/server/api/dashboard.js +25 -1
- package/src/server/api/gemini-sessions.js +6 -3
- package/src/server/api/hooks.js +17 -1
- package/src/server/api/opencode-sessions.js +6 -3
- package/src/server/api/plugins.js +24 -33
- package/src/server/api/sessions.js +6 -3
- package/src/server/index.js +6 -4
- package/src/server/services/codex-sessions.js +107 -9
- package/src/server/services/network-access.js +14 -0
- package/src/server/services/notification-hooks.js +175 -16
- package/src/server/services/plugins-service.js +502 -44
- package/src/server/services/session-launch-command.js +81 -0
- package/src/server/services/sessions.js +103 -33
- package/src/server/websocket-server.js +25 -1
- package/dist/web/assets/index-DG00t-zy.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
package/src/commands/stats.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
519
|
-
|
|
520
|
-
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
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,
|
package/src/server/api/hooks.js
CHANGED
|
@@ -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
|
|
354
|
-
|
|
355
|
-
|
|
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.
|
|
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
|
|
574
|
-
|
|
575
|
-
|
|
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,
|
package/src/server/index.js
CHANGED
|
@@ -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 =
|
|
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: '
|
|
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]
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|