feat(ui): 实现 Agent 模式切换和 Auto Edit 功能
- 添加 AgentModeSelector 组件,支持 Build/Plan 模式切换 - Build 模式下显示 Auto Edit 开关,自动授权文件写入/编辑 - 扩展 useChat hook 添加会话级别的 agentMode/autoApprove 状态 - 服务端支持解析和应用 Agent 模式配置 - 权限处理器实现 auto-approve 检查(仅 write/edit,不含 delete)
This commit is contained in:
@@ -11,7 +11,7 @@ import type { SessionStatus } from '../types.js';
|
||||
import { getSessionManager } from '../session/manager.js';
|
||||
import { broadcastToSession } from '../ws.js';
|
||||
import { emitStatusEvent, emitLogEvent } from '../sse.js';
|
||||
import { createServerPermissionCallback } from '../permission/handler.js';
|
||||
import { createServerPermissionCallback, setSessionAutoApprove } from '../permission/handler.js';
|
||||
|
||||
// ============================================================================
|
||||
// Core 模块接口定义(避免直接依赖 @ai-assistant/core 类型)
|
||||
@@ -97,6 +97,16 @@ interface ChatOptions {
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 模式选项
|
||||
*/
|
||||
export interface AgentModeOptions {
|
||||
/** Agent 模式 (build/plan) */
|
||||
agentMode?: 'build' | 'plan';
|
||||
/** 是否自动授权文件写入/编辑 */
|
||||
autoApprove?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 实例接口
|
||||
*/
|
||||
@@ -112,6 +122,9 @@ interface AgentInstance {
|
||||
shouldCompress(messages: unknown[]): boolean;
|
||||
};
|
||||
getHistory(): unknown[];
|
||||
setAgentMode?(mode: 'build' | 'plan'): void;
|
||||
setAutoApprove?(config: { file?: { write?: 'allow'; edit?: 'allow' } }): void;
|
||||
clearAutoApprove?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,7 +356,11 @@ export async function destroyAgent(sessionId: string): Promise<void> {
|
||||
/**
|
||||
* 处理用户消息并流式返回响应
|
||||
*/
|
||||
export async function processMessage(sessionId: string, content: string): Promise<void> {
|
||||
export async function processMessage(
|
||||
sessionId: string,
|
||||
content: string,
|
||||
options?: AgentModeOptions
|
||||
): Promise<void> {
|
||||
const sessionManager = getSessionManager();
|
||||
|
||||
// 取消之前可能存在的请求
|
||||
@@ -405,6 +422,22 @@ export async function processMessage(sessionId: string, content: string): Promis
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用 Agent 模式和自动授权配置
|
||||
if (options?.agentMode && agent.setAgentMode) {
|
||||
agent.setAgentMode(options.agentMode);
|
||||
}
|
||||
|
||||
// autoApprove 仅对 build 模式生效,且只允许 write/edit(不含 delete)
|
||||
if (options?.autoApprove && options?.agentMode !== 'plan') {
|
||||
// 设置会话级别的 auto-approve 配置(用于权限回调)
|
||||
setSessionAutoApprove(sessionId, {
|
||||
file: { write: 'allow', edit: 'allow' },
|
||||
});
|
||||
} else {
|
||||
// 清除 auto-approve 配置
|
||||
setSessionAutoApprove(sessionId, null);
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 Agent 的 chat 方法,使用流式回调和 AbortSignal
|
||||
const result = await agent.chat(content, {
|
||||
|
||||
@@ -19,4 +19,5 @@ export {
|
||||
type TokenUsage,
|
||||
type CompressionResult,
|
||||
type ContextUsageInfo,
|
||||
type AgentModeOptions,
|
||||
} from './adapter.js';
|
||||
|
||||
@@ -39,9 +39,56 @@ interface PendingRequest {
|
||||
|
||||
const pendingRequests = new Map<string, PendingRequest>();
|
||||
|
||||
// 会话级别的 auto-approve 配置
|
||||
// key: sessionId, value: auto-approve 配置
|
||||
const sessionAutoApprove = new Map<string, { file?: { write?: 'allow'; edit?: 'allow' } }>();
|
||||
|
||||
// 默认超时时间(60秒)
|
||||
const PERMISSION_TIMEOUT = 60000;
|
||||
|
||||
/**
|
||||
* 设置会话的 auto-approve 配置
|
||||
*/
|
||||
export function setSessionAutoApprove(
|
||||
sessionId: string,
|
||||
config: { file?: { write?: 'allow'; edit?: 'allow' } } | null
|
||||
): void {
|
||||
if (config) {
|
||||
sessionAutoApprove.set(sessionId, config);
|
||||
} else {
|
||||
sessionAutoApprove.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的 auto-approve 配置
|
||||
*/
|
||||
export function getSessionAutoApprove(sessionId: string): { file?: { write?: 'allow'; edit?: 'allow' } } | null {
|
||||
return sessionAutoApprove.get(sessionId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查操作是否被 auto-approve
|
||||
*/
|
||||
function isAutoApproved(sessionId: string, ctx: PermissionContext): boolean {
|
||||
const config = sessionAutoApprove.get(sessionId);
|
||||
if (!config) return false;
|
||||
|
||||
const command = ctx.command.toLowerCase();
|
||||
|
||||
// 检查是否为文件写入操作
|
||||
if (command.startsWith('write ') && config.file?.write === 'allow') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否为文件编辑操作
|
||||
if (command.startsWith('edit ') && config.file?.edit === 'allow') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从命令或上下文检测权限类型
|
||||
*/
|
||||
@@ -117,6 +164,13 @@ function buildRequestContext(ctx: PermissionContext): PermissionRequestContext {
|
||||
export function createServerPermissionCallback(sessionId: string) {
|
||||
return async (ctx: unknown): Promise<PermissionDecision> => {
|
||||
const permCtx = ctx as PermissionContext;
|
||||
|
||||
// 检查 auto-approve 配置
|
||||
if (isAutoApproved(sessionId, permCtx)) {
|
||||
console.log(`[Permission] Auto-approved: ${permCtx.command}`);
|
||||
return { allow: true, remember: false };
|
||||
}
|
||||
|
||||
const requestId = randomUUID();
|
||||
const permissionType = detectPermissionType(permCtx);
|
||||
const context = buildRequestContext(permCtx);
|
||||
|
||||
@@ -87,6 +87,9 @@ export type Tool = z.infer<typeof ToolSchema>;
|
||||
|
||||
// ============ WebSocket 消息 ============
|
||||
|
||||
// Agent 模式类型
|
||||
export type AgentModeType = 'build' | 'plan';
|
||||
|
||||
// 客户端发送的消息
|
||||
export interface ClientMessage {
|
||||
type: 'message' | 'cancel' | 'tool_response' | 'permission_response';
|
||||
@@ -99,6 +102,9 @@ export interface ClientMessage {
|
||||
requestId?: string;
|
||||
allow?: boolean;
|
||||
remember?: boolean;
|
||||
// Agent mode fields
|
||||
agentMode?: AgentModeType;
|
||||
autoApprove?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ export async function handleWebSocketMessage(
|
||||
case 'message': {
|
||||
// 用户发送消息
|
||||
let content = message.payload?.content || '';
|
||||
const agentMode = message.payload?.agentMode as 'build' | 'plan' | undefined;
|
||||
const autoApprove = message.payload?.autoApprove as boolean | undefined;
|
||||
|
||||
// 将 @filepath 转换为 ./filepath 格式(方便 AI 识别为文件路径)
|
||||
content = content.replace(/@([\w./-]+)/g, './$1');
|
||||
@@ -119,7 +121,7 @@ export async function handleWebSocketMessage(
|
||||
|
||||
// 调用 Agent 处理消息(异步,不阻塞)
|
||||
// 消息存储由 Core Agent 负责
|
||||
processMessage(sessionId, content).catch((error) => {
|
||||
processMessage(sessionId, content, { agentMode, autoApprove }).catch((error) => {
|
||||
console.error('[WS] Agent processing error:', error);
|
||||
});
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user