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,7 @@ const {
|
|
|
14
14
|
hasBackup,
|
|
15
15
|
readConfig
|
|
16
16
|
} = require('../services/codex-settings-manager');
|
|
17
|
-
const { getChannels, getEnabledChannels } = require('../services/codex-channels');
|
|
17
|
+
const { getChannels, getEnabledChannels, markChannelAsRecentlyUsed } = require('../services/codex-channels');
|
|
18
18
|
const { clearNativeOAuth } = require('../services/native-oauth-adapters');
|
|
19
19
|
const { clearAllLogs } = require('../websocket-server');
|
|
20
20
|
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
@@ -61,6 +61,31 @@ function resolveActiveChannel(channels, activeChannelId = null) {
|
|
|
61
61
|
|| null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function recoverChannelAfterMissingUpdate(previousChannel) {
|
|
65
|
+
const { channels } = getChannels();
|
|
66
|
+
const allChannels = Array.isArray(channels) ? channels : [];
|
|
67
|
+
const enabledChannels = allChannels.filter(channel => channel.enabled !== false);
|
|
68
|
+
|
|
69
|
+
const previousProviderKey = String(previousChannel?.providerKey || '').trim();
|
|
70
|
+
if (previousProviderKey) {
|
|
71
|
+
const sameProvider = enabledChannels.find(channel => channel.providerKey === previousProviderKey);
|
|
72
|
+
if (sameProvider) {
|
|
73
|
+
return sameProvider;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (allChannels.length > 0) {
|
|
78
|
+
return resolveActiveChannel(allChannels, null);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const refreshedEnabledChannels = getEnabledChannels();
|
|
82
|
+
if (Array.isArray(refreshedEnabledChannels) && refreshedEnabledChannels.length > 0) {
|
|
83
|
+
return refreshedEnabledChannels[0];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
// 保存激活渠道ID
|
|
65
90
|
function saveActiveChannelId(channelId) {
|
|
66
91
|
ensureStorageDirMigrated();
|
|
@@ -149,6 +174,24 @@ router.post('/start', async (req, res) => {
|
|
|
149
174
|
});
|
|
150
175
|
}
|
|
151
176
|
|
|
177
|
+
try {
|
|
178
|
+
currentChannel = markChannelAsRecentlyUsed(currentChannel.id) || currentChannel;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error?.message !== 'Channel not found') {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const recoveredChannel = recoverChannelAfterMissingUpdate(currentChannel);
|
|
185
|
+
if (!recoveredChannel) {
|
|
186
|
+
return res.status(400).json({
|
|
187
|
+
error: 'No enabled Codex channel found. Please create and enable a channel first.'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
currentChannel = recoveredChannel;
|
|
192
|
+
console.warn(`[Codex Proxy] Active channel disappeared before startup, falling back to ${currentChannel.name} (${currentChannel.id})`);
|
|
193
|
+
}
|
|
194
|
+
|
|
152
195
|
// 3. 保存当前激活渠道ID(用于代理模式)
|
|
153
196
|
saveActiveChannelId(currentChannel.id);
|
|
154
197
|
console.log(`[Codex Proxy] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
|
|
@@ -166,7 +209,7 @@ router.post('/start', async (req, res) => {
|
|
|
166
209
|
|
|
167
210
|
const updatedStatus = getCodexProxyStatus();
|
|
168
211
|
const { channels: allChannels } = getChannels();
|
|
169
|
-
const activeChannel = allChannels.
|
|
212
|
+
const activeChannel = allChannels.find(ch => ch.id === currentChannel.id) || currentChannel;
|
|
170
213
|
const { broadcastProxyState } = require('../websocket-server');
|
|
171
214
|
broadcastProxyState('codex', updatedStatus, activeChannel, allChannels);
|
|
172
215
|
|
|
@@ -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,
|