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:
2025-12-15 19:42:51 +08:00
parent f09f8f2b03
commit ec3c7bccf9
13 changed files with 409 additions and 7 deletions
+35 -2
View File
@@ -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, {
+1
View File
@@ -19,4 +19,5 @@ export {
type TokenUsage,
type CompressionResult,
type ContextUsageInfo,
type AgentModeOptions,
} from './adapter.js';
+54
View File
@@ -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);
+6
View File
@@ -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;
};
}
+3 -1
View File
@@ -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;