coding-tool-x 3.5.5 → 3.5.7
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 +25 -4
- package/bin/ctx.js +6 -1
- package/dist/web/assets/{Analytics-gvYu5sCM.js → Analytics-C6DEmD3D.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CPlH8Ehd.js → ConfigTemplates-Cf_iTpC4.js} +1 -1
- package/dist/web/assets/{Home-B-qbu3uk.js → Home-BtBmYLJ1.js} +1 -1
- package/dist/web/assets/{PluginManager-B2tQ_YUq.js → PluginManager-DEk8vSw5.js} +1 -1
- package/dist/web/assets/{ProjectList-kDadoXXs.js → ProjectList-BMVhA_Kh.js} +1 -1
- package/dist/web/assets/{SessionList-eLgITwTV.js → SessionList-B5ioAXxg.js} +1 -1
- package/dist/web/assets/{SkillManager-B7zEB5Op.js → SkillManager-DcZOiiSf.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-C-RzB3ud.js → WorkspaceManager-BHqI8aGV.js} +1 -1
- package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
- package/dist/web/assets/index-CtByKdkA.js +2 -0
- package/dist/web/assets/{index-BHeh2z0i.css → index-VGAxnLqi.css} +1 -1
- package/dist/web/index.html +3 -3
- package/docs/Caddyfile.example +19 -0
- package/docs/reverse-proxy-https.md +57 -0
- package/package.json +2 -1
- package/src/commands/daemon.js +33 -5
- package/src/commands/stats.js +41 -4
- package/src/commands/ui.js +12 -3
- package/src/config/paths.js +6 -0
- package/src/index.js +125 -34
- 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 +31 -9
- package/src/server/services/codex-sessions.js +107 -9
- package/src/server/services/https-cert.js +171 -0
- package/src/server/services/network-access.js +61 -2
- package/src/server/services/notification-hooks.js +181 -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/services/web-ui-runtime.js +54 -0
- package/src/server/websocket-server.js +35 -4
- package/dist/web/assets/index-DG00t-zy.js +0 -2
|
@@ -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
+
const https = require('https');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const chalk = require('chalk');
|
|
4
5
|
const { loadConfig } = require('../config/loader');
|
|
@@ -21,8 +22,10 @@ const { startProxyServer } = require('./proxy-server');
|
|
|
21
22
|
const { startCodexProxyServer } = require('./codex-proxy-server');
|
|
22
23
|
const { startGeminiProxyServer } = require('./gemini-proxy-server');
|
|
23
24
|
const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
|
|
24
|
-
const { createRemoteMutationGuard } = require('./services/network-access');
|
|
25
|
+
const { createRemoteMutationGuard, isRemoteMutationAllowed } = require('./services/network-access');
|
|
25
26
|
const { createApiRequestLogger } = require('./services/request-logger');
|
|
27
|
+
const { getLocalHttpsCredentials } = require('./services/https-cert');
|
|
28
|
+
const { getWebUiProtocol, isHttpsEnabled, getWebUiBaseUrl, getWebSocketBaseUrl } = require('./services/web-ui-runtime');
|
|
26
29
|
|
|
27
30
|
function getInquirer() {
|
|
28
31
|
return require('inquirer');
|
|
@@ -59,6 +62,8 @@ function printPortToolIssue(issue = getPortToolIssue()) {
|
|
|
59
62
|
async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
60
63
|
ensureStorageDirMigrated();
|
|
61
64
|
const config = loadConfig();
|
|
65
|
+
const webUiProtocol = getWebUiProtocol({ https: options.https });
|
|
66
|
+
const httpsEnabled = isHttpsEnabled({ protocol: webUiProtocol });
|
|
62
67
|
// 使用配置的端口,如果没有传入参数
|
|
63
68
|
if (!port) {
|
|
64
69
|
port = config.ports?.webUI || 19999;
|
|
@@ -132,7 +137,9 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
132
137
|
|
|
133
138
|
const app = express();
|
|
134
139
|
const lanMode = host === '0.0.0.0';
|
|
135
|
-
const allowRemoteMutation =
|
|
140
|
+
const allowRemoteMutation = lanMode
|
|
141
|
+
? isRemoteMutationAllowed(process.env.CC_TOOL_ALLOW_REMOTE_WRITE)
|
|
142
|
+
: true;
|
|
136
143
|
|
|
137
144
|
// Middleware
|
|
138
145
|
app.use(express.json({ limit: '100mb' }));
|
|
@@ -156,7 +163,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
156
163
|
app.use('/api', createRemoteMutationGuard({
|
|
157
164
|
enabled: true,
|
|
158
165
|
allowRemoteMutation,
|
|
159
|
-
message: '
|
|
166
|
+
message: '当前已禁用 LAN 远程写操作,可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 重新开启。'
|
|
160
167
|
}));
|
|
161
168
|
|
|
162
169
|
}
|
|
@@ -245,7 +252,18 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
// Start server(确保监听成功后才返回,避免命令误报“已启动”)
|
|
248
|
-
|
|
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);
|
|
249
267
|
await new Promise((resolve) => {
|
|
250
268
|
const onListening = () => {
|
|
251
269
|
server.off('error', onError);
|
|
@@ -272,18 +290,22 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
272
290
|
console.log(`\n[START] Coding-Tool Web UI running at:`);
|
|
273
291
|
if (host === '0.0.0.0') {
|
|
274
292
|
console.log(chalk.yellow(` [WARN] 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
|
|
275
|
-
console.log(`
|
|
276
|
-
console.log(chalk.gray(`
|
|
293
|
+
console.log(` ${getWebUiBaseUrl(port, { protocol: webUiProtocol })}`);
|
|
294
|
+
console.log(chalk.gray(` ${webUiProtocol}://<your-ip>:${port} (LAN 访问)`));
|
|
277
295
|
} else {
|
|
278
|
-
console.log(`
|
|
296
|
+
console.log(` ${getWebUiBaseUrl(port, { protocol: webUiProtocol })}`);
|
|
279
297
|
}
|
|
280
298
|
|
|
281
299
|
// 附加 WebSocket 服务器到同一个端口
|
|
282
300
|
attachWebSocketServer(server, { host });
|
|
283
|
-
console.log(`
|
|
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
|
+
}
|
|
284
306
|
|
|
285
307
|
if (host === '0.0.0.0' && !allowRemoteMutation) {
|
|
286
|
-
console.log(chalk.yellow(' [LOCK]
|
|
308
|
+
console.log(chalk.yellow(' [LOCK] 已禁用 LAN 远程写操作 (CC_TOOL_ALLOW_REMOTE_WRITE=false)'));
|
|
287
309
|
}
|
|
288
310
|
// 自动恢复代理状态
|
|
289
311
|
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
|
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const selfsigned = require('selfsigned');
|
|
6
|
+
const { PATHS } = require('../../config/paths');
|
|
7
|
+
|
|
8
|
+
function dedupeSorted(values = []) {
|
|
9
|
+
return [...new Set(values.filter(Boolean).map((value) => String(value).trim()).filter(Boolean))].sort();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isIpAddress(value) {
|
|
13
|
+
return net.isIP(String(value || '').trim()) !== 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isValidDnsLabel(value) {
|
|
17
|
+
return /^[A-Za-z0-9.-]+$/.test(String(value || '').trim());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectLocalCertificateHosts() {
|
|
21
|
+
const hosts = ['localhost', '127.0.0.1', '::1'];
|
|
22
|
+
const hostname = os.hostname();
|
|
23
|
+
if (isValidDnsLabel(hostname)) {
|
|
24
|
+
hosts.push(hostname);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const interfaces = os.networkInterfaces();
|
|
28
|
+
Object.values(interfaces).forEach((entries) => {
|
|
29
|
+
(entries || []).forEach((entry) => {
|
|
30
|
+
const address = String(entry?.address || '').trim();
|
|
31
|
+
if (!address) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (entry?.family === 'IPv4' || entry?.family === 4) {
|
|
36
|
+
hosts.push(address);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if ((entry?.family === 'IPv6' || entry?.family === 6) && address !== '::1' && !address.startsWith('fe80::')) {
|
|
41
|
+
hosts.push(address);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return dedupeSorted(hosts);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildSubjectAltNames(hosts = collectLocalCertificateHosts()) {
|
|
50
|
+
return dedupeSorted(hosts).map((value) => (
|
|
51
|
+
isIpAddress(value)
|
|
52
|
+
? { type: 7, ip: value }
|
|
53
|
+
: { type: 2, value }
|
|
54
|
+
));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureParentDir(filePath) {
|
|
58
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readMeta(metaPath) {
|
|
62
|
+
if (!fs.existsSync(metaPath)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function needsRegeneration(hosts, meta) {
|
|
74
|
+
if (!meta || !Array.isArray(meta.hosts)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const currentHosts = dedupeSorted(hosts);
|
|
78
|
+
const storedHosts = dedupeSorted(meta.hosts);
|
|
79
|
+
return currentHosts.length !== storedHosts.length || currentHosts.some((value, index) => value !== storedHosts[index]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generateSelfSignedCertificate(hosts) {
|
|
83
|
+
const primaryHost = hosts.find((value) => value === '127.0.0.1')
|
|
84
|
+
|| hosts.find((value) => !isIpAddress(value))
|
|
85
|
+
|| hosts[0]
|
|
86
|
+
|| '127.0.0.1';
|
|
87
|
+
|
|
88
|
+
const attrs = [{ name: 'commonName', value: primaryHost }];
|
|
89
|
+
return selfsigned.generate(attrs, {
|
|
90
|
+
algorithm: 'sha256',
|
|
91
|
+
days: 3650,
|
|
92
|
+
keySize: 2048,
|
|
93
|
+
extensions: [
|
|
94
|
+
{ name: 'basicConstraints', cA: false },
|
|
95
|
+
{
|
|
96
|
+
name: 'keyUsage',
|
|
97
|
+
digitalSignature: true,
|
|
98
|
+
keyEncipherment: true
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'extKeyUsage',
|
|
102
|
+
serverAuth: true
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'subjectAltName',
|
|
106
|
+
altNames: buildSubjectAltNames(hosts)
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ensureLocalHttpsCertificate(options = {}) {
|
|
113
|
+
const keyPath = options.keyPath || PATHS.https.key;
|
|
114
|
+
const certPath = options.certPath || PATHS.https.cert;
|
|
115
|
+
const metaPath = options.metaPath || PATHS.https.meta;
|
|
116
|
+
const hosts = dedupeSorted(options.hosts || collectLocalCertificateHosts());
|
|
117
|
+
const existingMeta = readMeta(metaPath);
|
|
118
|
+
const hasFiles = fs.existsSync(keyPath) && fs.existsSync(certPath);
|
|
119
|
+
|
|
120
|
+
if (hasFiles && !needsRegeneration(hosts, existingMeta)) {
|
|
121
|
+
return {
|
|
122
|
+
keyPath,
|
|
123
|
+
certPath,
|
|
124
|
+
metaPath,
|
|
125
|
+
hosts,
|
|
126
|
+
generated: false
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ensureParentDir(keyPath);
|
|
131
|
+
ensureParentDir(certPath);
|
|
132
|
+
ensureParentDir(metaPath);
|
|
133
|
+
|
|
134
|
+
const certificate = generateSelfSignedCertificate(hosts);
|
|
135
|
+
fs.writeFileSync(keyPath, certificate.private, 'utf8');
|
|
136
|
+
fs.writeFileSync(certPath, certificate.cert, 'utf8');
|
|
137
|
+
fs.writeFileSync(metaPath, JSON.stringify({
|
|
138
|
+
hosts,
|
|
139
|
+
generatedAt: new Date().toISOString()
|
|
140
|
+
}, null, 2), 'utf8');
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
keyPath,
|
|
144
|
+
certPath,
|
|
145
|
+
metaPath,
|
|
146
|
+
hosts,
|
|
147
|
+
generated: true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getLocalHttpsCredentials(options = {}) {
|
|
152
|
+
const certificate = ensureLocalHttpsCertificate(options);
|
|
153
|
+
return {
|
|
154
|
+
...certificate,
|
|
155
|
+
key: fs.readFileSync(certificate.keyPath),
|
|
156
|
+
cert: fs.readFileSync(certificate.certPath)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
ensureLocalHttpsCertificate,
|
|
162
|
+
getLocalHttpsCredentials,
|
|
163
|
+
_test: {
|
|
164
|
+
dedupeSorted,
|
|
165
|
+
isIpAddress,
|
|
166
|
+
isValidDnsLabel,
|
|
167
|
+
collectLocalCertificateHosts,
|
|
168
|
+
buildSubjectAltNames,
|
|
169
|
+
needsRegeneration
|
|
170
|
+
}
|
|
171
|
+
};
|