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:
@@ -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,
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 Frontmatter(Markdown 头部配置)
|
||||
*/
|
||||
export interface CommandFrontmatter {
|
||||
description?: string;
|
||||
agent?: string;
|
||||
model?: string;
|
||||
subtask?: boolean;
|
||||
}
|
||||
+12
-1
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user