feat: 添加 Commands 系统支持斜杠命令

实现类似 OpenCode 的 Commands 功能:
- 支持 Markdown 格式定义命令,带 YAML frontmatter
- 变量替换:$ARGUMENTS, $1/$2, @filepath, !`shell`
- 三级加载:builtin < user < project
- 7 个内置命令:init, review, test, fix, explain, commit, help
- 集成终端 UI 支持 /commands 列表和命令执行
- 完整单元测试覆盖 (46 tests)
This commit is contained in:
2025-12-11 16:12:28 +08:00
parent 723558ff22
commit a476a4240c
11 changed files with 1873 additions and 1 deletions
+197
View File
@@ -0,0 +1,197 @@
/**
* 内置 Commands
*
* 提供一些常用的预定义 Commands
*/
import type { Command } from '../types.js';
/**
* /init - 初始化项目配置
*/
export const initCommand: Command = {
name: 'init',
description: '分析代码库并创建 AGENTS.md 配置文件',
template: `Please analyze this codebase and create an AGENTS.md file containing:
1. **Build/lint/test commands** - Document how to build, lint, and test the project
2. **Code style guidelines** - Document the coding conventions used
3. **Project structure** - Overview of the directory structure
4. **Key dependencies** - Important libraries and frameworks used
Additional context: $ARGUMENTS
Start by exploring the project structure and package configuration files.`,
agent: 'explore',
subtask: false,
source: 'builtin',
};
/**
* /review - 代码审查
*/
export const reviewCommand: Command = {
name: 'review',
description: '审查代码变更',
template: `You are a code reviewer. Your job is to review code changes.
Input: $ARGUMENTS
## Determining What to Review
Based on the input, determine which type of review to perform:
1. **No arguments**: Review uncommitted changes using \`git diff\`
2. **Commit hash**: Review that specific commit using \`git show $ARGUMENTS\`
3. **Branch name**: Compare to specified branch using \`git diff $ARGUMENTS...HEAD\`
4. **PR URL**: Review the pull request (if gh CLI is available)
## What to Look For
- **Bugs**: Logic errors, edge cases, null/undefined handling, security issues
- **Structure**: Does it follow existing patterns? Is it maintainable?
- **Performance**: Only flag if obviously problematic
- **Tests**: Are there adequate tests for the changes?
## Output Format
Provide a structured review with:
1. Summary of changes
2. Issues found (categorized by severity)
3. Suggestions for improvement
4. Positive observations`,
agent: 'code-reviewer',
subtask: false,
source: 'builtin',
};
/**
* /test - 运行并修复测试
*/
export const testCommand: Command = {
name: 'test',
description: '运行测试并修复失败的测试',
template: `Run the test suite and analyze the results.
Focus on: $ARGUMENTS
## Instructions
1. First, run the test command for this project
2. If tests fail, analyze the failures
3. Identify the root cause of each failure
4. Fix the failing tests or the code they're testing
5. Re-run tests to verify fixes
If no specific focus is provided, run all tests.`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* /fix - 修复问题
*/
export const fixCommand: Command = {
name: 'fix',
description: '修复指定的问题或错误',
template: `Please fix the following issue:
$ARGUMENTS
## Instructions
1. Understand the problem described
2. Locate the relevant code
3. Analyze the root cause
4. Implement a fix
5. Verify the fix works
6. Check for any side effects`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* /explain - 解释代码
*/
export const explainCommand: Command = {
name: 'explain',
description: '解释代码或概念',
template: `Please explain the following:
$ARGUMENTS
Provide a clear, structured explanation that includes:
1. Overview - What it does at a high level
2. How it works - Step by step breakdown
3. Key concepts - Important patterns or techniques used
4. Examples - Practical usage examples if applicable`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* /commit - 生成 commit 消息
*/
export const commitCommand: Command = {
name: 'commit',
description: '根据变更生成 Git commit 消息',
template: `Generate a Git commit message for the current changes.
Additional context: $ARGUMENTS
## Instructions
1. Run \`git diff --staged\` to see staged changes (or \`git diff\` if nothing staged)
2. Analyze the changes
3. Generate a commit message following Conventional Commits format:
- feat: New feature
- fix: Bug fix
- docs: Documentation
- style: Formatting
- refactor: Code restructuring
- test: Adding tests
- chore: Maintenance
4. Format:
- First line: type(scope): short description (50 chars max)
- Blank line
- Body: Detailed explanation if needed
5. Present the commit message for review`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* /help - 显示帮助
*/
export const helpCommand: Command = {
name: 'help',
description: '显示可用的命令和帮助信息',
template: `Show help information about available commands.
Topic: $ARGUMENTS
If a specific command is mentioned, provide detailed help for that command.
Otherwise, list all available commands with their descriptions.`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* 所有内置 Commands
*/
export const builtinCommands: Command[] = [
initCommand,
reviewCommand,
testCommand,
fixCommand,
explainCommand,
commitCommand,
helpCommand,
];
+284
View File
@@ -0,0 +1,284 @@
/**
* Command 执行器
*
* 负责解析和执行 Command
* - 参数替换($ARGUMENTS, $1, $2, ...
* - 文件引用(@filepath
* - Shell 命令执行(!`command`
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import type { Command, CommandInput, CommandExecutionResult } from './types.js';
import { getCommandRegistry } from './registry.js';
const execAsync = promisify(exec);
/**
* Command 执行器
*/
export class CommandExecutor {
private workdir: string;
constructor(workdir: string = process.cwd()) {
this.workdir = workdir;
}
/**
* 解析用户输入的命令字符串
* 例如: "/review main..feature" → { command: "review", arguments: "main..feature", args: ["main..feature"] }
*/
parseInput(input: string): CommandInput | null {
// 移除开头的 /
const trimmed = input.trim();
if (!trimmed.startsWith('/')) {
return null;
}
const withoutSlash = trimmed.slice(1);
const spaceIndex = withoutSlash.indexOf(' ');
let commandName: string;
let argumentsStr: string;
if (spaceIndex === -1) {
commandName = withoutSlash;
argumentsStr = '';
} else {
commandName = withoutSlash.slice(0, spaceIndex);
argumentsStr = withoutSlash.slice(spaceIndex + 1).trim();
}
// 解析参数数组
const args = argumentsStr ? this.parseArgs(argumentsStr) : [];
return {
command: commandName,
arguments: argumentsStr,
args,
workdir: this.workdir,
};
}
/**
* 解析参数字符串为数组
* 支持引号包裹的参数
*/
private parseArgs(argsStr: string): string[] {
const args: string[] = [];
let current = '';
let inQuote = false;
let quoteChar = '';
for (const char of argsStr) {
if ((char === '"' || char === "'") && !inQuote) {
inQuote = true;
quoteChar = char;
} else if (char === quoteChar && inQuote) {
inQuote = false;
quoteChar = '';
} else if (char === ' ' && !inQuote) {
if (current) {
args.push(current);
current = '';
}
} else {
current += char;
}
}
if (current) {
args.push(current);
}
return args;
}
/**
* 执行 Command
*/
async execute(input: CommandInput): Promise<CommandExecutionResult> {
const registry = getCommandRegistry();
const command = registry.get(input.command);
if (!command) {
// 尝试搜索相似的 Command
const suggestions = registry.search(input.command, 3);
let errorMsg = `Command 不存在: /${input.command}`;
if (suggestions.length > 0) {
errorMsg += '\n\n你可能想要的 Command:\n';
for (const { command: cmd } of suggestions) {
errorMsg += `- /${cmd.name}`;
if (cmd.description) {
errorMsg += `: ${cmd.description}`;
}
errorMsg += '\n';
}
}
return {
success: false,
error: errorMsg,
};
}
try {
// 渲染模板
const prompt = await this.renderTemplate(command.template, input);
return {
success: true,
prompt,
agent: command.agent,
model: command.model,
subtask: command.subtask,
};
} catch (error) {
return {
success: false,
error: `Command 执行失败: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
/**
* 渲染模板
*/
async renderTemplate(
template: string,
input: CommandInput
): Promise<string> {
let result = template;
// 1. 替换位置参数 $1, $2, ...
result = this.replacePositionalArgs(result, input.args);
// 2. 替换 $ARGUMENTS
result = result.replace(/\$ARGUMENTS/g, input.arguments);
// 3. 处理文件引用 @filepath
result = await this.resolveFileReferences(result, input.workdir);
// 4. 执行 Shell 命令 !`command`
result = await this.executeShellCommands(result, input.workdir);
return result;
}
/**
* 替换位置参数
*/
private replacePositionalArgs(template: string, args: string[]): string {
// 找出模板中使用的最大参数索引
const paramRegex = /\$(\d+)/g;
let maxIndex = 0;
let match;
while ((match = paramRegex.exec(template)) !== null) {
const index = parseInt(match[1], 10);
if (index > maxIndex) {
maxIndex = index;
}
}
// 替换参数
let result = template;
for (let i = 1; i <= maxIndex; i++) {
const value = i === maxIndex
? args.slice(i - 1).join(' ') // 最后一个参数获取剩余所有
: args[i - 1] || '';
result = result.replace(new RegExp(`\\$${i}`, 'g'), value);
}
return result;
}
/**
* 解析文件引用
* @filepath → 文件内容
*/
private async resolveFileReferences(
template: string,
workdir: string
): Promise<string> {
const fileRefRegex = /@([^\s]+)/g;
const matches = [...template.matchAll(fileRefRegex)];
if (matches.length === 0) {
return template;
}
let result = template;
for (const match of matches) {
const [fullMatch, filePath] = match;
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(workdir, filePath);
try {
const content = await fs.readFile(absolutePath, 'utf-8');
// 替换为带有文件路径标记的内容
const replacement = `\`\`\`${path.extname(filePath).slice(1) || 'txt'}\n// ${filePath}\n${content}\n\`\`\``;
result = result.replace(fullMatch, replacement);
} catch (error) {
// 文件不存在,保留原样或提示
console.warn(`无法读取文件: ${absolutePath}`);
result = result.replace(fullMatch, `[文件不存在: ${filePath}]`);
}
}
return result;
}
/**
* 执行 Shell 命令
* !`command` → 命令输出
*/
private async executeShellCommands(
template: string,
workdir: string
): Promise<string> {
const shellRegex = /!\`([^`]+)\`/g;
const matches = [...template.matchAll(shellRegex)];
if (matches.length === 0) {
return template;
}
let result = template;
for (const match of matches) {
const [fullMatch, command] = match;
try {
const { stdout, stderr } = await execAsync(command, {
cwd: workdir,
timeout: 30000, // 30 秒超时
});
const output = (stdout + stderr).trim();
result = result.replace(fullMatch, output);
} catch (error) {
// 命令执行失败,替换为错误信息
const errorMsg = error instanceof Error ? error.message : String(error);
result = result.replace(fullMatch, `[命令执行失败: ${command}]\n${errorMsg}`);
}
}
return result;
}
}
/**
* 创建 Command 执行器
*/
export function createCommandExecutor(
workdir: string = process.cwd()
): CommandExecutor {
return new CommandExecutor(workdir);
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Commands 模块
*
* 提供 Command 系统的所有功能导出
*/
// 类型
export type {
Command,
CommandInput,
CommandExecutionResult,
CommandSearchResult,
CommandFrontmatter,
} from './types.js';
// 加载器
export { CommandLoader, commandLoader } from './loader.js';
// 注册表
export {
CommandRegistry,
getCommandRegistry,
resetCommandRegistry,
} from './registry.js';
// 执行器
export { CommandExecutor, createCommandExecutor } from './executor.js';
// 内置 Commands
export { builtinCommands } from './builtin/index.js';
+172
View File
@@ -0,0 +1,172 @@
/**
* Command 加载器
*
* 负责从文件系统加载 Markdown 格式的 Command 定义。
* 支持从以下位置加载:
* 1. 内置 Commands(代码中定义)
* 2. 用户 Commands~/.config/ai-terminal/commands/
* 3. 项目 Commands./.ai-terminal/commands/
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import * as yaml from 'yaml';
import type { Command, CommandFrontmatter } from './types.js';
/**
* Command 加载器
*/
export class CommandLoader {
/**
* 从目录加载所有 Commands
*/
async loadFromDirectory(
dir: string,
source: 'user' | 'project'
): Promise<Command[]> {
const commands: Command[] = [];
try {
const exists = await fs
.access(dir)
.then(() => true)
.catch(() => false);
if (!exists) {
return commands;
}
await this.scanDirectory(dir, dir, source, commands);
} catch (error) {
console.warn(`读取 Commands 目录失败: ${dir}`, error);
}
return commands;
}
/**
* 递归扫描目录
*/
private async scanDirectory(
baseDir: string,
currentDir: string,
source: 'user' | 'project',
commands: Command[]
): Promise<void> {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isFile() && entry.name.endsWith('.md')) {
try {
const command = await this.loadFromFile(fullPath, baseDir, source);
if (command) {
commands.push(command);
}
} catch (error) {
console.warn(`加载 Command 文件失败: ${fullPath}`, error);
}
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
// 递归加载子目录(支持嵌套路径如 deploy/staging
await this.scanDirectory(baseDir, fullPath, source, commands);
}
}
}
/**
* 从单个 Markdown 文件加载 Command
*/
async loadFromFile(
filePath: string,
baseDir: string,
source: 'user' | 'project'
): Promise<Command | null> {
const content = await fs.readFile(filePath, 'utf-8');
// 从文件路径推断命令名称
// baseDir/deploy/staging.md → deploy/staging
const relativePath = path.relative(baseDir, filePath);
const name = relativePath.slice(0, -3); // 移除 .md 后缀
return this.parseMarkdownCommand(content, name, source, filePath);
}
/**
* 解析 Markdown 格式的 Command
*
* 格式示例:
* ```markdown
* ---
* description: 代码审查
* agent: explore
* model: sonnet
* subtask: true
* ---
*
* You are a code reviewer...
*
* Input: $ARGUMENTS
* ```
*/
parseMarkdownCommand(
content: string,
name: string,
source: 'user' | 'project' | 'builtin',
sourcePath?: string
): Command | null {
// 解析 frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
let frontmatter: CommandFrontmatter = {};
let template: string;
if (frontmatterMatch) {
const [, frontmatterStr, bodyContent] = frontmatterMatch;
try {
frontmatter = yaml.parse(frontmatterStr) as CommandFrontmatter;
} catch (error) {
console.warn(`解析 Command frontmatter 失败: ${sourcePath}`, error);
}
template = bodyContent.trim();
} else {
// 没有 frontmatter,整个内容作为模板
template = content.trim();
}
if (!template) {
return null;
}
return {
name,
description: frontmatter.description,
template,
agent: frontmatter.agent,
model: frontmatter.model,
subtask: frontmatter.subtask,
source,
sourcePath,
};
}
/**
* 获取用户 Commands 目录
*/
getUserCommandsDir(): string {
const home = process.env.HOME || process.env.USERPROFILE || '';
return path.join(home, '.config', 'ai-terminal', 'commands');
}
/**
* 获取项目 Commands 目录
*/
getProjectCommandsDir(workdir: string = process.cwd()): string {
return path.join(workdir, '.ai-terminal', 'commands');
}
}
/**
* 全局 Command 加载器实例
*/
export const commandLoader = new CommandLoader();
+199
View File
@@ -0,0 +1,199 @@
/**
* Command 注册表
*
* 管理所有可用的 Commands,支持:
* - 注册/注销 Commands
* - 按名称查询
* - 搜索 Commands
*/
import type { Command, CommandSearchResult } from './types.js';
import { commandLoader } from './loader.js';
import { builtinCommands } from './builtin/index.js';
/**
* Command 注册表
*/
export class CommandRegistry {
private commands = new Map<string, Command>();
private initialized = false;
/**
* 初始化注册表
*/
async initialize(workdir: string = process.cwd()): Promise<void> {
if (this.initialized) {
return;
}
// 1. 注册内置 Commands
for (const command of builtinCommands) {
this.register(command);
}
// 2. 加载用户 Commands
const userDir = commandLoader.getUserCommandsDir();
const userCommands = await commandLoader.loadFromDirectory(userDir, 'user');
for (const command of userCommands) {
this.register(command);
}
// 3. 加载项目 Commands
const projectDir = commandLoader.getProjectCommandsDir(workdir);
const projectCommands = await commandLoader.loadFromDirectory(
projectDir,
'project'
);
for (const command of projectCommands) {
this.register(command);
}
this.initialized = true;
}
/**
* 注册 Command
*/
register(command: Command): void {
// 项目 Commands 优先级最高,可以覆盖同名的内置/用户 Commands
const existing = this.commands.get(command.name);
if (existing) {
// 优先级: project > user > builtin
const priority = { project: 3, user: 2, builtin: 1 };
if (priority[command.source] < priority[existing.source]) {
return; // 不覆盖更高优先级的 Command
}
}
this.commands.set(command.name, command);
}
/**
* 注销 Command
*/
unregister(name: string): boolean {
return this.commands.delete(name);
}
/**
* 获取 Command
*/
get(name: string): Command | undefined {
return this.commands.get(name);
}
/**
* 检查 Command 是否存在
*/
has(name: string): boolean {
return this.commands.has(name);
}
/**
* 获取所有 Commands
*/
getAll(): Command[] {
return Array.from(this.commands.values());
}
/**
* 搜索 Commands
*/
search(query: string, limit: number = 10): CommandSearchResult[] {
const queryLower = query.toLowerCase();
const results: CommandSearchResult[] = [];
for (const command of this.commands.values()) {
let score = 0;
// 精确名称匹配
if (command.name.toLowerCase() === queryLower) {
score = 100;
}
// 名称前缀匹配
else if (command.name.toLowerCase().startsWith(queryLower)) {
score = 80;
}
// 名称包含匹配
else if (command.name.toLowerCase().includes(queryLower)) {
score = 60;
}
// 描述匹配
else if (command.description?.toLowerCase().includes(queryLower)) {
score = 40;
}
if (score > 0) {
results.push({ command, score });
}
}
// 按分数降序排序
results.sort((a, b) => b.score - a.score);
return results.slice(0, limit);
}
/**
* 列出所有 Commands(用于帮助显示)
*/
list(): Array<{ name: string; description?: string; source: string }> {
return this.getAll()
.map((cmd) => ({
name: cmd.name,
description: cmd.description,
source: cmd.source,
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
/**
* 重新加载 Commands
*/
async reload(workdir: string = process.cwd()): Promise<void> {
this.commands.clear();
this.initialized = false;
await this.initialize(workdir);
}
/**
* 获取统计信息
*/
getStats(): {
total: number;
bySource: Record<string, number>;
} {
const commands = this.getAll();
const bySource: Record<string, number> = {};
for (const command of commands) {
bySource[command.source] = (bySource[command.source] || 0) + 1;
}
return {
total: commands.length,
bySource,
};
}
}
/**
* 全局 Command 注册表实例
*/
let commandRegistryInstance: CommandRegistry | null = null;
/**
* 获取全局 Command 注册表
*/
export function getCommandRegistry(): CommandRegistry {
if (!commandRegistryInstance) {
commandRegistryInstance = new CommandRegistry();
}
return commandRegistryInstance;
}
/**
* 重置全局 Command 注册表(用于测试)
*/
export function resetCommandRegistry(): void {
commandRegistryInstance = null;
}
+80
View File
@@ -0,0 +1,80 @@
/**
* Command 系统类型定义
*
* Command 是用户可通过斜杠命令触发的可复用提示词模板。
* 与 Skill 不同,Command 面向用户,可控制完整执行流程。
*/
/**
* Command 定义
*/
export interface Command {
/** Command 名称(从文件路径推断,如 deploy/staging */
name: string;
/** Command 描述 */
description?: string;
/** 提示词模板 */
template: string;
/** 指定使用的 Agent */
agent?: string;
/** 指定使用的模型 (sonnet/opus/haiku) */
model?: string;
/** 是否作为子任务执行 */
subtask?: boolean;
/** 来源 */
source: 'builtin' | 'user' | 'project';
/** 来源路径 */
sourcePath?: string;
}
/**
* Command 执行输入
*/
export interface CommandInput {
/** Command 名称 */
command: string;
/** 原始参数字符串 */
arguments: string;
/** 解析后的参数数组 */
args: string[];
/** 当前工作目录 */
workdir: string;
}
/**
* Command 执行结果
*/
export interface CommandExecutionResult {
/** 是否成功 */
success: boolean;
/** 渲染后的提示 */
prompt?: string;
/** 指定的 Agent */
agent?: string;
/** 指定的模型 */
model?: string;
/** 是否作为子任务 */
subtask?: boolean;
/** 错误信息 */
error?: string;
}
/**
* Command 搜索结果
*/
export interface CommandSearchResult {
/** Command */
command: Command;
/** 匹配分数 */
score: number;
}
/**
* Command FrontmatterMarkdown 头部配置)
*/
export interface CommandFrontmatter {
description?: string;
agent?: string;
model?: string;
subtask?: boolean;
}
+12 -1
View File
@@ -4,11 +4,13 @@ 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 } from './tools/index.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 {
printServerList,
installServer,
@@ -127,6 +129,15 @@ program.action(async () => {
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) {
+147
View File
@@ -2,6 +2,11 @@ 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';
export class TerminalUI {
private agent: Agent;
@@ -30,6 +35,7 @@ export class TerminalUI {
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(' - 压缩对话历史'));
@@ -58,6 +64,79 @@ export class TerminalUI {
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();
@@ -126,6 +205,12 @@ export class TerminalUI {
return this.handleAgentCommand(args);
}
// 检查 /commands 命令
if (command === '/commands') {
this.listCommands();
return true;
}
switch (command) {
case '/help':
console.log(chalk.cyan('\n📖 帮助信息:'));
@@ -226,9 +311,71 @@ export class TerminalUI {
// 处理命令
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;
}
// 发送给 AI