From e53035ffc0a715406f1c04be75bf42937763c242 Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 16 Dec 2025 21:28:19 +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 Core 模块职责: - 添加 inferPermissionType() 权限类型推断函数 - 添加 partToApiFormat()/partsToApiFormat() API 格式转换函数 - 添加 ApiPart/ApiTextPart/ApiToolPart/ApiReasoningPart 类型 - 统一导出 toolRegistry 供 Server 使用 Server 模块职责: - 重命名 PermissionRequestContext 为 PermissionDisplayContext - 移除本地 toolRegistry,直接使用 Core 的注册表 - 使用 Core 的 inferPermissionType 替代本地实现 - 使用 Core 的 partsToApiFormat 替代手动转换 文档更新: - 在 gui-server-client.md 添加第11章「模块职责边界」 - 明确 Core 和 Server 的职责划分 --- packages/core/src/index.ts | 15 ++- packages/core/src/permission/index.ts | 3 + packages/core/src/permission/types.ts | 40 +++++++ packages/core/src/session/converter.ts | 102 ++++++++++++++++ packages/core/src/session/index.ts | 3 + packages/server/src/index.ts | 3 +- packages/server/src/permission/handler.ts | 46 ++------ packages/server/src/routes/index.ts | 2 +- packages/server/src/routes/sessions.ts | 60 +++------- packages/server/src/routes/tools.ts | 48 ++++---- packages/server/src/types.ts | 26 ++-- .../server/tests/unit/routes/tools.test.ts | 111 +++++++----------- 12 files changed, 274 insertions(+), 185 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a057ed6..e77634e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -51,13 +51,26 @@ export type { ToolState, TodoItem, TodoList, + // API 格式类型 + ApiPart, + ApiTextPart, + ApiToolPart, + ApiReasoningPart, +} from './session/index.js'; + +// API 格式转换函数 +export { + partsToApiFormat, + partToApiFormat, + getToolInput, + getToolDuration, } from './session/index.js'; // Types export type { UserInput, ChatResult } from './types/index.js'; // Permission -export { getPermissionManager } from './permission/index.js'; +export { getPermissionManager, inferPermissionType } from './permission/index.js'; export type { PermissionType, PermissionContext, diff --git a/packages/core/src/permission/index.ts b/packages/core/src/permission/index.ts index bcb3c1d..bd79cab 100644 --- a/packages/core/src/permission/index.ts +++ b/packages/core/src/permission/index.ts @@ -16,6 +16,9 @@ export type { GitPermissionConfig, } from './types.js'; +// 工具函数 +export { inferPermissionType } from './types.js'; + export { matchPattern, matchRules, parseCommand, generateAskPattern } from './wildcard.js'; export { PermissionManager, getPermissionManager, resetPermissionManager } from './manager.js'; diff --git a/packages/core/src/permission/types.ts b/packages/core/src/permission/types.ts index 3a2cbbc..04609d2 100644 --- a/packages/core/src/permission/types.ts +++ b/packages/core/src/permission/types.ts @@ -170,3 +170,43 @@ export interface GitPermissionConfig { // 危险操作策略(force push, reset --hard 等,默认 ask) dangerousOperations: PermissionAction; } + +// ============ 工具函数 ============ + +/** + * 从上下文获取权限类型 + * 优先使用结构化的 permissionType 字段,否则从 command 字符串推断 + * + * @param ctx 权限上下文 + * @returns 权限类型 + */ +export function inferPermissionType(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', 'list']; + for (const op of fileOps) { + if (command.startsWith(`${op} `)) { + return 'file'; + } + } + + // Web 操作 + if (command.includes('fetch') || command.includes('http') || command.startsWith('web_search')) { + return 'web'; + } + + // 默认为 bash + return 'bash'; +} diff --git a/packages/core/src/session/converter.ts b/packages/core/src/session/converter.ts index c45e1d0..51ddef1 100644 --- a/packages/core/src/session/converter.ts +++ b/packages/core/src/session/converter.ts @@ -167,3 +167,105 @@ export function getToolDuration(toolPart: ToolPart): number | undefined { } return undefined; } + +// ============ API 格式转换 ============ +// 用于将存储格式的 Part 转换为前端/API 显示的扁平格式 + +/** + * API 响应用的文本 Part(扁平格式) + */ +export interface ApiTextPart { + type: 'text'; + id: string; + text: string; +} + +/** + * API 响应用的工具调用 Part(扁平格式) + */ +export interface ApiToolPart { + type: 'tool'; + id: string; + toolCallId: string; + toolName: string; + status: 'pending' | 'running' | 'completed' | 'error'; + arguments: Record; + result?: unknown; + error?: string; + duration?: number; +} + +/** + * API 响应用的推理 Part(扁平格式) + */ +export interface ApiReasoningPart { + type: 'reasoning'; + id: string; + text: string; +} + +/** + * API 响应用的 Part 联合类型 + */ +export type ApiPart = ApiTextPart | ApiToolPart | ApiReasoningPart; + +/** + * 将存储格式的 Part 转换为 API 响应格式(扁平结构) + * + * 存储格式使用状态机模式(ToolState 有 pending/running/completed/error) + * API 格式使用扁平结构(直接的 status + result/error 字段) + * + * @param part 存储格式的 Part + * @returns API 格式的 Part,如果是不支持的类型则返回 null + */ +export function partToApiFormat(part: Part): ApiPart | null { + switch (part.type) { + case 'text': + return { + type: 'text', + id: part.id, + text: part.text, + }; + + case 'reasoning': + return { + type: 'reasoning', + id: part.id, + text: part.text, + }; + + case 'tool': { + const toolPart = part as ToolPart; + const state = toolPart.state; + const startTime = state.status !== 'pending' ? state.time?.start : undefined; + const endTime = + state.status === 'completed' || state.status === 'error' ? state.time?.end : undefined; + + return { + type: 'tool', + id: part.id, + toolCallId: toolPart.toolCallId, + toolName: toolPart.toolName, + status: state.status, + arguments: state.status !== 'pending' ? getToolInput(toolPart) : {}, + result: state.status === 'completed' ? (state as { output: unknown }).output : undefined, + error: state.status === 'error' ? (state as { error: string }).error : undefined, + duration: startTime && endTime ? endTime - startTime : undefined, + }; + } + + default: + // 其他类型(step-start, step-finish, snapshot 等)不传给前端 + return null; + } +} + +/** + * 批量将存储格式的 Parts 转换为 API 响应格式 + * + * @param parts 存储格式的 Parts 数组 + * @returns API 格式的 Parts 数组(过滤掉不支持的类型) + */ +export function partsToApiFormat(parts: Part[]): ApiPart[] { + return parts.map(partToApiFormat).filter((p): p is ApiPart => p !== null); +} diff --git a/packages/core/src/session/index.ts b/packages/core/src/session/index.ts index 2db5dad..7d764d8 100644 --- a/packages/core/src/session/index.ts +++ b/packages/core/src/session/index.ts @@ -53,6 +53,9 @@ export { // 消息转换器 export { toModelMessages, getToolInput, getToolDuration } from './converter.js'; +// API 格式转换 +export { partToApiFormat, partsToApiFormat } from './converter.js'; +export type { ApiPart, ApiTextPart, ApiToolPart, ApiReasoningPart } from './converter.js'; // ID 生成器 export { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1e9eb4d..ca84a09 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -239,7 +239,8 @@ export async function startServer(options: ServerOptions = {}): Promise { // 导出 export { app, websocket }; export { getSessionMetadataManager, getSessionManager } from './session/manager.js'; -export { registerTool, getRegisteredTools } from './routes/tools.js'; +// Tool Registry 现在直接使用 Core 的 toolRegistry +// 导入方式: import { toolRegistry } from '@ai-assistant/core'; export { getConfig, setConfig } from './routes/config.js'; export { emitEvent, diff --git a/packages/server/src/permission/handler.ts b/packages/server/src/permission/handler.ts index 26e229d..8f1c7ea 100644 --- a/packages/server/src/permission/handler.ts +++ b/packages/server/src/permission/handler.ts @@ -7,9 +7,10 @@ import { randomUUID } from 'crypto'; import { broadcastToSession } from '../ws.js'; import type { PermissionRequestPayload, - PermissionRequestContext, + PermissionDisplayContext, ServerMessage, } from '../types.js'; +import { inferPermissionType } from '@ai-assistant/core'; import type { PermissionDecision, PermissionContext, PermissionType } from '@ai-assistant/core'; // 等待中的权限请求 @@ -79,42 +80,11 @@ function isAutoApproved(sessionId: string, ctx: PermissionContext): boolean { } /** - * 从上下文获取权限类型 - * 优先使用结构化的 permissionType 字段,否则解析 command 字符串 + * 构建权限请求显示上下文 + * 将 Core 的完整 PermissionContext 转换为用于前端显示的精简格式 */ -function getPermissionType(ctx: PermissionContext): PermissionType { - // 优先使用结构化字段 - if (ctx.permissionType) { - return ctx.permissionType; - } - - // 向后兼容:解析 command 字符串 - const command = ctx.command.toLowerCase(); - - if (command.startsWith('git ')) { - return 'git'; - } - - const fileOps = ['read', 'write', 'edit', 'delete', 'move', 'copy', 'mkdir']; - for (const op of fileOps) { - if (command.startsWith(`${op} `)) { - return 'file'; - } - } - - if (command.includes('fetch') || command.includes('http') || command.startsWith('web_search')) { - return 'web'; - } - - return 'bash'; -} - -/** - * 构建权限请求上下文 - * 使用 Core 传递的结构化字段,减少字符串解析 - */ -function buildRequestContext(ctx: PermissionContext): PermissionRequestContext { - const permType = getPermissionType(ctx); +function buildDisplayContext(ctx: PermissionContext): PermissionDisplayContext { + const permType = inferPermissionType(ctx); switch (permType) { case 'file': @@ -162,8 +132,8 @@ export function createServerPermissionCallback(sessionId: string) { } const requestId = randomUUID(); - const permissionType = getPermissionType(permCtx); - const context = buildRequestContext(permCtx); + const permissionType = inferPermissionType(permCtx); + const context = buildDisplayContext(permCtx); // 构建请求 payload const payload: PermissionRequestPayload = { diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 3dbbd88..7df9b05 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -5,7 +5,7 @@ */ export { sessionsRouter } from './sessions.js'; -export { toolsRouter, registerTool, getRegisteredTools } from './tools.js'; +export { toolsRouter } from './tools.js'; export { configRouter, getConfig, setConfig } from './config.js'; export { filesRouter, setWorkingDirectory, getWorkingDirectory } from './files.js'; export { commandsRouter } from './commands.js'; diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index 9386395..aadb0ad 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -12,8 +12,8 @@ import { type Message, type MessagePart, } from '../types.js'; -import type { MessageInfo, Part, ToolPart } from '@ai-assistant/core'; -import { MessageStorage, PartStorage } from '@ai-assistant/core'; +import type { MessageInfo, Part, ToolPart, ApiPart } from '@ai-assistant/core'; +import { MessageStorage, PartStorage, partsToApiFormat, getToolInput, getToolDuration } from '@ai-assistant/core'; export const sessionsRouter = new Hono(); @@ -135,33 +135,10 @@ sessionsRouter.get('/:id/messages', async (c) => { for (const msgInfo of messageInfos) { const parts = await PartStorage.getByIds(msgInfo.id, msgInfo.partIds); - // 转换 Parts 为前端格式(保持顺序) - const messageParts: MessagePart[] = parts - .filter((p) => p.type === 'text' || p.type === 'tool' || p.type === 'reasoning') - .map((p): MessagePart => { - if (p.type === 'text') { - return { type: 'text', id: p.id, text: p.text ?? '' }; - } - if (p.type === 'reasoning') { - return { type: 'reasoning', id: p.id, text: p.text ?? '' }; - } - // tool - 使用类型断言 - const toolPart = p as ToolPart; - const state = toolPart.state; - const startTime = state.status !== 'pending' ? state.time?.start : undefined; - const endTime = state.status === 'completed' || state.status === 'error' ? state.time?.end : undefined; - return { - type: 'tool', - id: p.id, - toolCallId: toolPart.toolCallId ?? '', - toolName: toolPart.toolName ?? '', - status: state.status, - arguments: state.status !== 'pending' ? (state.input as Record) : {}, - result: state.status === 'completed' ? state.output : undefined, - error: state.status === 'error' ? state.error : undefined, - duration: startTime && endTime ? endTime - startTime : undefined, - }; - }); + // 使用 Core 的转换函数将 Parts 转换为 API 格式 + const apiParts = partsToApiFormat(parts); + // 类型兼容:ApiPart 与 MessagePart 结构相同 + const messageParts = apiParts as MessagePart[]; // 兼容字段:提取文本内容 const textContent = parts @@ -169,23 +146,18 @@ sessionsRouter.get('/:id/messages', async (c) => { .map((p) => p.text ?? '') .join(''); - // 兼容字段:提取工具调用 + // 兼容字段:提取工具调用(使用 Core 工具函数) const toolCalls: ToolCallInfo[] = parts .filter((p): p is ToolPart => p.type === 'tool') - .map((p) => { - const state = p.state; - const startTime = state.status !== 'pending' ? state.time?.start : undefined; - const endTime = state.status === 'completed' || state.status === 'error' ? state.time?.end : undefined; - return { - id: p.toolCallId ?? '', - name: p.toolName ?? '', - arguments: state.status !== 'pending' ? (state.input as Record) : {}, - status: state.status, - result: state.status === 'completed' ? state.output : undefined, - error: state.status === 'error' ? state.error : undefined, - duration: startTime && endTime ? endTime - startTime : undefined, - }; - }); + .map((p) => ({ + id: p.toolCallId ?? '', + name: p.toolName ?? '', + arguments: getToolInput(p), + status: p.state.status, + result: p.state.status === 'completed' ? (p.state as { output: unknown }).output : undefined, + error: p.state.status === 'error' ? (p.state as { error: string }).error : undefined, + duration: getToolDuration(p), + })); messages.push({ id: msgInfo.id, diff --git a/packages/server/src/routes/tools.ts b/packages/server/src/routes/tools.ts index 5958950..d629cd3 100644 --- a/packages/server/src/routes/tools.ts +++ b/packages/server/src/routes/tools.ts @@ -2,39 +2,30 @@ * Tools API Routes * * 工具管理相关的 REST API + * 直接使用 Core 的 toolRegistry,不再维护独立的注册表 */ import { Hono } from 'hono'; -import type { Tool } from '../types.js'; +import { toolRegistry } from '@ai-assistant/core'; export const toolsRouter = new Hono(); -// 工具注册表 (后续会从 core 模块获取) -const toolRegistry: Map = new Map(); - -/** - * 注册工具 (内部使用) - */ -export function registerTool(tool: Tool): void { - toolRegistry.set(tool.name, tool); -} - -/** - * 获取所有已注册的工具 - */ -export function getRegisteredTools(): Tool[] { - return Array.from(toolRegistry.values()); -} - /** * GET /tools - 列出所有可用工具 */ toolsRouter.get('/', (c) => { - const tools = getRegisteredTools(); + const tools = toolRegistry.getAllTools(); + + // 转换为 API 格式(不包含 execute 函数) + const toolList = tools.map((t) => ({ + name: t.name, + description: t.description, + parameters: t.parameters, + })); return c.json({ success: true, - data: tools, + data: toolList, }); }); @@ -43,7 +34,7 @@ toolsRouter.get('/', (c) => { */ toolsRouter.get('/:name', (c) => { const name = c.req.param('name'); - const tool = toolRegistry.get(name); + const tool = toolRegistry.getTool(name); if (!tool) { return c.json( @@ -57,7 +48,11 @@ toolsRouter.get('/:name', (c) => { return c.json({ success: true, - data: tool, + data: { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + }, }); }); @@ -69,7 +64,7 @@ toolsRouter.get('/:name', (c) => { */ toolsRouter.post('/:name/execute', async (c) => { const name = c.req.param('name'); - const tool = toolRegistry.get(name); + const tool = toolRegistry.getTool(name); if (!tool) { return c.json( @@ -85,8 +80,9 @@ toolsRouter.post('/:name/execute', async (c) => { const body = await c.req.json(); const params = body.params || {}; - // TODO: 实际调用 core 模块的工具执行逻辑 - // const result = await executeTool(name, params); + // TODO: 实际调用工具执行逻辑 + // 需要设置正确的上下文(workdir、权限回调等) + // const result = await tool.execute(params, context); return c.json({ success: true, @@ -94,7 +90,7 @@ toolsRouter.post('/:name/execute', async (c) => { tool: name, params, result: null, // 占位,后续实现 - message: 'Tool execution not yet implemented', + message: 'Tool execution not yet implemented - use WebSocket for tool execution', }, }); } catch (error) { diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 48e0375..6c551e2 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -206,19 +206,29 @@ export type SubagentEventPayload = // PermissionType 和 PermissionContext 已从 Core 导入(见文件顶部) /** - * 权限请求上下文(Server 专用,已废弃) - * @deprecated 使用 PermissionContext(从 Core 导入)代替 + * 权限请求显示上下文(用于 WebSocket 传输和前端显示) + * + * 与 Core 的 PermissionContext 区别: + * - PermissionContext (Core): 完整上下文,包含文件内容等敏感数据 + * - PermissionDisplayContext (Server): 精简版,只包含显示所需的元信息 + * + * 这种分离是为了: + * 1. 安全性:不通过 WebSocket 传输文件内容 + * 2. 性能:减少传输数据量 */ -export interface PermissionRequestContext { - command?: string; // bash 命令 +export interface PermissionDisplayContext { + command?: string; // 命令字符串(用于显示) operation?: string; // 文件操作类型: read/write/edit/delete path?: string; // 文件路径 gitOperation?: string; // git 操作 query?: string; // web 查询 - patterns?: string[]; // 匹配模式 - externalPaths?: string[]; // 外部路径 + patterns?: string[]; // 匹配到的模式 + externalPaths?: string[]; // 访问的外部路径 } +/** @deprecated 使用 PermissionDisplayContext 代替 */ +export type PermissionRequestContext = PermissionDisplayContext; + /** * Diff 信息(文件写入/编辑时) */ @@ -242,12 +252,12 @@ export interface DiffInfo { } /** - * 权限请求消息 payload + * 权限请求消息 payload(WebSocket 传输格式) */ export interface PermissionRequestPayload { requestId: string; permissionType: PermissionType; - context: PermissionRequestContext; + context: PermissionDisplayContext; diff?: DiffInfo; } diff --git a/packages/server/tests/unit/routes/tools.test.ts b/packages/server/tests/unit/routes/tools.test.ts index 79132c1..10bef15 100644 --- a/packages/server/tests/unit/routes/tools.test.ts +++ b/packages/server/tests/unit/routes/tools.test.ts @@ -2,11 +2,13 @@ * Tools Route 测试 * * 测试工具管理 REST API 端点 + * 注意:现在使用 Core 的 toolRegistry,工具已预先注册 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Hono } from 'hono'; -import { toolsRouter, registerTool, getRegisteredTools } from '../../../src/routes/tools.js'; +import { toolsRouter } from '../../../src/routes/tools.js'; +import { toolRegistry } from '@ai-assistant/core'; // Create test app const app = new Hono(); @@ -14,71 +16,64 @@ app.route('/tools', toolsRouter); describe('Tools Route', () => { beforeEach(() => { - // Clear any previously registered tools - // Note: Since the toolRegistry is a module-level Map, we test what's already registered vi.clearAllMocks(); }); - describe('registerTool / getRegisteredTools - 工具注册', () => { - it('注册工具', () => { - registerTool({ - name: 'test-tool', - description: 'A test tool', - parameters: {}, - }); - - const tools = getRegisteredTools(); - expect(tools.some((t) => t.name === 'test-tool')).toBe(true); - }); - - it('获取所有已注册工具', () => { - const tools = getRegisteredTools(); - expect(Array.isArray(tools)).toBe(true); - }); - }); - describe('GET /tools - 列出所有工具', () => { it('返回工具列表', async () => { - // Register a tool first - registerTool({ - name: 'list-test-tool', - description: 'Tool for list test', - parameters: {}, - }); - const res = await app.request('/tools'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(Array.isArray(json.data)).toBe(true); + // Core toolRegistry 已注册工具 + expect(json.data.length).toBeGreaterThan(0); }); - it('返回数组类型数据', async () => { + it('工具包含必要字段', async () => { const res = await app.request('/tools'); const json = await res.json(); expect(res.status).toBe(200); - expect(json.success).toBe(true); - expect(Array.isArray(json.data)).toBe(true); + // 检查第一个工具的字段 + if (json.data.length > 0) { + const tool = json.data[0]; + expect(tool).toHaveProperty('name'); + expect(tool).toHaveProperty('description'); + expect(tool).toHaveProperty('parameters'); + // 不应该包含 execute 函数 + expect(tool).not.toHaveProperty('execute'); + } + }); + + it('返回的工具数量与 Core Registry 一致', async () => { + const res = await app.request('/tools'); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.data.length).toBe(toolRegistry.getAllTools().length); }); }); describe('GET /tools/:name - 获取单个工具', () => { - it('返回存在的工具', async () => { - registerTool({ - name: 'get-test-tool', - description: 'Tool for get test', - parameters: { path: { type: 'string' } }, - }); - - const res = await app.request('/tools/get-test-tool'); + it('返回存在的工具 (bash)', async () => { + const res = await app.request('/tools/bash'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); - expect(json.data.name).toBe('get-test-tool'); - expect(json.data.description).toBe('Tool for get test'); + expect(json.data.name).toBe('bash'); + expect(json.data.description).toBeDefined(); + }); + + it('返回存在的工具 (read_file)', async () => { + const res = await app.request('/tools/read_file'); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.success).toBe(true); + expect(json.data.name).toBe('read_file'); }); it('工具不存在返回 404', async () => { @@ -92,24 +87,20 @@ describe('Tools Route', () => { }); describe('POST /tools/:name/execute - 执行工具', () => { - it('执行存在的工具(占位实现)', async () => { - registerTool({ - name: 'execute-test-tool', - description: 'Tool for execute test', - parameters: {}, - }); - - const res = await app.request('/tools/execute-test-tool/execute', { + it('存在的工具返回占位响应', async () => { + const res = await app.request('/tools/bash/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ params: { test: 'value' } }), + body: JSON.stringify({ params: { command: 'echo test' } }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); - expect(json.data.tool).toBe('execute-test-tool'); - expect(json.data.params).toEqual({ test: 'value' }); + expect(json.data.tool).toBe('bash'); + expect(json.data.params).toEqual({ command: 'echo test' }); + // 当前是占位实现 + expect(json.data.message).toContain('not yet implemented'); }); it('工具不存在返回 404', async () => { @@ -126,13 +117,7 @@ describe('Tools Route', () => { }); it('无 params 时使用空对象', async () => { - registerTool({ - name: 'no-params-tool', - description: 'Tool without params', - parameters: {}, - }); - - const res = await app.request('/tools/no-params-tool/execute', { + const res = await app.request('/tools/bash/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), @@ -144,13 +129,7 @@ describe('Tools Route', () => { }); it('无效 JSON 返回 500', async () => { - registerTool({ - name: 'json-error-tool', - description: 'Tool for JSON error test', - parameters: {}, - }); - - const res = await app.request('/tools/json-error-tool/execute', { + const res = await app.request('/tools/bash/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: 'invalid json',