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
+192
View File
@@ -0,0 +1,192 @@
/**
* Server 端权限处理器
* 通过 WebSocket 发送权限请求并等待客户端响应
*/
import { randomUUID } from 'crypto';
import { broadcastToSession } from '../ws.js';
import type {
PermissionType,
PermissionRequestPayload,
PermissionRequestContext,
ServerMessage,
} from '../types.js';
/**
* 权限决策结果
*/
export interface PermissionDecision {
allow: boolean;
remember?: boolean;
}
/**
* 权限上下文(来自 core 模块)
*/
export interface PermissionContext {
command: string;
workdir: string;
patterns?: string[];
externalPaths?: string[];
}
// 等待中的权限请求
interface PendingRequest {
resolve: (decision: PermissionDecision) => void;
reject: (error: Error) => void;
timeout: ReturnType<typeof setTimeout>;
}
const pendingRequests = new Map<string, PendingRequest>();
// 默认超时时间(60秒)
const PERMISSION_TIMEOUT = 60000;
/**
* 从命令或上下文检测权限类型
*/
function detectPermissionType(ctx: PermissionContext): PermissionType {
const command = ctx.command.toLowerCase();
// 检测 git 操作
if (command.startsWith('git ')) {
return 'git';
}
// 检测文件操作
const fileOps = ['read', 'write', 'edit', 'delete', 'move', 'copy', 'mkdir'];
for (const op of fileOps) {
if (command.startsWith(`${op} `)) {
return 'file';
}
}
// 检测 web 操作
if (command.includes('fetch') || command.includes('http')) {
return 'web';
}
// 默认为 bash
return 'bash';
}
/**
* 构建权限请求上下文
*/
function buildRequestContext(ctx: PermissionContext): PermissionRequestContext {
const permType = detectPermissionType(ctx);
switch (permType) {
case 'file': {
const parts = ctx.command.split(' ');
const operation = parts[0];
const path = parts.slice(1).join(' ');
return {
operation,
path,
patterns: ctx.patterns,
externalPaths: ctx.externalPaths,
};
}
case 'git': {
const gitOp = ctx.command.replace(/^git\s+/, '').split(' ')[0];
return {
command: ctx.command,
gitOperation: gitOp,
};
}
case 'web': {
return {
command: ctx.command,
query: ctx.command,
};
}
default:
return {
command: ctx.command,
patterns: ctx.patterns,
externalPaths: ctx.externalPaths,
};
}
}
/**
* 创建 Server 端权限回调函数
* 用于在 Agent 创建时设置
*/
export function createServerPermissionCallback(sessionId: string) {
return async (ctx: unknown): Promise<PermissionDecision> => {
const permCtx = ctx as PermissionContext;
const requestId = randomUUID();
const permissionType = detectPermissionType(permCtx);
const context = buildRequestContext(permCtx);
// 构建请求 payload
const payload: PermissionRequestPayload = {
requestId,
permissionType,
context,
};
// 发送权限请求到客户端
const message: ServerMessage = {
type: 'permission_request',
sessionId,
payload,
};
broadcastToSession(sessionId, message);
// 等待响应(带超时)
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
pendingRequests.delete(requestId);
// 超时默认拒绝
resolve({ allow: false, remember: false });
}, PERMISSION_TIMEOUT);
pendingRequests.set(requestId, { resolve, reject, timeout });
});
};
}
/**
* 处理权限响应
* 由 WebSocket 消息处理器调用
*/
export function handlePermissionResponse(
requestId: string,
allow: boolean,
remember?: boolean
): boolean {
const pending = pendingRequests.get(requestId);
if (!pending) {
console.warn(`[Permission] No pending request found for: ${requestId}`);
return false;
}
clearTimeout(pending.timeout);
pendingRequests.delete(requestId);
pending.resolve({ allow, remember });
return true;
}
/**
* 取消会话的所有待处理权限请求
* 用于会话断开时清理
*/
export function cancelPendingRequests(sessionId: string): void {
// 由于我们没有存储 sessionId -> requestId 的映射,
// 这个函数目前只是一个占位符
// 在实际使用中,如果需要按会话取消请求,需要维护额外的映射
console.log(`[Permission] Cancelling pending requests for session: ${sessionId}`);
}
/**
* 获取待处理请求数量(用于调试)
*/
export function getPendingRequestCount(): number {
return pendingRequests.size;
}