bfe3bc63b3
- 在 AgentRegistry 添加 listPrimaryAgents() 方法 - 在 Agent 类添加 setAgentMode/getAgentMode 方法 - 在 TerminalUI 实现 /agent 命令: - /agent 显示当前模式和可用列表 - /agent <name> 切换到指定 Agent - /agent default 切换回默认模式 - 提示符显示当前 Agent 模式(如 @plan You >) - 为 build Agent 添加 prompt 配置
281 lines
10 KiB
TypeScript
281 lines
10 KiB
TypeScript
import * as readline from 'readline';
|
|
import chalk from 'chalk';
|
|
import type { Agent } from '../core/agent.js';
|
|
import { agentRegistry } from '../agent/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(' /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}]`);
|
|
}
|
|
|
|
// 处理 /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);
|
|
console.log(chalk.green('\n✓ 已切换到 default (通用助手) 模式\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);
|
|
console.log(chalk.green(`\n✓ 已切换到 ${agent.name} 模式`));
|
|
console.log(chalk.gray(` ${agent.description}\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);
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
// 发送给 AI
|
|
process.stdout.write(chalk.gray('思考中...'));
|
|
|
|
try {
|
|
let isFirstChunk = true;
|
|
|
|
await this.agent.chat(input, (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();
|
|
}
|
|
}
|
|
}
|