coding-tool-x 3.5.8 → 3.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +4 -25
- package/bin/ctx.js +1 -6
- package/dist/web/assets/{Analytics-BzoNzfbi.js → Analytics-DPb1zErn.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-O4ikBt1o.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
- package/dist/web/assets/{Home-BQjsnblU.js → Home-BKYHKSax.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{PluginManager-DS_DJnVc.js → PluginManager-jgeQOE24.js} +1 -1
- package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
- package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
- package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
- package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
- package/dist/web/assets/{SkillManager-DpNE02r0.js → SkillManager-CtEz5gGp.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DMY7_SHh.js → WorkspaceManager-D3B0Y5XE.js} +1 -1
- package/dist/web/assets/icons-lKCTySl7.js +1 -0
- package/dist/web/assets/index-BS7aplYp.css +1 -0
- package/dist/web/assets/index-BnyNm74G.js +2 -0
- package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
- package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
- package/dist/web/index.html +8 -8
- package/package.json +1 -2
- package/src/commands/daemon.js +5 -33
- package/src/commands/export-config.js +6 -6
- package/src/commands/stats.js +4 -41
- package/src/commands/ui.js +3 -12
- package/src/config/channel-preset-websites.js +114 -0
- package/src/config/default.js +9 -5
- package/src/config/loader.js +2 -2
- package/src/config/model-metadata.json +16 -20
- package/src/config/paths.js +33 -166
- package/src/index.js +34 -125
- package/src/server/api/agents.js +2 -52
- package/src/server/api/codex-proxy.js +4 -2
- package/src/server/api/codex-sessions.js +88 -9
- package/src/server/api/commands.js +2 -38
- package/src/server/api/dashboard.js +1 -25
- package/src/server/api/gemini-proxy.js +4 -2
- package/src/server/api/gemini-sessions.js +86 -9
- package/src/server/api/hooks.js +1 -17
- package/src/server/api/opencode-proxy.js +7 -2
- package/src/server/api/opencode-sessions.js +82 -7
- package/src/server/api/plugins.js +34 -128
- package/src/server/api/proxy.js +5 -3
- package/src/server/api/sessions.js +180 -24
- package/src/server/api/workspaces.js +0 -14
- package/src/server/index.js +9 -31
- package/src/server/services/agents-service.js +62 -269
- package/src/server/services/base/base-channel-service.js +3 -1
- package/src/server/services/channels.js +5 -0
- package/src/server/services/codex-channels.js +2 -0
- package/src/server/services/codex-sessions.js +78 -108
- package/src/server/services/commands-service.js +81 -281
- package/src/server/services/config-export-service.js +7 -7
- package/src/server/services/config-registry-service.js +9 -8
- package/src/server/services/config-sync-manager.js +41 -61
- package/src/server/services/config-sync-service.js +3 -3
- package/src/server/services/gemini-channels.js +17 -6
- package/src/server/services/gemini-config.js +4 -3
- package/src/server/services/gemini-sessions.js +51 -26
- package/src/server/services/gemini-settings-manager.js +3 -2
- package/src/server/services/mcp-service.js +14 -9
- package/src/server/services/native-oauth-adapters.js +3 -3
- package/src/server/services/network-access.js +2 -61
- package/src/server/services/notification-hooks.js +19 -184
- package/src/server/services/opencode-channels.js +9 -0
- package/src/server/services/opencode-sessions.js +4 -4
- package/src/server/services/opencode-settings-manager.js +3 -3
- package/src/server/services/plugins-service.js +62 -996
- package/src/server/services/prompts-service.js +9 -5
- package/src/server/services/sessions.js +128 -99
- package/src/server/services/skill-service.js +18 -155
- package/src/server/websocket-server.js +4 -35
- package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
- package/dist/web/assets/Home-qzk118Of.css +0 -1
- package/dist/web/assets/ProjectList-CqYDtsHx.js +0 -1
- package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
- package/dist/web/assets/SessionList-CfPtcq6Y.css +0 -1
- package/dist/web/assets/SessionList-DMlLtMCz.js +0 -1
- package/dist/web/assets/icons-CEq2hYB-.js +0 -1
- package/dist/web/assets/index-Clf0l3wc.js +0 -2
- package/dist/web/assets/index-Dih_bOsv.css +0 -1
- package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
- package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
- package/docs/Caddyfile.example +0 -19
- package/docs/reverse-proxy-https.md +0 -57
- package/src/server/services/https-cert.js +0 -171
- package/src/server/services/session-launch-command.js +0 -81
- 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
|
|
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
|
|
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',
|
|
486
|
+
router.get('/:name', (req, res) => {
|
|
579
487
|
try {
|
|
580
488
|
const { platform, service } = getPluginsService(req);
|
|
581
489
|
const { name } = req.params;
|
|
582
|
-
|
|
583
|
-
const plugin =
|
|
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({
|
package/src/server/api/proxy.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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 {
|
|
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 =
|
|
175
|
-
sessionFile =
|
|
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 =
|
|
180
|
-
sessionFile =
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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 = [];
|
|
@@ -571,11 +729,9 @@ module.exports = (config) => {
|
|
|
571
729
|
timestamp: Date.now()
|
|
572
730
|
});
|
|
573
731
|
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
args: ['-r', sessionId]
|
|
578
|
-
});
|
|
732
|
+
const command = `claude -r ${sessionId}`;
|
|
733
|
+
const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
|
|
734
|
+
const copyCommand = `cd ${quotedCwd} && ${command}`;
|
|
579
735
|
|
|
580
736
|
res.json({
|
|
581
737
|
success: true,
|
|
@@ -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) {
|
package/src/server/index.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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: '
|
|
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
|
-
|
|
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(`
|
|
294
|
-
console.log(chalk.gray(`
|
|
275
|
+
console.log(` http://localhost:${port}`);
|
|
276
|
+
console.log(chalk.gray(` http://<your-ip>:${port} (LAN 访问)`));
|
|
295
277
|
} else {
|
|
296
|
-
console.log(`
|
|
278
|
+
console.log(` http://localhost:${port}`);
|
|
297
279
|
}
|
|
298
280
|
|
|
299
281
|
// 附加 WebSocket 服务器到同一个端口
|
|
300
282
|
attachWebSocketServer(server, { host });
|
|
301
|
-
console.log(`
|
|
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]
|
|
286
|
+
console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认禁用'));
|
|
309
287
|
}
|
|
310
288
|
// 自动恢复代理状态
|
|
311
289
|
autoRestoreProxies();
|