mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
- Add Tauri sidecar architecture: Rust shell spawns claude-server binary, dynamic port allocation, health-check wait loop, graceful shutdown - Fix CORS middleware to accept `tauri://localhost` and `https://tauri.localhost` origins from Tauri WebView, and add CORS headers to /health endpoint - Enable native macOS window decorations (traffic lights) with Overlay title bar, add data-tauri-drag-region on sidebar for window dragging - Conditionally apply desktop-only padding (44px for traffic lights) vs web (12px) - Generate brand identity: light-background app icon, horizontal logo, full icon set (icns/ico/png) for Tauri bundle - Add brand mark + GitHub link in sidebar, replace mascot SVG with app icon in EmptySession page - Update README (zh/en) and docs hero image with new branding - Add sidecar build scripts and launcher entry points - Gitignore Rust target/, Tauri gen/, and brand-assets candidates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
190 lines
6.0 KiB
TypeScript
190 lines
6.0 KiB
TypeScript
/**
|
|
* Claude Code Desktop App — HTTP + WebSocket Server
|
|
*
|
|
* 为桌面端 UI 提供 REST API 和 WebSocket 实时通信。
|
|
* 读写与 CLI 完全相同的文件系统,确保 CLI/UI 数据互通。
|
|
*/
|
|
|
|
import { handleApiRequest } from './router.js'
|
|
import { handleWebSocket, type WebSocketData } from './ws/handler.js'
|
|
import { corsHeaders } from './middleware/cors.js'
|
|
import { requireAuth } from './middleware/auth.js'
|
|
import { teamWatcher } from './services/teamWatcher.js'
|
|
import { cronScheduler } from './services/cronScheduler.js'
|
|
|
|
function readArgValue(flag: string): string | undefined {
|
|
const args = process.argv.slice(2)
|
|
const index = args.indexOf(flag)
|
|
if (index === -1) return undefined
|
|
return args[index + 1]
|
|
}
|
|
|
|
function hasArgFlag(flag: string): boolean {
|
|
return process.argv.slice(2).includes(flag)
|
|
}
|
|
|
|
function resolveServerOptions() {
|
|
const portArg = readArgValue('--port')
|
|
const port = Number.parseInt(portArg || process.env.SERVER_PORT || '3456', 10)
|
|
const host = readArgValue('--host') || process.env.SERVER_HOST || '127.0.0.1'
|
|
const cliPath = readArgValue('--cli-path')
|
|
const authRequired = hasArgFlag('--auth-required')
|
|
|
|
if (cliPath) {
|
|
process.env.CLAUDE_CLI_PATH = cliPath
|
|
}
|
|
|
|
return { port, host, authRequired }
|
|
}
|
|
|
|
const SERVER_OPTIONS = resolveServerOptions()
|
|
const PORT = SERVER_OPTIONS.port
|
|
const HOST = SERVER_OPTIONS.host
|
|
|
|
export function startServer(port = PORT, host = HOST) {
|
|
const localConnectHost =
|
|
host === '0.0.0.0' || host === '127.0.0.1' || host === 'localhost'
|
|
? '127.0.0.1'
|
|
: host
|
|
|
|
/**
|
|
* Auth is required when explicitly opted in or when bound to a non-localhost address.
|
|
* - Default localhost dev: no auth needed (tests pass as-is).
|
|
* - Production / non-localhost (e.g. 0.0.0.0): auth enforced automatically.
|
|
* - Explicit opt-in: SERVER_AUTH_REQUIRED=1 forces auth even on localhost.
|
|
*/
|
|
const authRequired =
|
|
SERVER_OPTIONS.authRequired ||
|
|
process.env.SERVER_AUTH_REQUIRED === '1' ||
|
|
host !== '127.0.0.1'
|
|
|
|
const server = Bun.serve<WebSocketData>({
|
|
port,
|
|
hostname: host,
|
|
|
|
async fetch(req, server) {
|
|
const url = new URL(req.url)
|
|
|
|
const origin = req.headers.get('Origin')
|
|
|
|
// Handle CORS preflight
|
|
if (req.method === 'OPTIONS') {
|
|
return new Response(null, { status: 204, headers: corsHeaders(origin) })
|
|
}
|
|
|
|
// WebSocket upgrade
|
|
if (url.pathname.startsWith('/ws/')) {
|
|
// Enforce authentication when required
|
|
if (authRequired) {
|
|
const authError = requireAuth(req)
|
|
if (authError) {
|
|
const headers = new Headers(authError.headers)
|
|
for (const [key, value] of Object.entries(corsHeaders(origin))) {
|
|
headers.set(key, value)
|
|
}
|
|
return new Response(authError.body, { status: authError.status, headers })
|
|
}
|
|
}
|
|
|
|
// Validate session ID format
|
|
const sessionId = url.pathname.split('/').pop() || ''
|
|
if (!sessionId || !/^[0-9a-zA-Z_-]{1,64}$/.test(sessionId)) {
|
|
return new Response('Invalid session ID', { status: 400 })
|
|
}
|
|
const upgraded = server.upgrade(req, {
|
|
data: {
|
|
sessionId,
|
|
connectedAt: Date.now(),
|
|
channel: 'client',
|
|
sdkToken: null,
|
|
serverPort: port,
|
|
serverHost: localConnectHost,
|
|
},
|
|
})
|
|
if (upgraded) return undefined
|
|
return new Response('WebSocket upgrade failed', { status: 400 })
|
|
}
|
|
|
|
// Internal SDK WebSocket used by the spawned Claude CLI.
|
|
if (url.pathname.startsWith('/sdk/')) {
|
|
const sessionId = url.pathname.split('/').pop() || ''
|
|
if (!sessionId || !/^[0-9a-zA-Z_-]{1,64}$/.test(sessionId)) {
|
|
return new Response('Invalid session ID', { status: 400 })
|
|
}
|
|
const upgraded = server.upgrade(req, {
|
|
data: {
|
|
sessionId,
|
|
connectedAt: Date.now(),
|
|
channel: 'sdk',
|
|
sdkToken: url.searchParams.get('token'),
|
|
serverPort: port,
|
|
serverHost: localConnectHost,
|
|
},
|
|
})
|
|
if (upgraded) return undefined
|
|
return new Response('WebSocket upgrade failed', { status: 400 })
|
|
}
|
|
|
|
// REST API
|
|
if (url.pathname.startsWith('/api/')) {
|
|
// Enforce authentication when required
|
|
if (authRequired) {
|
|
const authError = requireAuth(req)
|
|
if (authError) {
|
|
const headers = new Headers(authError.headers)
|
|
for (const [key, value] of Object.entries(corsHeaders(origin))) {
|
|
headers.set(key, value)
|
|
}
|
|
return new Response(authError.body, { status: authError.status, headers })
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await handleApiRequest(req, url)
|
|
// Add CORS headers to all responses
|
|
const headers = new Headers(response.headers)
|
|
for (const [key, value] of Object.entries(corsHeaders(origin))) {
|
|
headers.set(key, value)
|
|
}
|
|
return new Response(response.body, {
|
|
status: response.status,
|
|
headers,
|
|
})
|
|
} catch (error) {
|
|
console.error('[Server] API error:', error)
|
|
return Response.json(
|
|
{ error: 'Internal server error' },
|
|
{ status: 500, headers: corsHeaders() }
|
|
)
|
|
}
|
|
}
|
|
|
|
// Health check
|
|
if (url.pathname === '/health') {
|
|
return Response.json(
|
|
{ status: 'ok', timestamp: new Date().toISOString() },
|
|
{ headers: corsHeaders(origin) },
|
|
)
|
|
}
|
|
|
|
return new Response('Not Found', { status: 404 })
|
|
},
|
|
|
|
websocket: handleWebSocket,
|
|
})
|
|
|
|
// Start watching ~/.claude/teams/ for real-time WebSocket push
|
|
teamWatcher.start()
|
|
|
|
// Start the cron scheduler to execute scheduled tasks
|
|
cronScheduler.start()
|
|
|
|
console.log(`[Server] Claude Code API server running at http://${host}:${port}`)
|
|
return server
|
|
}
|
|
|
|
// Direct execution
|
|
if (import.meta.main) {
|
|
startServer()
|
|
}
|