coding-tool-x 3.5.9 → 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-C5W3axXs.js → Analytics-DPb1zErn.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
- package/dist/web/assets/{Home-C9TQNB6f.js → Home-BKYHKSax.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{PluginManager-9B_brLWT.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-vST8DRRg.js → SkillManager-CtEz5gGp.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-ov1KgRXR.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 +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 +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 +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 +161 -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
|
@@ -11,7 +11,6 @@ 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');
|
|
15
14
|
|
|
16
15
|
const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
|
|
17
16
|
|
|
@@ -22,6 +21,24 @@ function logPerf(route, startMs, detail = '') {
|
|
|
22
21
|
console.log(`[Codex Perf] ${route}: ${duration}ms${suffix}`);
|
|
23
22
|
}
|
|
24
23
|
|
|
24
|
+
function normalizeForkOptions(body = {}) {
|
|
25
|
+
const alias = typeof body.alias === 'string' ? body.alias.trim() : '';
|
|
26
|
+
const rawNumber = body.afterUserMessageNumber;
|
|
27
|
+
const hasExplicitNumber = rawNumber !== undefined && rawNumber !== null && rawNumber !== '';
|
|
28
|
+
const parsedNumber = hasExplicitNumber ? parseInt(rawNumber, 10) : null;
|
|
29
|
+
|
|
30
|
+
if (hasExplicitNumber && (!Number.isInteger(parsedNumber) || parsedNumber <= 0)) {
|
|
31
|
+
const error = new Error('afterUserMessageNumber must be a positive integer');
|
|
32
|
+
error.statusCode = 400;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
afterUserMessageNumber: parsedNumber,
|
|
38
|
+
alias: alias || null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
module.exports = (config) => {
|
|
26
43
|
// ============================================
|
|
27
44
|
// 静态路由必须放在参数路由之前
|
|
@@ -203,6 +220,67 @@ module.exports = (config) => {
|
|
|
203
220
|
}
|
|
204
221
|
});
|
|
205
222
|
|
|
223
|
+
router.get('/:projectName/:sessionId/status', (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
if (!isCodexInstalled()) {
|
|
226
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { sessionId } = req.params;
|
|
230
|
+
const session = getSessionById(sessionId);
|
|
231
|
+
if (!session || !session.filePath) {
|
|
232
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const fs = require('fs');
|
|
236
|
+
const stats = fs.statSync(session.filePath);
|
|
237
|
+
res.json({
|
|
238
|
+
sessionId,
|
|
239
|
+
lastModified: stats.mtime.toISOString(),
|
|
240
|
+
size: stats.size
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error('[Codex API] Failed to get session status:', err);
|
|
244
|
+
res.status(500).json({ error: err.message });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
router.get('/:projectName/:sessionId/outline', (req, res) => {
|
|
249
|
+
try {
|
|
250
|
+
if (!isCodexInstalled()) {
|
|
251
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { sessionId } = req.params;
|
|
255
|
+
const session = getSessionById(sessionId);
|
|
256
|
+
if (!session) {
|
|
257
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let userMessageNumber = 0;
|
|
261
|
+
const items = [];
|
|
262
|
+
for (const msg of session.messages || []) {
|
|
263
|
+
if (msg.role !== 'user') continue;
|
|
264
|
+
const preview = typeof msg.content === 'string' ? msg.content.trim() : '';
|
|
265
|
+
if (!preview) continue;
|
|
266
|
+
userMessageNumber += 1;
|
|
267
|
+
items.push({
|
|
268
|
+
userMessageNumber,
|
|
269
|
+
preview: preview.length > 42 ? `${preview.slice(0, 42)}...` : preview,
|
|
270
|
+
timestamp: msg.timestamp || null
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
res.json({
|
|
275
|
+
sessionId,
|
|
276
|
+
items
|
|
277
|
+
});
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error('[Codex API] Failed to get session outline:', err);
|
|
280
|
+
res.status(500).json({ error: err.message });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
206
284
|
/**
|
|
207
285
|
* GET /api/codex/sessions/:projectName/:sessionId/messages
|
|
208
286
|
* 获取会话的消息列表
|
|
@@ -228,14 +306,17 @@ module.exports = (config) => {
|
|
|
228
306
|
// 转换消息格式为前端期望的格式
|
|
229
307
|
const convertedMessages = [];
|
|
230
308
|
|
|
309
|
+
let userMessageNumber = 0;
|
|
231
310
|
for (const msg of session.messages) {
|
|
232
311
|
// 用户消息
|
|
233
312
|
if (msg.role === 'user') {
|
|
313
|
+
userMessageNumber += 1;
|
|
234
314
|
convertedMessages.push({
|
|
235
315
|
type: 'user',
|
|
236
316
|
content: msg.content || '[空消息]',
|
|
237
317
|
timestamp: msg.timestamp,
|
|
238
|
-
model: null
|
|
318
|
+
model: null,
|
|
319
|
+
userMessageNumber
|
|
239
320
|
});
|
|
240
321
|
}
|
|
241
322
|
// 助手消息(普通回复)
|
|
@@ -422,12 +503,12 @@ module.exports = (config) => {
|
|
|
422
503
|
}
|
|
423
504
|
|
|
424
505
|
const { sessionId } = req.params;
|
|
425
|
-
const result = forkSession(sessionId);
|
|
506
|
+
const result = forkSession(sessionId, normalizeForkOptions(req.body));
|
|
426
507
|
|
|
427
508
|
res.json(result);
|
|
428
509
|
} catch (err) {
|
|
429
510
|
console.error('[Codex API] Failed to fork session:', err);
|
|
430
|
-
res.status(500).json({ error: err.message });
|
|
511
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
431
512
|
}
|
|
432
513
|
});
|
|
433
514
|
|
|
@@ -468,7 +549,6 @@ module.exports = (config) => {
|
|
|
468
549
|
}
|
|
469
550
|
|
|
470
551
|
const { sessionId } = req.params;
|
|
471
|
-
const fork = Boolean(req.body?.fork);
|
|
472
552
|
const fs = require('fs');
|
|
473
553
|
|
|
474
554
|
// 获取会话详情
|
|
@@ -511,17 +591,15 @@ module.exports = (config) => {
|
|
|
511
591
|
broadcastLog({
|
|
512
592
|
type: 'action',
|
|
513
593
|
action: 'launch_codex_session',
|
|
514
|
-
message:
|
|
594
|
+
message: `复制 Codex 会话启动命令 ${alias || sessionId.substring(0, 8)}`,
|
|
515
595
|
sessionId: sessionId,
|
|
516
596
|
alias: alias || null,
|
|
517
597
|
timestamp: Date.now()
|
|
518
598
|
});
|
|
519
599
|
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
args: fork ? ['fork', sessionId] : ['resume', sessionId]
|
|
524
|
-
});
|
|
600
|
+
const command = `codex resume ${sessionId}`;
|
|
601
|
+
const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
|
|
602
|
+
const copyCommand = `cd ${quotedCwd} && ${command}`;
|
|
525
603
|
|
|
526
604
|
res.json({
|
|
527
605
|
success: true,
|
|
@@ -529,7 +607,6 @@ module.exports = (config) => {
|
|
|
529
607
|
sessionFile: session.filePath,
|
|
530
608
|
sessionId,
|
|
531
609
|
tool: 'codex',
|
|
532
|
-
fork,
|
|
533
610
|
command,
|
|
534
611
|
copyCommand
|
|
535
612
|
});
|
|
@@ -35,11 +35,8 @@ function getCommandsService(req) {
|
|
|
35
35
|
router.get('/', (req, res) => {
|
|
36
36
|
try {
|
|
37
37
|
const { platform, service } = getCommandsService(req);
|
|
38
|
-
const { projectPath
|
|
39
|
-
const
|
|
40
|
-
const result = service.listCommands(projectPath || null, {
|
|
41
|
-
syncManagedLocalCommands: forceRefresh
|
|
42
|
-
});
|
|
38
|
+
const { projectPath } = req.query;
|
|
39
|
+
const result = service.listCommands(projectPath || null);
|
|
43
40
|
|
|
44
41
|
res.json({
|
|
45
42
|
success: true,
|
|
@@ -449,39 +446,6 @@ router.post('/install', async (req, res) => {
|
|
|
449
446
|
}
|
|
450
447
|
});
|
|
451
448
|
|
|
452
|
-
/**
|
|
453
|
-
* 安装本地托管命令
|
|
454
|
-
* POST /api/commands/install-local
|
|
455
|
-
* Body: { path }
|
|
456
|
-
*/
|
|
457
|
-
router.post('/install-local', (req, res) => {
|
|
458
|
-
try {
|
|
459
|
-
const { platform, service } = getCommandsService(req);
|
|
460
|
-
const { path } = req.body;
|
|
461
|
-
|
|
462
|
-
if (!path) {
|
|
463
|
-
return res.status(400).json({
|
|
464
|
-
success: false,
|
|
465
|
-
message: 'Missing path'
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const result = service.installLocalCommand(path);
|
|
470
|
-
|
|
471
|
-
res.json({
|
|
472
|
-
success: true,
|
|
473
|
-
platform,
|
|
474
|
-
...result
|
|
475
|
-
});
|
|
476
|
-
} catch (err) {
|
|
477
|
-
console.error('[Commands API] Install local command error:', err);
|
|
478
|
-
res.status(500).json({
|
|
479
|
-
success: false,
|
|
480
|
-
message: err.message
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
|
|
485
449
|
/**
|
|
486
450
|
* 卸载命令
|
|
487
451
|
* POST /api/commands/uninstall
|
|
@@ -26,30 +26,6 @@ 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
|
-
|
|
53
29
|
/**
|
|
54
30
|
* GET /api/dashboard/init
|
|
55
31
|
* 聚合首页所需的所有数据,一次请求返回
|
|
@@ -147,7 +123,7 @@ router.get('/init', async (req, res) => {
|
|
|
147
123
|
opencode: opencodeCounts || { projectCount: 0, sessionCount: 0 }
|
|
148
124
|
},
|
|
149
125
|
todayStats: {
|
|
150
|
-
claude: formatStats(
|
|
126
|
+
claude: formatStats(claudeTodayStats),
|
|
151
127
|
codex: formatStats(codexTodayStats),
|
|
152
128
|
gemini: formatStats(geminiTodayStats),
|
|
153
129
|
opencode: formatStats(opencodeTodayStats)
|
|
@@ -15,7 +15,7 @@ const {
|
|
|
15
15
|
hasBackup,
|
|
16
16
|
readEnv
|
|
17
17
|
} = require('../services/gemini-settings-manager');
|
|
18
|
-
const { getChannels, getEnabledChannels } = require('../services/gemini-channels');
|
|
18
|
+
const { getChannels, getEnabledChannels, markChannelAsRecentlyUsed } = require('../services/gemini-channels');
|
|
19
19
|
const { clearNativeOAuth } = require('../services/native-oauth-adapters');
|
|
20
20
|
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
21
21
|
const fs = require('fs');
|
|
@@ -147,6 +147,8 @@ router.post('/start', async (req, res) => {
|
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
|
|
151
|
+
|
|
150
152
|
// 3. 保存当前激活渠道ID(用于代理模式)
|
|
151
153
|
saveActiveChannelId(currentChannel.id);
|
|
152
154
|
console.log(`[Gemini Proxy] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
|
|
@@ -165,7 +167,7 @@ router.post('/start', async (req, res) => {
|
|
|
165
167
|
const { broadcastProxyState } = require('../websocket-server');
|
|
166
168
|
const proxyStatus = getGeminiProxyStatus();
|
|
167
169
|
const { channels: allChannels } = getChannels();
|
|
168
|
-
const activeChannel = allChannels.
|
|
170
|
+
const activeChannel = allChannels.find(ch => ch.id === currentChannel.id) || currentChannel;
|
|
169
171
|
broadcastProxyState('gemini', proxyStatus, activeChannel, allChannels);
|
|
170
172
|
|
|
171
173
|
res.json({
|
|
@@ -13,7 +13,24 @@ const {
|
|
|
13
13
|
} = require('../services/gemini-sessions');
|
|
14
14
|
const { isGeminiInstalled } = require('../services/gemini-config');
|
|
15
15
|
const { loadAliases } = require('../services/alias');
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
function normalizeForkOptions(body = {}) {
|
|
18
|
+
const alias = typeof body.alias === 'string' ? body.alias.trim() : '';
|
|
19
|
+
const rawNumber = body.afterUserMessageNumber;
|
|
20
|
+
const hasExplicitNumber = rawNumber !== undefined && rawNumber !== null && rawNumber !== '';
|
|
21
|
+
const parsedNumber = hasExplicitNumber ? parseInt(rawNumber, 10) : null;
|
|
22
|
+
|
|
23
|
+
if (hasExplicitNumber && (!Number.isInteger(parsedNumber) || parsedNumber <= 0)) {
|
|
24
|
+
const error = new Error('afterUserMessageNumber must be a positive integer');
|
|
25
|
+
error.statusCode = 400;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
afterUserMessageNumber: parsedNumber,
|
|
31
|
+
alias: alias || null
|
|
32
|
+
};
|
|
33
|
+
}
|
|
17
34
|
|
|
18
35
|
module.exports = (config) => {
|
|
19
36
|
/**
|
|
@@ -146,6 +163,65 @@ module.exports = (config) => {
|
|
|
146
163
|
}
|
|
147
164
|
});
|
|
148
165
|
|
|
166
|
+
router.get('/:projectHash/:sessionId/status', (req, res) => {
|
|
167
|
+
try {
|
|
168
|
+
if (!isGeminiInstalled()) {
|
|
169
|
+
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const { sessionId } = req.params;
|
|
173
|
+
const session = getSessionById(sessionId);
|
|
174
|
+
if (!session) {
|
|
175
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
res.json({
|
|
179
|
+
sessionId,
|
|
180
|
+
lastModified: session.lastUpdated || session.mtime || null,
|
|
181
|
+
size: session.size || 0
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error('[Gemini API] Failed to get session status:', err);
|
|
185
|
+
res.status(500).json({ error: err.message });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
router.get('/:projectHash/:sessionId/outline', (req, res) => {
|
|
190
|
+
try {
|
|
191
|
+
if (!isGeminiInstalled()) {
|
|
192
|
+
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { sessionId } = req.params;
|
|
196
|
+
const session = getSessionById(sessionId);
|
|
197
|
+
if (!session) {
|
|
198
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let userMessageNumber = 0;
|
|
202
|
+
const items = [];
|
|
203
|
+
for (const msg of session.messages || []) {
|
|
204
|
+
if (msg.type !== 'user') continue;
|
|
205
|
+
const preview = typeof msg.content === 'string' ? msg.content.trim() : '';
|
|
206
|
+
if (!preview) continue;
|
|
207
|
+
userMessageNumber += 1;
|
|
208
|
+
items.push({
|
|
209
|
+
userMessageNumber,
|
|
210
|
+
preview: preview.length > 42 ? `${preview.slice(0, 42)}...` : preview,
|
|
211
|
+
timestamp: msg.timestamp || null
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
res.json({
|
|
216
|
+
sessionId,
|
|
217
|
+
items
|
|
218
|
+
});
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error('[Gemini API] Failed to get session outline:', err);
|
|
221
|
+
res.status(500).json({ error: err.message });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
149
225
|
/**
|
|
150
226
|
* GET /api/gemini/sessions/:projectHash/:sessionId/messages
|
|
151
227
|
* 获取会话的消息列表
|
|
@@ -168,14 +244,17 @@ module.exports = (config) => {
|
|
|
168
244
|
// 转换消息格式为前端期望的格式
|
|
169
245
|
const convertedMessages = [];
|
|
170
246
|
|
|
247
|
+
let userMessageNumber = 0;
|
|
171
248
|
for (const msg of session.messages || []) {
|
|
172
249
|
// 用户消息
|
|
173
250
|
if (msg.type === 'user') {
|
|
251
|
+
userMessageNumber += 1;
|
|
174
252
|
convertedMessages.push({
|
|
175
253
|
type: 'user',
|
|
176
254
|
content: msg.content || '[空消息]',
|
|
177
255
|
timestamp: msg.timestamp,
|
|
178
|
-
model: null
|
|
256
|
+
model: null,
|
|
257
|
+
userMessageNumber
|
|
179
258
|
});
|
|
180
259
|
}
|
|
181
260
|
// Gemini 助手消息(type 是 'gemini' 而不是 'assistant')
|
|
@@ -322,12 +401,12 @@ module.exports = (config) => {
|
|
|
322
401
|
}
|
|
323
402
|
|
|
324
403
|
const { sessionId } = req.params;
|
|
325
|
-
const result = forkSession(sessionId);
|
|
404
|
+
const result = forkSession(sessionId, normalizeForkOptions(req.body));
|
|
326
405
|
|
|
327
406
|
res.json(result);
|
|
328
407
|
} catch (err) {
|
|
329
408
|
console.error('[Gemini API] Failed to fork session:', err);
|
|
330
|
-
res.status(500).json({ error: err.message });
|
|
409
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
331
410
|
}
|
|
332
411
|
});
|
|
333
412
|
|
|
@@ -408,11 +487,9 @@ module.exports = (config) => {
|
|
|
408
487
|
const resumeIndex = sessionIndex + 1;
|
|
409
488
|
|
|
410
489
|
// 构建 Gemini CLI 命令(使用 --resume <index> 恢复特定会话)
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
args: ['--resume', String(resumeIndex)]
|
|
415
|
-
});
|
|
490
|
+
const command = `gemini --resume ${resumeIndex}`;
|
|
491
|
+
const quotedCwd = `"${String(projectPath).replace(/"/g, '\\"')}"`;
|
|
492
|
+
const copyCommand = `cd ${quotedCwd} && ${command}`;
|
|
416
493
|
|
|
417
494
|
res.json({
|
|
418
495
|
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
|
|
4
|
+
const { createSameOriginGuard } = require('../services/network-access');
|
|
5
5
|
|
|
6
6
|
router.use(createSameOriginGuard({
|
|
7
7
|
message: '禁止跨站访问通知配置接口'
|
|
@@ -42,20 +42,4 @@ 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
|
-
|
|
61
45
|
module.exports = router;
|
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
getChannels,
|
|
22
22
|
getEnabledChannels,
|
|
23
23
|
applyChannelToSettings,
|
|
24
|
+
markChannelAsRecentlyUsed,
|
|
24
25
|
getEffectiveApiKeyCandidates
|
|
25
26
|
} = require('../services/opencode-channels');
|
|
26
27
|
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
@@ -218,7 +219,9 @@ router.post('/start', async (req, res) => {
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
const { channels: allChannels } = getChannels();
|
|
221
|
-
|
|
222
|
+
let currentChannel = findActiveChannelFromNativeConfig(allChannels) || enabledChannels[0];
|
|
223
|
+
|
|
224
|
+
currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
|
|
222
225
|
|
|
223
226
|
// 2. 保存当前激活渠道ID
|
|
224
227
|
saveActiveChannelId(currentChannel.id);
|
|
@@ -285,7 +288,9 @@ router.post('/start', async (req, res) => {
|
|
|
285
288
|
// 5. 广播状态更新
|
|
286
289
|
const { broadcastProxyState } = require('../websocket-server');
|
|
287
290
|
const updatedStatus = getOpenCodeProxyStatus();
|
|
288
|
-
|
|
291
|
+
const { channels: latestChannels } = getChannels();
|
|
292
|
+
const activeChannel = latestChannels.find(channel => channel.id === currentChannel.id) || currentChannel;
|
|
293
|
+
broadcastProxyState('opencode', updatedStatus, activeChannel, latestChannels);
|
|
289
294
|
|
|
290
295
|
res.json({
|
|
291
296
|
success: true,
|
|
@@ -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
|
});
|