coding-tool-x 3.5.9 → 3.5.11
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-C5W3axXs.js → Analytics-Bw-0r4Ys.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-C6QRIeDj.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{Home-C9TQNB6f.js → Home-B_Z9ythw.js} +1 -1
- package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-Ce-mws6Q.js} +1 -1
- package/dist/web/assets/ProjectList-9Z7FFCIT.js +1 -0
- package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
- package/dist/web/assets/SessionList-CyETQcs8.js +1 -0
- package/dist/web/assets/SessionList-D12muXvx.css +1 -0
- package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-DSUcP1se.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-Cvbc11bJ.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-CTeg1xG-.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-channels.js +4 -0
- package/src/server/api/codex-proxy.js +45 -2
- package/src/server/api/codex-sessions.js +89 -12
- 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 +83 -10
- package/src/server/api/plugins.js +34 -128
- package/src/server/api/proxy.js +5 -3
- package/src/server/api/sessions.js +181 -27
- 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 +88 -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 +59 -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 +6 -16
- 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 +171 -183
- 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-Bjt6mrsV.js +0 -1
- package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
- package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
- package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
- package/dist/web/assets/icons-CEq2hYB-.js +0 -1
- package/dist/web/assets/index-Dih_bOsv.css +0 -1
- package/dist/web/assets/index-Duc7QP4e.js +0 -2
- 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 -58
- package/src/server/services/web-ui-runtime.js +0 -54
|
@@ -14,7 +14,6 @@ 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');
|
|
18
17
|
const { HOME_DIR } = require('../../config/paths');
|
|
19
18
|
|
|
20
19
|
function isNotFoundError(error) {
|
|
@@ -152,6 +151,73 @@ module.exports = (config) => {
|
|
|
152
151
|
}
|
|
153
152
|
});
|
|
154
153
|
|
|
154
|
+
/**
|
|
155
|
+
* GET /api/opencode/sessions/:projectName/:sessionId/status
|
|
156
|
+
* 获取会话轻量状态(用于实时刷新)
|
|
157
|
+
*/
|
|
158
|
+
router.get('/:projectName/:sessionId/status', (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
if (!isOpenCodeInstalled()) {
|
|
161
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { sessionId } = req.params;
|
|
165
|
+
const session = getSessionById(sessionId);
|
|
166
|
+
if (!session) {
|
|
167
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
res.json({
|
|
171
|
+
sessionId,
|
|
172
|
+
lastModified: session.mtime || null,
|
|
173
|
+
size: session.size || 0
|
|
174
|
+
});
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error('[OpenCode API] Failed to get session status:', err);
|
|
177
|
+
res.status(500).json({ error: err.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* GET /api/opencode/sessions/:projectName/:sessionId/outline
|
|
183
|
+
* 获取用户消息目录
|
|
184
|
+
*/
|
|
185
|
+
router.get('/:projectName/:sessionId/outline', (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
if (!isOpenCodeInstalled()) {
|
|
188
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { sessionId } = req.params;
|
|
192
|
+
const session = getSessionById(sessionId);
|
|
193
|
+
if (!session) {
|
|
194
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let userMessageNumber = 0;
|
|
198
|
+
const items = [];
|
|
199
|
+
for (const msg of getSessionMessages(sessionId)) {
|
|
200
|
+
if (msg.type !== 'user') continue;
|
|
201
|
+
const preview = typeof msg.content === 'string' ? msg.content.trim() : '';
|
|
202
|
+
if (!preview) continue;
|
|
203
|
+
userMessageNumber += 1;
|
|
204
|
+
items.push({
|
|
205
|
+
userMessageNumber,
|
|
206
|
+
preview: preview.length > 42 ? `${preview.slice(0, 42)}...` : preview,
|
|
207
|
+
timestamp: msg.timestamp || null
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
res.json({
|
|
212
|
+
sessionId,
|
|
213
|
+
items
|
|
214
|
+
});
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error('[OpenCode API] Failed to get session outline:', err);
|
|
217
|
+
res.status(500).json({ error: err.message });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
155
221
|
/**
|
|
156
222
|
* GET /api/opencode/sessions/:projectName/:sessionId/messages
|
|
157
223
|
* 获取会话的消息列表
|
|
@@ -168,7 +234,18 @@ module.exports = (config) => {
|
|
|
168
234
|
if (!session) {
|
|
169
235
|
return res.status(404).json({ error: 'Session not found' });
|
|
170
236
|
}
|
|
171
|
-
const
|
|
237
|
+
const baseMessages = getSessionMessages(sessionId);
|
|
238
|
+
let userMessageNumber = 0;
|
|
239
|
+
const convertedMessages = baseMessages.map((message) => {
|
|
240
|
+
if (message.type !== 'user') {
|
|
241
|
+
return message;
|
|
242
|
+
}
|
|
243
|
+
userMessageNumber += 1;
|
|
244
|
+
return {
|
|
245
|
+
...message,
|
|
246
|
+
userMessageNumber
|
|
247
|
+
};
|
|
248
|
+
});
|
|
172
249
|
|
|
173
250
|
// 分页处理
|
|
174
251
|
const pageNum = parseInt(page);
|
|
@@ -342,7 +419,6 @@ module.exports = (config) => {
|
|
|
342
419
|
}
|
|
343
420
|
|
|
344
421
|
const { projectName, sessionId } = req.params;
|
|
345
|
-
const fork = Boolean(req.body?.fork);
|
|
346
422
|
|
|
347
423
|
const session = getSessionById(sessionId);
|
|
348
424
|
if (!session) {
|
|
@@ -352,16 +428,14 @@ module.exports = (config) => {
|
|
|
352
428
|
const projects = getProjects();
|
|
353
429
|
const project = projects.find(p => p.name === projectName);
|
|
354
430
|
const cwd = session.directory || project?.fullPath || HOME_DIR;
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
args: fork ? ['--session', sessionId, '--fork'] : ['--session', sessionId]
|
|
359
|
-
});
|
|
431
|
+
const command = `opencode -r ${sessionId}`;
|
|
432
|
+
const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
|
|
433
|
+
const copyCommand = `cd ${quotedCwd} && ${command}`;
|
|
360
434
|
|
|
361
435
|
broadcastLog({
|
|
362
436
|
type: 'action',
|
|
363
437
|
action: 'launch_opencode_session',
|
|
364
|
-
message:
|
|
438
|
+
message: `复制 OpenCode 会话启动命令 ${sessionId.substring(0, 8)}`,
|
|
365
439
|
sessionId,
|
|
366
440
|
tool: 'opencode',
|
|
367
441
|
timestamp: Date.now()
|
|
@@ -372,7 +446,6 @@ module.exports = (config) => {
|
|
|
372
446
|
cwd,
|
|
373
447
|
sessionId,
|
|
374
448
|
tool: 'opencode',
|
|
375
|
-
fork,
|
|
376
449
|
command,
|
|
377
450
|
copyCommand
|
|
378
451
|
});
|
|
@@ -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 = [];
|
|
@@ -472,7 +630,6 @@ module.exports = (config) => {
|
|
|
472
630
|
router.post('/:projectName/:sessionId/launch', async (req, res) => {
|
|
473
631
|
try {
|
|
474
632
|
const { projectName, sessionId } = req.params;
|
|
475
|
-
const fork = Boolean(req.body?.fork);
|
|
476
633
|
const path = require('path');
|
|
477
634
|
const fs = require('fs');
|
|
478
635
|
|
|
@@ -565,18 +722,16 @@ module.exports = (config) => {
|
|
|
565
722
|
broadcastLog({
|
|
566
723
|
type: 'action',
|
|
567
724
|
action: 'launch_session',
|
|
568
|
-
message:
|
|
725
|
+
message: `复制会话启动命令 ${alias || sessionId.substring(0, 8)} (claude)`,
|
|
569
726
|
sessionId,
|
|
570
727
|
alias: alias || null,
|
|
571
728
|
tool: 'claude',
|
|
572
729
|
timestamp: Date.now()
|
|
573
730
|
});
|
|
574
731
|
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
args: fork ? ['-r', sessionId, '--fork-session'] : ['-r', sessionId]
|
|
579
|
-
});
|
|
732
|
+
const command = `claude -r ${sessionId}`;
|
|
733
|
+
const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
|
|
734
|
+
const copyCommand = `cd ${quotedCwd} && ${command}`;
|
|
580
735
|
|
|
581
736
|
res.json({
|
|
582
737
|
success: true,
|
|
@@ -584,7 +739,6 @@ module.exports = (config) => {
|
|
|
584
739
|
sessionFile,
|
|
585
740
|
sessionId,
|
|
586
741
|
tool: 'claude',
|
|
587
|
-
fork,
|
|
588
742
|
command,
|
|
589
743
|
copyCommand
|
|
590
744
|
});
|