This commit is contained in:
2026-06-09 21:19:30 +08:00
parent 8b41ff9360
commit 0484e79978
66 changed files with 4043 additions and 533 deletions
+55 -2
View File
@@ -5,7 +5,7 @@
// - accumulate events into a ref array
// - expose sessionStatus + connection-state status
import { ref, onScopeDispose, type Ref } from 'vue'
import { openSessionWS, type AcpEvent } from '@/api/acp'
import { openSessionWS, acpApi, type AcpEvent, type PermissionRequest } from '@/api/acp'
import { useAuthStore } from '@/stores/auth'
export type StreamStatus = 'idle' | 'connecting' | 'streaming' | 'closed' | 'error'
@@ -15,12 +15,26 @@ export interface UseAcpStreamReturn {
status: Ref<StreamStatus>
sessionStatus: Ref<SessionStatus>
events: Ref<AcpEvent[]>
pendingPermissions: Ref<PermissionRequest[]>
prompt: (text: string, agentSessionID: string) => void
cancel: (agentSessionID: string) => void
reconnect: () => void
close: () => void
}
// 把控制帧(ID=0)的 payload 归一为对象,无论 WS 传来字符串还是已解析对象。
function decodePayload(raw: unknown): Record<string, unknown> {
if (typeof raw === 'string') {
try {
return JSON.parse(raw) as Record<string, unknown>
} catch {
return {}
}
}
if (raw && typeof raw === 'object') return raw as Record<string, unknown>
return {}
}
interface ServerStateEvent {
kind: 'state'
status: SessionStatus
@@ -34,8 +48,18 @@ export function useAcpStream(sessionId: string): UseAcpStreamReturn {
const status = ref<StreamStatus>('idle')
const sessionStatus = ref<SessionStatus>('starting')
const events = ref<AcpEvent[]>([])
const pendingPermissions = ref<PermissionRequest[]>([])
const lastEventID = ref(0)
function upsertPermission(p: PermissionRequest) {
if (!pendingPermissions.value.some((x) => x.id === p.id)) {
pendingPermissions.value.push(p)
}
}
function removePermission(reqID: string) {
pendingPermissions.value = pendingPermissions.value.filter((x) => x.id !== reqID)
}
let ws: WebSocket | null = null
let retried = 0
let manualClose = false
@@ -53,6 +77,13 @@ export function useAcpStream(sessionId: string): UseAcpStreamReturn {
sock.onopen = () => {
status.value = 'streaming'
retried = 0
// 控制帧(permission_*)不落 acp_events,重连不补发;连上后主动拉取当前待审。
acpApi.sessions
.permissions(sessionId)
.then((list) => {
for (const p of list) upsertPermission(p)
})
.catch(() => {})
}
sock.onmessage = (ev: MessageEvent) => {
@@ -65,6 +96,28 @@ export function useAcpStream(sessionId: string): UseAcpStreamReturn {
if (!parsed || typeof parsed !== 'object') return
const obj = parsed as Record<string, unknown>
// 权限请求/决议控制帧(不进 events 流,单独维护 pendingPermissions)。
if (obj.kind === 'permission_request') {
const p = decodePayload(obj.payload)
upsertPermission({
id: String(p.request_id ?? ''),
session_id: String(p.session_id ?? ''),
tool_name: String(p.tool_name ?? ''),
tool_call: p.tool_call,
options: Array.isArray(p.options)
? (p.options as { id: string; name?: string }[])
: [],
status: String(p.status ?? 'pending'),
created_at: String(p.created_at ?? ''),
})
return
}
if (obj.kind === 'permission_resolved') {
const p = decodePayload(obj.payload)
removePermission(String(p.request_id ?? ''))
return
}
// Initial state event from server
if (obj.kind === 'state') {
const s = (parsed as ServerStateEvent).status
@@ -156,5 +209,5 @@ export function useAcpStream(sessionId: string): UseAcpStreamReturn {
connect()
onScopeDispose(close)
return { status, sessionStatus, events, prompt, cancel, reconnect, close }
return { status, sessionStatus, events, pendingPermissions, prompt, cancel, reconnect, close }
}