cc-haha/src/server/ws/events.ts
程序员阿江(Relakkes) 773a8a703f Keep desktop chats on the selected provider/model
The desktop model picker now stores a session-scoped provider/model selection instead of relying on the global active provider. That selection is replayed on connect, passed into the CLI startup path, and preserved across turns until the user changes it again.

To make that true end-to-end, the server now restarts the session process when runtime selection changes, injects provider-scoped env for third-party providers, and routes proxy traffic by provider id. The selector UI was also tightened so provider grouping stays visible while the actual model choice remains readable.

Constraint: Different providers can expose the same model id, so chat runtime selection cannot be derived from model id alone
Constraint: A desktop session reuses one CLI subprocess across turns, so runtime changes must restart that process to take effect
Rejected: Keep using Settings active provider as the chat selector | conflates defaults with live session state and breaks overlapping models
Rejected: UI-only runtime switching without server restart | later turns would continue using the old CLI subprocess configuration
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep provider defaults and session runtime overrides separate, and preserve provider-scoped proxy routing when extending model selection surfaces
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run test src/stores/chatStore.test.ts
Tested: cd desktop && bun run test src/__tests__/generalSettings.test.tsx
Tested: bun test src/server/__tests__/conversation-service.test.ts
Tested: bun test src/server/__tests__/conversations.test.ts
Tested: bun -e "await import('./src/server/services/titleService.ts'); await import('./src/server/ws/handler.ts')"
Not-tested: Real third-party provider round-trip from the desktop UI against a live upstream account
2026-04-23 13:41:27 +08:00

149 lines
4.4 KiB
TypeScript

/**
* WebSocket event type definitions
*
* 定义客户端与服务器之间 WebSocket 通信的消息类型。
*/
// ============================================================================
// Client → Server
// ============================================================================
export type ClientMessage =
| { type: 'user_message'; content: string; attachments?: AttachmentRef[] }
| {
type: 'permission_response'
requestId: string
allowed: boolean
rule?: string
updatedInput?: Record<string, unknown>
}
| {
type: 'computer_use_permission_response'
requestId: string
response: ComputerUsePermissionResponse
}
| { type: 'set_permission_mode'; mode: string }
| { type: 'set_runtime_config'; providerId: string | null; modelId: string }
| { type: 'stop_generation' }
| { type: 'ping' }
export type AttachmentRef = {
type: 'file' | 'image'
name?: string
path?: string
data?: string // base64 for images
mimeType?: string
}
// ============================================================================
// Server → Client
// ============================================================================
export type ServerMessage =
| { type: 'connected'; sessionId: string }
| { type: 'content_start'; blockType: 'text' | 'tool_use'; toolName?: string; toolUseId?: string; parentToolUseId?: string }
| { type: 'content_delta'; text?: string; toolInput?: string }
| { type: 'tool_use_complete'; toolName: string; toolUseId: string; input: unknown; parentToolUseId?: string }
| { type: 'tool_result'; toolUseId: string; content: unknown; isError: boolean; parentToolUseId?: string }
| {
type: 'permission_request'
requestId: string
toolName: string
toolUseId?: string
input: unknown
description?: string
}
| {
type: 'computer_use_permission_request'
requestId: string
request: ComputerUsePermissionRequest
}
| { type: 'message_complete'; usage: TokenUsage }
| { type: 'thinking'; text: string }
| { type: 'status'; state: ChatState; verb?: string; elapsed?: number; tokens?: number }
| { type: 'error'; message: string; code: string; retryable?: boolean }
| { type: 'system_notification'; subtype: string; message?: string; data?: unknown }
| { type: 'pong' }
| { type: 'team_update'; teamName: string; members: TeamMemberStatus[] }
| { type: 'team_created'; teamName: string }
| { type: 'team_deleted'; teamName: string }
| { type: 'task_update'; taskId: string; status: string; progress?: string }
| { type: 'session_title_updated'; sessionId: string; title: string }
export type TokenUsage = {
input_tokens: number
output_tokens: number
cache_read_tokens?: number
cache_creation_tokens?: number
}
export type ChatState = 'idle' | 'thinking' | 'tool_executing' | 'streaming' | 'permission_pending'
export type TeamMemberStatus = {
agentId: string
role: string
status: 'running' | 'idle' | 'completed' | 'error'
currentTask?: string
}
export type ComputerUseGrantFlags = {
clipboardRead: boolean
clipboardWrite: boolean
systemKeyCombos: boolean
}
export type ComputerUseResolvedApp = {
bundleId: string
displayName: string
path?: string
iconDataUrl?: string
}
export type ComputerUseResolvedAppRequest = {
requestedName: string
resolved?: ComputerUseResolvedApp
isSentinel: boolean
alreadyGranted: boolean
proposedTier: 'read' | 'click' | 'full'
}
export type ComputerUsePermissionRequest = {
requestId: string
reason: string
apps: ComputerUseResolvedAppRequest[]
requestedFlags: Partial<ComputerUseGrantFlags>
screenshotFiltering: 'native' | 'none'
tccState?: {
accessibility: boolean
screenRecording: boolean
}
willHide?: Array<{ bundleId: string; displayName: string }>
autoUnhideEnabled?: boolean
}
export type ComputerUsePermissionResponse = {
granted: Array<{
bundleId: string
displayName: string
grantedAt: number
tier?: 'read' | 'click' | 'full'
}>
denied: Array<{
bundleId: string
reason: 'user_denied' | 'not_installed'
}>
flags: ComputerUseGrantFlags
userConsented?: boolean
}
// ============================================================================
// Internal types
// ============================================================================
export type WebSocketSession = {
sessionId: string
connectedAt: number
abortController?: AbortController
isGenerating: boolean
}