refactor(core): 移除 UI 交互层,保持 core 为纯能力模块
- 删除 terminal.ts 及其测试(UI 交互应属于 cli 包) - 清理 index.ts 中的 CLI 入口代码 - 合并 lib.ts 导出到 index.ts - 移除 commander、inquirer 依赖 - 更新 package.json 导出配置
This commit is contained in:
@@ -55,6 +55,7 @@
|
||||
"chalk": "^5.3.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^10.1.1",
|
||||
"nanoid": "^5.1.6",
|
||||
"qwen-ai-provider-v5": "^1.0.2",
|
||||
"simple-git": "^3.30.0",
|
||||
"tree-sitter-bash": "^0.25.1",
|
||||
|
||||
+84
-431
@@ -1,36 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
export { Agent } from './core/agent.js';
|
||||
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
|
||||
export { loadConfig, initConfig } from './utils/config.js';
|
||||
export { SessionStorage } from './session/storage.js';
|
||||
export { SessionManager } from './session/index.js';
|
||||
export type { SessionData, SessionSummary } from './session/types.js';
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { Agent } from './core/agent.js';
|
||||
import { TerminalUI } from './ui/terminal.js';
|
||||
import { loadConfig, initConfig } from './utils/config.js';
|
||||
import { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
|
||||
import { getPermissionManager, promptPermission } from './permission/index.js';
|
||||
import { SessionManager } from './session/index.js';
|
||||
import { agentRegistry } from './agent/index.js';
|
||||
import { initLSP, shutdownLSP } from './lsp/index.js';
|
||||
import { getCommandRegistry } from './commands/index.js';
|
||||
import { getSkillRegistry } from './skills/index.js';
|
||||
import {
|
||||
// Types
|
||||
export type { UserInput } from './types/index.js';
|
||||
|
||||
// Permission
|
||||
export { getPermissionManager, promptPermission } from './permission/index.js';
|
||||
|
||||
// LSP
|
||||
export { initLSP, shutdownLSP } from './lsp/index.js';
|
||||
export {
|
||||
printServerList,
|
||||
installServer,
|
||||
installAllServers,
|
||||
showServerInfo,
|
||||
} from './lsp/cli.js';
|
||||
import {
|
||||
getMCPManager,
|
||||
loadMCPConfig,
|
||||
createMCPToolAdapter,
|
||||
} from './mcp/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// 库导出(供 server 等包使用)
|
||||
// ============================================================================
|
||||
export { Agent } from './core/agent.js';
|
||||
export { toolRegistry } from './tools/index.js';
|
||||
export { loadConfig } from './utils/config.js';
|
||||
export { SessionStorage } from './session/storage.js';
|
||||
export type { SessionData, SessionSummary } from './session/types.js';
|
||||
// Skills
|
||||
export { getSkillRegistry } from './skills/index.js';
|
||||
|
||||
// Image utils
|
||||
export {
|
||||
extractImageReferences,
|
||||
loadImages,
|
||||
loadImage,
|
||||
formatFileSize,
|
||||
isImagePath,
|
||||
IMAGE_EXTENSIONS,
|
||||
} from './utils/image.js';
|
||||
export type { ImageInfo, ImageLoadResult } from './utils/image.js';
|
||||
|
||||
// Commands
|
||||
export { getCommandRegistry, createCommandExecutor, createCommandManager } from './commands/index.js';
|
||||
@@ -91,408 +93,59 @@ export type {
|
||||
PathValidationResult,
|
||||
} from './checkpoint/index.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
// MCP 管理器实例
|
||||
let mcpInitialized = false;
|
||||
|
||||
/**
|
||||
* 初始化 MCP 系统
|
||||
* 加载配置、连接服务器、注册工具
|
||||
*/
|
||||
async function initMCP(workdir: string): Promise<void> {
|
||||
if (mcpInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mcpConfig = loadMCPConfig(workdir);
|
||||
|
||||
// 如果没有 MCP 配置,跳过初始化
|
||||
if (!mcpConfig.mcp || Object.keys(mcpConfig.mcp).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mcpManager = getMCPManager();
|
||||
|
||||
// 监听工具变化事件
|
||||
mcpManager.on('tools:changed', () => {
|
||||
registerMCPTools(mcpManager);
|
||||
});
|
||||
|
||||
// 监听服务器事件(用于日志)
|
||||
mcpManager.on('server:connected', (name) => {
|
||||
console.log(`🔌 MCP 服务器已连接: ${name}`);
|
||||
});
|
||||
|
||||
mcpManager.on('server:disconnected', (name) => {
|
||||
console.log(`🔌 MCP 服务器已断开: ${name}`);
|
||||
});
|
||||
|
||||
mcpManager.on('server:error', (name, error) => {
|
||||
console.error(`❌ MCP 服务器 ${name} 错误:`, error);
|
||||
});
|
||||
|
||||
try {
|
||||
await mcpManager.initialize(mcpConfig);
|
||||
registerMCPTools(mcpManager);
|
||||
mcpInitialized = true;
|
||||
|
||||
// 显示 MCP 状态
|
||||
const statuses = mcpManager.getServerStatuses();
|
||||
const connected = statuses.filter((s) => s.status === 'connected');
|
||||
if (connected.length > 0) {
|
||||
const totalTools = connected.reduce((sum, s) => sum + s.toolCount, 0);
|
||||
console.log(
|
||||
`🔌 MCP: ${connected.length} 个服务器已连接,${totalTools} 个工具可用`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'❌ MCP 初始化失败:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 工具注册到工具注册表
|
||||
*/
|
||||
function registerMCPTools(
|
||||
mcpManager: ReturnType<typeof getMCPManager>
|
||||
): void {
|
||||
const adapter = createMCPToolAdapter(mcpManager);
|
||||
const mcpTools = mcpManager.getTools();
|
||||
const adaptedTools = adapter.adaptTools(mcpTools);
|
||||
|
||||
// 注册到工具注册表
|
||||
toolRegistry.registerAll(adaptedTools);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 MCP 系统
|
||||
*/
|
||||
async function shutdownMCP(): Promise<void> {
|
||||
if (mcpInitialized) {
|
||||
const mcpManager = getMCPManager();
|
||||
await mcpManager.shutdown();
|
||||
mcpInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.name('ai-assist')
|
||||
.description('AI Terminal Assistant - 终端中的 AI 编程助手')
|
||||
.version('1.0.0');
|
||||
|
||||
// 初始化命令
|
||||
program
|
||||
.command('init')
|
||||
.description('初始化配置(设置 API Key 等)')
|
||||
.action(async () => {
|
||||
await initConfig();
|
||||
});
|
||||
|
||||
// LSP 命令组
|
||||
const lspCommand = program
|
||||
.command('lsp')
|
||||
.description('语言服务器管理');
|
||||
|
||||
lspCommand
|
||||
.command('list')
|
||||
.description('列出所有语言服务器及其安装状态')
|
||||
.action(() => {
|
||||
printServerList();
|
||||
});
|
||||
|
||||
lspCommand
|
||||
.command('install [servers...]')
|
||||
.description('安装指定的语言服务器')
|
||||
.option('-a, --all', '安装所有语言服务器')
|
||||
.action(async (servers: string[], options: { all?: boolean }) => {
|
||||
if (options.all) {
|
||||
await installAllServers();
|
||||
} else if (servers.length === 0) {
|
||||
console.log('用法: ai-assist lsp install <server> [server2] ...');
|
||||
console.log(' ai-assist lsp install --all');
|
||||
console.log('\n运行 "ai-assist lsp list" 查看可用的服务器');
|
||||
} else {
|
||||
for (const server of servers) {
|
||||
await installServer(server);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lspCommand
|
||||
.command('info <server>')
|
||||
.description('显示语言服务器详细信息')
|
||||
.action((server: string) => {
|
||||
showServerInfo(server);
|
||||
});
|
||||
|
||||
// MCP 命令组
|
||||
const mcpCommand = program.command('mcp').description('MCP 服务器管理');
|
||||
|
||||
mcpCommand
|
||||
.command('list')
|
||||
.description('列出所有 MCP 服务器及其状态')
|
||||
.action(async () => {
|
||||
const mcpConfig = loadMCPConfig(process.cwd());
|
||||
|
||||
if (!mcpConfig.mcp || Object.keys(mcpConfig.mcp).length === 0) {
|
||||
console.log('没有配置 MCP 服务器');
|
||||
console.log('\n配置方法:');
|
||||
console.log(' 在 ~/.ai-assist/config.json 或 .ai-assist/config.json 中添加 mcp 配置');
|
||||
console.log('\n示例:');
|
||||
console.log(' {');
|
||||
console.log(' "mcp": {');
|
||||
console.log(' "filesystem": {');
|
||||
console.log(' "type": "local",');
|
||||
console.log(' "command": ["npx", "-y", "@anthropic-ai/mcp-server-filesystem", "/path/to/dir"]');
|
||||
console.log(' }');
|
||||
console.log(' }');
|
||||
console.log(' }');
|
||||
return;
|
||||
}
|
||||
|
||||
const mcpManager = getMCPManager();
|
||||
|
||||
// 尝试连接以获取工具数量
|
||||
try {
|
||||
if (!mcpManager.isInitialized()) {
|
||||
await mcpManager.initialize(mcpConfig);
|
||||
}
|
||||
} catch {
|
||||
// 忽略连接错误,仍然显示配置的服务器
|
||||
}
|
||||
|
||||
const statuses = mcpManager.getServerStatuses();
|
||||
|
||||
console.log('\nMCP 服务器列表:\n');
|
||||
|
||||
const statusIcons: Record<string, string> = {
|
||||
connected: '✅',
|
||||
connecting: '🔄',
|
||||
disconnected: '⭕',
|
||||
disabled: '🚫',
|
||||
error: '❌',
|
||||
};
|
||||
|
||||
for (const status of statuses) {
|
||||
const icon = statusIcons[status.status] || '❓';
|
||||
const toolInfo = status.toolCount > 0 ? ` (${status.toolCount} 个工具)` : '';
|
||||
const errorInfo = status.error ? ` - ${status.error}` : '';
|
||||
console.log(
|
||||
` ${icon} ${status.name} [${status.type}] - ${status.status}${toolInfo}${errorInfo}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// 关闭连接
|
||||
await mcpManager.shutdown();
|
||||
});
|
||||
|
||||
mcpCommand
|
||||
.command('tools [server]')
|
||||
.description('列出 MCP 服务器提供的工具')
|
||||
.action(async (server?: string) => {
|
||||
const mcpConfig = loadMCPConfig(process.cwd());
|
||||
|
||||
if (!mcpConfig.mcp || Object.keys(mcpConfig.mcp).length === 0) {
|
||||
console.log('没有配置 MCP 服务器');
|
||||
return;
|
||||
}
|
||||
|
||||
const mcpManager = getMCPManager();
|
||||
|
||||
try {
|
||||
if (!mcpManager.isInitialized()) {
|
||||
await mcpManager.initialize(mcpConfig);
|
||||
}
|
||||
|
||||
const tools = mcpManager.getTools();
|
||||
|
||||
if (tools.length === 0) {
|
||||
console.log('没有可用的 MCP 工具');
|
||||
return;
|
||||
}
|
||||
|
||||
// 按服务器分组
|
||||
const toolsByServer = new Map<string, typeof tools>();
|
||||
for (const tool of tools) {
|
||||
if (server && tool.server !== server) {
|
||||
continue;
|
||||
}
|
||||
const serverTools = toolsByServer.get(tool.server) || [];
|
||||
serverTools.push(tool);
|
||||
toolsByServer.set(tool.server, serverTools);
|
||||
}
|
||||
|
||||
if (toolsByServer.size === 0) {
|
||||
console.log(server ? `服务器 "${server}" 没有提供工具` : '没有可用的工具');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\nMCP 工具列表:\n');
|
||||
|
||||
for (const [serverName, serverTools] of toolsByServer) {
|
||||
console.log(`📦 ${serverName}:`);
|
||||
for (const tool of serverTools) {
|
||||
console.log(` ${tool.name}`);
|
||||
if (tool.description) {
|
||||
console.log(` ${tool.description.substring(0, 80)}${tool.description.length > 80 ? '...' : ''}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'获取工具列表失败:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
} finally {
|
||||
await mcpManager.shutdown();
|
||||
}
|
||||
});
|
||||
|
||||
mcpCommand
|
||||
.command('test <server>')
|
||||
.description('测试 MCP 服务器连接')
|
||||
.action(async (server: string) => {
|
||||
const mcpConfig = loadMCPConfig(process.cwd());
|
||||
|
||||
if (!mcpConfig.mcp?.[server]) {
|
||||
console.log(`❌ 未找到服务器配置: ${server}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🔄 正在连接 ${server}...`);
|
||||
|
||||
const mcpManager = getMCPManager();
|
||||
|
||||
try {
|
||||
await mcpManager.initialize({
|
||||
mcp: { [server]: mcpConfig.mcp[server] },
|
||||
tools: mcpConfig.tools,
|
||||
});
|
||||
|
||||
const status = mcpManager.getServerStatus(server);
|
||||
|
||||
if (status?.status === 'connected') {
|
||||
console.log(`✅ 连接成功!`);
|
||||
console.log(` 工具数量: ${status.toolCount}`);
|
||||
|
||||
const tools = mcpManager.getTools();
|
||||
if (tools.length > 0) {
|
||||
console.log(' 可用工具:');
|
||||
for (const tool of tools) {
|
||||
console.log(` - ${tool.originalName}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`❌ 连接失败: ${status?.error || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ 连接失败:`,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
} finally {
|
||||
await mcpManager.shutdown();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化权限系统
|
||||
function setupPermissions(): void {
|
||||
const permissionManager = getPermissionManager();
|
||||
permissionManager.setAskCallback(promptPermission);
|
||||
}
|
||||
|
||||
// 单次查询命令
|
||||
program
|
||||
.command('ask <question>')
|
||||
.description('单次提问(不进入交互模式)')
|
||||
.action(async (question: string) => {
|
||||
setupPermissions();
|
||||
const config = loadConfig();
|
||||
const agent = new Agent(config);
|
||||
|
||||
// 设置工具注册表(支持动态工具发现)
|
||||
agent.setRegistry(toolRegistry);
|
||||
|
||||
try {
|
||||
await agent.chat(question, (text) => {
|
||||
process.stdout.write(text);
|
||||
});
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'错误:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// 默认:交互模式
|
||||
program.action(async () => {
|
||||
setupPermissions();
|
||||
const config = loadConfig();
|
||||
const agent = new Agent(config);
|
||||
|
||||
// 初始化 LSP 系统
|
||||
initLSP(process.cwd());
|
||||
|
||||
// 初始化 MCP 系统(加载外部工具服务器)
|
||||
await initMCP(process.cwd());
|
||||
|
||||
// 设置工具注册表(支持动态工具发现)
|
||||
agent.setRegistry(toolRegistry);
|
||||
|
||||
// 初始化会话管理器(支持会话持久化)
|
||||
const sessionManager = new SessionManager();
|
||||
await sessionManager.init(process.cwd());
|
||||
agent.setSessionManager(sessionManager);
|
||||
|
||||
// 初始化 todoManager(让 todo 工具可以访问会话)
|
||||
todoManager.setSessionManager(sessionManager);
|
||||
|
||||
// 初始化 Agent 注册表(加载预设和用户配置)
|
||||
await agentRegistry.init(process.cwd());
|
||||
|
||||
// 初始化 Task 工具上下文
|
||||
initTaskContext(config, sessionManager);
|
||||
updateTaskDescription();
|
||||
|
||||
// 初始化 Skill 注册表
|
||||
const skillRegistry = getSkillRegistry();
|
||||
await skillRegistry.initialize(process.cwd());
|
||||
updateSkillDescription();
|
||||
|
||||
// 初始化 Command 注册表
|
||||
const commandRegistry = getCommandRegistry();
|
||||
await commandRegistry.initialize(process.cwd());
|
||||
|
||||
// 显示会话恢复信息
|
||||
const session = sessionManager.getSession();
|
||||
if (session && session.messages.length > 0) {
|
||||
console.log(`\n📂 已恢复会话 (${session.messages.length} 条消息)`);
|
||||
}
|
||||
|
||||
// 启动终端 UI
|
||||
const ui = new TerminalUI(agent);
|
||||
|
||||
// 优雅退出
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n\n👋 再见!');
|
||||
await shutdownMCP();
|
||||
await shutdownLSP();
|
||||
await sessionManager.close();
|
||||
ui.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
await ui.start();
|
||||
});
|
||||
|
||||
program.parse();
|
||||
// Hooks
|
||||
export {
|
||||
HookManager,
|
||||
getHookManager,
|
||||
initHookManager,
|
||||
resetHookManager,
|
||||
loadProjectConfig,
|
||||
loadHookConfig,
|
||||
loadPluginList,
|
||||
createDefaultConfig,
|
||||
getConfigFilePath,
|
||||
} from './hooks/index.js';
|
||||
|
||||
export type {
|
||||
HookType,
|
||||
HookConfig,
|
||||
HookEvent,
|
||||
HookEventListener,
|
||||
ShellCommandConfig,
|
||||
FileHookConfig,
|
||||
Hooks,
|
||||
Plugin,
|
||||
PluginInput,
|
||||
ToolExecuteBeforeInput,
|
||||
ToolExecuteBeforeOutput,
|
||||
ToolExecuteAfterInput,
|
||||
ToolExecuteAfterOutput,
|
||||
SessionStartInput,
|
||||
SessionEndInput,
|
||||
MessageBeforeInput,
|
||||
MessageBeforeOutput,
|
||||
MessageAfterInput,
|
||||
FileChangeInput,
|
||||
FileChangeOutput,
|
||||
ProjectConfig,
|
||||
} from './hooks/index.js';
|
||||
|
||||
// Agent Registry & Presets
|
||||
export { agentRegistry, AgentRegistry } from './agent/index.js';
|
||||
export { loadAgentConfig, saveAgentConfig, getConfigTemplate } from './agent/index.js';
|
||||
export { presetAgents, isPresetAgent, getPresetAgentNames } from './agent/index.js';
|
||||
export type {
|
||||
AgentMode,
|
||||
AgentInfo,
|
||||
AgentConfigFile,
|
||||
AgentModelConfig,
|
||||
AgentToolConfig,
|
||||
AgentPermission,
|
||||
} from './agent/index.js';
|
||||
|
||||
// MCP
|
||||
export {
|
||||
getMCPManager,
|
||||
loadMCPConfig,
|
||||
createMCPToolAdapter,
|
||||
} from './mcp/index.js';
|
||||
|
||||
@@ -1,499 +0,0 @@
|
||||
import * as readline from 'readline';
|
||||
import chalk from 'chalk';
|
||||
import type { Agent } from '../core/agent.js';
|
||||
import { agentRegistry } from '../agent/index.js';
|
||||
import {
|
||||
getCommandRegistry,
|
||||
createCommandExecutor,
|
||||
type CommandExecutionResult,
|
||||
} from '../commands/index.js';
|
||||
import {
|
||||
extractImageReferences,
|
||||
loadImages,
|
||||
formatFileSize,
|
||||
} from '../utils/image.js';
|
||||
import type { UserInput } from '../types/index.js';
|
||||
|
||||
export class TerminalUI {
|
||||
private agent: Agent;
|
||||
private rl: readline.Interface;
|
||||
private isClosed = false;
|
||||
|
||||
constructor(agent: Agent) {
|
||||
this.agent = agent;
|
||||
this.rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: true,
|
||||
});
|
||||
|
||||
// 监听关闭事件
|
||||
this.rl.on('close', () => {
|
||||
this.isClosed = true;
|
||||
});
|
||||
}
|
||||
|
||||
// 显示欢迎信息
|
||||
private showWelcome(): void {
|
||||
console.log(chalk.cyan('\n╔════════════════════════════════════════╗'));
|
||||
console.log(chalk.cyan('║') + chalk.bold.white(' 🤖 AI Terminal Assistant ') + chalk.cyan('║'));
|
||||
console.log(chalk.cyan('║') + chalk.gray(' Powered by DeepSeek / Claude ') + chalk.cyan('║'));
|
||||
console.log(chalk.cyan('╚════════════════════════════════════════╝\n'));
|
||||
console.log(chalk.gray('输入你的问题,或使用以下命令:'));
|
||||
console.log(chalk.yellow(' /help') + chalk.gray(' - 显示帮助'));
|
||||
console.log(chalk.yellow(' /commands') + chalk.gray(' - 列出所有可用命令'));
|
||||
console.log(chalk.yellow(' /agent') + chalk.gray(' - 切换 Agent 模式'));
|
||||
console.log(chalk.yellow(' /clear') + chalk.gray(' - 清空对话历史'));
|
||||
console.log(chalk.yellow(' /compact') + chalk.gray(' - 压缩对话历史'));
|
||||
console.log(chalk.yellow(' /context') + chalk.gray(' - 查看上下文使用情况'));
|
||||
console.log(chalk.yellow(' /exit') + chalk.gray(' - 退出程序'));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 格式化上下文使用情况(带颜色)
|
||||
private formatContextUsage(): string {
|
||||
const usage = this.agent.getContextUsage();
|
||||
const percent = usage.usagePercent;
|
||||
|
||||
// 根据使用率选择颜色
|
||||
let colorFn: (text: string) => string;
|
||||
if (percent < 50) {
|
||||
colorFn = chalk.green;
|
||||
} else if (percent < 80) {
|
||||
colorFn = chalk.yellow;
|
||||
} else {
|
||||
colorFn = chalk.red;
|
||||
}
|
||||
|
||||
const used = usage.input >= 1000 ? `${(usage.input / 1000).toFixed(1)}k` : `${usage.input}`;
|
||||
const limit = `${(usage.available / 1000).toFixed(0)}k`;
|
||||
return colorFn(`[${used}/${limit}]`);
|
||||
}
|
||||
|
||||
// 列出所有可用的 Commands
|
||||
private listCommands(): void {
|
||||
const registry = getCommandRegistry();
|
||||
const commands = registry.list();
|
||||
|
||||
console.log(chalk.cyan('\n📋 可用的 Commands:\n'));
|
||||
|
||||
if (commands.length === 0) {
|
||||
console.log(chalk.gray(' 没有可用的命令'));
|
||||
} else {
|
||||
// 按来源分组
|
||||
const builtin = commands.filter((c) => c.source === 'builtin');
|
||||
const user = commands.filter((c) => c.source === 'user');
|
||||
const project = commands.filter((c) => c.source === 'project');
|
||||
|
||||
if (builtin.length > 0) {
|
||||
console.log(chalk.white(' 内置命令:'));
|
||||
for (const cmd of builtin) {
|
||||
console.log(
|
||||
chalk.yellow(` /${cmd.name}`) +
|
||||
chalk.gray(cmd.description ? ` - ${cmd.description}` : '')
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (user.length > 0) {
|
||||
console.log(chalk.white(' 用户命令:'));
|
||||
for (const cmd of user) {
|
||||
console.log(
|
||||
chalk.yellow(` /${cmd.name}`) +
|
||||
chalk.gray(cmd.description ? ` - ${cmd.description}` : '')
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (project.length > 0) {
|
||||
console.log(chalk.white(' 项目命令:'));
|
||||
for (const cmd of project) {
|
||||
console.log(
|
||||
chalk.yellow(` /${cmd.name}`) +
|
||||
chalk.gray(cmd.description ? ` - ${cmd.description}` : '')
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.gray(' 用法: /<command> [arguments]'));
|
||||
console.log(chalk.gray(' 示例: /review main..feature'));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 尝试执行自定义 Command
|
||||
private async tryExecuteCommand(input: string): Promise<CommandExecutionResult | null> {
|
||||
const executor = createCommandExecutor(process.cwd());
|
||||
const parsed = executor.parseInput(input);
|
||||
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const registry = getCommandRegistry();
|
||||
|
||||
// 检查是否是自定义 Command(不是内置的系统命令)
|
||||
if (!registry.has(parsed.command)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await executor.execute(parsed);
|
||||
}
|
||||
|
||||
// 处理 /agent 命令
|
||||
private handleAgentCommand(args: string): boolean {
|
||||
const agentName = args.trim();
|
||||
|
||||
// 无参数:显示当前模式和可用 Agent
|
||||
if (!agentName) {
|
||||
const currentMode = this.agent.getAgentModeName();
|
||||
const availableAgents = agentRegistry.listPrimaryAgents();
|
||||
|
||||
console.log(chalk.cyan('\n🤖 Agent 模式:'));
|
||||
console.log(chalk.white(` 当前: ${currentMode === 'default' ? chalk.green('default (通用助手)') : chalk.yellow(currentMode)}`));
|
||||
console.log('');
|
||||
console.log(chalk.white(' 可用 Agent:'));
|
||||
console.log(chalk.gray(' • default - 通用编程助手'));
|
||||
for (const agent of availableAgents) {
|
||||
const marker = agent.name === currentMode ? chalk.green(' ✓') : '';
|
||||
console.log(chalk.gray(` • ${agent.name} - ${agent.description}${marker}`));
|
||||
}
|
||||
console.log('');
|
||||
console.log(chalk.gray(' 用法: /agent <name> 切换到指定 Agent'));
|
||||
console.log(chalk.gray(' /agent default 切换回默认模式'));
|
||||
console.log('');
|
||||
return true;
|
||||
}
|
||||
|
||||
// 切换到默认模式
|
||||
if (agentName === 'default') {
|
||||
this.agent.setAgentMode(null);
|
||||
const toolCount = this.agent.getToolCount();
|
||||
console.log(chalk.green('\n✓ 已切换到 default (通用助手) 模式'));
|
||||
console.log(chalk.gray(` 可用工具: ${toolCount.total} 个\n`));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 切换到指定 Agent
|
||||
const agent = agentRegistry.get(agentName);
|
||||
if (!agent) {
|
||||
const availableNames = ['default', ...agentRegistry.listPrimaryAgents().map(a => a.name)];
|
||||
console.log(chalk.red(`\n✗ 未找到 Agent: ${agentName}`));
|
||||
console.log(chalk.gray(` 可用: ${availableNames.join(', ')}\n`));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否为 subagent 模式(不能作为主交互 Agent)
|
||||
if (agent.mode === 'subagent') {
|
||||
console.log(chalk.yellow(`\n⚠ Agent "${agentName}" 是 subagent 模式,只能通过 task 工具调用`));
|
||||
console.log(chalk.gray(' 提示: 使用 /agent 查看可交互的 Agent 列表\n'));
|
||||
return true;
|
||||
}
|
||||
|
||||
this.agent.setAgentMode(agent);
|
||||
const toolCount = this.agent.getToolCount();
|
||||
console.log(chalk.green(`\n✓ 已切换到 ${agent.name} 模式`));
|
||||
console.log(chalk.gray(` ${agent.description}`));
|
||||
console.log(chalk.gray(` 可用工具: ${toolCount.total} 个\n`));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理特殊命令
|
||||
private async handleCommand(input: string): Promise<boolean> {
|
||||
const command = input.toLowerCase().trim();
|
||||
|
||||
// 检查 /agent 命令(带参数)
|
||||
if (command.startsWith('/agent')) {
|
||||
const args = input.substring(6).trim();
|
||||
return this.handleAgentCommand(args);
|
||||
}
|
||||
|
||||
// 检查 /commands 命令
|
||||
if (command === '/commands') {
|
||||
this.listCommands();
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case '/help':
|
||||
console.log(chalk.cyan('\n📖 帮助信息:'));
|
||||
console.log(chalk.white(' 这是一个 AI 编程助手,可以帮你:'));
|
||||
console.log(chalk.gray(' • 读写文件'));
|
||||
console.log(chalk.gray(' • 执行 bash 命令'));
|
||||
console.log(chalk.gray(' • 搜索代码'));
|
||||
console.log(chalk.gray(' • 回答编程问题'));
|
||||
console.log('');
|
||||
console.log(chalk.white(' 命令:'));
|
||||
console.log(chalk.gray(' • /help - 显示此帮助'));
|
||||
console.log(chalk.gray(' • /agent - 切换 Agent 模式'));
|
||||
console.log(chalk.gray(' • /clear - 清空对话历史'));
|
||||
console.log(chalk.gray(' • /compact - 压缩对话历史,释放上下文空间'));
|
||||
console.log(chalk.gray(' • /context - 显示当前上下文使用情况'));
|
||||
console.log(chalk.gray(' • /exit - 退出程序'));
|
||||
console.log('');
|
||||
return true;
|
||||
|
||||
case '/clear':
|
||||
await this.agent.clearHistory();
|
||||
console.log(chalk.green('✓ 对话历史已清空\n'));
|
||||
return true;
|
||||
|
||||
case '/compact':
|
||||
console.log(chalk.yellow('正在压缩对话历史...\n'));
|
||||
try {
|
||||
const beforeUsage = this.agent.getContextUsage();
|
||||
const result = await this.agent.compactHistory();
|
||||
const afterUsage = this.agent.getContextUsage();
|
||||
|
||||
if (result.freedTokens > 0) {
|
||||
console.log(chalk.green(`✓ 压缩完成!`));
|
||||
console.log(chalk.gray(` 策略: ${result.type}`));
|
||||
console.log(chalk.gray(` 释放: ${(result.freedTokens / 1000).toFixed(1)}k tokens`));
|
||||
console.log(chalk.gray(` 之前: ${(beforeUsage.input / 1000).toFixed(1)}k`));
|
||||
console.log(chalk.gray(` 之后: ${(afterUsage.input / 1000).toFixed(1)}k`));
|
||||
} else {
|
||||
console.log(chalk.yellow('没有可压缩的内容'));
|
||||
}
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`压缩失败: ${error instanceof Error ? error.message : String(error)}\n`));
|
||||
}
|
||||
return true;
|
||||
|
||||
case '/context':
|
||||
const usage = this.agent.getContextUsage();
|
||||
console.log(chalk.cyan('\n📊 上下文使用情况:'));
|
||||
console.log(chalk.gray(` 已使用: ${(usage.input / 1000).toFixed(1)}k tokens`));
|
||||
console.log(chalk.gray(` 可用: ${(usage.available / 1000).toFixed(0)}k tokens`));
|
||||
console.log(chalk.gray(` 上下文限制: ${(usage.contextLimit / 1000).toFixed(0)}k tokens`));
|
||||
console.log(chalk.gray(` 使用率: ${usage.usagePercent.toFixed(1)}%`));
|
||||
console.log('');
|
||||
return true;
|
||||
|
||||
case '/exit':
|
||||
case '/quit':
|
||||
console.log(chalk.cyan('\n👋 再见!\n'));
|
||||
this.close();
|
||||
process.exit(0);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理包含图片引用的输入
|
||||
private async processImageInput(
|
||||
input: string
|
||||
): Promise<{ userInput: UserInput; hasImages: boolean } | null> {
|
||||
const { imagePaths, textContent } = extractImageReferences(input);
|
||||
|
||||
// 没有图片引用,返回纯文本
|
||||
if (imagePaths.length === 0) {
|
||||
return {
|
||||
userInput: { text: input },
|
||||
hasImages: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 加载图片
|
||||
console.log(chalk.gray(`\n正在加载 ${imagePaths.length} 张图片...`));
|
||||
const { images, errors } = await loadImages(imagePaths, process.cwd());
|
||||
|
||||
// 显示加载错误
|
||||
for (const err of errors) {
|
||||
console.log(chalk.red(` ✗ ${err.path}: ${err.error}`));
|
||||
}
|
||||
|
||||
// 如果没有成功加载任何图片
|
||||
if (images.length === 0) {
|
||||
console.log(chalk.red('没有成功加载任何图片\n'));
|
||||
return null;
|
||||
}
|
||||
|
||||
// 显示成功加载的图片
|
||||
for (const img of images) {
|
||||
console.log(
|
||||
chalk.green(` ✓ ${img.filename}`) +
|
||||
chalk.gray(` (${formatFileSize(img.size)})`)
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
return {
|
||||
userInput: {
|
||||
text: textContent,
|
||||
images: images.map((img) => ({
|
||||
data: img.base64,
|
||||
mimeType: img.mimeType,
|
||||
filename: img.filename,
|
||||
})),
|
||||
},
|
||||
hasImages: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 提问并获取用户输入
|
||||
private prompt(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isClosed) {
|
||||
reject(new Error('readline closed'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示带上下文使用情况的提示符
|
||||
const contextInfo = this.formatContextUsage();
|
||||
const agentMode = this.agent.getAgentModeName();
|
||||
const agentIndicator = agentMode === 'default' ? '' : chalk.magenta(`@${agentMode} `);
|
||||
this.rl.question(`${contextInfo} ${agentIndicator}${chalk.green('You >')} `, (answer) => {
|
||||
resolve(answer ?? '');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 主循环
|
||||
async start(): Promise<void> {
|
||||
this.showWelcome();
|
||||
|
||||
while (!this.isClosed) {
|
||||
try {
|
||||
const input = await this.prompt();
|
||||
|
||||
// 跳过空输入
|
||||
if (!input.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理命令
|
||||
if (input.startsWith('/')) {
|
||||
// 先检查系统内置命令
|
||||
if (await this.handleCommand(input)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 尝试执行自定义 Command
|
||||
const cmdResult = await this.tryExecuteCommand(input);
|
||||
if (cmdResult) {
|
||||
if (!cmdResult.success) {
|
||||
console.log(chalk.red(`\n❌ ${cmdResult.error}\n`));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Command 执行成功,将渲染后的提示发送给 AI
|
||||
console.log(chalk.cyan(`\n📝 执行命令: ${input.split(' ')[0]}`));
|
||||
if (cmdResult.agent) {
|
||||
console.log(chalk.gray(` Agent: ${cmdResult.agent}`));
|
||||
}
|
||||
if (cmdResult.model) {
|
||||
console.log(chalk.gray(` Model: ${cmdResult.model}`));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// 使用渲染后的 prompt 作为输入
|
||||
const promptToSend = cmdResult.prompt || '';
|
||||
|
||||
// TODO: 如果指定了 agent/model/subtask,需要相应处理
|
||||
// 目前简单地将渲染后的 prompt 发送给当前 agent
|
||||
process.stdout.write(chalk.gray('思考中...'));
|
||||
|
||||
try {
|
||||
let isFirstChunk = true;
|
||||
|
||||
await this.agent.chat(promptToSend, (text) => {
|
||||
if (isFirstChunk) {
|
||||
process.stdout.write('\r' + ' '.repeat(20) + '\r');
|
||||
process.stdout.write(chalk.blue('AI > '));
|
||||
isFirstChunk = false;
|
||||
}
|
||||
|
||||
if (text.startsWith('\n[调用工具:')) {
|
||||
process.stdout.write(chalk.yellow(text));
|
||||
} else if (text.startsWith('[结果:') || text.startsWith('[错误:')) {
|
||||
process.stdout.write(chalk.gray(text));
|
||||
} else {
|
||||
process.stdout.write(text);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n');
|
||||
} catch (error) {
|
||||
process.stdout.write('\r' + ' '.repeat(20) + '\r');
|
||||
console.log(
|
||||
chalk.red(
|
||||
`❌ 错误: ${error instanceof Error ? error.message : String(error)}\n`
|
||||
)
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 未知命令
|
||||
console.log(chalk.yellow(`\n未知命令: ${input.split(' ')[0]}`));
|
||||
console.log(chalk.gray('使用 /commands 查看所有可用命令\n'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理图片引用
|
||||
const processed = await this.processImageInput(input);
|
||||
if (!processed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { userInput, hasImages } = processed;
|
||||
|
||||
// 发送给 AI(如果模型不支持 vision,Agent 会自动委托 Vision Agent 处理)
|
||||
process.stdout.write(chalk.gray('思考中...'));
|
||||
|
||||
try {
|
||||
let isFirstChunk = true;
|
||||
|
||||
// 根据是否有图片选择发送格式
|
||||
const messageToSend = hasImages ? userInput : userInput.text;
|
||||
|
||||
await this.agent.chat(messageToSend, (text) => {
|
||||
if (isFirstChunk) {
|
||||
// 清除 "思考中..." 并显示 AI 前缀
|
||||
process.stdout.write('\r' + ' '.repeat(20) + '\r');
|
||||
process.stdout.write(chalk.blue('AI > '));
|
||||
isFirstChunk = false;
|
||||
}
|
||||
|
||||
// 处理工具调用的输出
|
||||
if (text.startsWith('\n[调用工具:')) {
|
||||
process.stdout.write(chalk.yellow(text));
|
||||
} else if (text.startsWith('[结果:') || text.startsWith('[错误:')) {
|
||||
process.stdout.write(chalk.gray(text));
|
||||
} else {
|
||||
process.stdout.write(text);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n');
|
||||
} catch (error) {
|
||||
// 清除 "思考中..."
|
||||
process.stdout.write('\r' + ' '.repeat(20) + '\r');
|
||||
console.log(
|
||||
chalk.red(
|
||||
`❌ 错误: ${error instanceof Error ? error.message : String(error)}\n`
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// readline 关闭,退出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('\n👋 再见!\n'));
|
||||
}
|
||||
|
||||
// 关闭
|
||||
close(): void {
|
||||
if (!this.isClosed) {
|
||||
this.isClosed = true;
|
||||
this.rl.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { TerminalUI } from '../../../src/ui/terminal.js';
|
||||
|
||||
// Mock readline
|
||||
const mockReadline = {
|
||||
question: vi.fn(),
|
||||
close: vi.fn(),
|
||||
on: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('readline', () => ({
|
||||
createInterface: vi.fn(() => mockReadline),
|
||||
}));
|
||||
|
||||
// Mock chalk
|
||||
vi.mock('chalk', () => ({
|
||||
default: {
|
||||
cyan: vi.fn((s: string) => s),
|
||||
white: vi.fn((s: string) => s),
|
||||
gray: vi.fn((s: string) => s),
|
||||
yellow: vi.fn((s: string) => s),
|
||||
green: vi.fn((s: string) => s),
|
||||
red: vi.fn((s: string) => s),
|
||||
blue: vi.fn((s: string) => s),
|
||||
magenta: vi.fn((s: string) => s),
|
||||
bold: { white: vi.fn((s: string) => s) },
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock agent registry
|
||||
vi.mock('../../../src/agent/index.js', () => ({
|
||||
agentRegistry: {
|
||||
listPrimaryAgents: vi.fn(() => [
|
||||
{ name: 'code-reviewer', description: '代码审查', mode: 'primary' },
|
||||
]),
|
||||
get: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import * as readline from 'readline';
|
||||
import { agentRegistry } from '../../../src/agent/index.js';
|
||||
|
||||
// Mock Agent class
|
||||
const mockAgent = {
|
||||
getContextUsage: vi.fn(() => ({
|
||||
input: 1000,
|
||||
available: 10000,
|
||||
contextLimit: 128000,
|
||||
usagePercent: 10,
|
||||
})),
|
||||
getAgentModeName: vi.fn(() => 'default'),
|
||||
setAgentMode: vi.fn(),
|
||||
getToolCount: vi.fn(() => ({ total: 10 })),
|
||||
clearHistory: vi.fn(),
|
||||
compactHistory: vi.fn().mockResolvedValue({
|
||||
type: 'compact',
|
||||
freedTokens: 500,
|
||||
}),
|
||||
chat: vi.fn(),
|
||||
};
|
||||
|
||||
describe('TerminalUI - 终端界面', () => {
|
||||
let ui: TerminalUI;
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||
let stdoutWriteSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ui = new TerminalUI(mockAgent as any);
|
||||
|
||||
// 模拟 close 事件监听
|
||||
const closeHandler = vi.mocked(mockReadline.on).mock.calls.find(
|
||||
call => call[0] === 'close'
|
||||
)?.[1];
|
||||
if (closeHandler) {
|
||||
// 保存 close handler 以便测试
|
||||
}
|
||||
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('构造函数', () => {
|
||||
it('创建 readline 接口', () => {
|
||||
expect(readline.createInterface).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('监听 close 事件', () => {
|
||||
expect(mockReadline.on).toHaveBeenCalledWith('close', expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('close - 关闭', () => {
|
||||
it('关闭 readline', () => {
|
||||
ui.close();
|
||||
|
||||
expect(mockReadline.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('多次关闭只执行一次', () => {
|
||||
ui.close();
|
||||
ui.close();
|
||||
|
||||
expect(mockReadline.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatContextUsage (通过 prompt 间接测试)', () => {
|
||||
it('低使用率显示绿色', () => {
|
||||
mockAgent.getContextUsage.mockReturnValue({
|
||||
input: 1000,
|
||||
available: 100000,
|
||||
contextLimit: 128000,
|
||||
usagePercent: 10,
|
||||
});
|
||||
|
||||
// 通过创建新实例触发格式化
|
||||
new TerminalUI(mockAgent as any);
|
||||
expect(mockAgent.getContextUsage).toBeDefined();
|
||||
});
|
||||
|
||||
it('中等使用率显示黄色', () => {
|
||||
mockAgent.getContextUsage.mockReturnValue({
|
||||
input: 60000,
|
||||
available: 100000,
|
||||
contextLimit: 128000,
|
||||
usagePercent: 60,
|
||||
});
|
||||
|
||||
new TerminalUI(mockAgent as any);
|
||||
expect(mockAgent.getContextUsage).toBeDefined();
|
||||
});
|
||||
|
||||
it('高使用率显示红色', () => {
|
||||
mockAgent.getContextUsage.mockReturnValue({
|
||||
input: 100000,
|
||||
available: 20000,
|
||||
contextLimit: 128000,
|
||||
usagePercent: 90,
|
||||
});
|
||||
|
||||
new TerminalUI(mockAgent as any);
|
||||
expect(mockAgent.getContextUsage).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('命令处理', () => {
|
||||
describe('/help 命令', () => {
|
||||
it('显示帮助信息', async () => {
|
||||
// 模拟 handleCommand 通过 question 回调
|
||||
mockReadline.question.mockImplementation((_, callback: (answer: string) => void) => {
|
||||
callback('/help');
|
||||
});
|
||||
|
||||
// 验证帮助方法可以被调用
|
||||
expect(mockAgent.getContextUsage).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('/clear 命令', () => {
|
||||
it('清空历史', async () => {
|
||||
mockAgent.clearHistory.mockResolvedValue(undefined);
|
||||
|
||||
// 验证方法存在
|
||||
expect(mockAgent.clearHistory).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('/compact 命令', () => {
|
||||
it('压缩历史', async () => {
|
||||
expect(mockAgent.compactHistory).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('/context 命令', () => {
|
||||
it('显示上下文使用', async () => {
|
||||
expect(mockAgent.getContextUsage).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('/agent 命令', () => {
|
||||
it('无参数显示当前模式', () => {
|
||||
expect(mockAgent.getAgentModeName).toBeDefined();
|
||||
expect(agentRegistry.listPrimaryAgents).toBeDefined();
|
||||
});
|
||||
|
||||
it('切换到 default 模式', () => {
|
||||
expect(mockAgent.setAgentMode).toBeDefined();
|
||||
expect(mockAgent.getToolCount).toBeDefined();
|
||||
});
|
||||
|
||||
it('切换到指定 Agent', () => {
|
||||
vi.mocked(agentRegistry.get).mockReturnValue({
|
||||
name: 'code-reviewer',
|
||||
description: '代码审查',
|
||||
mode: 'primary',
|
||||
prompt: '你是代码审查助手',
|
||||
});
|
||||
|
||||
expect(agentRegistry.get).toBeDefined();
|
||||
});
|
||||
|
||||
it('subagent 模式不能作为主交互', () => {
|
||||
vi.mocked(agentRegistry.get).mockReturnValue({
|
||||
name: 'explore',
|
||||
description: '探索',
|
||||
mode: 'subagent',
|
||||
prompt: '你是探索助手',
|
||||
});
|
||||
|
||||
// 验证 mode 检查
|
||||
const agent = agentRegistry.get('explore');
|
||||
expect(agent?.mode).toBe('subagent');
|
||||
});
|
||||
|
||||
it('未找到 Agent 显示错误', () => {
|
||||
vi.mocked(agentRegistry.get).mockReturnValue(undefined);
|
||||
|
||||
expect(agentRegistry.get('nonexistent')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('chat 交互', () => {
|
||||
it('调用 agent.chat', async () => {
|
||||
mockAgent.chat.mockResolvedValue('response');
|
||||
|
||||
expect(mockAgent.chat).toBeDefined();
|
||||
});
|
||||
|
||||
it('处理流式输出', async () => {
|
||||
mockAgent.chat.mockImplementation((_input: string, callback: (text: string) => void) => {
|
||||
callback('Hello');
|
||||
callback(' World');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// 验证回调被调用
|
||||
await mockAgent.chat('test', (text: string) => {
|
||||
expect(['Hello', ' World']).toContain(text);
|
||||
});
|
||||
});
|
||||
|
||||
it('处理工具调用输出', async () => {
|
||||
mockAgent.chat.mockImplementation((_input: string, callback: (text: string) => void) => {
|
||||
callback('\n[调用工具: bash]');
|
||||
callback('[结果: success]');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await mockAgent.chat('test', () => {});
|
||||
expect(mockAgent.chat).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('处理错误', async () => {
|
||||
mockAgent.chat.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await expect(mockAgent.chat('test', () => {})).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Agent 模式显示', () => {
|
||||
it('default 模式不显示指示器', () => {
|
||||
mockAgent.getAgentModeName.mockReturnValue('default');
|
||||
|
||||
const mode = mockAgent.getAgentModeName();
|
||||
expect(mode).toBe('default');
|
||||
});
|
||||
|
||||
it('其他模式显示 @ 指示器', () => {
|
||||
mockAgent.getAgentModeName.mockReturnValue('code-reviewer');
|
||||
|
||||
const mode = mockAgent.getAgentModeName();
|
||||
expect(mode).toBe('code-reviewer');
|
||||
});
|
||||
});
|
||||
});
|
||||
Generated
+10
@@ -72,6 +72,9 @@ importers:
|
||||
minimatch:
|
||||
specifier: ^10.1.1
|
||||
version: 10.1.1
|
||||
nanoid:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
qwen-ai-provider-v5:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2(zod@4.1.13)
|
||||
@@ -3625,6 +3628,11 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.6:
|
||||
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
neotraverse@0.6.18:
|
||||
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -8675,6 +8683,8 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@5.1.6: {}
|
||||
|
||||
neotraverse@0.6.18: {}
|
||||
|
||||
nlcst-to-string@4.0.0:
|
||||
|
||||
Reference in New Issue
Block a user