mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-27 23:05:30 +08:00
Merge upstream/main into feat/voice (resolve voice vs browser-use + websocket-unify)
This commit is contained in:
118
server/index.js
118
server/index.js
@@ -22,35 +22,24 @@ import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
|
||||
import {
|
||||
queryClaudeSDK,
|
||||
abortClaudeSDKSession,
|
||||
isClaudeSDKSessionActive,
|
||||
getActiveClaudeSDKSessions,
|
||||
resolveToolApproval,
|
||||
getPendingApprovalsForSession,
|
||||
reconnectSessionWriter,
|
||||
} from './claude-sdk.js';
|
||||
import {
|
||||
spawnCursor,
|
||||
abortCursorSession,
|
||||
isCursorSessionActive,
|
||||
getActiveCursorSessions,
|
||||
} from './cursor-cli.js';
|
||||
import {
|
||||
queryCodex,
|
||||
abortCodexSession,
|
||||
isCodexSessionActive,
|
||||
getActiveCodexSessions,
|
||||
} from './openai-codex.js';
|
||||
import {
|
||||
spawnGemini,
|
||||
abortGeminiSession,
|
||||
isGeminiSessionActive,
|
||||
getActiveGeminiSessions,
|
||||
} from './gemini-cli.js';
|
||||
import {
|
||||
spawnOpenCode,
|
||||
abortOpenCodeSession,
|
||||
isOpenCodeSessionActive,
|
||||
getActiveOpenCodeSessions,
|
||||
} from './opencode-cli.js';
|
||||
import sessionManager from './sessionManager.js';
|
||||
import {
|
||||
@@ -73,6 +62,9 @@ import geminiRoutes from './routes/gemini.js';
|
||||
import pluginsRoutes from './routes/plugins.js';
|
||||
import providerRoutes from './modules/providers/provider.routes.js';
|
||||
import voiceRoutes from './voice-proxy.js';
|
||||
import browserUseRoutes from './modules/browser-use/browser-use.routes.js';
|
||||
import browserUseMcpRoutes from './modules/browser-use/browser-use-mcp.routes.js';
|
||||
import { browserUseService } from './modules/browser-use/browser-use.service.js';
|
||||
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
||||
import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js';
|
||||
import { configureWebPush } from './services/vapid-keys.js';
|
||||
@@ -106,32 +98,35 @@ const wss = createWebSocketServer(server, {
|
||||
authenticateWebSocket,
|
||||
},
|
||||
chat: {
|
||||
queryClaudeSDK,
|
||||
spawnCursor,
|
||||
queryCodex,
|
||||
spawnGemini,
|
||||
spawnOpenCode,
|
||||
abortClaudeSDKSession,
|
||||
abortCursorSession,
|
||||
abortCodexSession,
|
||||
abortGeminiSession,
|
||||
abortOpenCodeSession,
|
||||
spawnFns: {
|
||||
claude: queryClaudeSDK,
|
||||
cursor: spawnCursor,
|
||||
codex: queryCodex,
|
||||
gemini: spawnGemini,
|
||||
opencode: spawnOpenCode,
|
||||
},
|
||||
abortFns: {
|
||||
claude: abortClaudeSDKSession,
|
||||
cursor: abortCursorSession,
|
||||
codex: abortCodexSession,
|
||||
gemini: abortGeminiSession,
|
||||
opencode: abortOpenCodeSession,
|
||||
},
|
||||
resolveToolApproval,
|
||||
isClaudeSDKSessionActive,
|
||||
isCursorSessionActive,
|
||||
isCodexSessionActive,
|
||||
isGeminiSessionActive,
|
||||
isOpenCodeSessionActive,
|
||||
reconnectSessionWriter,
|
||||
getPendingApprovalsForSession,
|
||||
getActiveClaudeSDKSessions,
|
||||
getActiveCursorSessions,
|
||||
getActiveCodexSessions,
|
||||
getActiveGeminiSessions,
|
||||
getActiveOpenCodeSessions,
|
||||
},
|
||||
shell: {
|
||||
getSessionById: (sessionId) => sessionManager.getSession(sessionId),
|
||||
resolveProviderSessionId: (sessionId, provider) => {
|
||||
const dbSession = sessionsDb.getSessionById(sessionId);
|
||||
const legacyGeminiSession =
|
||||
provider === 'gemini' ? sessionManager.getSession(sessionId) : null;
|
||||
|
||||
if (dbSession) {
|
||||
return dbSession.provider_session_id ?? legacyGeminiSession?.cliSessionId ?? null;
|
||||
}
|
||||
|
||||
return legacyGeminiSession?.cliSessionId;
|
||||
},
|
||||
stripAnsiSequences,
|
||||
normalizeDetectedUrl,
|
||||
extractUrlsFromText,
|
||||
@@ -202,6 +197,12 @@ app.use('/api/gemini', authenticateToken, geminiRoutes);
|
||||
// Plugins API Routes (protected)
|
||||
app.use('/api/plugins', authenticateToken, pluginsRoutes);
|
||||
|
||||
// Browser MCP bridge API (local token protected)
|
||||
app.use('/api/browser-use-mcp', browserUseMcpRoutes);
|
||||
|
||||
// Browser API Routes (protected)
|
||||
app.use('/api/browser-use', authenticateToken, browserUseRoutes);
|
||||
|
||||
// Unified provider MCP routes (protected)
|
||||
app.use('/api/providers', authenticateToken, providerRoutes);
|
||||
|
||||
@@ -1146,7 +1147,6 @@ app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req
|
||||
app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectId, sessionId } = req.params;
|
||||
const { provider = 'claude' } = req.query;
|
||||
const homeDir = os.homedir();
|
||||
|
||||
// Allow only safe characters in sessionId
|
||||
@@ -1155,6 +1155,18 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
||||
return res.status(400).json({ error: 'Invalid sessionId' });
|
||||
}
|
||||
|
||||
// Provider artifacts on disk (JSONL file names, OpenCode sqlite rows)
|
||||
// are keyed by the provider-native session id, while the caller sends
|
||||
// the app-facing id. Resolve provider and id mapping from the indexed
|
||||
// session row so the frontend does not choose provider-specific paths.
|
||||
const sessionRow = sessionsDb.getSessionById(safeSessionId);
|
||||
if (!sessionRow) {
|
||||
return res.status(404).json({ error: 'Session not found', sessionId: safeSessionId });
|
||||
}
|
||||
|
||||
const provider = sessionRow.provider || 'claude';
|
||||
const providerNativeSessionId = sessionRow?.provider_session_id || safeSessionId;
|
||||
|
||||
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
||||
if (provider === 'cursor') {
|
||||
return res.json({
|
||||
@@ -1255,7 +1267,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
||||
tokens_cache_write AS cacheWriteTokens
|
||||
FROM session
|
||||
WHERE id = ?
|
||||
`).get(safeSessionId);
|
||||
`).get(providerNativeSessionId);
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: 'OpenCode session not found', sessionId: safeSessionId });
|
||||
@@ -1296,7 +1308,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
||||
if (entry.isDirectory()) {
|
||||
const found = await findSessionFile(fullPath);
|
||||
if (found) return found;
|
||||
} else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
|
||||
} else if (entry.name.includes(providerNativeSessionId) && entry.name.endsWith('.jsonl')) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
@@ -1380,12 +1392,19 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
||||
const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
||||
|
||||
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
||||
// Prefer the indexed transcript path (already produced by the trusted
|
||||
// session synchronizer); fall back to the conventional location
|
||||
// derived from the provider-native session id.
|
||||
let jsonlPath = sessionRow?.jsonl_path;
|
||||
if (!jsonlPath) {
|
||||
jsonlPath = path.join(projectDir, `${providerNativeSessionId}.jsonl`);
|
||||
|
||||
// Constrain to projectDir
|
||||
const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
return res.status(400).json({ error: 'Invalid path' });
|
||||
// Constrain the constructed path to projectDir (the id is
|
||||
// caller-influenced in this fallback branch).
|
||||
const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
return res.status(400).json({ error: 'Invalid path' });
|
||||
}
|
||||
}
|
||||
|
||||
// Read and parse the JSONL file
|
||||
@@ -1697,12 +1716,21 @@ async function startServer() {
|
||||
|
||||
await closeSessionsWatcher();
|
||||
// Clean up plugin processes on shutdown
|
||||
const shutdownPlugins = async () => {
|
||||
await stopAllPlugins();
|
||||
const shutdownRuntimeServices = async () => {
|
||||
try {
|
||||
await browserUseService.stopAllSessions();
|
||||
} catch (err) {
|
||||
console.error('[Browser] Error stopping sessions during shutdown:', err?.message || err);
|
||||
}
|
||||
try {
|
||||
await stopAllPlugins();
|
||||
} catch (err) {
|
||||
console.error('[Plugins] Error stopping plugins during shutdown:', err?.message || err);
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGTERM', () => void shutdownPlugins());
|
||||
process.on('SIGINT', () => void shutdownPlugins());
|
||||
process.on('SIGTERM', () => void shutdownRuntimeServices());
|
||||
process.on('SIGINT', () => void shutdownRuntimeServices());
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Failed to start server:', error);
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user