feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
import {
|
||||
generateText,
|
||||
streamText,
|
||||
stepCountIs,
|
||||
type ModelMessage,
|
||||
type Tool as AITool,
|
||||
type LanguageModel,
|
||||
} from 'ai';
|
||||
import type { Tool, ToolResult, AgentConfig, ContentBlock } from '../types/index.js';
|
||||
import { buildZodSchema } from '../types/index.js';
|
||||
import { ToolRegistry } from '../tools/registry.js';
|
||||
import type {
|
||||
AgentInfo,
|
||||
AgentExecutionContext,
|
||||
AgentExecutionResult,
|
||||
ImageData,
|
||||
} from './types.js';
|
||||
import { checkBashPermission } from './permission-merger.js';
|
||||
import { getModelFactory } from '../core/providers.js';
|
||||
|
||||
/**
|
||||
* Agent 执行器
|
||||
* 根据 Agent 配置执行任务,支持工具过滤和权限控制
|
||||
*/
|
||||
export class AgentExecutor {
|
||||
private agentInfo: AgentInfo;
|
||||
private baseConfig: AgentConfig;
|
||||
private toolRegistry: ToolRegistry;
|
||||
private getModel: (model: string) => LanguageModel;
|
||||
|
||||
constructor(
|
||||
agentInfo: AgentInfo,
|
||||
baseConfig: AgentConfig,
|
||||
toolRegistry: ToolRegistry
|
||||
) {
|
||||
this.agentInfo = agentInfo;
|
||||
this.baseConfig = baseConfig;
|
||||
this.toolRegistry = toolRegistry;
|
||||
|
||||
// 获取模型工厂
|
||||
const provider = agentInfo.model?.provider ?? baseConfig.provider;
|
||||
this.getModel = getModelFactory(provider, {
|
||||
apiKey: baseConfig.apiKey,
|
||||
baseUrl: baseConfig.baseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*/
|
||||
async execute(
|
||||
prompt: string,
|
||||
context: AgentExecutionContext
|
||||
): Promise<AgentExecutionResult> {
|
||||
const { onStream, onToolCall, onToolResult, images } = context;
|
||||
|
||||
// 获取过滤后的工具
|
||||
const tools = this.getFilteredTools();
|
||||
const vercelTools = this.buildVercelTools(tools);
|
||||
|
||||
// 构建系统提示词
|
||||
const systemPrompt = this.buildSystemPrompt();
|
||||
|
||||
// 获取模型配置
|
||||
const modelName = this.agentInfo.model?.model ?? this.baseConfig.model;
|
||||
const maxSteps = this.agentInfo.maxSteps ?? 10;
|
||||
const maxTokens = this.agentInfo.model?.maxTokens ?? this.baseConfig.maxTokens;
|
||||
|
||||
// 构建消息内容(支持图片)
|
||||
const messageContent = this.buildMessageContent(prompt, images);
|
||||
|
||||
// 构建初始消息
|
||||
const messages: ModelMessage[] = [
|
||||
{
|
||||
role: 'user',
|
||||
content: messageContent,
|
||||
},
|
||||
];
|
||||
|
||||
let fullResponse = '';
|
||||
let steps = 0;
|
||||
|
||||
try {
|
||||
if (onStream) {
|
||||
// 流式模式
|
||||
const result = streamText({
|
||||
model: this.getModel(modelName),
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
tools: vercelTools,
|
||||
maxOutputTokens: maxTokens,
|
||||
stopWhen: stepCountIs(maxSteps),
|
||||
onChunk: ({ chunk }) => {
|
||||
if (chunk.type === 'tool-call') {
|
||||
steps++;
|
||||
const toolArgs = 'input' in chunk ? chunk.input : {};
|
||||
onToolCall?.(chunk.toolName, toolArgs as Record<string, unknown>);
|
||||
onStream(`\n[调用工具: ${chunk.toolName}]\n`);
|
||||
} else if (chunk.type === 'tool-result') {
|
||||
const output = (chunk as { output?: ToolResult }).output;
|
||||
onToolResult?.(
|
||||
(chunk as { toolName?: string }).toolName ?? 'unknown',
|
||||
output
|
||||
);
|
||||
if (output && typeof output === 'object') {
|
||||
if (output.success) {
|
||||
const displayOutput =
|
||||
output.output.length > 500
|
||||
? output.output.substring(0, 500) + '...(截断)'
|
||||
: output.output;
|
||||
onStream(`[结果: ${displayOutput}]\n`);
|
||||
} else {
|
||||
onStream(`[错误: ${output.error}]\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
for await (const chunk of result.textStream) {
|
||||
fullResponse += chunk;
|
||||
onStream(chunk);
|
||||
}
|
||||
|
||||
await result.response;
|
||||
} else {
|
||||
// 非流式模式
|
||||
const result = await generateText({
|
||||
model: this.getModel(modelName),
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
tools: vercelTools,
|
||||
maxOutputTokens: maxTokens,
|
||||
stopWhen: stepCountIs(maxSteps),
|
||||
});
|
||||
|
||||
fullResponse = result.text;
|
||||
steps = result.steps.length;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
text: fullResponse,
|
||||
steps,
|
||||
sessionId: context.parentSessionId ?? 'standalone',
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
text: '',
|
||||
steps,
|
||||
sessionId: context.parentSessionId ?? 'standalone',
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过滤后的工具列表
|
||||
*/
|
||||
private getFilteredTools(): Tool[] {
|
||||
const allTools = this.toolRegistry.getAllTools();
|
||||
const toolConfig = this.agentInfo.tools;
|
||||
|
||||
// 如果没有工具配置,返回所有工具
|
||||
if (!toolConfig) {
|
||||
return allTools;
|
||||
}
|
||||
|
||||
let filteredTools = allTools;
|
||||
|
||||
// 如果指定了 enabled,只保留这些工具
|
||||
if (toolConfig.enabled && toolConfig.enabled.length > 0) {
|
||||
const enabledSet = new Set(toolConfig.enabled);
|
||||
filteredTools = filteredTools.filter((t) => enabledSet.has(t.name));
|
||||
}
|
||||
|
||||
// 移除 disabled 的工具
|
||||
if (toolConfig.disabled && toolConfig.disabled.length > 0) {
|
||||
const disabledSet = new Set(toolConfig.disabled);
|
||||
filteredTools = filteredTools.filter((t) => !disabledSet.has(t.name));
|
||||
}
|
||||
|
||||
// 如果禁止嵌套 Task,移除 task 工具
|
||||
if (toolConfig.noTask) {
|
||||
filteredTools = filteredTools.filter((t) => t.name !== 'task');
|
||||
}
|
||||
|
||||
return filteredTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Vercel AI SDK 工具格式
|
||||
*/
|
||||
private buildVercelTools(tools: Tool[]): Record<string, AITool> {
|
||||
const vercelTools: Record<string, AITool> = {};
|
||||
|
||||
for (const tool of tools) {
|
||||
const schema = buildZodSchema(tool.parameters);
|
||||
|
||||
vercelTools[tool.name] = {
|
||||
description: tool.description,
|
||||
inputSchema: schema,
|
||||
execute: async (params) => {
|
||||
// 权限检查
|
||||
const permissionResult = await this.checkToolPermission(
|
||||
tool.name,
|
||||
params as Record<string, unknown>
|
||||
);
|
||||
if (!permissionResult.allowed) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限拒绝: ${permissionResult.reason}`,
|
||||
};
|
||||
}
|
||||
|
||||
return tool.execute(params as Record<string, unknown>);
|
||||
},
|
||||
} as AITool;
|
||||
}
|
||||
|
||||
return vercelTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查工具调用权限
|
||||
*/
|
||||
private async checkToolPermission(
|
||||
toolName: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<{ allowed: boolean; reason?: string }> {
|
||||
const permission = this.agentInfo.permission;
|
||||
if (!permission) {
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
// Bash 权限检查
|
||||
if (toolName === 'bash' && permission.bash) {
|
||||
const command = params.command as string;
|
||||
if (!command) {
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
const action = checkBashPermission(command, permission.bash);
|
||||
if (action === 'deny') {
|
||||
return { allowed: false, reason: `命令被禁止: ${command}` };
|
||||
}
|
||||
// ask 在这里视为允许(实际的 ask 逻辑在权限管理器中处理)
|
||||
}
|
||||
|
||||
// 文件写入权限检查
|
||||
if (['write_file', 'edit_file', 'delete_file'].includes(toolName)) {
|
||||
const filePermission = permission.file;
|
||||
if (filePermission) {
|
||||
const operation = toolName === 'write_file' ? 'write' :
|
||||
toolName === 'edit_file' ? 'edit' : 'delete';
|
||||
const action = filePermission[operation];
|
||||
if (action === 'deny') {
|
||||
return { allowed: false, reason: `${operation} 操作被禁止` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Git 写操作权限检查
|
||||
const gitWriteTools = ['git_add', 'git_commit', 'git_push', 'git_checkout', 'git_stash'];
|
||||
if (gitWriteTools.includes(toolName) && permission.git?.write === 'deny') {
|
||||
return { allowed: false, reason: 'Git 写操作被禁止' };
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建系统提示词
|
||||
*/
|
||||
private buildSystemPrompt(): string {
|
||||
// 如果 Agent 有自定义 prompt,使用它
|
||||
if (this.agentInfo.prompt) {
|
||||
return this.agentInfo.prompt;
|
||||
}
|
||||
|
||||
// 否则使用基础配置的 systemPrompt
|
||||
return this.baseConfig.systemPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建消息内容(支持图片)
|
||||
*/
|
||||
private buildMessageContent(
|
||||
prompt: string,
|
||||
images?: ImageData[]
|
||||
): string | ContentBlock[] {
|
||||
// 如果没有图片,直接返回文本
|
||||
if (!images || images.length === 0) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
// 构建多模态内容
|
||||
const blocks: ContentBlock[] = [];
|
||||
|
||||
// 先添加图片
|
||||
for (const img of images) {
|
||||
blocks.push({
|
||||
type: 'image',
|
||||
image: img.data,
|
||||
mimeType: img.mimeType,
|
||||
});
|
||||
}
|
||||
|
||||
// 再添加文本
|
||||
if (prompt) {
|
||||
blocks.push({
|
||||
type: 'text',
|
||||
text: prompt,
|
||||
});
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user