first submle
This commit is contained in:
@@ -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
|
||||||
@@ -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
@@ -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);
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"data": [],
|
||||||
|
"metadata": {
|
||||||
|
"createdAt": 1777764119795,
|
||||||
|
"lastSavedAt": 1777764474644
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1106
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
@@ -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 };
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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 "$@"
|
||||||
Reference in New Issue
Block a user