first submle

This commit is contained in:
gcdsfh145
2026-05-03 07:34:04 +08:00
parent a29fd59c30
commit c0620c2064
15 changed files with 3798 additions and 2 deletions
+31
View File
@@ -0,0 +1,31 @@
# ExCLI Agent Configuration
# OpenAI API 配置
OPENAI_API_KEY=your-api-key-here
OPENAI_BASE_URL=https://api.openai.com/v1 # OpenAI 或兼容 API 的地址
OPENAI_MODEL=gpt-4o-mini
# Agent 行为配置
AGENT_NAME=excli
AGENT_TEMPERATURE=0.7
AGENT_MAX_TOKENS=4096
AGENT_TRACE_MODE=false
# Memory 配置
MEMORY_DB_PATH=./data/memory.db
MAX_SHORT_TERM_MEMORY=20 # 短期记忆最大条目数
MEMORY_EXTRACTION_INTERVAL=5 # 每隔多少轮提取一次长期记忆
# 工具配置
TOOL_TIMEOUT_MS=30000
TOOL_MAX_RETRIES=2
# 日志配置
LOG_LEVEL=info
LOG_DIR=./logs
LOG_MAX_FILES=10
# CLI 配置
CLI_PROMPT_PREFIX=excli>
CLI_WELCOME_MESSAGE=欢迎使用 ExCLI!你可以用自然语言与 AI 交互,输入 help 查看命令帮助。
CLI_COLOR_OUTPUT=true
+180 -2
View File
@@ -1,3 +1,181 @@
# excli # ExCLI - Agent CLI 框架
一个简单可以使用的agent 一个具有更好记忆能力、输出能力和工具使用能力的 Agent CLI 框架。
## 项目目标
这个 Agent CLI 解决什么问题:
- 更智能的上下文理解:通过短期和长期记忆记住用户偏好和项目背景
- 结构化输出:根据任务类型自动选择最佳输出风格
- 高效工具集成:通过统一的工具接口完成文件、搜索、执行等任务
适合场景:
- 个人 AI 助手
- 代码开发和调试
- 文档查询和管理
- 自动化任务执行
核心能力:
- 记忆系统:自动提取和检索长期记忆
- 输出风格:自动判断简洁/详细/技术型/写作型
- 工具调用:智能选择和执行工具
## 技术选型
- 语言:Node.js 18+
- CLI:原生 readline + Chalk + Ora
- 数据存储:JSON 文件存储
- 模型调用:OpenAI API (兼容任何 OpenAI 兼容 API)
- 日志:Pino
- 配置:环境变量 + .env
## 安装
```bash
# 克隆项目
cd excli
# 安装依赖
npm install
# 配置环境变量
cp .env.example .env
# 编辑 .env 配置 OPENAI_API_KEY
```
## 使用
```bash
# 直接运行
node bin/cli.js
# 或使用启动脚本
chmod +x start.sh
./start.sh
```
## CLI 命令
- `help` - 显示帮助
- `clear` - 清屏
- `memory` - 查看记忆状态
- `preferences` - 查看用户偏好
- `tools` - 查看可用工具
- `stats` - 查看统计信息
- `quit/exit` - 退出
## 项目结构
```
excli/
├── bin/
│ └── cli.js # CLI 入口
├── src/
│ ├── index.js # 主程序入口
│ ├── config/ # 配置模块
│ ├── core/ # 核心模块
│ │ ├── agent.js # Agent 主循环
│ │ └── model.js # 模型适配层
│ ├── memory/ # 记忆模块
│ ├── prompts/ # Prompt 模板
│ ├── tools/ # 工具模块
│ └── utils/ # 工具函数
├── data/ # 数据目录
│ ├── memory/ # 长期记忆
│ └── sessions/ # 会话数据
├── logs/ # 日志目录
├── bin/ # 可执行脚本
├── package.json
└── .env.example
```
## 功能说明
### 记忆系统
- **短期记忆**:保存当前会话上下文,包含用户输入、助手输出、工具结果
- **长期记忆**:JSON 文件持久化存储,区分敏感信息,自动提取
- **记忆提取**:每隔 N 轮对话自动提取重要信息
### 工具系统
- `read_file` - 读取文件
- `write_file` - 写入文件
- `list_files` - 列出文件
- `run_shell` - 执行命令
- `search_text` - 搜索文本
### 输出风格
根据任务类型自动选择:
- 简洁模式:简短回答
- 详细模式:完整解释
- 技术型:代码和命令
- 写作型:文档和说明
## 配置
主要环境变量:
- `OPENAI_API_KEY` - OpenAI API 密钥
- `OPENAI_BASE_URL` - API 地址(默认 OpenAI
- `OPENAI_MODEL` - 模型名称
- `AGENT_TEMPERATURE` - 温度参数
- `MEMORY_DB_PATH` - 记忆数据库路径
- `LOG_LEVEL` - 日志级别
## 扩展
### 添加新工具
`src/tools/index.js` 添加新的工具类:
```javascript
export class MyTool extends BaseTool {
constructor() {
super('my_tool', '工具描述', {
// JSON Schema
});
}
async execute(args) {
// 实现逻辑
return { success: true, result: '...' };
}
}
```
### 修改 System Prompt
`src/prompts/index.js` 修改 `SYSTEM_PROMPT`
## 示例
运行示例:
```
$ ./start.sh
欢迎使用 ExCLI!你可以用自然语言 AI 交互,输入 help 查看命令帮助。
excli> 帮助
# 显示帮助...
excli> 列出当前目录的 js 文件
# AI 处理并返回结果...
excli> memory
# 显示当前记忆状态...
excli> quit
再见!
```
## 注意事项
1. 首次运行需要配置 `OPENAI_API_KEY`
2. 工具执行可能会根据命令不同而有风险,请谨慎使用
3. 敏感信息不会写入长期记忆
4. 模型调用可能产生费用,请注意使用量
## License
MIT
Executable
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env node
// ExCLI 入口脚本
import { run } from '../src/index.js';
// 全局错误处理
process.on('uncaughtException', (error) => {
// 忽略 readline 关闭错误(使用管道输入时会出现)
if (error.message === 'readline was closed' || error.message === 'Cannot read properties of undefined (reading \'isPaused\')') {
return;
}
console.error('未捕获的错误:', error.message);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
// 忽略 readline 关闭错误
if (reason?.message === 'readline was closed') {
return;
}
console.error('未处理的 Promise 拒绝:', reason);
process.exit(1);
});
// 运行程序
run().catch((error) => {
// 忽略 readline 关闭错误
if (error?.message === 'readline was closed') {
return;
}
console.error('启动错误:', error.message);
process.exit(1);
});
+7
View File
@@ -0,0 +1,7 @@
{
"data": [],
"metadata": {
"createdAt": 1777764119795,
"lastSavedAt": 1777764474644
}
}
+1106
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "excli",
"version": "1.0.0",
"description": "一个具有更好记忆能力、输出能力和工具使用能力的 Agent CLI",
"main": "src/index.js",
"type": "module",
"bin": {
"excli": "./bin/cli.js"
},
"scripts": {
"start": "node bin/cli.js",
"dev": "node --watch bin/cli.js",
"test": "node --test src/**/*.test.js"
},
"dependencies": {
"openai": "^4.28.0",
"chalk": "^5.3.0",
"ora": "^7.0.1",
"dotenv": "^16.4.5",
"pino": "^8.18.0",
"pino-pretty": "^10.3.1"
},
"engines": {
"node": ">=18.0.0"
}
}
+133
View File
@@ -0,0 +1,133 @@
import pino from 'pino';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
// 加载 .env 文件
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 默认配置
const DEFAULT_CONFIG = {
// OpenAI API 配置
OPENAI_API_KEY: process.env.OPENAI_API_KEY || '',
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
OPENAI_MODEL: process.env.OPENAI_MODEL || 'gpt-4o-mini',
// Agent 行为配置
AGENT_NAME: process.env.AGENT_NAME || 'excli',
AGENT_TEMPERATURE: parseFloat(process.env.AGENT_TEMPERATURE || '0.7'),
AGENT_MAX_TOKENS: parseInt(process.env.AGENT_MAX_TOKENS || '4096', 10),
AGENT_TRACE_MODE: process.env.AGENT_TRACE_MODE === 'true',
// Memory 配置
MEMORY_DB_PATH: process.env.MEMORY_DB_PATH || './data/memory.db',
MAX_SHORT_TERM_MEMORY: parseInt(process.env.MAX_SHORT_TERM_MEMORY || '20', 10),
MEMORY_EXTRACTION_INTERVAL: parseInt(process.env.MEMORY_EXTRACTION_INTERVAL || '5', 10),
// 工具配置
TOOL_TIMEOUT_MS: parseInt(process.env.TOOL_TIMEOUT_MS || '30000', 10),
TOOL_MAX_RETRIES: parseInt(process.env.TOOL_MAX_RETRIES || '2', 10),
// 日志配置
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
LOG_DIR: process.env.LOG_DIR || './logs',
LOG_MAX_FILES: parseInt(process.env.LOG_MAX_FILES || '10', 10),
// CLI 配置
CLI_PROMPT_PREFIX: process.env.CLI_PROMPT_PREFIX || 'excli>',
CLI_WELCOME_MESSAGE: process.env.CLI_WELCOME_MESSAGE || '欢迎使用 ExCLI!你可以用自然语言与 AI 交互,输入 help 查看命令帮助。',
CLI_COLOR_OUTPUT: process.env.CLI_COLOR_OUTPUT !== 'false',
};
// 配置类
class Config {
constructor() {
this.config = { ...DEFAULT_CONFIG };
this.logger = null;
}
// 初始化配置
init() {
this.ensureDirectories();
this.initLogger();
return this;
}
// 确保必要目录存在
ensureDirectories() {
const dirs = [
path.dirname(this.config.MEMORY_DB_PATH),
this.config.LOG_DIR,
];
for (const dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
}
// 初始化日志器
initLogger() {
const logLevel = this.config.LOG_LEVEL;
const logDir = this.config.LOG_DIR;
// 创建日志目录
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
// 使用 pino 创建日志器
this.logger = pino({
level: logLevel,
transport: {
targets: [
{
target: 'pino/file',
options: { destination: path.join(logDir, 'excli.log') },
},
{
target: 'pino-pretty',
options: {
colorize: this.config.CLI_COLOR_OUTPUT,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
],
},
});
this.logger.info('ExCLI 配置初始化完成');
return this.logger;
}
// 获取配置值
get(key) {
return this.config[key];
}
// 设置配置值
set(key, value) {
this.config[key] = value;
return this;
}
// 获取日志器
getLogger() {
return this.logger;
}
// 获取完整配置
getAll() {
return { ...this.config };
}
}
// 导出单例
export const config = new Config();
export default config;
+277
View File
@@ -0,0 +1,277 @@
// Agent 核心循环模块
import { config } from '../config/index.js';
import { MemoryManager } from '../memory/index.js';
import { ToolRegistry } from '../tools/index.js';
import { PromptBuilder } from '../prompts/index.js';
import { ModelClient } from './model.js';
export class AgentLoop {
constructor(options = {}) {
this.memory = options.memory || new MemoryManager();
this.tools = options.tools || new ToolRegistry();
this.model = options.model || new ModelClient();
this.promptBuilder = new PromptBuilder();
this.logger = options.logger || null;
this.running = false;
this.sessionId = null;
}
// 初始化
async init() {
// 初始化日志器
this.logger = config.getLogger();
this.model.setLogger(this.logger);
this.tools.setLogger(this.logger);
// 初始化组件
this.memory.init();
this.model.init();
// 创建新会话
this.sessionId = this.generateSessionId();
this.log('AgentLoop 初始化完成', { sessionId: this.sessionId });
return this;
}
// 主循环 - 处理用户输入
async processInput(userInput) {
// 添加到短期记忆
this.memory.addUserInput(userInput);
// 搜索相关记忆
const relatedMemories = await this.searchMemory(userInput);
// 构造上下文
const context = await this.buildContext(userInput, relatedMemories);
// 判断是否需要工具
const shouldUseTool = await this.shouldUseTool(context);
let toolResult = null;
let toolUsed = false;
if (shouldUseTool) {
// 使用工具
const toolResponse = await this.decideTool(context);
if (toolResponse.use_tool && toolResponse.tool_name) {
this.log('使用工具', toolResponse);
toolResult = await this.executeTool(
toolResponse.tool_name,
toolResponse.tool_args
);
toolUsed = true;
// 添加工具结果到记忆
if (toolResult) {
this.memory.addToolResult(
JSON.stringify(toolResult),
toolResponse.tool_name
);
}
}
}
// 生成最终回答
const memories = relatedMemories.memories || [];
const finalContext = {
...context,
toolResults: toolResult ? JSON.stringify(toolResult) : null,
relatedMemories: memories.map(m => m.content).join('\n'),
};
const finalAnswer = await this.generateAnswer(finalContext);
// 添加助手输出到记忆
this.memory.addAssistantOutput(finalAnswer);
// 提取长期记忆
await this.extractLongTermMemory();
// 增加会话计数
this.memory.incrementConversationCount();
return {
answer: finalAnswer,
toolUsed,
toolResult,
relatedMemories: relatedMemories.length,
};
}
// 搜索相关记忆
async searchMemory(query) {
// 搜索长期记忆
const memories = this.memory.searchLongTerm(query, {
limit: 5,
minImportance: 3,
});
// 同时搜索用户偏好
const preferences = this.memory.getAllPreferences();
return {
memories,
preferences,
};
}
// 构建上下文
async buildContext(userInput, relatedMemories) {
const conversationHistory = this.memory.getRecentContext(10);
return {
sessionId: this.sessionId,
userInput,
conversationHistory: conversationHistory.map(h => ({
type: h.type,
content: h.content,
})),
relatedMemories: relatedMemories.memories,
preferences: relatedMemories.preferences,
};
}
// 判断是否需要使用工具
async shouldUseTool(context) {
const prompt = this.promptBuilder.buildToolUsePrompt(
context.userInput,
[] // 不传递工具 schema 让模型判断
);
const messages = [
{ role: 'system', content: prompt },
...this.promptBuilder.buildContext({
...context,
conversationHistory: context.conversationHistory.slice(-5),
}),
];
const response = await this.model.completeJSON(prompt);
// 默认不需要工具,除非明确返回需要
if (!response.json || response.json.use_tool !== true) {
return false;
}
return true;
}
// 决定使用哪个工具
async decideTool(context) {
const toolsSchema = this.tools.getAllSchemas();
const prompt = this.promptBuilder.buildToolUsePrompt(
context.userInput,
toolsSchema
);
const response = await this.model.completeJSON(prompt);
return response.json || { use_tool: false };
}
// 执行工具
async executeTool(toolName, args) {
// 处理嵌套的 JSON 字符串
let parsedArgs = args;
if (typeof args === 'string') {
try {
parsedArgs = JSON.parse(args);
} catch (e) {
// 保持原样
}
}
const result = await this.tools.execute(toolName, parsedArgs, {
sessionId: this.sessionId,
});
return result;
}
// 生成最终回答
async generateAnswer(context) {
const prompt = this.promptBuilder.buildFinalAnswerPrompt(context);
const messages = this.promptBuilder.buildContext({
userInput: context.userInput,
conversationHistory: context.conversationHistory,
relatedMemories: context.relatedMemories,
toolResults: context.toolResults,
});
const response = await this.model.chat(messages, { stream: true });
if (response.success) {
return response.content;
}
return `抱歉,处理您的请求时出现错误:${response.error}`;
}
// 提取长期记忆
async extractLongTermMemory() {
const extractionInterval = config.get('MEMORY_EXTRACTION_INTERVAL');
const conversationCount = this.memory.conversationCount;
// 每隔 N 轮提取一次
if (conversationCount % extractionInterval !== 0) {
return;
}
const conversationHistory = this.memory.getConversationHistory();
// 跳过太少的对话
if (conversationHistory.length < 3) {
return;
}
const historyText = conversationHistory
.slice(-extractionInterval)
.map(h => h.content)
.join('\n\n');
const prompt = this.promptBuilder.buildMemoryExtractionPrompt(historyText);
const response = await this.model.completeJSON(prompt);
if (response.json && response.json.should_extract) {
this.memory.extractToLongTerm(response.json);
this.log('提取长期记忆', response.json);
}
}
// 生成会话 ID
generateSessionId() {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// 日志
log(message, data = null) {
if (this.logger) {
this.logger.info(message, data);
}
}
// 获取统计信息
getStats() {
return {
sessionId: this.sessionId,
conversationCount: this.memory.conversationCount,
memory: this.memory.getStats(),
tools: this.tools.getStats(),
};
}
// 关闭
async close() {
this.memory.close();
this.log('AgentLoop 关闭');
}
}
export default AgentLoop;
+180
View File
@@ -0,0 +1,180 @@
// Model 适配层 - 统一管理模型调用
import OpenAI from 'openai';
import { config } from '../config/index.js';
// OpenAI 兼容 API 客户端
export class ModelClient {
constructor(options = {}) {
this.client = null;
this.model = options.model || config.get('OPENAI_MODEL');
this.temperature = options.temperature || config.get('AGENT_TEMPERATURE');
this.maxTokens = options.maxTokens || config.get('AGENT_MAX_TOKENS');
this.logger = null;
this.streamingCallback = null;
}
// 设置日志器
setLogger(logger) {
this.logger = logger;
return this;
}
// 设置流式输出回调
setStreamingCallback(callback) {
this.streamingCallback = callback;
return this;
}
// 初始化客户端
init() {
const apiKey = config.get('OPENAI_API_KEY');
const baseURL = config.get('OPENAI_BASE_URL');
if (!apiKey) {
throw new Error('请在 .env 中配置 OPENAI_API_KEY');
}
this.client = new OpenAI({
apiKey,
baseURL,
});
this.log('ModelClient 初始化完成', {
model: this.model,
baseURL,
});
return this;
}
// 发送聊天请求
async chat(messages, options = {}) {
if (!this.client) {
this.init();
}
const request = {
model: options.model || this.model,
messages,
temperature: options.temperature || this.temperature,
max_tokens: options.maxTokens || this.maxTokens,
tools: options.tools,
};
// 如果启用流式输出
if (options.stream && this.streamingCallback) {
return this.chatStreaming(request);
}
this.log('发送请求到模型', { model: request.model });
try {
const response = await this.client.chat.completions.create(request);
this.log('模型响应', {
choices: response.choices?.length,
usage: response.usage,
});
return {
success: true,
content: response.choices?.[0]?.message?.content || '',
tool_calls: response.choices?.[0]?.message?.tool_calls,
usage: response.usage,
finish_reason: response.choices?.[0]?.finish_reason,
raw: response,
};
} catch (error) {
this.log('模型调用错误', { error: error.message });
return {
success: false,
error: error.message,
};
}
}
// 流式聊天
async chatStreaming(request) {
this.log('发送流式请求到模型', { model: request.model });
try {
const stream = await this.client.chat.completions.create({
...request,
stream: true,
});
let fullContent = '';
let firstChunk = true;
for await (const chunk of stream) {
const delta = chunk.choices?.[0]?.delta?.content || '';
if (delta) {
fullContent += delta;
// 调用流式回调
if (this.streamingCallback) {
this.streamingCallback(delta, firstChunk);
firstChunk = false;
}
}
}
this.log('流式响应完成', { length: fullContent.length });
return {
success: true,
content: fullContent,
finish_reason: 'stop',
};
} catch (error) {
this.log('流式调用错误', { error: error.message });
return {
success: false,
error: error.message,
};
}
}
// 发送带工具的聊天请求
async chatWithTools(messages, tools) {
return this.chat(messages, { tools });
}
// 简单文本补全
async complete(prompt, options = {}) {
const messages = [{ role: 'user', content: prompt }];
return this.chat(messages, options);
}
// JSON 补全(强制输出 JSON
async completeJSON(prompt, options = {}) {
const messages = [
{ role: 'system', content: '你是一个 JSON 生成器。请只输出有效的 JSON,不要有其他任何文字。' },
{ role: 'user', content: prompt },
];
const result = await this.chat(messages, options);
if (result.success) {
try {
result.json = JSON.parse(result.content);
} catch (e) {
result.json = null;
result.parseError = e.message;
}
}
return result;
}
// 日志
log(message, data = null) {
if (this.logger) {
this.logger.info(message, data);
}
}
}
export default ModelClient;
+258
View File
@@ -0,0 +1,258 @@
// ExCLI 主入口
import { config } from './config/index.js';
import { AgentLoop } from './core/agent.js';
import {
printWelcome,
printHelp,
printJSON,
createSpinner,
createInterface,
isExitCommand,
isHelpCommand,
isClearCommand,
clearScreen,
exit,
sleep,
} from './utils/index.js';
import chalk from 'chalk';
// 主程序
class CLI {
constructor() {
this.agent = null;
this.running = false;
this.rl = null;
}
// 初始化 - 先显示欢迎信息,再初始化其他组件
async init() {
// 先显示欢迎信息(此时日志还未初始化)
console.log(chalk.cyan('欢迎使用 ExCLI!你可以用自然语言与 AI 交互,输入 help 查看命令帮助。\n'));
// 加载配置并初始化日志
config.init();
// 创建 Agent
this.agent = new AgentLoop();
await this.agent.init();
// 创建输入接口
this.rl = createInterface();
return this;
}
// 运行主循环
async run() {
this.running = true;
while (this.running) {
const input = await this.promptUser();
if (!input || input.trim() === '') {
continue;
}
// 检查内置命令
if (await this.handleCommand(input)) {
continue;
}
// 处理用户输入
await this.processInput(input);
}
}
// 提示用户输入
promptUser() {
return new Promise((resolve) => {
this.rl.question(
chalk.cyan(config.get('CLI_PROMPT_PREFIX') + ' '),
(answer) => {
resolve(answer);
}
);
});
}
// 处理内置命令
async handleCommand(input) {
const cmd = input.trim().toLowerCase();
const parts = input.split(' ');
const arg = parts.slice(1).join(' ');
// 退出
if (isExitCommand(input)) {
console.log(chalk.cyan('再见!'));
await this.close();
exit(0);
return true;
}
// 帮助
if (isHelpCommand(input)) {
printHelp();
return true;
}
// 清屏
if (isClearCommand(input)) {
clearScreen();
return true;
}
// 记忆状态
if (cmd === 'memory' || cmd === 'mem') {
const stats = this.agent.memory.getStats();
console.log(chalk.gray('短期记忆:'));
console.log(printJSON(stats.shortTerm));
console.log(chalk.gray('\n长期记忆:'));
console.log(printJSON(stats.longTerm));
return true;
}
// 偏好
if (cmd === 'preferences' || cmd === 'pref') {
const prefs = this.agent.memory.getAllPreferences();
console.log(chalk.gray('用户偏好:'));
if (prefs.length === 0) {
console.log(chalk.gray('暂无偏好'));
} else {
for (const p of prefs) {
console.log(` ${chalk.cyan(p.key)}: ${JSON.stringify(p.value)}`);
}
}
return true;
}
// 工具列表
if (cmd === 'tools') {
const schemas = this.agent.tools.getAllSchemas();
console.log(chalk.gray('可用工具:'));
for (const tool of schemas) {
console.log(` ${chalk.cyan(tool.name)} - ${tool.description}`);
}
return true;
}
// 统计
if (cmd === 'stats') {
console.log(printJSON(this.agent.getStats()));
return true;
}
return false;
}
// 处理用户输入
async processInput(input) {
const spinner = createSpinner('处理中...');
spinner.start();
// 用于流式输出的答案片段
let answerFragments = [];
// 设置流式输出回调
this.agent.model.setStreamingCallback((chunk, isFirst) => {
if (isFirst) {
spinner.stop();
process.stdout.write(chalk.cyan('🤖 '));
}
process.stdout.write(chunk);
answerFragments.push(chunk);
});
try {
const result = await this.agent.processInput(input);
spinner.stop();
// 如果不是流式输出,手动打印
if (answerFragments.length === 0) {
console.log();
this.printAnswer(result.answer, result.toolUsed);
} else {
console.log(); // 流式输出后换行
}
// 如果使用了工具,显示工具结果摘要
if (result.toolUsed && result.toolResult) {
this.printToolResult(result.toolResult);
}
// 显示相关记忆数量
if (result.relatedMemories > 0) {
console.log(chalk.gray(`[相关记忆: ${result.relatedMemories} 条]`));
}
console.log();
} catch (error) {
spinner.stop();
console.error(chalk.red('处理错误: ' + error.message));
}
}
// 打印回答
printAnswer(answer, toolUsed = false) {
if (toolUsed) {
console.log(chalk.cyan('🤖 ') + answer);
} else {
console.log(answer);
}
}
// 打印工具结果
printToolResult(result) {
if (result.success) {
const summary = this.summarizeToolResult(result);
if (summary) {
console.log(chalk.gray(`[工具结果: ${summary}]`));
}
} else {
console.log(chalk.red(`[工具错误: ${result.error}]`));
}
}
// 总结工具结果
summarizeToolResult(result) {
if (result.files) {
return `${result.count} 个文件`;
}
if (result.stdout !== undefined) {
const lines = result.stdout.split('\n').length;
return `${lines} 行输出`;
}
if (result.results) {
return `${result.count} 个匹配`;
}
if (result.path) {
return result.path;
}
return null;
}
// 关闭
async close() {
this.running = false;
if (this.rl) {
this.rl.close();
}
if (this.agent) {
await this.agent.close();
}
}
}
// 导出运行函数
export async function run() {
const cli = new CLI();
await cli.init();
await cli.run();
}
export default { run };
+521
View File
@@ -0,0 +1,521 @@
// Memory 模块核心实现 - 使用 JSON 文件存储
import fs from 'fs';
import path from 'path';
import { config } from '../config/index.js';
// 基础存储类
class BaseStorage {
constructor(filePath) {
this.filePath = filePath;
this.data = [];
this.metadata = {
createdAt: Date.now(),
lastSavedAt: Date.now(),
};
}
// 加载数据
load() {
try {
if (fs.existsSync(this.filePath)) {
const content = fs.readFileSync(this.filePath, 'utf-8');
const parsed = JSON.parse(content);
this.data = parsed.data || [];
this.metadata = parsed.metadata || this.metadata;
}
} catch (e) {
this.data = [];
}
return this;
}
// 保存数据
save() {
const dir = path.dirname(this.filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(
this.filePath,
JSON.stringify({
data: this.data,
metadata: {
...this.metadata,
lastSavedAt: Date.now(),
},
}, null, 2),
'utf-8'
);
return this;
}
// 清空数据
clear() {
this.data = [];
return this;
}
}
// 短期记忆管理器
export class ShortTermMemory {
constructor(options = {}) {
this.maxItems = options.maxItems || config.get('MAX_SHORT_TERM_MEMORY');
this.items = [];
this.metadata = {
createdAt: Date.now(),
lastAccessedAt: Date.now(),
};
}
// 添加记忆条目
add(item) {
const entry = {
id: this.generateId(),
type: item.type || 'general',
content: item.content,
importance: item.importance || 0,
tokens: item.tokens || 0,
timestamp: Date.now(),
metadata: item.metadata || {},
};
if (entry.importance >= 7) {
this.items.unshift(entry);
} else {
this.items.push(entry);
}
this.prune();
return entry;
}
// 修剪多余的记忆条目
prune() {
if (this.items.length > this.maxItems) {
this.items.sort((a, b) => {
if (a.importance !== b.importance) {
return b.importance - a.importance;
}
return b.timestamp - a.timestamp;
});
this.items = this.items.slice(0, this.maxItems);
}
}
// 获取最近的 N 条记忆
getRecent(n = 10) {
this.metadata.lastAccessedAt = Date.now();
return this.items.slice(0, n);
}
// 根据类型获取记忆
getByType(type) {
return this.items.filter(item => item.type === type);
}
// 获取所有用户输入
getUserInputs() {
return this.getByType('user_input');
}
// 获取所有 Assistant 输出
getAssistantOutputs() {
return this.getByType('assistant_output');
}
// 获取工具执行结果
getToolResults() {
return this.getByType('tool_result');
}
// 搜索记忆
search(query) {
const lowerQuery = query.toLowerCase();
return this.items.filter(item =>
item.content.toLowerCase().includes(lowerQuery)
);
}
// 生成唯一 ID
generateId() {
return `stm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// 清空所有记忆
clear() {
this.items = [];
return this;
}
// 获取统计信息
getStats() {
return {
totalItems: this.items.length,
maxItems: this.maxItems,
byType: this.items.reduce((acc, item) => {
acc[item.type] = (acc[item.type] || 0) + 1;
return acc;
}, {}),
lastAccessedAt: this.metadata.lastAccessedAt,
};
}
// 导出为 JSON
toJSON() {
return {
items: this.items,
metadata: this.metadata,
};
}
// 从 JSON 导入
static fromJSON(json) {
const memory = new ShortTermMemory();
memory.items = json.items || [];
memory.metadata = json.metadata || { createdAt: Date.now(), lastAccessedAt: Date.now() };
return memory;
}
}
// 长期记忆管理器 - 使用 JSON 文件
export class LongTermMemory {
constructor(options = {}) {
this.dataDir = options.dataDir || './data/memory';
this.memoriesFile = path.join(this.dataDir, 'memories.json');
this.preferencesFile = path.join(this.dataDir, 'preferences.json');
this.storage = new BaseStorage(this.memoriesFile);
this.preferencesStorage = new BaseStorage(this.preferencesFile);
}
// 初始化
init() {
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
}
this.storage.load();
this.preferencesStorage.load();
return this;
}
// 添加记忆
add(entry) {
const id = entry.id || this.generateId();
const now = Date.now();
const memory = {
id,
type: entry.type || 'general',
content: entry.content,
importance: entry.importance || 5,
category: entry.category || null,
tags: entry.tags || [],
source: entry.source || 'extraction',
createdAt: now,
updatedAt: now,
accessedAt: now,
accessCount: 0,
isSensitive: entry.isSensitive || false,
isActive: true,
};
this.storage.data.push(memory);
this.storage.save();
return id;
}
// 检索记忆
search(query, options = {}) {
const { category, type, limit = 10, minImportance = 0 } = options;
let results = this.storage.data.filter(m => {
if (!m.isActive) return false;
if (m.importance < minImportance) return false;
if (!m.content.toLowerCase().includes(query.toLowerCase())) return false;
if (category && m.category !== category) return false;
if (type && m.type !== type) return false;
return true;
});
// 更新访问信息
for (const m of results) {
m.accessedAt = Date.now();
m.accessCount = (m.accessCount || 0) + 1;
}
this.storage.save();
return results.slice(0, limit);
}
// 获取某类别的所有记忆
getByCategory(category, limit = 20) {
return this.storage.data
.filter(m => m.isActive && m.category === category)
.sort((a, b) => b.importance - a.importance)
.slice(0, limit);
}
// 获取用户偏好
getPreference(key) {
const pref = this.preferencesStorage.data.find(p => p.key === key);
return pref ? pref.value : null;
}
// 设置用户偏好
setPreference(key, value, category = 'general') {
const id = this.generateId();
const now = Date.now();
const existing = this.preferencesStorage.data.findIndex(p => p.key === key);
const pref = {
id,
key,
value,
category,
createdAt: existing >= 0 ? this.preferencesStorage.data[existing].createdAt : now,
updatedAt: now,
};
if (existing >= 0) {
this.preferencesStorage.data[existing] = pref;
} else {
this.preferencesStorage.data.push(pref);
}
this.preferencesStorage.save();
return id;
}
// 获取所有偏好
getAllPreferences(category = null) {
if (category) {
return this.preferencesStorage.data
.filter(p => p.category === category)
.map(p => ({ key: p.key, value: p.value, category: p.category }));
}
return this.preferencesStorage.data.map(p => ({
key: p.key,
value: p.value,
category: p.category,
}));
}
// 更新记忆
update(id, updates) {
const index = this.storage.data.findIndex(m => m.id === id);
if (index < 0) return false;
if (updates.content !== undefined) {
this.storage.data[index].content = updates.content;
}
if (updates.importance !== undefined) {
this.storage.data[index].importance = updates.importance;
}
if (updates.category !== undefined) {
this.storage.data[index].category = updates.category;
}
if (updates.tags !== undefined) {
this.storage.data[index].tags = updates.tags;
}
this.storage.data[index].updatedAt = Date.now();
this.storage.save();
return true;
}
// 删除记忆(软删除)
delete(id) {
const index = this.storage.data.findIndex(m => m.id === id);
if (index < 0) return false;
this.storage.data[index].isActive = false;
this.storage.data[index].updatedAt = Date.now();
this.storage.save();
return true;
}
// 生成唯一 ID
generateId() {
return `ltm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// 获取统计信息
getStats() {
const activeMemories = this.storage.data.filter(m => m.isActive);
const sensitiveMemories = activeMemories.filter(m => m.isSensitive);
const avgImportance = activeMemories.length > 0
? activeMemories.reduce((sum, m) => sum + m.importance, 0) / activeMemories.length
: 0;
return {
totalMemories: this.storage.data.length,
activeMemories: activeMemories.length,
sensitiveMemories: sensitiveMemories.length,
avgImportance: Math.round(avgImportance * 10) / 10,
totalPreferences: this.preferencesStorage.data.length,
};
}
// 关闭
close() {
this.storage.save();
this.preferencesStorage.save();
}
}
// 统一的 Memory 管理器
export class MemoryManager {
constructor(options = {}) {
this.shortTerm = new ShortTermMemory(options);
this.longTerm = new LongTermMemory(options);
this.conversationCount = 0;
}
// 初始化
init() {
this.longTerm.init();
return this;
}
// 添加用户输入到短期记忆
addUserInput(content) {
return this.shortTerm.add({
type: 'user_input',
content,
importance: 5,
});
}
// 添加助手输出到短期记忆
addAssistantOutput(content) {
return this.shortTerm.add({
type: 'assistant_output',
content,
importance: 5,
});
}
// 添加工具结果到短期记忆
addToolResult(content, toolName) {
return this.shortTerm.add({
type: 'tool_result',
content,
toolName,
importance: 3,
});
}
// 检索长期记忆
searchLongTerm(query, options = {}) {
return this.longTerm.search(query, options);
}
// 获取用户偏好
getPreference(key) {
return this.longTerm.getPreference(key);
}
// 设置用户偏好
setPreference(key, value, category) {
return this.longTerm.setPreference(key, value, category);
}
// 获取所有用户偏好
getAllPreferences(category) {
return this.longTerm.getAllPreferences(category);
}
// 获取最近的会话上下文
getRecentContext(limit = 10) {
return this.shortTerm.getRecent(limit);
}
// 获取当前会话的所有用户输入
getConversationHistory() {
return this.shortTerm.getUserInputs();
}
// 提取需要存储到长期记忆的信息
extractToLongTerm(extractionResult) {
if (!extractionResult || !extractionResult.memories) return [];
const ids = [];
for (const item of extractionResult.memories) {
if (this.isSensitiveContent(item.content)) {
continue;
}
const id = this.longTerm.add({
type: item.type || 'extracted',
content: item.content,
importance: item.importance || 5,
category: item.category,
tags: item.tags,
source: 'extraction',
isSensitive: false,
});
ids.push(id);
}
return ids;
}
// 检查是否包含敏感信息
isSensitiveContent(content) {
const sensitivePatterns = [
/password/i,
/api[_-]?key/i,
/secret/i,
/token/i,
/credential/i,
/ssh[_-]?key/i,
/private[_-]?key/i,
];
for (const pattern of sensitivePatterns) {
if (pattern.test(content)) {
return true;
}
}
return false;
}
// 增加会话计数
incrementConversationCount() {
this.conversationCount++;
return this.conversationCount;
}
// 获取统计信息
getStats() {
return {
shortTerm: this.shortTerm.getStats(),
longTerm: this.longTerm.getStats(),
conversationCount: this.conversationCount,
};
}
// 关闭
close() {
this.longTerm.close();
}
}
export default MemoryManager;
+263
View File
@@ -0,0 +1,263 @@
// Prompts 模块 - 统一管理所有 prompt 模板
import { config } from '../config/index.js';
// System Prompt - Agent 的核心身份和行为定义
export const SYSTEM_PROMPT = `你是一个强大的 AI Agent CLI,名为 ExCLI。
你的核心能力包括:
1. 更好的记忆 - 能记住用户的偏好、常见任务、历史上下文
2. 更好的输出 - 根据任务类型选择最佳输出风格
3. 更好的工具使用 - 能准确调用合适的工具完成任务
## 基本行为准则
- 用中文与用户交流
- 回答要简洁、直接、少废话
- 如果是复杂任务,先给结论,再给步骤
- 如果是代码任务,优先给可运行结果
- 如果信息不足,先提最关键的澄清问题
- 不要重复用户说的话
## 输出风格选择
- 简洁模式:简短回答,不超过 3 句话
- 详细模式:完整解释,包含背景和步骤
- 技术型:代码、配置、命令等技术支持
- 写作型:文档、说明等文字内容
根据任务自动判断使用哪种风格。
## 记忆行为准则
- 自动识别并记住用户的长期偏好
- 不把一次性信息存入长期记忆
- 能根据当前任务检索最相关的记忆
- 用户新偏好出现时可以覆盖旧偏好
## 工具使用准则
- 每次调用工具前说明目的
- 工具执行后总结结果
- 工具失败时给出替代方案或重试
- 尽量减少无意义工具调用`;
// Planner Prompt - 任务规划
export const PLANNER_PROMPT = `你是一个任务规划专家。当用户提出复杂任务时,你需要:
## 分析任务结构
1. 理解任务目标
2. 识别需要的子任务
3. 确定任务依赖关系
4. 评估任务的可行性和风险
## 输出格式
请以如下 JSON 格式输出任务规划:
{
"goal": "任务的最终目标",
"need_plan": true/false, // 是否需要多步骤规划
"steps": [
{
"id": 1,
"description": "步骤描述",
"tool_needed": true/false, // 是否需要工具
"tool_name": "工具名称(如果有)",
"tool_args": { /* 工具参数 */ },
"depends_on": [], // 依赖的步骤 ID
}
],
"estimated_steps": 预期步骤数,
"risks": ["风险描述"],
"clarifications": ["需要澄清的问题"]
}
## 判断标准
需要规划的情况:
- 任务包含多个子任务
- 任务需要先查询信息才能执行
- 任务有明确的步骤顺序
- 任务复杂度较高
不需要规划的情况:
- 简单问答
- 直接可以回答的问题
- 不需要执行操作的任务
请根据任务自行判断是否需要规划。`;
// Memory Extraction Prompt - 记忆提取
export const MEMORY_EXTRACTION_PROMPT = `你是一个记忆分析专家。需要从对话历史中提取需要长期记忆的信息。
## 提取原则
需要记住的信息:
- 用户明确表达的偏好(如:喜欢的编程语言、代码风格)
- 用户的项目背景信息
- 用户多次提到的习惯或模式
- 重要的结论或决定
- 常用的工作流程或命令
不需要记住的信息:
- 一次性问答的内容
- 临时查询的信息
- 不重要的闲聊
- 明显的一次性任务
## 输出格式
请以如下 JSON 格式输出记忆提取结果:
{
"should_extract": true/false, // 是否有需要提取的信息
"memories": [
{
"type": "preference | background | workflow | conclusion",
"content": "记忆内容",
"importance": 1-10, // 重要性评分
"category": "分类",
"tags": ["标签"]
}
],
"reason": "取理由"
}
请分析对话历史,给出记忆提取结果。`;
// Tool Use Prompt - 工具使用指导
export const TOOL_USE_PROMPT = `你是一个工具使用专家。你可以根据需要调用各种工具来完成用户的任务。
## 可用工具
{{TOOLS}}
## 工具使用原则
1. 每次调用工具前说明目的
2. 选择最小化的工具集
3. 先理解工具的参数要求
4. 工具执行失败时尝试重试或替代方案
## 工具选择指导
- 读取/搜索代码:使用 search_text + read_file
- 写入配置文件:使用 write_file
- 执行命令:使用 run_shell
- 查看目录:使用 list_files
## 输出格式
请根据需要决定是否使用工具。如果需要使用工具,请按以下格式输出:
{
"use_tool": true/false,
"tool_name": "工具名称",
"tool_args": { /* 工具参数 */ },
"purpose": "使用目的说明"
}
请根据用户的任务决定是否需要使用工具。`;
// Final Answer Prompt - 最终回答生成
export const FINAL_ANSWER_PROMPT = `你是一个专业的 AI 助手。现在你需要根据上下文生成最终回答。
## 当前上下文
对话历史:{{CONVERSATION_HISTORY}}
相关记忆:{{RELATED_MEMORIES}}
工具结果:{{TOOL_RESULTS}}
当前任务:{{USER_INPUT}}
## 回答要求
- 根据任务类型选择合适的输出风格
- 如果是复杂任务,先给结论,再给步骤
- 如果是代码任务,优先给可运行结果
- 结合相关记忆和个人偏好
- 保持简洁、直接、少废话
- 用中文回答
## 输出风格
- 简洁模式:简短回答,不超过 3 句话
- 详细模式:完整解释,包含背景和步骤
- 技术型:代码、配置、命令等技术支持
- 写作型:文档、说明等文字内容
请生成最终回答。`;
// Prompt 构建器
export class PromptBuilder {
constructor() {
this.systemPrompt = SYSTEM_PROMPT;
this.plannerPrompt = PLANNER_PROMPT;
this.memoryExtractionPrompt = MEMORY_EXTRACTION_PROMPT;
this.toolUsePrompt = TOOL_USE_PROMPT;
this.finalAnswerPrompt = FINAL_ANSWER_PROMPT;
}
// 构建 System Prompt
buildSystemPrompt(context = {}) {
let prompt = this.systemPrompt;
// 添加用户偏好
if (context.preferences) {
prompt += `\n\n## 用户偏好\n${context.preferences}`;
}
return prompt;
}
// 构建 Planner Prompt
buildPlannerPrompt(task) {
return `${this.plannerPrompt}\n\n## 当前任务\n${task}`;
}
// 构建 Memory Extraction Prompt
buildMemoryExtractionPrompt(conversationHistory) {
return `${this.memoryExtractionPrompt}\n\n## 对话历史\n${conversationHistory}`;
}
// 构建 Tool Use Prompt
buildToolUsePrompt(task, toolsSchema) {
const toolsJSON = JSON.stringify(toolsSchema, null, 2);
const prompt = this.toolUsePrompt.replace('{{TOOLS}}', toolsJSON);
return `${prompt}\n\n## 当前任务\n${task}`;
}
// 构建 Final Answer Prompt
buildFinalAnswerPrompt(context) {
let prompt = this.finalAnswerPrompt
.replace('{{CONVERSATION_HISTORY}}', context.conversationHistory || '无')
.replace('{{RELATED_MEMORIES}}', context.relatedMemories || '无')
.replace('{{TOOL_RESULTS}}', context.toolResults || '无')
.replace('{{USER_INPUT}}', context.userInput || '无');
return prompt;
}
// 构建完整的对话上下文
buildContext(context) {
const messages = [];
// System prompt
messages.push({
role: 'system',
content: this.buildSystemPrompt(context),
});
// 对话历史
if (context.conversationHistory && context.conversationHistory.length > 0) {
for (const item of context.conversationHistory) {
if (item.type === 'user_input') {
messages.push({
role: 'user',
content: item.content,
});
} else if (item.type === 'assistant_output') {
messages.push({
role: 'assistant',
content: item.content,
});
} else if (item.type === 'tool_result') {
messages.push({
role: 'user',
content: `[工具执行结果]: ${item.content}`,
});
}
}
}
// 最终用户输入
messages.push({
role: 'user',
content: context.userInput,
});
return messages;
}
}
export default PromptBuilder;
+529
View File
@@ -0,0 +1,529 @@
// 工具模块
import { config } from '../config/index.js';
// 基础工具类
export class BaseTool {
constructor(name, description, schema) {
this.name = name;
this.description = description;
this.schema = schema;
this.usageCount = 0;
}
async execute(args, context = {}) {
throw new Error('Tool must implement execute method');
}
getSchema() {
return {
name: this.name,
description: this.description,
parameters: this.schema,
};
}
}
// 读取文件工具
export class ReadFileTool extends BaseTool {
constructor() {
super(
'read_file',
'读取文件内容',
{
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' },
offset: { type: 'number', description: '起始行号 (从 1 开始)', default: 1 },
limit: { type: 'number', description: '读取行数', default: 100 },
encoding: { type: 'string', description: '文件编码', default: 'utf-8' },
},
required: ['path'],
}
);
}
async execute(args) {
const fs = await import('fs/promises');
const path = await import('path');
const {
path: filePath,
offset = 1,
limit = 100,
encoding = 'utf-8'
} = args;
try {
const content = await fs.readFile(filePath, encoding);
const lines = content.split('\n');
// 计算实际读取范围
const start = Math.max(0, offset - 1);
const end = Math.min(lines.length, start + limit);
const selectedLines = lines.slice(start, end);
return {
success: true,
content: selectedLines.join('\n'),
lines: {
from: start + 1,
to: end,
total: lines.length,
},
path: filePath,
};
} catch (error) {
return {
success: false,
error: error.message,
path: filePath,
};
}
}
}
// 写入文件工具
export class WriteFileTool extends BaseTool {
constructor() {
super(
'write_file',
'写入文件内容',
{
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' },
content: { type: 'string', description: '要写入的内容' },
mode: { type: 'string', description: '写入模式: write | append', default: 'write' },
},
required: ['path', 'content'],
}
);
}
async execute(args) {
const fs = await import('fs/promises');
const path = await import('path');
const { path: filePath, content, mode = 'write' } = args;
try {
// 确保目录存在
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
if (mode === 'append') {
await fs.appendFile(filePath, content, 'utf-8');
} else {
await fs.writeFile(filePath, content, 'utf-8');
}
return {
success: true,
path: filePath,
mode,
bytes: content.length,
};
} catch (error) {
return {
success: false,
error: error.message,
path: filePath,
};
}
}
}
// 列出文件工具
export class ListFilesTool extends BaseTool {
constructor() {
super(
'list_files',
'列出目录中的文件',
{
type: 'object',
properties: {
path: { type: 'string', description: '目录路径', default: '.' },
pattern: { type: 'string', description: '文件过滤模式 (glob)', default: '*' },
recursive: { type: 'boolean', description: '是否递归查找', default: false },
},
}
);
}
async execute(args) {
const fs = await import('fs');
const path = await import('path');
const { path: dirPath = '.', pattern = '*', recursive = false } = args;
try {
const files = [];
if (recursive) {
// 递归查找
const walk = async (dir) => {
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
} else if (entry.isFile()) {
files.push(fullPath);
}
}
};
await walk(dirPath);
} else {
// 非递归
const entries = await fs.promises.readdir(dirPath);
for (const entry of entries) {
const fullPath = path.join(dirPath, entry);
const stat = await fs.promises.stat(fullPath);
if (stat.isFile()) {
files.push(fullPath);
}
}
}
// 过滤模式
let filtered = files;
if (pattern && pattern !== '*') {
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
filtered = files.filter(f => regex.test(path.basename(f)));
}
return {
success: true,
files: filtered,
count: filtered.length,
};
} catch (error) {
return {
success: false,
error: error.message,
};
}
}
}
// 执行 Shell 命令工具
export class RunShellTool extends BaseTool {
constructor() {
super(
'run_shell',
'执行 Shell 命令',
{
type: 'object',
properties: {
command: { type: 'string', description: '要执行的命令' },
cwd: { type: 'string', description: '工作目录', default: '.' },
timeout: { type: 'number', description: '超时时间 (毫秒)', default: 30000 },
},
required: ['command'],
}
);
}
async execute(args) {
const { exec } = await import('child_process');
const util = await import('util');
const execPromise = util.promisify(exec);
const { command, cwd = '.', timeout = config.get('TOOL_TIMEOUT_MS') } = args;
try {
const { stdout, stderr } = await execPromise(command, {
cwd,
timeout,
maxBuffer: 10 * 1024 * 1024, // 10MB
});
return {
success: true,
stdout: stdout || '',
stderr: stderr || '',
output: stdout || stderr,
};
} catch (error) {
return {
success: false,
error: error.message,
stdout: error.stdout || '',
stderr: error.stderr || '',
};
}
}
}
// 搜索文本工具
export class SearchTextTool extends BaseTool {
constructor() {
super(
'search_text',
'在文件中搜索文本',
{
type: 'object',
properties: {
pattern: { type: 'string', description: '搜索的正则表达式或文本' },
path: { type: 'string', description: '搜索路径', default: '.' },
include: { type: 'string', description: '文件包含模式', default: '*' },
exclude: { type: 'string', description: '文件排除模式' },
isRegex: { type: 'boolean', description: '是否使用正则表达式', default: false },
caseSensitive: { type: 'boolean', description: '是否区分大小写', default: true },
maxResults: { type: 'number', description: '最大结果数', default: 100 },
},
required: ['pattern'],
}
);
}
async execute(args) {
const fs = await import('fs');
const path = await import('path');
const {
pattern,
path: searchPath = '.',
include = '*',
exclude,
isRegex = false,
caseSensitive = true,
maxResults = 100,
} = args;
try {
const results = [];
let regex;
if (isRegex) {
regex = new RegExp(pattern, caseSensitive ? 'g' : 'gi');
} else {
const flags = caseSensitive ? 'g' : 'gi';
regex = new RegExp(this.escapeRegex(pattern), flags);
}
// 搜索文件
const searchFile = async (filePath) => {
try {
const content = await fs.promises.readFile(filePath, 'utf-8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (regex.test(lines[i])) {
results.push({
file: filePath,
line: i + 1,
content: lines[i].trim(),
});
if (results.length >= maxResults) return true;
}
}
} catch (e) {
// 跳过无法读取的文件
}
return false;
};
// 遍历文件
const searchDir = async (dir) => {
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// 跳过排除的目录
if (exclude && new RegExp(exclude).test(entry.name)) continue;
await searchDir(fullPath);
} else if (entry.isFile()) {
// 检查包含模式
if (include && include !== '*') {
const includeRegex = new RegExp(include.replace(/\*/g, '.*').replace(/\?/g, '.'));
if (!includeRegex.test(entry.name)) continue;
}
if (await searchFile(fullPath)) return;
}
}
};
await searchDir(searchPath);
return {
success: true,
results,
count: results.length,
pattern: pattern,
};
} catch (error) {
return {
success: false,
error: error.message,
};
}
}
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
// 工具注册表
export class ToolRegistry {
constructor() {
this.tools = new Map();
this.logger = null;
this.registerDefaultTools();
}
// 设置日志器
setLogger(logger) {
this.logger = logger;
}
// 注册默认工具
registerDefaultTools() {
this.register(new ReadFileTool());
this.register(new WriteFileTool());
this.register(new ListFilesTool());
this.register(new RunShellTool());
this.register(new SearchTextTool());
}
// 注册工具
register(tool) {
if (!(tool instanceof BaseTool)) {
throw new Error('Tool must extend BaseTool');
}
this.tools.set(tool.name, tool);
this.log(`工具已注册: ${tool.name}`);
return this;
}
// 获取工具
get(name) {
return this.tools.get(name);
}
// 获取所有工具的 schema
getAllSchemas() {
const schemas = [];
for (const tool of this.tools.values()) {
schemas.push(tool.getSchema());
}
return schemas;
}
// 执行工具
async execute(toolName, args, context = {}) {
const tool = this.tools.get(toolName);
if (!tool) {
return {
success: false,
error: `工具不存在: ${toolName}`,
};
}
this.log(`执行工具: ${toolName}`, args);
try {
// 参数验证
const validationError = this.validateArgs(tool.schema, args);
if (validationError) {
return {
success: false,
error: validationError,
};
}
// 执行工具
const retries = config.get('TOOL_MAX_RETRIES');
let lastError = null;
let result = null;
for (let i = 0; i <= retries; i++) {
try {
result = await tool.execute(args, context);
break;
} catch (error) {
lastError = error;
this.log(`工具执行失败 (尝试 ${i + 1}/${retries + 1}): ${error.message}`);
if (i < retries) {
// 等待后重试
await this.sleep(1000 * (i + 1));
}
}
}
if (!result) {
result = {
success: false,
error: lastError?.message || '工具执行失败',
};
}
tool.usageCount++;
this.log(`工具执行完成: ${toolName}`, result);
return result;
} catch (error) {
this.log(`工具执行错误: ${toolName}`, { error: error.message });
return {
success: false,
error: error.message,
};
}
}
// 验证参数
validateArgs(schema, args) {
if (!schema || !schema.properties) return null;
const required = schema.required || [];
// 检查必需参数
for (const param of required) {
if (args[param] === undefined) {
return `缺少必需参数: ${param}`;
}
}
return null;
}
// 日志
log(message, data = null) {
if (this.logger) {
this.logger.info(message, data);
}
}
// 等待
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 获取工具使用统计
getStats() {
const stats = [];
for (const [name, tool] of this.tools.entries()) {
stats.push({
name,
usageCount: tool.usageCount,
});
}
return stats;
}
}
export default ToolRegistry;
+217
View File
@@ -0,0 +1,217 @@
// 工具函数模块
import readline from 'readline';
import chalk from 'chalk';
import ora from 'ora';
// 创建 CLI 输入接口
export function createInterface() {
return readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true,
});
}
// 打印欢迎信息
export function printWelcome(message) {
console.log(chalk.cyan(message));
console.log();
}
// 打印帮助信息
export function printHelp() {
console.log(`
${chalk.cyan('ExCLI 帮助')}
${chalk.gray('命令:')}
help - 显示帮助信息
clear - 清除屏幕
memory - 查看记忆状态
preferences - 查看用户偏好
tools - 查看可用工具
stats - 查看统计信息
quit/exit - 退出程序
${chalk.gray('快捷键:')}
Ctrl+C - 退出程序
Ctrl+L - 清除屏幕
`);
}
// 格式化输出
export function formatOutput(content, style = 'default') {
switch (style) {
case 'code':
return chalk.gray('```\n') + content + '\n```';
case 'error':
return chalk.red(content);
case 'success':
return chalk.green(content);
case 'warning':
return chalk.yellow(content);
case 'info':
return chalk.blue(content);
default:
return content;
}
}
// 创建加载动画
export function createSpinner(text) {
return ora({
text,
spinner: 'dots',
color: 'cyan',
});
}
// 打印分隔线
export function printDivider() {
console.log(chalk.gray('─'.repeat(50)));
}
// 打印列表项
export function printList(items, options = {}) {
const { numbered = false, indent = 2 } = options;
const prefix = ' '.repeat(indent);
items.forEach((item, index) => {
const label = numbered ? `${index + 1}. ` : '• ';
console.log(prefix + label + item);
});
}
// 打印表格
export function printTable(data, options = {}) {
const { headers = [], align = [] } = options;
if (headers.length > 0) {
console.log(headers.join(' | '));
console.log(align.map(a => '-'.repeat(a)).join('-+-'));
}
data.forEach(row => {
console.log(row.join(' | '));
});
}
// 打印 JSON
export function printJSON(data, indent = 2) {
return JSON.stringify(data, null, indent);
}
// 打印富文本(Markdown
export function printMarkdown(content) {
// 简单的 Markdown 渲染
let output = content;
// 代码块
output = output.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
return chalk.gray(code.trim());
});
// 行内代码
output = output.replace(/`([^`]+)`/g, (_, code) => {
return chalk.gray(code);
});
// 粗体
output = output.replace(/\*\*([^*]+)\*\*/g, (_, text) => {
return chalk.bold(text);
});
// 斜体
output = output.replace(/\*([^*]+)\*/g, (_, text) => {
return chalk.italic(text);
});
return output;
}
// 清屏
export function clearScreen() {
console.clear();
}
// 退出
export function exit(code = 0) {
process.exit(code);
}
// 延迟
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 检查退出命令
export function isExitCommand(input) {
const cmd = input.trim().toLowerCase();
return ['quit', 'exit', 'q', '退出'].includes(cmd);
}
// 检查帮助命令
export function isHelpCommand(input) {
const cmd = input.trim().toLowerCase();
return ['help', 'h', '帮助', '?'].includes(cmd);
}
// 检查清屏命令
export function isClearCommand(input) {
const cmd = input.trim().toLowerCase();
return ['clear', 'cls', '清屏'].includes(cmd);
}
// 解析命令行参数
export function parseArgs(args) {
const result = {
flags: {},
options: {},
rest: [],
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const next = args[i + 1];
if (next && !next.startsWith('-')) {
result.options[key] = next;
i++;
} else {
result.flags[key] = true;
}
} else if (arg.startsWith('-')) {
const flags = arg.slice(1).split('');
for (const flag of flags) {
result.flags[flag] = true;
}
} else {
result.rest.push(arg);
}
}
return result;
}
export default {
createInterface,
printWelcome,
printHelp,
formatOutput,
createSpinner,
printDivider,
printList,
printTable,
printJSON,
printMarkdown,
clearScreen,
exit,
sleep,
isExitCommand,
isHelpCommand,
isClearCommand,
parseArgs,
};
Executable
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# ExCLI 启动脚本
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_DIR"
# 检查 Node.js 版本
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo "错误: 需要 Node.js 18 或更高版本"
exit 1
fi
# 检查依赖
if [ ! -d "node_modules" ]; then
echo "正在安装依赖..."
npm install
if [ $? -ne 0 ]; then
echo "安装依赖失败"
exit 1
fi
fi
# 复制环境配置
if [ ! -f ".env" ]; then
if [ -f ".env.example" ]; then
cp .env.example .env
echo "已创建 .env 配置文件,请配置 OPENAI_API_KEY"
fi
fi
# 启动程序
exec node bin/cli.js "$@"