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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user