refactor(core,server): 统一模块职责并消除类型重复

- 统一 ToolStatus 类型(Core 导出,Server 引用)
- 重命名 Server SessionManager 为 SessionMetadataManager
- 扩展 Core PermissionContext 添加结构化字段
- 统一 Part 类型导出(Core 定义存储格式,Server 定义展示格式)
- 简化 Message 格式(移除 MergedMessage,统一使用 Message)
- 添加向后兼容的类型别名和 @deprecated 注释
This commit is contained in:
2025-12-16 21:06:41 +08:00
parent 1b7d55848d
commit 0a26c3ab72
15 changed files with 177 additions and 95 deletions
+5
View File
@@ -45,6 +45,8 @@ export type {
Part, Part,
PartType, PartType,
ToolPart, ToolPart,
TextPart,
ReasoningPart,
ToolStatus, ToolStatus,
ToolState, ToolState,
TodoItem, TodoItem,
@@ -57,11 +59,14 @@ export type { UserInput, ChatResult } from './types/index.js';
// Permission // Permission
export { getPermissionManager } from './permission/index.js'; export { getPermissionManager } from './permission/index.js';
export type { export type {
PermissionType,
PermissionContext, PermissionContext,
PermissionDecision, PermissionDecision,
PermissionCheckResult, PermissionCheckResult,
FilePermissionContext, FilePermissionContext,
FileOperation,
GitPermissionContext, GitPermissionContext,
GitOperation,
WebPermissionContext, WebPermissionContext,
} from './permission/index.js'; } from './permission/index.js';
@@ -227,6 +227,7 @@ export class BashPermissionChecker implements PermissionChecker {
const decision = await this.askCallback({ const decision = await this.askCallback({
...ctx, ...ctx,
permissionType: 'bash',
externalPaths, externalPaths,
}); });
@@ -273,6 +274,7 @@ export class BashPermissionChecker implements PermissionChecker {
const decision = await this.askCallback({ const decision = await this.askCallback({
...ctx, ...ctx,
permissionType: 'bash',
patterns: askPatterns, patterns: askPatterns,
}); });
@@ -262,10 +262,15 @@ export class FilePermissionChecker implements PermissionChecker {
}; };
} }
// 构造兼容的 PermissionContext // 构造带结构化信息的 PermissionContext
const permCtx: PermissionContext = { const permCtx: PermissionContext = {
command: `${ctx.operation} ${ctx.path}`, command: `${ctx.operation} ${ctx.path}`,
workdir: ctx.workdir, workdir: ctx.workdir,
permissionType: 'file',
fileOperation: ctx.operation,
filePath: ctx.path,
newContent: ctx.newContent,
oldContent: ctx.oldContent,
patterns: [ctx.operation], patterns: [ctx.operation],
externalPaths: this.isInProjectDirectory(absolutePath) ? undefined : [absolutePath], externalPaths: this.isInProjectDirectory(absolutePath) ? undefined : [absolutePath],
}; };
+7 -1
View File
@@ -178,10 +178,16 @@ export class GitPermissionChecker implements PermissionChecker {
}; };
} }
// 调用回调询问用户 // 调用回调询问用户(带结构化信息)
const decision = await this.askCallback({ const decision = await this.askCallback({
command: description, command: description,
workdir: process.cwd(), workdir: process.cwd(),
permissionType: 'git',
gitOperation: ctx.operation,
gitTarget: ctx.target,
gitRemote: ctx.remote,
gitForce: ctx.force,
gitMessage: ctx.message,
}); });
if (decision.remember) { if (decision.remember) {
+3 -1
View File
@@ -109,10 +109,12 @@ export class WebPermissionChecker implements PermissionChecker {
}; };
} }
// 调用回调询问用户 // 调用回调询问用户(带结构化信息)
const decision = await this.askCallback({ const decision = await this.askCallback({
command: `web_search: ${query}`, command: `web_search: ${query}`,
workdir: process.cwd(), workdir: process.cwd(),
permissionType: 'web',
webQuery: query,
}); });
if (decision.remember) { if (decision.remember) {
+1
View File
@@ -2,6 +2,7 @@ export type {
PermissionAction, PermissionAction,
PermissionRule, PermissionRule,
BashPermissionConfig, BashPermissionConfig,
PermissionType,
PermissionContext, PermissionContext,
PermissionCheckResult, PermissionCheckResult,
PermissionDecision, PermissionDecision,
+25 -2
View File
@@ -17,12 +17,35 @@ export interface BashPermissionConfig {
default: PermissionAction; default: PermissionAction;
} }
// 权限类型
export type PermissionType = 'bash' | 'file' | 'git' | 'web';
// 权限请求上下文 // 权限请求上下文
export interface PermissionContext { export interface PermissionContext {
command: string; // 必选字段
workdir: string; command: string; // 命令字符串(用于显示和向后兼容)
workdir: string; // 工作目录
// 可选的结构化字段(用于更精确的类型判断)
permissionType?: PermissionType; // 权限类型
patterns?: string[]; // 匹配到的模式 patterns?: string[]; // 匹配到的模式
externalPaths?: 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; // 搜索查询
} }
// 文件操作类型 // 文件操作类型
+18 -1
View File
@@ -11,7 +11,24 @@ export {
createMessageInfo, createMessageInfo,
} from './message.js'; } 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 { export {
PartSchema, PartSchema,
TextPartSchema, TextPartSchema,
+1 -1
View File
@@ -238,7 +238,7 @@ export async function startServer(options: ServerOptions = {}): Promise<void> {
// 导出 // 导出
export { app, websocket }; export { app, websocket };
export { getSessionManager } from './session/manager.js'; export { getSessionMetadataManager, getSessionManager } from './session/manager.js';
export { registerTool, getRegisteredTools } from './routes/tools.js'; export { registerTool, getRegisteredTools } from './routes/tools.js';
export { getConfig, setConfig } from './routes/config.js'; export { getConfig, setConfig } from './routes/config.js';
export { export {
+36 -29
View File
@@ -6,12 +6,11 @@
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { broadcastToSession } from '../ws.js'; import { broadcastToSession } from '../ws.js';
import type { import type {
PermissionType,
PermissionRequestPayload, PermissionRequestPayload,
PermissionRequestContext, PermissionRequestContext,
ServerMessage, ServerMessage,
} from '../types.js'; } from '../types.js';
import type { PermissionDecision, PermissionContext } from '@ai-assistant/core'; import type { PermissionDecision, PermissionContext, PermissionType } from '@ai-assistant/core';
// 等待中的权限请求 // 等待中的权限请求
interface PendingRequest { interface PendingRequest {
@@ -57,14 +56,21 @@ function isAutoApproved(sessionId: string, ctx: PermissionContext): boolean {
const config = sessionAutoApprove.get(sessionId); const config = sessionAutoApprove.get(sessionId);
if (!config) return false; 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') { if (command.startsWith('write ') && config.file?.write === 'allow') {
return true; return true;
} }
// 检查是否为文件编辑操作
if (command.startsWith('edit ') && config.file?.edit === 'allow') { if (command.startsWith('edit ') && config.file?.edit === 'allow') {
return true; 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(); const command = ctx.command.toLowerCase();
// 检测 git 操作
if (command.startsWith('git ')) { if (command.startsWith('git ')) {
return 'git'; return 'git';
} }
// 检测文件操作
const fileOps = ['read', 'write', 'edit', 'delete', 'move', 'copy', 'mkdir']; const fileOps = ['read', 'write', 'edit', 'delete', 'move', 'copy', 'mkdir'];
for (const op of fileOps) { for (const op of fileOps) {
if (command.startsWith(`${op} `)) { if (command.startsWith(`${op} `)) {
@@ -91,46 +102,42 @@ function detectPermissionType(ctx: PermissionContext): PermissionType {
} }
} }
// 检测 web 操作 if (command.includes('fetch') || command.includes('http') || command.startsWith('web_search')) {
if (command.includes('fetch') || command.includes('http')) {
return 'web'; return 'web';
} }
// 默认为 bash
return 'bash'; return 'bash';
} }
/** /**
* 构建权限请求上下文 * 构建权限请求上下文
* 使用 Core 传递的结构化字段,减少字符串解析
*/ */
function buildRequestContext(ctx: PermissionContext): PermissionRequestContext { function buildRequestContext(ctx: PermissionContext): PermissionRequestContext {
const permType = detectPermissionType(ctx); const permType = getPermissionType(ctx);
switch (permType) { switch (permType) {
case 'file': { case 'file':
const parts = ctx.command.split(' ');
const operation = parts[0];
const path = parts.slice(1).join(' ');
return { return {
operation, command: ctx.command,
path, operation: ctx.fileOperation || ctx.command.split(' ')[0],
path: ctx.filePath || ctx.command.split(' ').slice(1).join(' '),
patterns: ctx.patterns, patterns: ctx.patterns,
externalPaths: ctx.externalPaths, externalPaths: ctx.externalPaths,
}; };
}
case 'git': { case 'git':
const gitOp = ctx.command.replace(/^git\s+/, '').split(' ')[0];
return { return {
command: ctx.command, command: ctx.command,
gitOperation: gitOp, gitOperation: ctx.gitOperation || ctx.command.replace(/^git\s+/, '').split(' ')[0],
}; };
}
case 'web': { case 'web':
return { return {
command: ctx.command, command: ctx.command,
query: ctx.command, query: ctx.webQuery || ctx.command,
}; };
}
default: default:
return { return {
command: ctx.command, command: ctx.command,
@@ -155,7 +162,7 @@ export function createServerPermissionCallback(sessionId: string) {
} }
const requestId = randomUUID(); const requestId = randomUUID();
const permissionType = detectPermissionType(permCtx); const permissionType = getPermissionType(permCtx);
const context = buildRequestContext(permCtx); const context = buildRequestContext(permCtx);
// 构建请求 payload // 构建请求 payload
+2 -2
View File
@@ -9,7 +9,7 @@ import { getSessionManager } from '../session/manager.js';
import { import {
CreateSessionInputSchema, CreateSessionInputSchema,
type ToolCallInfo, type ToolCallInfo,
type MergedMessage, type Message,
type MessagePart, type MessagePart,
} from '../types.js'; } from '../types.js';
import type { MessageInfo, Part, ToolPart } from '@ai-assistant/core'; 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 messageInfos = await MessageStorage.listBySession(id);
// 转换为前端格式 // 转换为前端格式
const messages: MergedMessage[] = []; const messages: Message[] = [];
for (const msgInfo of messageInfos) { for (const msgInfo of messageInfos) {
const parts = await PartStorage.getByIds(msgInfo.id, msgInfo.partIds); const parts = await PartStorage.getByIds(msgInfo.id, msgInfo.partIds);
+18 -8
View File
@@ -1,7 +1,11 @@
/** /**
* Session Manager * Session Metadata Manager
* *
* 管理所有活跃的会话元数据(不存储消息,消息由 Core 负责) * 管理所有活跃的会话元数据(不存储消息,消息由 Core 负责)
*
* 注意:此类与 Core 的 SessionManager 职责不同:
* - SessionMetadataManager(本类):管理会话元数据(id、status、messageCount
* - Core SessionManager:管理完整的会话数据(消息、Parts、Todos)
*/ */
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@@ -13,7 +17,7 @@ import type {
} from '@ai-assistant/core'; } from '@ai-assistant/core';
import { SessionManager as CoreSessionManager } from '@ai-assistant/core'; import { SessionManager as CoreSessionManager } from '@ai-assistant/core';
export class SessionManager { export class SessionMetadataManager {
private sessions: Map<string, Session> = new Map(); private sessions: Map<string, Session> = new Map();
private sessionProjects: Map<string, string> = new Map(); // sessionId -> projectId private sessionProjects: Map<string, string> = new Map(); // sessionId -> projectId
private coreManager: CoreSessionManager | null = null; private coreManager: CoreSessionManager | null = null;
@@ -48,7 +52,7 @@ export class SessionManager {
// 加载已持久化的 sessions(所有项目) // 加载已持久化的 sessions(所有项目)
const summaries = await this.coreManager.listAllSessions(); 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) { for (const summary of summaries) {
// 转换为 Server Session 格式(只保存元数据,不存储消息) // 转换为 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) { } 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; 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) { if (!instance) {
instance = new SessionManager(); instance = new SessionMetadataManager();
} }
return instance; return instance;
} }
// 向后兼容别名(已废弃)
/** @deprecated 使用 SessionMetadataManager 和 getSessionMetadataManager 代替 */
export const SessionManager = SessionMetadataManager;
/** @deprecated 使用 getSessionMetadataManager 代替 */
export const getSessionManager = getSessionMetadataManager;
+43 -43
View File
@@ -4,6 +4,22 @@
import { z } from 'zod'; 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 相关 ============ // ============ Session 相关 ============
export const SessionStatusSchema = z.enum(['idle', 'active', 'busy', 'running', 'paused']); export const SessionStatusSchema = z.enum(['idle', 'active', 'busy', 'running', 'paused']);
@@ -30,34 +46,8 @@ export type CreateSessionInput = z.infer<typeof CreateSessionInputSchema>;
// ============ Message 相关 ============ // ============ Message 相关 ============
export const MessageRoleSchema = z.enum(['user', 'assistant', 'system', 'tool']); // 消息角色(仅用于 SendMessageInput,实际消息只有 user/assistant
export const MessageRoleSchema = z.enum(['user', 'assistant']);
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<typeof MessageSchema>;
export const SendMessageInputSchema = z.object({ export const SendMessageInputSchema = z.object({
role: MessageRoleSchema.default('user'), role: MessageRoleSchema.default('user'),
@@ -213,10 +203,11 @@ export type SubagentEventPayload =
// ============ Permission 相关 ============ // ============ Permission 相关 ============
export type PermissionType = 'bash' | 'file' | 'git' | 'web'; // PermissionType 和 PermissionContext 已从 Core 导入(见文件顶部)
/** /**
* 权限请求上下文 * 权限请求上下文Server 专用,已废弃)
* @deprecated 使用 PermissionContext(从 Core 导入)代替
*/ */
export interface PermissionRequestContext { export interface PermissionRequestContext {
command?: string; // bash 命令 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;
/** /**
* 工具调用状态 * 文本 Part(前端展示格式)
*/
export type ToolCallStatus = 'pending' | 'running' | 'completed' | 'error';
/**
* 文本 Part
*/ */
export interface TextMessagePart { export interface TextMessagePart {
type: 'text'; type: 'text';
@@ -297,14 +292,15 @@ export interface TextMessagePart {
} }
/** /**
* 工具调用 Part * 工具调用 Part(前端展示格式)
* 与 Core 的 ToolPart 不同,这里使用扁平结构而非状态机
*/ */
export interface ToolMessagePart { export interface ToolMessagePart {
type: 'tool'; type: 'tool';
id: string; id: string;
toolCallId: string; toolCallId: string;
toolName: string; toolName: string;
status: ToolCallStatus; status: ToolStatus;
arguments: Record<string, unknown>; arguments: Record<string, unknown>;
result?: unknown; result?: unknown;
error?: string; error?: string;
@@ -312,7 +308,7 @@ export interface ToolMessagePart {
} }
/** /**
* 推理 Part * 推理 Part(前端展示格式)
*/ */
export interface ReasoningMessagePart { export interface ReasoningMessagePart {
type: 'reasoning'; type: 'reasoning';
@@ -321,7 +317,8 @@ export interface ReasoningMessagePart {
} }
/** /**
* 消息 Part 联合类型 * 消息 Part 联合类型(前端展示格式)
* 用于 API 响应和 WebSocket 消息
*/ */
export type MessagePart = TextMessagePart | ToolMessagePart | ReasoningMessagePart; export type MessagePart = TextMessagePart | ToolMessagePart | ReasoningMessagePart;
@@ -332,7 +329,7 @@ export interface ToolCallInfo {
id: string; id: string;
name: string; name: string;
arguments: Record<string, unknown>; arguments: Record<string, unknown>;
status: ToolCallStatus; status: ToolStatus;
result?: unknown; result?: unknown;
error?: string; error?: string;
duration?: number; duration?: number;
@@ -345,7 +342,7 @@ export interface ToolCallInfo {
* - user: 用户输入 * - user: 用户输入
* - assistant: AI 回复(包含文本和工具调用,按原始顺序) * - assistant: AI 回复(包含文本和工具调用,按原始顺序)
*/ */
export interface MergedMessage { export interface Message {
id: string; id: string;
sessionId: string; sessionId: string;
role: 'user' | 'assistant'; role: 'user' | 'assistant';
@@ -363,6 +360,9 @@ export interface MergedMessage {
}; };
} }
/** @deprecated 使用 Message 代替 */
export type MergedMessage = Message;
// ============ API 响应 ============ // ============ API 响应 ============
export interface ApiResponse<T> { export interface ApiResponse<T> {
+8 -4
View File
@@ -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; id: string;
name: string; name: string;
arguments: Record<string, unknown>; arguments: Record<string, unknown>;
status: ToolCallStatus; status: ToolStatus;
result?: unknown; result?: unknown;
error?: string; error?: string;
duration?: number; // 执行时长 ms duration?: number; // 执行时长 ms
@@ -48,7 +52,7 @@ export interface ToolMessagePart {
id: string; id: string;
toolCallId: string; toolCallId: string;
toolName: string; toolName: string;
status: ToolCallStatus; status: ToolStatus;
arguments: Record<string, unknown>; arguments: Record<string, unknown>;
result?: unknown; result?: unknown;
error?: string; error?: string;
@@ -977,7 +981,7 @@ export type SubagentEventPayload =
export interface SubagentToolInfo { export interface SubagentToolInfo {
id: string; id: string;
toolName: string; toolName: string;
status: ToolCallStatus; status: ToolStatus;
args: Record<string, unknown>; args: Record<string, unknown>;
result?: unknown; result?: unknown;
error?: string; error?: string;
+2 -2
View File
@@ -22,7 +22,7 @@ import { fadeInUp, smoothTransition } from '../utils/animations';
import { getAgentDisplayName } from '../utils/agent'; import { getAgentDisplayName } from '../utils/agent';
import { Markdown } from './Markdown'; import { Markdown } from './Markdown';
import { FileMentionText } from './FileMentionTag'; 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 { interface ChatMessageProps {
message: Message; message: Message;
@@ -319,7 +319,7 @@ function ToolPartItem({ part }: ToolPartItemProps) {
/** /**
* 获取工具状态图标 * 获取工具状态图标
*/ */
function getStatusIcon(status: ToolCallStatus) { function getStatusIcon(status: ToolStatus) {
switch (status) { switch (status) {
case 'pending': case 'pending':
return <Clock size={14} className="text-yellow-500" />; return <Clock size={14} className="text-yellow-500" />;