feat(permission): 实现 WebSocket 权限确认机制

重构权限系统,将终端 UI 代码从 core 模块移除,实现基于 WebSocket 的权限确认流程:

Core 模块清理:
- 删除 permission/prompt.ts 和 file-prompt.ts(终端交互)
- 删除 diff.ts 中的 chalk 渲染函数
- 删除 config.ts 中的 inquirer 交互
- 移除 chalk 依赖

Server 权限处理:
- 新增 permission/handler.ts,实现 WebSocket 权限请求/响应
- 更新 agent/adapter.ts 设置权限回调
- 更新 ws.ts 处理 permission_response 消息

Web 权限组件:
- 新增 PermissionDialog 组件,显示权限请求详情和 Diff
- 更新 useChat hook 管理权限状态
- 更新 Chat 页面集成权限弹窗
This commit is contained in:
2025-12-13 01:09:35 +08:00
parent 5d4afecd48
commit 1d69fd876d
20 changed files with 739 additions and 1560 deletions
+55 -1
View File
@@ -6,6 +6,7 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { createWebSocket, getMessages, type Message } from '../api/client.js';
import type { PermissionRequest } from '../components/PermissionDialog.js';
interface UseChatOptions {
sessionId: string;
@@ -19,6 +20,7 @@ interface ChatState {
isConnected: boolean;
isLoading: boolean;
streamingContent: string;
permissionRequest: PermissionRequest | null;
}
export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated }: UseChatOptions) {
@@ -27,10 +29,11 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
isConnected: false,
isLoading: false,
streamingContent: '',
permissionRequest: null,
});
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const reconnectAttemptsRef = useRef(0);
const maxReconnectAttempts = 5;
// 标记是否正在主动关闭连接(切换 session 时)
@@ -144,6 +147,16 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
onSessionUpdatedRef.current?.(message.payload.id, message.payload.name);
}
break;
case 'permission_request':
// 权限请求
if (message.payload) {
setState((prev) => ({
...prev,
permissionRequest: message.payload as PermissionRequest,
}));
}
break;
}
} catch {
// 忽略解析错误
@@ -188,6 +201,44 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
setState((prev) => ({ ...prev, isLoading: false, streamingContent: '' }));
}, [sessionId]);
// 发送权限响应
const respondToPermission = useCallback(
(requestId: string, allow: boolean, remember?: boolean) => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
onErrorRef.current?.(new Error('WebSocket not connected'));
return;
}
wsRef.current.send(
JSON.stringify({
type: 'permission_response',
sessionId,
payload: { requestId, allow, remember },
})
);
// 清除权限请求状态
setState((prev) => ({ ...prev, permissionRequest: null }));
},
[sessionId]
);
// 允许权限请求
const allowPermission = useCallback(
(requestId: string, remember?: boolean) => {
respondToPermission(requestId, true, remember);
},
[respondToPermission]
);
// 拒绝权限请求
const denyPermission = useCallback(
(requestId: string, remember?: boolean) => {
respondToPermission(requestId, false, remember);
},
[respondToPermission]
);
// 初始化
useEffect(() => {
// 重置状态
@@ -197,6 +248,7 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
isConnected: false,
isLoading: false,
streamingContent: '',
permissionRequest: null,
});
reconnectAttemptsRef.current = 0;
@@ -225,5 +277,7 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
sendMessage,
cancelProcessing,
reload: loadMessages,
allowPermission,
denyPermission,
};
}