mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
api_retry heartbeats already reach the desktop status bar, but the
streaming-to-non-streaming fallback had no signal at all: the CLI only
flipped an internal flag, and the one-shot fallback response can take
minutes with zero incremental output, so the UI showed a bare spinner
the whole time.
- CLI: yield a {type:'system', subtype:'streaming_fallback', cause}
message at both fallback sites (stream error/watchdog and 404 stream
creation), mirroring the existing api_error -> api_retry path through
query.ts passthrough, QueryEngine SDK output, and the SDK schema.
- Server: translate it to a streaming_fallback ServerMessage;
unrecognized causes normalize to 'unknown' instead of dropping the
event.
- Desktop: track it as active-turn state (cleared at the same 12 sites
as apiRetry; a fallback notice supersedes the stale retry banner) and
render a neutral pill with the turn timer - expected state, not an
error, so no amber styling and nothing in the diagnostics panel.
- Retry banner now shows "retrying now" once the countdown elapses
instead of sticking at "waiting 0s".
With 62241a31 disabling the non-streaming fallback for desktop CLI
sessions, this notice mainly covers the 404 gateway path (which skips
the disable check), callers that re-enable fallback via env, and
non-desktop SDK consumers.
Constraint: Retries and fallbacks are expected states per the
diagnostics severity standard - lightweight active-turn UI only, no
error-panel entries, no transcript persistence.
Tested: bun test src/server/__tests__/ws-memory-events.test.ts
Tested: bun run check:server
Tested: cd desktop && bun run test -- --run
Tested: cd desktop && bun run lint
Not-tested: live provider outage reproduction; verified via unit
coverage of the translation, store lifecycle, and indicator rendering.
174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
/**
|
||
* WebSocket event type definitions
|
||
*
|
||
* 定义客户端与服务器之间 WebSocket 通信的消息类型。
|
||
*/
|
||
|
||
// ============================================================================
|
||
// Client → Server
|
||
// ============================================================================
|
||
|
||
export type ClientMessage =
|
||
| { type: 'prewarm_session' }
|
||
| { type: 'user_message'; content: string; attachments?: AttachmentRef[] }
|
||
| {
|
||
type: 'permission_response'
|
||
requestId: string
|
||
allowed: boolean
|
||
rule?: string
|
||
updatedInput?: Record<string, unknown>
|
||
denyMessage?: string
|
||
permissionUpdates?: 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; effortLevel?: string }
|
||
| { type: 'stop_generation' }
|
||
| { type: 'ping' }
|
||
|
||
export type AttachmentRef = {
|
||
type: 'file' | 'image'
|
||
name?: string
|
||
path?: string
|
||
data?: string // base64 for images
|
||
mimeType?: string
|
||
isDirectory?: boolean
|
||
}
|
||
|
||
// ============================================================================
|
||
// 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: 'user_message_replay'; content: string }
|
||
| { type: 'message_complete'; usage: TokenUsage }
|
||
| { type: 'thinking'; text: string }
|
||
| { type: 'status'; state: ChatState; verb?: string }
|
||
// CLI 是权限模式的唯一真相来源。当 CLI 内部 mode 变化(如 ExitPlanMode 后
|
||
// 恢复到进入 plan 前的模式、Shift+Tab 切换)时,把新模式回传给前端,让桌面端
|
||
// 选择器与 CLI 保持同步,而不是停留在本地影子值上。
|
||
| { type: 'permission_mode_changed'; mode: string }
|
||
| {
|
||
type: 'api_retry'
|
||
attempt: number
|
||
maxRetries: number
|
||
retryDelayMs: number
|
||
errorStatus: number | null
|
||
errorType?: string
|
||
errorMessage?: string
|
||
}
|
||
// 流式请求失败、CLI 已降级为非流式重试。非流式响应要等完整生成才返回,
|
||
// 期间没有任何增量输出,前端据此显示"慢速模式"轻提示而不是裸转圈。
|
||
| { type: 'streaming_fallback'; cause: StreamingFallbackCause }
|
||
| { type: 'error'; message: string; code: string; retryable?: boolean; businessErrorCode?: string }
|
||
| { 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' | 'compacting' | 'tool_executing' | 'streaming' | 'permission_pending'
|
||
|
||
// 与 CLI 的 streaming_fallback cause 对齐;unknown 兜底未来新增的 cause 值,
|
||
// 避免新 CLI + 旧 server 组合下丢消息。
|
||
export type StreamingFallbackCause = 'watchdog' | 'stream_error' | '404_stream_creation' | 'unknown'
|
||
|
||
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
|
||
}
|