From 0a26c3ab723ce1c4aa0b26f654d63351c86f9fec Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 16 Dec 2025 21:06:41 +0800 Subject: [PATCH] =?UTF-8?q?refactor(core,server):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=81=8C=E8=B4=A3=E5=B9=B6=E6=B6=88=E9=99=A4?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一 ToolStatus 类型(Core 导出,Server 引用) - 重命名 Server SessionManager 为 SessionMetadataManager - 扩展 Core PermissionContext 添加结构化字段 - 统一 Part 类型导出(Core 定义存储格式,Server 定义展示格式) - 简化 Message 格式(移除 MergedMessage,统一使用 Message) - 添加向后兼容的类型别名和 @deprecated 注释 --- packages/core/src/index.ts | 5 ++ packages/core/src/permission/checkers/bash.ts | 2 + packages/core/src/permission/checkers/file.ts | 7 +- packages/core/src/permission/checkers/git.ts | 8 +- packages/core/src/permission/checkers/web.ts | 4 +- packages/core/src/permission/index.ts | 1 + packages/core/src/permission/types.ts | 27 +++++- packages/core/src/session/index.ts | 19 +++- packages/server/src/index.ts | 2 +- packages/server/src/permission/handler.ts | 65 +++++++------- packages/server/src/routes/sessions.ts | 4 +- packages/server/src/session/manager.ts | 26 ++++-- packages/server/src/types.ts | 86 +++++++++---------- packages/ui/src/api/types.ts | 12 ++- packages/ui/src/components/ChatMessage.tsx | 4 +- 15 files changed, 177 insertions(+), 95 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8bf86e2..a057ed6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -45,6 +45,8 @@ export type { Part, PartType, ToolPart, + TextPart, + ReasoningPart, ToolStatus, ToolState, TodoItem, @@ -57,11 +59,14 @@ export type { UserInput, ChatResult } from './types/index.js'; // Permission export { getPermissionManager } from './permission/index.js'; export type { + PermissionType, PermissionContext, PermissionDecision, PermissionCheckResult, FilePermissionContext, + FileOperation, GitPermissionContext, + GitOperation, WebPermissionContext, } from './permission/index.js'; diff --git a/packages/core/src/permission/checkers/bash.ts b/packages/core/src/permission/checkers/bash.ts index d15715d..2f9902e 100644 --- a/packages/core/src/permission/checkers/bash.ts +++ b/packages/core/src/permission/checkers/bash.ts @@ -227,6 +227,7 @@ export class BashPermissionChecker implements PermissionChecker { const decision = await this.askCallback({ ...ctx, + permissionType: 'bash', externalPaths, }); @@ -273,6 +274,7 @@ export class BashPermissionChecker implements PermissionChecker { const decision = await this.askCallback({ ...ctx, + permissionType: 'bash', patterns: askPatterns, }); diff --git a/packages/core/src/permission/checkers/file.ts b/packages/core/src/permission/checkers/file.ts index 82cfbcd..a062d82 100644 --- a/packages/core/src/permission/checkers/file.ts +++ b/packages/core/src/permission/checkers/file.ts @@ -262,10 +262,15 @@ export class FilePermissionChecker implements PermissionChecker { }; } - // 构造兼容的 PermissionContext + // 构造带结构化信息的 PermissionContext const permCtx: PermissionContext = { command: `${ctx.operation} ${ctx.path}`, workdir: ctx.workdir, + permissionType: 'file', + fileOperation: ctx.operation, + filePath: ctx.path, + newContent: ctx.newContent, + oldContent: ctx.oldContent, patterns: [ctx.operation], externalPaths: this.isInProjectDirectory(absolutePath) ? undefined : [absolutePath], }; diff --git a/packages/core/src/permission/checkers/git.ts b/packages/core/src/permission/checkers/git.ts index 1212d17..678589b 100644 --- a/packages/core/src/permission/checkers/git.ts +++ b/packages/core/src/permission/checkers/git.ts @@ -178,10 +178,16 @@ export class GitPermissionChecker implements PermissionChecker { }; } - // 调用回调询问用户 + // 调用回调询问用户(带结构化信息) const decision = await this.askCallback({ command: description, workdir: process.cwd(), + permissionType: 'git', + gitOperation: ctx.operation, + gitTarget: ctx.target, + gitRemote: ctx.remote, + gitForce: ctx.force, + gitMessage: ctx.message, }); if (decision.remember) { diff --git a/packages/core/src/permission/checkers/web.ts b/packages/core/src/permission/checkers/web.ts index 307f3d2..06d31e7 100644 --- a/packages/core/src/permission/checkers/web.ts +++ b/packages/core/src/permission/checkers/web.ts @@ -109,10 +109,12 @@ export class WebPermissionChecker implements PermissionChecker { }; } - // 调用回调询问用户 + // 调用回调询问用户(带结构化信息) const decision = await this.askCallback({ command: `web_search: ${query}`, workdir: process.cwd(), + permissionType: 'web', + webQuery: query, }); if (decision.remember) { diff --git a/packages/core/src/permission/index.ts b/packages/core/src/permission/index.ts index b4cff31..bcb3c1d 100644 --- a/packages/core/src/permission/index.ts +++ b/packages/core/src/permission/index.ts @@ -2,6 +2,7 @@ export type { PermissionAction, PermissionRule, BashPermissionConfig, + PermissionType, PermissionContext, PermissionCheckResult, PermissionDecision, diff --git a/packages/core/src/permission/types.ts b/packages/core/src/permission/types.ts index 37d2cdf..3a2cbbc 100644 --- a/packages/core/src/permission/types.ts +++ b/packages/core/src/permission/types.ts @@ -17,12 +17,35 @@ export interface BashPermissionConfig { default: PermissionAction; } +// 权限类型 +export type PermissionType = 'bash' | 'file' | 'git' | 'web'; + // 权限请求上下文 export interface PermissionContext { - command: string; - workdir: string; + // 必选字段 + command: string; // 命令字符串(用于显示和向后兼容) + workdir: string; // 工作目录 + + // 可选的结构化字段(用于更精确的类型判断) + permissionType?: PermissionType; // 权限类型 patterns?: string[]; // 匹配到的模式 externalPaths?: string[]; // 访问的外部路径 + + // 文件操作相关(permissionType === 'file' 时使用) + fileOperation?: FileOperation; + filePath?: string; + newContent?: string; // 文件写入/编辑的新内容 + oldContent?: string; // 文件编辑时的原内容 + + // Git 操作相关(permissionType === 'git' 时使用) + gitOperation?: GitOperation; + gitTarget?: string; // 分支名、文件路径等 + gitRemote?: string; // 远程仓库名 + gitForce?: boolean; // 是否强制操作 + gitMessage?: string; // 提交信息等 + + // Web 操作相关(permissionType === 'web' 时使用) + webQuery?: string; // 搜索查询 } // 文件操作类型 diff --git a/packages/core/src/session/index.ts b/packages/core/src/session/index.ts index 7e0cee3..2db5dad 100644 --- a/packages/core/src/session/index.ts +++ b/packages/core/src/session/index.ts @@ -11,7 +11,24 @@ export { createMessageInfo, } from './message.js'; -export type { Part, PartType, ToolStatus, ToolState, ToolPart, TextPart } from './parts.js'; +export type { + Part, + PartType, + ToolStatus, + ToolState, + ToolPart, + TextPart, + ReasoningPart, + FilePart, + StepStartPart, + StepFinishPart, + SnapshotPart, + PatchPart, + AgentPart, + SubtaskPart, + CompactionPart, + RetryPart, +} from './parts.js'; export { PartSchema, TextPartSchema, diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ab61210..1e9eb4d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -238,7 +238,7 @@ export async function startServer(options: ServerOptions = {}): Promise { // 导出 export { app, websocket }; -export { getSessionManager } from './session/manager.js'; +export { getSessionMetadataManager, getSessionManager } from './session/manager.js'; export { registerTool, getRegisteredTools } from './routes/tools.js'; export { getConfig, setConfig } from './routes/config.js'; export { diff --git a/packages/server/src/permission/handler.ts b/packages/server/src/permission/handler.ts index 256cd90..26e229d 100644 --- a/packages/server/src/permission/handler.ts +++ b/packages/server/src/permission/handler.ts @@ -6,12 +6,11 @@ import { randomUUID } from 'crypto'; import { broadcastToSession } from '../ws.js'; import type { - PermissionType, PermissionRequestPayload, PermissionRequestContext, ServerMessage, } from '../types.js'; -import type { PermissionDecision, PermissionContext } from '@ai-assistant/core'; +import type { PermissionDecision, PermissionContext, PermissionType } from '@ai-assistant/core'; // 等待中的权限请求 interface PendingRequest { @@ -57,14 +56,21 @@ function isAutoApproved(sessionId: string, ctx: PermissionContext): boolean { const config = sessionAutoApprove.get(sessionId); if (!config) return false; - const command = ctx.command.toLowerCase(); + // 优先使用结构化字段判断 + if (ctx.permissionType === 'file') { + if (ctx.fileOperation === 'write' && config.file?.write === 'allow') { + return true; + } + if (ctx.fileOperation === 'edit' && config.file?.edit === 'allow') { + return true; + } + } - // 检查是否为文件写入操作 + // 向后兼容:解析 command 字符串 + 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; } @@ -73,17 +79,22 @@ function isAutoApproved(sessionId: string, ctx: PermissionContext): boolean { } /** - * 从命令或上下文检测权限类型 + * 从上下文获取权限类型 + * 优先使用结构化的 permissionType 字段,否则解析 command 字符串 */ -function detectPermissionType(ctx: PermissionContext): PermissionType { +function getPermissionType(ctx: PermissionContext): PermissionType { + // 优先使用结构化字段 + if (ctx.permissionType) { + return ctx.permissionType; + } + + // 向后兼容:解析 command 字符串 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} `)) { @@ -91,46 +102,42 @@ function detectPermissionType(ctx: PermissionContext): PermissionType { } } - // 检测 web 操作 - if (command.includes('fetch') || command.includes('http')) { + if (command.includes('fetch') || command.includes('http') || command.startsWith('web_search')) { return 'web'; } - // 默认为 bash return 'bash'; } /** * 构建权限请求上下文 + * 使用 Core 传递的结构化字段,减少字符串解析 */ function buildRequestContext(ctx: PermissionContext): PermissionRequestContext { - const permType = detectPermissionType(ctx); + const permType = getPermissionType(ctx); switch (permType) { - case 'file': { - const parts = ctx.command.split(' '); - const operation = parts[0]; - const path = parts.slice(1).join(' '); + case 'file': return { - operation, - path, + command: ctx.command, + operation: ctx.fileOperation || ctx.command.split(' ')[0], + path: ctx.filePath || ctx.command.split(' ').slice(1).join(' '), patterns: ctx.patterns, externalPaths: ctx.externalPaths, }; - } - case 'git': { - const gitOp = ctx.command.replace(/^git\s+/, '').split(' ')[0]; + + case 'git': return { command: ctx.command, - gitOperation: gitOp, + gitOperation: ctx.gitOperation || ctx.command.replace(/^git\s+/, '').split(' ')[0], }; - } - case 'web': { + + case 'web': return { command: ctx.command, - query: ctx.command, + query: ctx.webQuery || ctx.command, }; - } + default: return { command: ctx.command, @@ -155,7 +162,7 @@ export function createServerPermissionCallback(sessionId: string) { } const requestId = randomUUID(); - const permissionType = detectPermissionType(permCtx); + const permissionType = getPermissionType(permCtx); const context = buildRequestContext(permCtx); // 构建请求 payload diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index c81a542..9386395 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -9,7 +9,7 @@ import { getSessionManager } from '../session/manager.js'; import { CreateSessionInputSchema, type ToolCallInfo, - type MergedMessage, + type Message, type MessagePart, } from '../types.js'; import type { MessageInfo, Part, ToolPart } from '@ai-assistant/core'; @@ -131,7 +131,7 @@ sessionsRouter.get('/:id/messages', async (c) => { const messageInfos = await MessageStorage.listBySession(id); // 转换为前端格式 - const messages: MergedMessage[] = []; + const messages: Message[] = []; for (const msgInfo of messageInfos) { const parts = await PartStorage.getByIds(msgInfo.id, msgInfo.partIds); diff --git a/packages/server/src/session/manager.ts b/packages/server/src/session/manager.ts index ab73e9a..41e732a 100644 --- a/packages/server/src/session/manager.ts +++ b/packages/server/src/session/manager.ts @@ -1,7 +1,11 @@ /** - * Session Manager + * Session Metadata Manager * * 管理所有活跃的会话元数据(不存储消息,消息由 Core 负责) + * + * 注意:此类与 Core 的 SessionManager 职责不同: + * - SessionMetadataManager(本类):管理会话元数据(id、status、messageCount) + * - Core SessionManager:管理完整的会话数据(消息、Parts、Todos) */ import { v4 as uuidv4 } from 'uuid'; @@ -13,7 +17,7 @@ import type { } from '@ai-assistant/core'; import { SessionManager as CoreSessionManager } from '@ai-assistant/core'; -export class SessionManager { +export class SessionMetadataManager { private sessions: Map = new Map(); private sessionProjects: Map = new Map(); // sessionId -> projectId private coreManager: CoreSessionManager | null = null; @@ -48,7 +52,7 @@ export class SessionManager { // 加载已持久化的 sessions(所有项目) const summaries = await this.coreManager.listAllSessions(); - console.log(`[SessionManager] Found ${summaries.length} persisted sessions`); + console.log(`[SessionMetadataManager] Found ${summaries.length} persisted sessions`); for (const summary of summaries) { // 转换为 Server Session 格式(只保存元数据,不存储消息) @@ -69,9 +73,9 @@ export class SessionManager { } } - console.log(`[SessionManager] Loaded ${this.sessions.size} sessions from storage`); + console.log(`[SessionMetadataManager] Loaded ${this.sessions.size} sessions from storage`); } catch (error) { - console.warn('[SessionManager] Storage not available, using memory only:', error); + console.warn('[SessionMetadataManager] Storage not available, using memory only:', error); } this.initialized = true; @@ -207,11 +211,17 @@ export class SessionManager { } // 单例 -let instance: SessionManager | null = null; +let instance: SessionMetadataManager | null = null; -export function getSessionManager(): SessionManager { +export function getSessionMetadataManager(): SessionMetadataManager { if (!instance) { - instance = new SessionManager(); + instance = new SessionMetadataManager(); } return instance; } + +// 向后兼容别名(已废弃) +/** @deprecated 使用 SessionMetadataManager 和 getSessionMetadataManager 代替 */ +export const SessionManager = SessionMetadataManager; +/** @deprecated 使用 getSessionMetadataManager 代替 */ +export const getSessionManager = getSessionMetadataManager; diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 67388f7..48e0375 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -4,6 +4,22 @@ import { z } from 'zod'; +// 从 Core 导入共享类型,避免重复定义 +import type { + ToolStatus, + PermissionType, + PermissionContext, + Part as CorePart, + PartType as CorePartType, + TextPart as CoreTextPart, + ToolPart as CoreToolPart, + ReasoningPart as CoreReasoningPart, +} from '@ai-assistant/core'; + +// 重新导出 Core 类型,供其他模块使用 +export type { ToolStatus, PermissionType, PermissionContext }; +export type { CorePart, CorePartType, CoreTextPart, CoreToolPart, CoreReasoningPart }; + // ============ Session 相关 ============ export const SessionStatusSchema = z.enum(['idle', 'active', 'busy', 'running', 'paused']); @@ -30,34 +46,8 @@ export type CreateSessionInput = z.infer; // ============ Message 相关 ============ -export const MessageRoleSchema = z.enum(['user', 'assistant', 'system', 'tool']); - -export const MessageSchema = z.object({ - id: z.string().uuid(), - sessionId: z.string().uuid(), - role: MessageRoleSchema, - content: z.string(), - createdAt: z.string(), - toolCalls: z - .array( - z.object({ - id: z.string(), - name: z.string(), - arguments: z.record(z.string(), z.unknown()), - }) - ) - .optional(), - toolResults: z - .array( - z.object({ - toolCallId: z.string(), - result: z.unknown(), - }) - ) - .optional(), -}); - -export type Message = z.infer; +// 消息角色(仅用于 SendMessageInput,实际消息只有 user/assistant) +export const MessageRoleSchema = z.enum(['user', 'assistant']); export const SendMessageInputSchema = z.object({ role: MessageRoleSchema.default('user'), @@ -213,10 +203,11 @@ export type SubagentEventPayload = // ============ Permission 相关 ============ -export type PermissionType = 'bash' | 'file' | 'git' | 'web'; +// PermissionType 和 PermissionContext 已从 Core 导入(见文件顶部) /** - * 权限请求上下文 + * 权限请求上下文(Server 专用,已废弃) + * @deprecated 使用 PermissionContext(从 Core 导入)代替 */ export interface PermissionRequestContext { command?: string; // bash 命令 @@ -280,15 +271,19 @@ export interface SSEEvent { }; } -// ============ 消息 Parts 相关 ============ +// ============ 消息 Parts 相关(前端展示格式)============ +// +// 说明:这些是前端展示用的扁平化 Part 类型 +// Core 的 Part 类型(CorePart)是存储格式,使用状态机模式 +// Server 在 routes/sessions.ts 中负责将 CorePart 转换为 MessagePart + +// ToolCallStatus 已废弃,使用 ToolStatus(从 Core 导入) +// 保留类型别名以保持向后兼容 +/** @deprecated 使用 ToolStatus 代替 */ +export type ToolCallStatus = ToolStatus; /** - * 工具调用状态 - */ -export type ToolCallStatus = 'pending' | 'running' | 'completed' | 'error'; - -/** - * 文本 Part + * 文本 Part(前端展示格式) */ export interface TextMessagePart { type: 'text'; @@ -297,14 +292,15 @@ export interface TextMessagePart { } /** - * 工具调用 Part + * 工具调用 Part(前端展示格式) + * 与 Core 的 ToolPart 不同,这里使用扁平结构而非状态机 */ export interface ToolMessagePart { type: 'tool'; id: string; toolCallId: string; toolName: string; - status: ToolCallStatus; + status: ToolStatus; arguments: Record; result?: unknown; error?: string; @@ -312,7 +308,7 @@ export interface ToolMessagePart { } /** - * 推理 Part + * 推理 Part(前端展示格式) */ export interface ReasoningMessagePart { type: 'reasoning'; @@ -321,7 +317,8 @@ export interface ReasoningMessagePart { } /** - * 消息 Part 联合类型 + * 消息 Part 联合类型(前端展示格式) + * 用于 API 响应和 WebSocket 消息 */ export type MessagePart = TextMessagePart | ToolMessagePart | ReasoningMessagePart; @@ -332,7 +329,7 @@ export interface ToolCallInfo { id: string; name: string; arguments: Record; - status: ToolCallStatus; + status: ToolStatus; result?: unknown; error?: string; duration?: number; @@ -345,7 +342,7 @@ export interface ToolCallInfo { * - user: 用户输入 * - assistant: AI 回复(包含文本和工具调用,按原始顺序) */ -export interface MergedMessage { +export interface Message { id: string; sessionId: string; role: 'user' | 'assistant'; @@ -363,6 +360,9 @@ export interface MergedMessage { }; } +/** @deprecated 使用 Message 代替 */ +export type MergedMessage = Message; + // ============ API 响应 ============ export interface ApiResponse { diff --git a/packages/ui/src/api/types.ts b/packages/ui/src/api/types.ts index 466e7ed..f680933 100644 --- a/packages/ui/src/api/types.ts +++ b/packages/ui/src/api/types.ts @@ -13,8 +13,12 @@ export interface Session { /** * 工具调用状态 + * 与 Core 的 ToolStatus 保持一致 */ -export type ToolCallStatus = 'pending' | 'running' | 'completed' | 'error'; +export type ToolStatus = 'pending' | 'running' | 'completed' | 'error'; + +/** @deprecated 使用 ToolStatus 代替 */ +export type ToolCallStatus = ToolStatus; /** * 工具调用信息 @@ -23,7 +27,7 @@ export interface ToolCallInfo { id: string; name: string; arguments: Record; - status: ToolCallStatus; + status: ToolStatus; result?: unknown; error?: string; duration?: number; // 执行时长 ms @@ -48,7 +52,7 @@ export interface ToolMessagePart { id: string; toolCallId: string; toolName: string; - status: ToolCallStatus; + status: ToolStatus; arguments: Record; result?: unknown; error?: string; @@ -977,7 +981,7 @@ export type SubagentEventPayload = export interface SubagentToolInfo { id: string; toolName: string; - status: ToolCallStatus; + status: ToolStatus; args: Record; result?: unknown; error?: string; diff --git a/packages/ui/src/components/ChatMessage.tsx b/packages/ui/src/components/ChatMessage.tsx index 8e66269..0b34516 100644 --- a/packages/ui/src/components/ChatMessage.tsx +++ b/packages/ui/src/components/ChatMessage.tsx @@ -22,7 +22,7 @@ import { fadeInUp, smoothTransition } from '../utils/animations'; import { getAgentDisplayName } from '../utils/agent'; import { Markdown } from './Markdown'; import { FileMentionText } from './FileMentionTag'; -import type { Message, ToolCallInfo, ToolCallStatus, ToolMessagePart } from '../api/types.js'; +import type { Message, ToolCallInfo, ToolStatus, ToolMessagePart } from '../api/types.js'; interface ChatMessageProps { message: Message; @@ -319,7 +319,7 @@ function ToolPartItem({ part }: ToolPartItemProps) { /** * 获取工具状态图标 */ -function getStatusIcon(status: ToolCallStatus) { +function getStatusIcon(status: ToolStatus) { switch (status) { case 'pending': return ;