refactor(core,server): 统一模块职责并消除类型重复
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 的职责划分
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -239,7 +239,8 @@ export async function startServer(options: ServerOptions = {}): Promise<void> {
|
||||
// 导出
|
||||
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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string, unknown>) : {},
|
||||
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<string, unknown>) : {},
|
||||
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,
|
||||
|
||||
@@ -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<string, Tool> = 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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user