feat: 完成DPML协议体系0~1阶段开发 - 三层协议架构100%实现,智能路径检测系统,@package://与package.json完美集成,用户项目集成方案,CLI框架完整实现,132/137核心测试通过(96.3%通过率)
This commit is contained in:
109
src/bin/promptx.js
Executable file
109
src/bin/promptx.js
Executable file
@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { Command } = require('commander');
|
||||
const chalk = require('chalk');
|
||||
const packageJson = require('../../package.json');
|
||||
|
||||
// 导入命令模块
|
||||
const helloCommand = require('../lib/commands/hello');
|
||||
const initCommand = require('../lib/commands/init');
|
||||
const learnCommand = require('../lib/commands/learn');
|
||||
const recallCommand = require('../lib/commands/recall');
|
||||
const rememberCommand = require('../lib/commands/remember');
|
||||
|
||||
// 创建主程序
|
||||
const program = new Command();
|
||||
|
||||
// 设置程序信息
|
||||
program
|
||||
.name('promptx')
|
||||
.description(packageJson.description)
|
||||
.version(packageJson.version, '-v, --version', 'display version number');
|
||||
|
||||
// 添加五大核心命令
|
||||
program
|
||||
.command('init')
|
||||
.description('🏗️ 项目集成 - 在当前项目中初始化PromptX集成')
|
||||
.option('-f, --force', '强制重新初始化(覆盖已存在的配置)')
|
||||
.action(initCommand);
|
||||
|
||||
program
|
||||
.command('hello')
|
||||
.description('🎯 系统入口 - AI助手接待用户并展示可用角色')
|
||||
.action(helloCommand);
|
||||
|
||||
program
|
||||
.command('learn <resource>')
|
||||
.description('📚 学习命令 - AI获取和理解提示词内容')
|
||||
.option('-f, --format <type>', '输出格式 (text|json)', 'text')
|
||||
.action(learnCommand);
|
||||
|
||||
program
|
||||
.command('recall')
|
||||
.description('🔍 记忆检索 - AI回忆和检索记忆内容')
|
||||
.option('-r, --recent', '显示最近的记忆')
|
||||
.option('-i, --important', '显示重要记忆 (评分≥7)')
|
||||
.option('-l, --limit <number>', '限制返回数量', '10')
|
||||
.action(recallCommand);
|
||||
|
||||
program
|
||||
.command('remember <content>')
|
||||
.description('🧠 记忆保存 - AI保存重要信息和经验')
|
||||
.option('-s, --score <number>', '重要性评分 (1-10)', '5')
|
||||
.option('-d, --duration <time>', '有效期 (短期|中期|长期)', '短期')
|
||||
.action(rememberCommand);
|
||||
|
||||
// 全局错误处理
|
||||
program.configureHelp({
|
||||
helpWidth: 100,
|
||||
sortSubcommands: true
|
||||
});
|
||||
|
||||
// 添加示例说明
|
||||
program.addHelpText('after', `
|
||||
|
||||
${chalk.cyan('示例:')}
|
||||
${chalk.gray('# 项目集成,初始化PromptX')}
|
||||
promptx init
|
||||
promptx init --force
|
||||
|
||||
${chalk.gray('# 系统入口,展示可用角色')}
|
||||
promptx hello
|
||||
|
||||
${chalk.gray('# 学习协议和核心内容')}
|
||||
promptx learn protocols
|
||||
promptx learn core
|
||||
|
||||
${chalk.gray('# 学习特定角色')}
|
||||
promptx learn prompt/domain/scrum/role/product-owner.role.md
|
||||
|
||||
${chalk.gray('# 检索记忆')}
|
||||
promptx recall --recent
|
||||
promptx recall --important
|
||||
|
||||
${chalk.gray('# 保存记忆')}
|
||||
promptx remember "重要发现" --score 8
|
||||
promptx remember "用户反馈" --score 7 --duration 长期
|
||||
|
||||
${chalk.cyan('AI认知循环:')}
|
||||
🏗️ ${chalk.cyan('init')} → 👋 ${chalk.yellow('hello')} → 📚 ${chalk.blue('learn')} → 🔍 ${chalk.green('recall')} → 🧠 ${chalk.magenta('remember')} → 循环
|
||||
|
||||
${chalk.cyan('更多信息:')}
|
||||
GitHub: ${chalk.underline('https://github.com/Deepractice/PromptX')}
|
||||
文档: ${chalk.underline('https://deepractice.ai')}
|
||||
`);
|
||||
|
||||
// 处理未知命令
|
||||
program.on('command:*', () => {
|
||||
console.error(chalk.red(`错误: 未知命令 '${program.args.join(' ')}'`));
|
||||
console.log('');
|
||||
program.help();
|
||||
});
|
||||
|
||||
// 如果没有参数,显示帮助
|
||||
if (process.argv.length === 2) {
|
||||
program.help();
|
||||
}
|
||||
|
||||
// 解析命令行参数
|
||||
program.parse(process.argv);
|
||||
31
src/lib/commands/hello.js
Normal file
31
src/lib/commands/hello.js
Normal file
@ -0,0 +1,31 @@
|
||||
const chalk = require('chalk');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* promptx hello 命令
|
||||
* 系统入口 - AI助手接待用户并展示可用角色
|
||||
*/
|
||||
async function helloCommand(options) {
|
||||
try {
|
||||
logger.step('PromptX Hello - 系统初始化中...');
|
||||
|
||||
// TODO: 实现在任务 2.1 中
|
||||
console.log(chalk.cyan(`
|
||||
🎯 PromptX 系统入口
|
||||
|
||||
${chalk.yellow('功能:')} AI助手接待用户并展示可用角色
|
||||
${chalk.yellow('状态:')} 待实现 (任务 2.1)
|
||||
|
||||
${chalk.green('下一步:')}
|
||||
请执行任务 2.1 来实现完整的 hello 命令功能
|
||||
`));
|
||||
|
||||
logger.info('Hello命令框架已就绪,等待具体实现');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Hello命令执行失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = helloCommand;
|
||||
185
src/lib/commands/init.js
Normal file
185
src/lib/commands/init.js
Normal file
@ -0,0 +1,185 @@
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* promptx init 命令
|
||||
* 在用户项目中初始化PromptX集成
|
||||
*/
|
||||
async function init(options = {}) {
|
||||
console.log(chalk.blue.bold('🚀 初始化 PromptX 项目集成...\n'));
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
const promptxDir = path.join(projectRoot, '.promptx');
|
||||
const memoryDir = path.join(promptxDir, 'memory');
|
||||
|
||||
// 检查是否已经初始化
|
||||
try {
|
||||
await fs.access(promptxDir);
|
||||
console.log(chalk.yellow('⚠️ 项目已经初始化过 PromptX 集成'));
|
||||
console.log(chalk.gray(` .promptx 目录已存在: ${promptxDir}`));
|
||||
|
||||
if (!options.force) {
|
||||
console.log(chalk.gray(' 使用 --force 参数强制重新初始化'));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.blue('🔄 强制重新初始化...'));
|
||||
} catch (error) {
|
||||
// 目录不存在,继续初始化
|
||||
}
|
||||
|
||||
// 创建 .promptx 目录
|
||||
await fs.mkdir(promptxDir, { recursive: true });
|
||||
console.log(chalk.green('✅ 创建 .promptx 目录'));
|
||||
|
||||
// 创建 .promptx/memory 目录
|
||||
await fs.mkdir(memoryDir, { recursive: true });
|
||||
console.log(chalk.green('✅ 创建 memory 目录'));
|
||||
|
||||
// 创建基础记忆文件
|
||||
const memoryFiles = [
|
||||
{
|
||||
name: 'declarative.md',
|
||||
content: `# 声明式记忆
|
||||
|
||||
## 项目重要信息
|
||||
- 项目初始化时间: ${new Date().toISOString()}
|
||||
- PromptX 集成状态: ✅ 已完成
|
||||
|
||||
## 使用说明
|
||||
在这里记录项目的重要决策、配置信息和关键知识点。
|
||||
|
||||
### 示例条目
|
||||
**时间**: 2024-01-01T00:00:00.000Z
|
||||
**重要性**: 8/10
|
||||
**内容**: 项目使用 PromptX 进行 AI 助手集成
|
||||
**有效期**: 长期
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'episodic.md',
|
||||
content: `# 情景记忆
|
||||
|
||||
## 项目历程记录
|
||||
记录项目开发过程中的重要事件和里程碑。
|
||||
|
||||
### 项目初始化
|
||||
- **时间**: ${new Date().toISOString()}
|
||||
- **事件**: PromptX 集成初始化完成
|
||||
- **详情**: 使用 \`promptx init\` 命令完成项目集成设置
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'procedural.md',
|
||||
content: `# 程序记忆
|
||||
|
||||
## 项目工作流程
|
||||
|
||||
### PromptX 使用流程
|
||||
1. **学习阶段**: \`promptx learn <resource>\`
|
||||
2. **记忆保存**: \`promptx remember <content>\`
|
||||
3. **记忆检索**: \`promptx recall\`
|
||||
4. **助手切换**: \`promptx hello\`
|
||||
|
||||
### 项目开发流程
|
||||
在这里记录项目特有的开发流程和最佳实践。
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'semantic.md',
|
||||
content: `# 语义记忆
|
||||
|
||||
## 项目知识图谱
|
||||
|
||||
### PromptX 协议体系
|
||||
- **@project://**: 指向当前项目根目录
|
||||
- **@memory://**: 指向项目记忆系统
|
||||
- **@package://**: 指向 PromptX 包资源
|
||||
- **@prompt://**: 指向提示词资源
|
||||
|
||||
### 项目特定概念
|
||||
在这里定义项目中的重要概念和术语。
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
for (const file of memoryFiles) {
|
||||
const filePath = path.join(memoryDir, file.name);
|
||||
await fs.writeFile(filePath, file.content, 'utf8');
|
||||
console.log(chalk.green(`✅ 创建记忆文件: ${file.name}`));
|
||||
}
|
||||
|
||||
// 创建 .promptx/config.json 配置文件
|
||||
const config = {
|
||||
version: "0.0.1",
|
||||
initialized: new Date().toISOString(),
|
||||
settings: {
|
||||
memoryPath: "memory",
|
||||
defaultRole: null,
|
||||
autoRemember: false
|
||||
},
|
||||
protocols: {
|
||||
project: {
|
||||
root: ".",
|
||||
identifiers: [".promptx", "package.json", ".git"]
|
||||
},
|
||||
memory: {
|
||||
types: ["declarative", "episodic", "procedural", "semantic"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const configPath = path.join(promptxDir, 'config.json');
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
console.log(chalk.green('✅ 创建配置文件: config.json'));
|
||||
|
||||
// 创建 .gitignore (如果需要)
|
||||
const gitignorePath = path.join(projectRoot, '.gitignore');
|
||||
try {
|
||||
let gitignoreContent = '';
|
||||
try {
|
||||
gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
|
||||
} catch (error) {
|
||||
// .gitignore 不存在
|
||||
}
|
||||
|
||||
if (!gitignoreContent.includes('.promptx')) {
|
||||
const appendContent = gitignoreContent.length > 0 ? '\n# PromptX\n.promptx/config.json\n' : '# PromptX\n.promptx/config.json\n';
|
||||
await fs.appendFile(gitignorePath, appendContent, 'utf8');
|
||||
console.log(chalk.green('✅ 更新 .gitignore 文件'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow('⚠️ 无法更新 .gitignore 文件'));
|
||||
}
|
||||
|
||||
// 完成提示
|
||||
console.log(chalk.green.bold('\n🎉 PromptX 项目集成初始化完成!\n'));
|
||||
|
||||
console.log(chalk.blue('📁 创建的文件结构:'));
|
||||
console.log(chalk.gray(' .promptx/'));
|
||||
console.log(chalk.gray(' ├── config.json'));
|
||||
console.log(chalk.gray(' └── memory/'));
|
||||
console.log(chalk.gray(' ├── declarative.md'));
|
||||
console.log(chalk.gray(' ├── episodic.md'));
|
||||
console.log(chalk.gray(' ├── procedural.md'));
|
||||
console.log(chalk.gray(' └── semantic.md'));
|
||||
|
||||
console.log(chalk.blue('\n🚀 可用的协议:'));
|
||||
console.log(chalk.gray(' @project:// - 访问项目文件'));
|
||||
console.log(chalk.gray(' @memory:// - 访问项目记忆'));
|
||||
console.log(chalk.gray(' @prompt:// - 访问提示词资源'));
|
||||
|
||||
console.log(chalk.blue('\n🎯 下一步:'));
|
||||
console.log(chalk.gray(' 1. 使用 promptx hello 选择 AI 角色'));
|
||||
console.log(chalk.gray(' 2. 使用 promptx learn 学习项目知识'));
|
||||
console.log(chalk.gray(' 3. 使用 promptx remember 保存重要信息'));
|
||||
console.log(chalk.gray(' 4. 使用 promptx recall 检索记忆内容'));
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ 初始化失败:'), error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
35
src/lib/commands/learn.js
Normal file
35
src/lib/commands/learn.js
Normal file
@ -0,0 +1,35 @@
|
||||
const chalk = require('chalk');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* promptx learn 命令
|
||||
* 学习命令 - AI获取和理解提示词内容
|
||||
*/
|
||||
async function learnCommand(resource, options) {
|
||||
try {
|
||||
logger.step(`学习资源: ${resource}`);
|
||||
|
||||
// TODO: 实现在任务 2.2 中
|
||||
console.log(chalk.blue(`
|
||||
📚 PromptX Learn 命令
|
||||
|
||||
${chalk.yellow('资源:')} ${resource}
|
||||
${chalk.yellow('格式:')} ${options.format}
|
||||
${chalk.yellow('状态:')} 待实现 (任务 2.2)
|
||||
|
||||
${chalk.green('计划功能:')}
|
||||
- 支持打包参数 (protocols, core, domain)
|
||||
- 支持具体文件路径
|
||||
- 替代现有 node promptx.js 功能
|
||||
- 向后兼容现有AI bootstrap流程
|
||||
`));
|
||||
|
||||
logger.info('Learn命令框架已就绪,等待具体实现');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Learn命令执行失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = learnCommand;
|
||||
36
src/lib/commands/recall.js
Normal file
36
src/lib/commands/recall.js
Normal file
@ -0,0 +1,36 @@
|
||||
const chalk = require('chalk');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* promptx recall 命令
|
||||
* 记忆检索 - AI回忆和检索记忆内容
|
||||
*/
|
||||
async function recallCommand(options) {
|
||||
try {
|
||||
logger.step('检索记忆内容...');
|
||||
|
||||
// TODO: 实现在任务 2.3 中
|
||||
console.log(chalk.green(`
|
||||
🔍 PromptX Recall 命令
|
||||
|
||||
${chalk.yellow('选项:')}
|
||||
- 最近记忆: ${options.recent || false}
|
||||
- 重要记忆: ${options.important || false}
|
||||
- 限制数量: ${options.limit}
|
||||
${chalk.yellow('状态:')} 待实现 (任务 2.3)
|
||||
|
||||
${chalk.green('计划功能:')}
|
||||
- 读取 .memory/declarative.md 文件
|
||||
- 基础筛选功能 (--recent, --important)
|
||||
- 为未来高级记忆体系打基础
|
||||
`));
|
||||
|
||||
logger.info('Recall命令框架已就绪,等待具体实现');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Recall命令执行失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = recallCommand;
|
||||
36
src/lib/commands/remember.js
Normal file
36
src/lib/commands/remember.js
Normal file
@ -0,0 +1,36 @@
|
||||
const chalk = require('chalk');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* promptx remember 命令
|
||||
* 记忆保存 - AI保存重要信息和经验
|
||||
*/
|
||||
async function rememberCommand(content, options) {
|
||||
try {
|
||||
logger.step('保存记忆中...');
|
||||
|
||||
// TODO: 实现在任务 2.4 中
|
||||
console.log(chalk.magenta(`
|
||||
🧠 PromptX Remember 命令
|
||||
|
||||
${chalk.yellow('内容:')} ${content}
|
||||
${chalk.yellow('评分:')} ${options.score}
|
||||
${chalk.yellow('有效期:')} ${options.duration}
|
||||
${chalk.yellow('状态:')} 待实现 (任务 2.4)
|
||||
|
||||
${chalk.green('计划功能:')}
|
||||
- 写入 .memory/declarative.md 文件
|
||||
- 结构化参数设计 (--score, --duration)
|
||||
- 替代复杂标签系统
|
||||
- 支持智能默认值
|
||||
`));
|
||||
|
||||
logger.info('Remember命令框架已就绪,等待具体实现');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Remember命令执行失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = rememberCommand;
|
||||
67
src/lib/core/resource/index.js
Normal file
67
src/lib/core/resource/index.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* PromptX Resource Module
|
||||
* 基于DPML资源协议的统一资源管理模块
|
||||
*
|
||||
* 提供完整的资源协议解析、注册表管理、资源加载功能
|
||||
*/
|
||||
|
||||
// 核心管理器
|
||||
const ResourceManager = require('./resourceManager');
|
||||
|
||||
// 核心组件
|
||||
const ResourceProtocolParser = require('./resourceProtocolParser');
|
||||
const ResourceRegistry = require('./resourceRegistry');
|
||||
|
||||
// 数据类型
|
||||
const {
|
||||
LoadingSemantics,
|
||||
ParsedReference,
|
||||
QueryParams,
|
||||
NestedReference,
|
||||
ResourceContent,
|
||||
LazyResource,
|
||||
ProcessedResult,
|
||||
ResourceResult,
|
||||
ProtocolInfo
|
||||
} = require('./types');
|
||||
|
||||
// 导出主接口
|
||||
module.exports = {
|
||||
// 主管理器
|
||||
ResourceManager,
|
||||
|
||||
// 核心组件
|
||||
ResourceProtocolParser,
|
||||
ResourceRegistry,
|
||||
|
||||
// 数据类型
|
||||
LoadingSemantics,
|
||||
ParsedReference,
|
||||
QueryParams,
|
||||
NestedReference,
|
||||
ResourceContent,
|
||||
LazyResource,
|
||||
ProcessedResult,
|
||||
ResourceResult,
|
||||
ProtocolInfo,
|
||||
|
||||
// 便捷方法 - 创建默认实例
|
||||
createManager: (options) => new ResourceManager(options),
|
||||
|
||||
// 便捷方法 - 快速解析
|
||||
parse: (resourceRef) => {
|
||||
const parser = new ResourceProtocolParser();
|
||||
return parser.parse(resourceRef);
|
||||
},
|
||||
|
||||
// 便捷方法 - 快速验证
|
||||
validate: (resourceRef) => {
|
||||
try {
|
||||
const parser = new ResourceProtocolParser();
|
||||
parser.parse(resourceRef);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
505
src/lib/core/resource/protocols/PackageProtocol.js
Normal file
505
src/lib/core/resource/protocols/PackageProtocol.js
Normal file
@ -0,0 +1,505 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const fsPromises = require('fs').promises;
|
||||
const ResourceProtocol = require('./ResourceProtocol');
|
||||
const { QueryParams } = require('../types');
|
||||
|
||||
/**
|
||||
* 包协议实现
|
||||
* 实现@package://协议,智能检测并访问NPM包资源
|
||||
* 支持:本地开发、npm install、npm -g、npx、monorepo等场景
|
||||
*/
|
||||
class PackageProtocol extends ResourceProtocol {
|
||||
constructor(options = {}) {
|
||||
super('package', options);
|
||||
|
||||
// 包安装模式检测缓存
|
||||
this.installModeCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议信息
|
||||
*/
|
||||
getProtocolInfo() {
|
||||
return {
|
||||
name: this.name,
|
||||
description: '包协议 - 智能访问NPM包资源,支持多种安装模式',
|
||||
examples: [
|
||||
'@package://package.json',
|
||||
'@package://src/index.js',
|
||||
'@package://docs/README.md',
|
||||
'@package://prompt/core/thought.md',
|
||||
'@package://templates/basic/template.md'
|
||||
],
|
||||
installModes: [
|
||||
'development', // 开发模式
|
||||
'local', // 本地npm install
|
||||
'global', // 全局npm install -g
|
||||
'npx', // npx执行
|
||||
'monorepo', // monorepo workspace
|
||||
'link' // npm link
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测当前包安装模式
|
||||
*/
|
||||
detectInstallMode() {
|
||||
const cacheKey = 'currentInstallMode';
|
||||
if (this.installModeCache.has(cacheKey)) {
|
||||
return this.installModeCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const mode = this._performInstallModeDetection();
|
||||
this.installModeCache.set(cacheKey, mode);
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行安装模式检测
|
||||
*/
|
||||
_performInstallModeDetection() {
|
||||
const cwd = process.cwd();
|
||||
const execPath = process.argv[0];
|
||||
const scriptPath = process.argv[1];
|
||||
|
||||
// 检测npx执行
|
||||
if (this._isNpxExecution()) {
|
||||
return 'npx';
|
||||
}
|
||||
|
||||
// 检测全局安装
|
||||
if (this._isGlobalInstall()) {
|
||||
return 'global';
|
||||
}
|
||||
|
||||
// 检测开发模式
|
||||
if (this._isDevelopmentMode()) {
|
||||
return 'development';
|
||||
}
|
||||
|
||||
// 检测monorepo
|
||||
if (this._isMonorepoWorkspace()) {
|
||||
return 'monorepo';
|
||||
}
|
||||
|
||||
// 检测npm link
|
||||
if (this._isNpmLink()) {
|
||||
return 'link';
|
||||
}
|
||||
|
||||
// 默认为本地安装
|
||||
return 'local';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是npx执行
|
||||
*/
|
||||
_isNpxExecution() {
|
||||
// 检查环境变量
|
||||
if (process.env.npm_execpath && process.env.npm_execpath.includes('npx')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查npm_config_cache路径
|
||||
if (process.env.npm_config_cache && process.env.npm_config_cache.includes('_npx')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查执行路径
|
||||
const scriptPath = process.argv[1];
|
||||
if (scriptPath && scriptPath.includes('_npx')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是全局安装
|
||||
*/
|
||||
_isGlobalInstall() {
|
||||
const currentPath = __dirname;
|
||||
|
||||
// 常见全局安装路径
|
||||
const globalPaths = [
|
||||
'/usr/lib/node_modules',
|
||||
'/usr/local/lib/node_modules',
|
||||
'/opt/homebrew/lib/node_modules',
|
||||
path.join(process.env.HOME || '', '.npm-global'),
|
||||
path.join(process.env.APPDATA || '', 'npm', 'node_modules'),
|
||||
path.join(process.env.PREFIX || '', 'lib', 'node_modules')
|
||||
];
|
||||
|
||||
return globalPaths.some(globalPath =>
|
||||
currentPath.startsWith(globalPath)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是开发模式
|
||||
*/
|
||||
_isDevelopmentMode() {
|
||||
// 检查NODE_ENV
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否在node_modules外
|
||||
const currentPath = __dirname;
|
||||
if (!currentPath.includes('node_modules')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查package.json中的main字段是否指向源文件
|
||||
try {
|
||||
const packageJsonPath = this.findPackageJson();
|
||||
if (packageJsonPath) {
|
||||
const packageJson = require(packageJsonPath);
|
||||
const mainFile = packageJson.main || 'index.js';
|
||||
return mainFile.startsWith('src/') || mainFile.startsWith('lib/');
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误,继续其他检测
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是monorepo workspace
|
||||
*/
|
||||
_isMonorepoWorkspace() {
|
||||
try {
|
||||
const packageJsonPath = this.findPackageJson();
|
||||
if (packageJsonPath) {
|
||||
const packageJson = require(packageJsonPath);
|
||||
|
||||
// 检查workspaces字段
|
||||
if (packageJson.workspaces) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否在workspace包内
|
||||
const rootPackageJsonPath = this.findRootPackageJson();
|
||||
if (rootPackageJsonPath && rootPackageJsonPath !== packageJsonPath) {
|
||||
const rootPackageJson = require(rootPackageJsonPath);
|
||||
return !!rootPackageJson.workspaces;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是npm link
|
||||
*/
|
||||
_isNpmLink() {
|
||||
try {
|
||||
const currentPath = __dirname;
|
||||
const stats = require('fs').lstatSync(currentPath);
|
||||
return stats.isSymbolicLink();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找package.json文件
|
||||
*/
|
||||
findPackageJson(startPath = __dirname) {
|
||||
let currentPath = path.resolve(startPath);
|
||||
|
||||
while (currentPath !== path.parse(currentPath).root) {
|
||||
const packageJsonPath = path.join(currentPath, 'package.json');
|
||||
if (require('fs').existsSync(packageJsonPath)) {
|
||||
return packageJsonPath;
|
||||
}
|
||||
currentPath = path.dirname(currentPath);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根package.json文件(用于monorepo检测)
|
||||
*/
|
||||
findRootPackageJson() {
|
||||
let currentPath = process.cwd();
|
||||
let lastValidPackageJson = null;
|
||||
|
||||
while (currentPath !== path.parse(currentPath).root) {
|
||||
const packageJsonPath = path.join(currentPath, 'package.json');
|
||||
if (require('fs').existsSync(packageJsonPath)) {
|
||||
lastValidPackageJson = packageJsonPath;
|
||||
}
|
||||
currentPath = path.dirname(currentPath);
|
||||
}
|
||||
|
||||
return lastValidPackageJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取包根目录
|
||||
*/
|
||||
async getPackageRoot() {
|
||||
const mode = this.detectInstallMode();
|
||||
|
||||
switch (mode) {
|
||||
case 'development':
|
||||
// 开发模式:查找项目根目录
|
||||
return this._findProjectRoot();
|
||||
|
||||
case 'global':
|
||||
// 全局安装:查找全局包目录
|
||||
return this._findGlobalPackageRoot();
|
||||
|
||||
case 'npx':
|
||||
// npx:查找临时包目录
|
||||
return this._findNpxPackageRoot();
|
||||
|
||||
case 'monorepo':
|
||||
// monorepo:查找workspace包目录
|
||||
return this._findWorkspacePackageRoot();
|
||||
|
||||
case 'link':
|
||||
// npm link:解析符号链接
|
||||
return this._findLinkedPackageRoot();
|
||||
|
||||
case 'local':
|
||||
default:
|
||||
// 本地安装:查找node_modules中的包目录
|
||||
return this._findLocalPackageRoot();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找项目根目录
|
||||
*/
|
||||
_findProjectRoot() {
|
||||
const packageJsonPath = this.findPackageJson();
|
||||
return packageJsonPath ? path.dirname(packageJsonPath) : process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找全局包根目录
|
||||
*/
|
||||
_findGlobalPackageRoot() {
|
||||
// 从当前模块路径向上查找,直到找到package.json
|
||||
return this._findProjectRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找npx包根目录
|
||||
*/
|
||||
_findNpxPackageRoot() {
|
||||
// npx通常将包缓存在特定目录
|
||||
const packageJsonPath = this.findPackageJson();
|
||||
return packageJsonPath ? path.dirname(packageJsonPath) : process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找workspace包根目录
|
||||
*/
|
||||
_findWorkspacePackageRoot() {
|
||||
// 在monorepo中查找当前workspace的根目录
|
||||
return this._findProjectRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找链接包根目录
|
||||
*/
|
||||
_findLinkedPackageRoot() {
|
||||
try {
|
||||
// 解析符号链接
|
||||
const realPath = require('fs').realpathSync(__dirname);
|
||||
const packageJsonPath = this.findPackageJson(realPath);
|
||||
return packageJsonPath ? path.dirname(packageJsonPath) : realPath;
|
||||
} catch (error) {
|
||||
return this._findProjectRoot();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找本地包根目录
|
||||
*/
|
||||
_findLocalPackageRoot() {
|
||||
// 在node_modules中查找包根目录
|
||||
return this._findProjectRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析路径到具体的文件系统路径
|
||||
* @param {string} relativePath - 相对于包根目录的路径
|
||||
* @param {QueryParams} params - 查询参数
|
||||
* @returns {Promise<string>} 解析后的绝对路径
|
||||
*/
|
||||
async resolvePath(relativePath, params = null) {
|
||||
// 获取包根目录
|
||||
const packageRoot = await this.getPackageRoot();
|
||||
|
||||
// 验证路径是否在package.json的files字段中
|
||||
this.validateFileAccess(packageRoot, relativePath);
|
||||
|
||||
// 直接处理路径,不需要目录映射
|
||||
const relativePathClean = relativePath.replace(/^\/+/, '');
|
||||
const fullPath = path.resolve(packageRoot, relativePathClean);
|
||||
|
||||
// 安全检查:确保路径在包根目录内
|
||||
if (!fullPath.startsWith(packageRoot)) {
|
||||
throw new Error(`Path traversal detected: ${relativePath}`);
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件访问权限(基于package.json的files字段)
|
||||
* @param {string} packageRoot - 包根目录
|
||||
* @param {string} relativePath - 相对路径
|
||||
*/
|
||||
validateFileAccess(packageRoot, relativePath) {
|
||||
try {
|
||||
const packageJsonPath = path.join(packageRoot, 'package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
// 如果没有files字段,允许访问所有文件(开发模式)
|
||||
if (!packageJson.files || !Array.isArray(packageJson.files)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标准化路径
|
||||
const normalizedPath = relativePath.replace(/^\/+/, '').replace(/\\/g, '/');
|
||||
|
||||
// 检查是否匹配files字段中的任何模式
|
||||
const isAllowed = packageJson.files.some(filePattern => {
|
||||
// 标准化文件模式
|
||||
const normalizedPattern = filePattern.replace(/^\/+/, '').replace(/\\/g, '/');
|
||||
|
||||
// 精确匹配
|
||||
if (normalizedPattern === normalizedPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 目录匹配(以/结尾或包含/*)
|
||||
if (normalizedPattern.endsWith('/') || normalizedPattern.endsWith('/*')) {
|
||||
const dirPattern = normalizedPattern.replace(/\/?\*?$/, '/');
|
||||
return normalizedPath.startsWith(dirPattern);
|
||||
}
|
||||
|
||||
// 通配符匹配
|
||||
if (normalizedPattern.includes('*')) {
|
||||
const regexPattern = normalizedPattern
|
||||
.replace(/\./g, '\\.')
|
||||
.replace(/\*/g, '.*');
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(normalizedPath);
|
||||
}
|
||||
|
||||
// 目录前缀匹配
|
||||
if (normalizedPath.startsWith(normalizedPattern + '/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!isAllowed) {
|
||||
// 在生产环境严格检查,开发环境只警告
|
||||
const installMode = this.detectInstallMode();
|
||||
if (installMode === 'development') {
|
||||
console.warn(`⚠️ Warning: Path '${relativePath}' not in package.json files field. This may cause issues after publishing.`);
|
||||
} else {
|
||||
throw new Error(`Access denied: Path '${relativePath}' is not included in package.json files field`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果读取package.json失败,在开发模式下允许访问
|
||||
const installMode = this.detectInstallMode();
|
||||
if (installMode === 'development') {
|
||||
console.warn(`⚠️ Warning: Could not validate file access for '${relativePath}': ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资源是否存在
|
||||
*/
|
||||
async exists(resourcePath, queryParams) {
|
||||
try {
|
||||
const resolvedPath = await this.resolvePath(resourcePath, queryParams);
|
||||
await fsPromises.access(resolvedPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载资源内容
|
||||
*/
|
||||
async loadContent(resourcePath, queryParams) {
|
||||
const resolvedPath = await this.resolvePath(resourcePath, queryParams);
|
||||
|
||||
try {
|
||||
await fsPromises.access(resolvedPath);
|
||||
const content = await fsPromises.readFile(resolvedPath, 'utf8');
|
||||
const stats = await fsPromises.stat(resolvedPath);
|
||||
|
||||
return {
|
||||
content,
|
||||
path: resolvedPath,
|
||||
protocol: this.name,
|
||||
installMode: this.detectInstallMode(),
|
||||
metadata: {
|
||||
size: content.length,
|
||||
lastModified: stats.mtime,
|
||||
absolutePath: resolvedPath,
|
||||
relativePath: resourcePath
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new Error(`包资源不存在: ${resourcePath} (解析为: ${resolvedPath})`);
|
||||
}
|
||||
throw new Error(`加载包资源失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调试信息
|
||||
*/
|
||||
getDebugInfo() {
|
||||
const mode = this.detectInstallMode();
|
||||
|
||||
return {
|
||||
protocol: this.name,
|
||||
installMode: mode,
|
||||
packageRoot: this.getPackageRoot(),
|
||||
currentWorkingDirectory: process.cwd(),
|
||||
moduleDirectory: __dirname,
|
||||
environment: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
npm_execpath: process.env.npm_execpath,
|
||||
npm_config_cache: process.env.npm_config_cache
|
||||
},
|
||||
cacheSize: this.cache.size
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
*/
|
||||
clearCache() {
|
||||
super.clearCache();
|
||||
this.installModeCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PackageProtocol;
|
||||
351
src/lib/core/resource/protocols/ProjectProtocol.js
Normal file
351
src/lib/core/resource/protocols/ProjectProtocol.js
Normal file
@ -0,0 +1,351 @@
|
||||
const ResourceProtocol = require('./ResourceProtocol');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
/**
|
||||
* 项目协议实现
|
||||
* 实现@project://协议,通过查找.promptx目录确定项目根目录
|
||||
*/
|
||||
class ProjectProtocol extends ResourceProtocol {
|
||||
constructor(options = {}) {
|
||||
super('project', options);
|
||||
|
||||
// 支持的项目结构目录映射
|
||||
this.projectDirs = {
|
||||
'root': '', // 项目根目录
|
||||
'src': 'src', // 源代码目录
|
||||
'lib': 'lib', // 库目录
|
||||
'build': 'build', // 构建输出目录
|
||||
'dist': 'dist', // 分发目录
|
||||
'docs': 'docs', // 文档目录
|
||||
'test': 'test', // 测试目录
|
||||
'tests': 'tests', // 测试目录(复数)
|
||||
'spec': 'spec', // 规范测试目录
|
||||
'config': 'config', // 配置目录
|
||||
'scripts': 'scripts', // 脚本目录
|
||||
'assets': 'assets', // 资源目录
|
||||
'public': 'public', // 公共资源目录
|
||||
'static': 'static', // 静态资源目录
|
||||
'templates': 'templates', // 模板目录
|
||||
'examples': 'examples', // 示例目录
|
||||
'tools': 'tools' // 工具目录
|
||||
};
|
||||
|
||||
// 项目根目录缓存
|
||||
this.projectRootCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议信息
|
||||
* @returns {object} 协议信息
|
||||
*/
|
||||
getProtocolInfo() {
|
||||
return {
|
||||
name: 'project',
|
||||
description: '项目协议,通过.promptx目录标识提供项目结构访问',
|
||||
location: 'project://{directory}/{path}',
|
||||
examples: [
|
||||
'project://src/index.js',
|
||||
'project://lib/utils.js',
|
||||
'project://docs/README.md',
|
||||
'project://root/package.json',
|
||||
'project://test/unit/'
|
||||
],
|
||||
supportedDirectories: Object.keys(this.projectDirs),
|
||||
projectMarker: '.promptx',
|
||||
params: this.getSupportedParams()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的查询参数
|
||||
* @returns {object} 参数说明
|
||||
*/
|
||||
getSupportedParams() {
|
||||
return {
|
||||
...super.getSupportedParams(),
|
||||
from: 'string - 指定搜索起始目录',
|
||||
create: 'boolean - 如果目录不存在是否创建',
|
||||
exists: 'boolean - 仅返回存在的文件/目录',
|
||||
type: 'string - 过滤类型 (file|dir|both)'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证项目协议路径
|
||||
* @param {string} resourcePath - 资源路径
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
validatePath(resourcePath) {
|
||||
if (!super.validatePath(resourcePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析路径的第一部分(目录类型)
|
||||
const parts = resourcePath.split('/');
|
||||
const dirType = parts[0];
|
||||
|
||||
return this.projectDirs.hasOwnProperty(dirType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上查找指定目录的同步版本
|
||||
* @param {string} targetDir - 要查找的目录名(如 '.promptx')
|
||||
* @param {string} startDir - 开始搜索的目录
|
||||
* @returns {string|null} 找到的目录路径或null
|
||||
*/
|
||||
findUpDirectorySync(targetDir, startDir = process.cwd()) {
|
||||
let currentDir = path.resolve(startDir);
|
||||
const rootDir = path.parse(currentDir).root;
|
||||
|
||||
while (currentDir !== rootDir) {
|
||||
const targetPath = path.join(currentDir, targetDir);
|
||||
|
||||
try {
|
||||
const stats = require('fs').statSync(targetPath);
|
||||
if (stats.isDirectory()) {
|
||||
return targetPath;
|
||||
}
|
||||
} catch (error) {
|
||||
// 目录不存在,继续向上查找
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
// 已到达根目录
|
||||
break;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找项目根目录
|
||||
* @param {string} startDir - 开始搜索的目录
|
||||
* @returns {Promise<string|null>} 项目根目录路径
|
||||
*/
|
||||
async findProjectRoot(startDir = process.cwd()) {
|
||||
// 检查缓存
|
||||
const cacheKey = path.resolve(startDir);
|
||||
if (this.projectRootCache.has(cacheKey)) {
|
||||
return this.projectRootCache.get(cacheKey);
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用自实现的向上查找
|
||||
const promptxPath = this.findUpDirectorySync('.promptx', startDir);
|
||||
|
||||
let projectRoot = null;
|
||||
if (promptxPath) {
|
||||
// .promptx 目录的父目录就是项目根目录
|
||||
projectRoot = path.dirname(promptxPath);
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
this.projectRootCache.set(cacheKey, projectRoot);
|
||||
|
||||
return projectRoot;
|
||||
} catch (error) {
|
||||
throw new Error(`查找项目根目录失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析项目路径
|
||||
* @param {string} resourcePath - 原始资源路径,如 "src/index.js"
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 解析后的绝对路径
|
||||
*/
|
||||
async resolvePath(resourcePath, queryParams) {
|
||||
const parts = resourcePath.split('/');
|
||||
const dirType = parts[0];
|
||||
const relativePath = parts.slice(1).join('/');
|
||||
|
||||
// 验证目录类型
|
||||
if (!this.projectDirs.hasOwnProperty(dirType)) {
|
||||
throw new Error(`不支持的项目目录类型: ${dirType}。支持的类型: ${Object.keys(this.projectDirs).join(', ')}`);
|
||||
}
|
||||
|
||||
// 确定搜索起始点
|
||||
const startDir = queryParams?.get('from') || process.cwd();
|
||||
|
||||
// 查找项目根目录
|
||||
const projectRoot = await this.findProjectRoot(startDir);
|
||||
if (!projectRoot) {
|
||||
throw new Error(`未找到项目根目录(.promptx标识)。请确保在项目目录内或使用 'from' 参数指定项目路径`);
|
||||
}
|
||||
|
||||
// 构建目标目录路径
|
||||
const projectDirPath = this.projectDirs[dirType];
|
||||
const targetDir = projectDirPath ? path.join(projectRoot, projectDirPath) : projectRoot;
|
||||
|
||||
// 如果没有相对路径,返回目录本身
|
||||
if (!relativePath) {
|
||||
return targetDir;
|
||||
}
|
||||
|
||||
// 拼接完整路径
|
||||
const fullPath = path.join(targetDir, relativePath);
|
||||
|
||||
// 安全检查:确保路径在项目目录内
|
||||
const resolvedPath = path.resolve(fullPath);
|
||||
const resolvedProjectRoot = path.resolve(projectRoot);
|
||||
|
||||
if (!resolvedPath.startsWith(resolvedProjectRoot)) {
|
||||
throw new Error(`安全错误:路径超出项目目录范围: ${resolvedPath}`);
|
||||
}
|
||||
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载资源内容
|
||||
* @param {string} resolvedPath - 解析后的路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 资源内容
|
||||
*/
|
||||
async loadContent(resolvedPath, queryParams) {
|
||||
try {
|
||||
// 检查路径是否存在
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return await this.loadDirectoryContent(resolvedPath, queryParams);
|
||||
} else if (stats.isFile()) {
|
||||
return await this.loadFileContent(resolvedPath, queryParams);
|
||||
} else {
|
||||
throw new Error(`不支持的文件类型: ${resolvedPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// 检查是否需要创建目录
|
||||
if (queryParams?.get('create') === 'true') {
|
||||
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
||||
return ''; // 返回空内容
|
||||
}
|
||||
|
||||
// 如果设置了exists参数为false,返回空内容而不是错误
|
||||
if (queryParams?.get('exists') === 'false') {
|
||||
return '';
|
||||
}
|
||||
throw new Error(`文件或目录不存在: ${resolvedPath}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载文件内容
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 文件内容
|
||||
*/
|
||||
async loadFileContent(filePath, queryParams) {
|
||||
const encoding = queryParams?.get('encoding') || 'utf8';
|
||||
return await fs.readFile(filePath, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载目录内容
|
||||
* @param {string} dirPath - 目录路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 目录内容列表
|
||||
*/
|
||||
async loadDirectoryContent(dirPath, queryParams) {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
// 应用类型过滤
|
||||
const typeFilter = queryParams?.get('type');
|
||||
let filteredEntries = entries;
|
||||
|
||||
if (typeFilter) {
|
||||
filteredEntries = entries.filter(entry => {
|
||||
switch (typeFilter) {
|
||||
case 'file': return entry.isFile();
|
||||
case 'dir': return entry.isDirectory();
|
||||
case 'both': return true;
|
||||
default: return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化输出
|
||||
const format = queryParams?.get('format') || 'list';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return JSON.stringify(
|
||||
filteredEntries.map(entry => ({
|
||||
name: entry.name,
|
||||
type: entry.isDirectory() ? 'directory' : 'file',
|
||||
path: path.join(dirPath, entry.name)
|
||||
})),
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
case 'paths':
|
||||
return filteredEntries
|
||||
.map(entry => path.join(dirPath, entry.name))
|
||||
.join('\n');
|
||||
|
||||
case 'list':
|
||||
default:
|
||||
return filteredEntries
|
||||
.map(entry => {
|
||||
const type = entry.isDirectory() ? '[DIR]' : '[FILE]';
|
||||
return `${type} ${entry.name}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出项目结构信息
|
||||
* @param {string} startDir - 开始搜索的目录
|
||||
* @returns {Promise<object>} 项目信息
|
||||
*/
|
||||
async getProjectInfo(startDir = process.cwd()) {
|
||||
const projectRoot = await this.findProjectRoot(startDir);
|
||||
if (!projectRoot) {
|
||||
return { error: '未找到项目根目录' };
|
||||
}
|
||||
|
||||
const result = {
|
||||
projectRoot,
|
||||
promptxPath: path.join(projectRoot, '.promptx'),
|
||||
directories: {}
|
||||
};
|
||||
|
||||
for (const [dirType, dirPath] of Object.entries(this.projectDirs)) {
|
||||
const fullPath = dirPath ? path.join(projectRoot, dirPath) : projectRoot;
|
||||
try {
|
||||
const stats = await fs.stat(fullPath);
|
||||
result.directories[dirType] = {
|
||||
path: fullPath,
|
||||
exists: true,
|
||||
type: stats.isDirectory() ? 'directory' : 'file'
|
||||
};
|
||||
} catch (error) {
|
||||
result.directories[dirType] = {
|
||||
path: fullPath,
|
||||
exists: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache() {
|
||||
super.clearCache();
|
||||
this.projectRootCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProjectProtocol;
|
||||
208
src/lib/core/resource/protocols/ResourceProtocol.js
Normal file
208
src/lib/core/resource/protocols/ResourceProtocol.js
Normal file
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 资源协议接口基类
|
||||
* 定义所有DPML资源协议的统一规范
|
||||
*/
|
||||
class ResourceProtocol {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {string} name - 协议名称
|
||||
* @param {object} options - 配置选项
|
||||
*/
|
||||
constructor(name, options = {}) {
|
||||
if (new.target === ResourceProtocol) {
|
||||
throw new Error('ResourceProtocol是抽象类,不能直接实例化');
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.options = options;
|
||||
this.cache = new Map();
|
||||
this.enableCache = options.enableCache !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议信息 - 需要子类实现
|
||||
* @returns {object} 协议信息
|
||||
*/
|
||||
getProtocolInfo() {
|
||||
throw new Error('子类必须实现 getProtocolInfo() 方法');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析资源路径 - 需要子类实现
|
||||
* @param {string} resourcePath - 原始资源路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 解析后的路径
|
||||
*/
|
||||
async resolvePath(resourcePath, queryParams) {
|
||||
throw new Error('子类必须实现 resolvePath() 方法');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载资源内容 - 需要子类实现
|
||||
* @param {string} resolvedPath - 解析后的路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 资源内容
|
||||
*/
|
||||
async loadContent(resolvedPath, queryParams) {
|
||||
throw new Error('子类必须实现 loadContent() 方法');
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证资源路径格式 - 可选实现
|
||||
* @param {string} resourcePath - 资源路径
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
validatePath(resourcePath) {
|
||||
return typeof resourcePath === 'string' && resourcePath.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的查询参数列表 - 可选实现
|
||||
* @returns {object} 参数说明
|
||||
*/
|
||||
getSupportedParams() {
|
||||
return {
|
||||
line: 'string - 行范围,如 "1-10"',
|
||||
format: 'string - 输出格式',
|
||||
cache: 'boolean - 是否缓存'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的资源解析入口点
|
||||
* @param {string} resourcePath - 资源路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 资源内容
|
||||
*/
|
||||
async resolve(resourcePath, queryParams) {
|
||||
// 1. 验证路径格式
|
||||
if (!this.validatePath(resourcePath)) {
|
||||
throw new Error(`无效的资源路径: ${resourcePath}`);
|
||||
}
|
||||
|
||||
// 2. 生成缓存键
|
||||
const cacheKey = this.generateCacheKey(resourcePath, queryParams);
|
||||
|
||||
// 3. 检查缓存
|
||||
if (this.enableCache && this.cache.has(cacheKey)) {
|
||||
return this.cache.get(cacheKey);
|
||||
}
|
||||
|
||||
// 4. 解析路径
|
||||
const resolvedPath = await this.resolvePath(resourcePath, queryParams);
|
||||
|
||||
// 5. 加载内容
|
||||
const content = await this.loadContent(resolvedPath, queryParams);
|
||||
|
||||
// 6. 应用通用查询参数过滤
|
||||
const filteredContent = this.applyCommonParams(content, queryParams);
|
||||
|
||||
// 7. 缓存结果
|
||||
if (this.enableCache) {
|
||||
this.cache.set(cacheKey, filteredContent);
|
||||
}
|
||||
|
||||
return filteredContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缓存键
|
||||
* @param {string} resourcePath - 资源路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {string} 缓存键
|
||||
*/
|
||||
generateCacheKey(resourcePath, queryParams) {
|
||||
const params = queryParams ? queryParams.getAll() : {};
|
||||
return `${this.name}:${resourcePath}:${JSON.stringify(params)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用通用查询参数
|
||||
* @param {string} content - 原始内容
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {string} 过滤后的内容
|
||||
*/
|
||||
applyCommonParams(content, queryParams) {
|
||||
if (!queryParams) {
|
||||
return content;
|
||||
}
|
||||
|
||||
let result = content;
|
||||
|
||||
// 应用行过滤
|
||||
if (queryParams.line) {
|
||||
result = this.applyLineFilter(result, queryParams.line);
|
||||
}
|
||||
|
||||
// 应用格式化(基础实现,子类可以重写)
|
||||
if (queryParams.format && queryParams.format !== 'text') {
|
||||
result = this.applyFormat(result, queryParams.format);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用行过滤
|
||||
* @param {string} content - 内容
|
||||
* @param {string} lineRange - 行范围,如 "5-10" 或 "5"
|
||||
* @returns {string} 过滤后的内容
|
||||
*/
|
||||
applyLineFilter(content, lineRange) {
|
||||
const lines = content.split('\n');
|
||||
|
||||
if (lineRange.includes('-')) {
|
||||
const [start, end] = lineRange.split('-').map(n => parseInt(n.trim(), 10));
|
||||
const startIndex = Math.max(0, start - 1);
|
||||
const endIndex = Math.min(lines.length, end);
|
||||
return lines.slice(startIndex, endIndex).join('\n');
|
||||
} else {
|
||||
const lineNum = parseInt(lineRange, 10);
|
||||
const lineIndex = lineNum - 1;
|
||||
return lines[lineIndex] || '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用格式化
|
||||
* @param {string} content - 内容
|
||||
* @param {string} format - 格式
|
||||
* @returns {string} 格式化后的内容
|
||||
*/
|
||||
applyFormat(content, format) {
|
||||
// 基础实现,子类可以重写
|
||||
switch (format) {
|
||||
case 'json':
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(content), null, 2);
|
||||
} catch {
|
||||
return content;
|
||||
}
|
||||
case 'trim':
|
||||
return content.trim();
|
||||
default:
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计
|
||||
* @returns {object} 缓存统计信息
|
||||
*/
|
||||
getCacheStats() {
|
||||
return {
|
||||
protocol: this.name,
|
||||
size: this.cache.size,
|
||||
enabled: this.enableCache
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResourceProtocol;
|
||||
299
src/lib/core/resource/protocols/UserProtocol.js
Normal file
299
src/lib/core/resource/protocols/UserProtocol.js
Normal file
@ -0,0 +1,299 @@
|
||||
const ResourceProtocol = require('./ResourceProtocol');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// 延迟加载platform-folders以处理可能的原生模块依赖
|
||||
let platformFolders = null;
|
||||
const getPlatformFolders = () => {
|
||||
if (!platformFolders) {
|
||||
try {
|
||||
platformFolders = require('platform-folders');
|
||||
} catch (error) {
|
||||
// 如果platform-folders不可用,回退到os.homedir()
|
||||
const os = require('os');
|
||||
platformFolders = {
|
||||
getHomeFolder: () => os.homedir(),
|
||||
getDesktopFolder: () => path.join(os.homedir(), 'Desktop'),
|
||||
getDocumentsFolder: () => path.join(os.homedir(), 'Documents'),
|
||||
getDownloadsFolder: () => path.join(os.homedir(), 'Downloads'),
|
||||
getMusicFolder: () => path.join(os.homedir(), 'Music'),
|
||||
getPicturesFolder: () => path.join(os.homedir(), 'Pictures'),
|
||||
getVideosFolder: () => path.join(os.homedir(), 'Videos')
|
||||
};
|
||||
console.warn('platform-folders不可用,使用os.homedir()回退方案');
|
||||
}
|
||||
}
|
||||
return platformFolders;
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户目录协议实现
|
||||
* 实现@user://协议,用于访问用户的标准目录(Documents、Desktop、Downloads等)
|
||||
*/
|
||||
class UserProtocol extends ResourceProtocol {
|
||||
constructor(options = {}) {
|
||||
super('user', options);
|
||||
|
||||
// 支持的用户目录映射
|
||||
this.userDirs = {
|
||||
'home': 'getHomeFolder',
|
||||
'desktop': 'getDesktopFolder',
|
||||
'documents': 'getDocumentsFolder',
|
||||
'downloads': 'getDownloadsFolder',
|
||||
'music': 'getMusicFolder',
|
||||
'pictures': 'getPicturesFolder',
|
||||
'videos': 'getVideosFolder'
|
||||
};
|
||||
|
||||
// 目录路径缓存
|
||||
this.dirCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议信息
|
||||
* @returns {object} 协议信息
|
||||
*/
|
||||
getProtocolInfo() {
|
||||
return {
|
||||
name: 'user',
|
||||
description: '用户目录协议,提供跨平台的用户标准目录访问',
|
||||
location: 'user://{directory}/{path}',
|
||||
examples: [
|
||||
'user://documents/notes.txt',
|
||||
'user://desktop/readme.md',
|
||||
'user://downloads/',
|
||||
'user://home/.bashrc'
|
||||
],
|
||||
supportedDirectories: Object.keys(this.userDirs),
|
||||
params: this.getSupportedParams()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的查询参数
|
||||
* @returns {object} 参数说明
|
||||
*/
|
||||
getSupportedParams() {
|
||||
return {
|
||||
...super.getSupportedParams(),
|
||||
exists: 'boolean - 仅返回存在的文件/目录',
|
||||
type: 'string - 过滤类型 (file|dir|both)'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户协议路径
|
||||
* @param {string} resourcePath - 资源路径
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
validatePath(resourcePath) {
|
||||
if (!super.validatePath(resourcePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析路径的第一部分(目录类型)
|
||||
const parts = resourcePath.split('/');
|
||||
const dirType = parts[0];
|
||||
|
||||
return this.userDirs.hasOwnProperty(dirType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析用户目录路径
|
||||
* @param {string} resourcePath - 原始资源路径,如 "documents/notes.txt"
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 解析后的绝对路径
|
||||
*/
|
||||
async resolvePath(resourcePath, queryParams) {
|
||||
const parts = resourcePath.split('/');
|
||||
const dirType = parts[0];
|
||||
const relativePath = parts.slice(1).join('/');
|
||||
|
||||
// 验证目录类型
|
||||
if (!this.userDirs[dirType]) {
|
||||
throw new Error(`不支持的用户目录类型: ${dirType}。支持的类型: ${Object.keys(this.userDirs).join(', ')}`);
|
||||
}
|
||||
|
||||
// 获取用户目录路径
|
||||
const userDirPath = await this.getUserDirectory(dirType);
|
||||
|
||||
// 如果没有相对路径,返回目录本身
|
||||
if (!relativePath) {
|
||||
return userDirPath;
|
||||
}
|
||||
|
||||
// 拼接完整路径
|
||||
const fullPath = path.join(userDirPath, relativePath);
|
||||
|
||||
// 安全检查:确保路径在用户目录内
|
||||
const resolvedPath = path.resolve(fullPath);
|
||||
const resolvedUserDir = path.resolve(userDirPath);
|
||||
|
||||
if (!resolvedPath.startsWith(resolvedUserDir)) {
|
||||
throw new Error(`安全错误:路径超出用户目录范围: ${resolvedPath}`);
|
||||
}
|
||||
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户目录路径
|
||||
* @param {string} dirType - 目录类型
|
||||
* @returns {Promise<string>} 目录路径
|
||||
*/
|
||||
async getUserDirectory(dirType) {
|
||||
// 检查缓存
|
||||
if (this.dirCache.has(dirType)) {
|
||||
return this.dirCache.get(dirType);
|
||||
}
|
||||
|
||||
const folders = getPlatformFolders();
|
||||
const methodName = this.userDirs[dirType];
|
||||
|
||||
if (!folders[methodName]) {
|
||||
throw new Error(`未找到用户目录获取方法: ${methodName}`);
|
||||
}
|
||||
|
||||
try {
|
||||
let dirPath;
|
||||
|
||||
// 调用platform-folders方法
|
||||
if (typeof folders[methodName] === 'function') {
|
||||
dirPath = await folders[methodName]();
|
||||
} else {
|
||||
dirPath = folders[methodName];
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
this.dirCache.set(dirType, dirPath);
|
||||
|
||||
return dirPath;
|
||||
} catch (error) {
|
||||
throw new Error(`获取用户目录失败 (${dirType}): ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载资源内容
|
||||
* @param {string} resolvedPath - 解析后的路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 资源内容
|
||||
*/
|
||||
async loadContent(resolvedPath, queryParams) {
|
||||
try {
|
||||
// 检查路径是否存在
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return await this.loadDirectoryContent(resolvedPath, queryParams);
|
||||
} else if (stats.isFile()) {
|
||||
return await this.loadFileContent(resolvedPath, queryParams);
|
||||
} else {
|
||||
throw new Error(`不支持的文件类型: ${resolvedPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// 如果设置了exists参数为false,返回空内容而不是错误
|
||||
if (queryParams && queryParams.get('exists') === 'false') {
|
||||
return '';
|
||||
}
|
||||
throw new Error(`文件或目录不存在: ${resolvedPath}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载文件内容
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 文件内容
|
||||
*/
|
||||
async loadFileContent(filePath, queryParams) {
|
||||
const encoding = queryParams?.get('encoding') || 'utf8';
|
||||
return await fs.readFile(filePath, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载目录内容
|
||||
* @param {string} dirPath - 目录路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 目录内容列表
|
||||
*/
|
||||
async loadDirectoryContent(dirPath, queryParams) {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
// 应用类型过滤
|
||||
const typeFilter = queryParams?.get('type');
|
||||
let filteredEntries = entries;
|
||||
|
||||
if (typeFilter) {
|
||||
filteredEntries = entries.filter(entry => {
|
||||
switch (typeFilter) {
|
||||
case 'file': return entry.isFile();
|
||||
case 'dir': return entry.isDirectory();
|
||||
case 'both': return true;
|
||||
default: return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化输出
|
||||
const format = queryParams?.get('format') || 'list';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return JSON.stringify(
|
||||
filteredEntries.map(entry => ({
|
||||
name: entry.name,
|
||||
type: entry.isDirectory() ? 'directory' : 'file',
|
||||
path: path.join(dirPath, entry.name)
|
||||
})),
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
case 'paths':
|
||||
return filteredEntries
|
||||
.map(entry => path.join(dirPath, entry.name))
|
||||
.join('\n');
|
||||
|
||||
case 'list':
|
||||
default:
|
||||
return filteredEntries
|
||||
.map(entry => {
|
||||
const type = entry.isDirectory() ? '[DIR]' : '[FILE]';
|
||||
return `${type} ${entry.name}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有支持的用户目录
|
||||
* @returns {Promise<object>} 目录信息
|
||||
*/
|
||||
async listUserDirectories() {
|
||||
const result = {};
|
||||
|
||||
for (const dirType of Object.keys(this.userDirs)) {
|
||||
try {
|
||||
result[dirType] = await this.getUserDirectory(dirType);
|
||||
} catch (error) {
|
||||
result[dirType] = { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除目录缓存
|
||||
*/
|
||||
clearCache() {
|
||||
super.clearCache();
|
||||
this.dirCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserProtocol;
|
||||
321
src/lib/core/resource/resourceManager.js
Normal file
321
src/lib/core/resource/resourceManager.js
Normal file
@ -0,0 +1,321 @@
|
||||
const ResourceProtocolParser = require('./resourceProtocolParser');
|
||||
const ResourceRegistry = require('./resourceRegistry');
|
||||
const { ResourceResult } = require('./types');
|
||||
const logger = require('../../utils/logger');
|
||||
|
||||
/**
|
||||
* 资源管理器
|
||||
* 基于DPML资源协议的统一资源管理入口
|
||||
*/
|
||||
class ResourceManager {
|
||||
constructor(options = {}) {
|
||||
this.parser = new ResourceProtocolParser();
|
||||
this.registry = new ResourceRegistry();
|
||||
this.workingDirectory = options.workingDirectory || process.cwd();
|
||||
|
||||
// 暂时直接实现简单的加载功能,后续可扩展为独立组件
|
||||
this.cache = new Map();
|
||||
this.enableCache = options.enableCache !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并获取资源
|
||||
* @param {string} resourceRef - DPML资源引用
|
||||
* @param {object} options - 选项
|
||||
* @returns {Promise<ResourceResult>} 资源结果
|
||||
*/
|
||||
async resolve(resourceRef, options = {}) {
|
||||
try {
|
||||
logger.debug(`Resolving resource: ${resourceRef}`);
|
||||
|
||||
// 1. 解析资源引用
|
||||
const parsed = this.parser.parse(resourceRef);
|
||||
logger.debug(`Parsed reference:`, parsed);
|
||||
|
||||
// 2. 通过注册表解析路径
|
||||
const resolvedPath = this.registry.resolve(parsed.protocol, parsed.path);
|
||||
logger.debug(`Resolved path: ${resolvedPath}`);
|
||||
|
||||
// 3. 处理可能的嵌套引用
|
||||
if (resolvedPath.startsWith('@')) {
|
||||
logger.debug(`Detected nested reference: ${resolvedPath}`);
|
||||
return await this.resolve(resolvedPath, options);
|
||||
}
|
||||
|
||||
// 4. 加载资源内容
|
||||
let content = await this.loadResource(resolvedPath, parsed, options);
|
||||
|
||||
// 5. 检查内容是否是另一个资源引用(用于嵌套引用)
|
||||
if (content.trim().startsWith('@')) {
|
||||
logger.debug(`Content is a nested reference: ${content.trim()}`);
|
||||
return await this.resolve(content.trim(), options);
|
||||
}
|
||||
|
||||
// 6. 创建结果
|
||||
const result = ResourceResult.success(content, {
|
||||
originalRef: resourceRef,
|
||||
resolvedPath: resolvedPath,
|
||||
protocol: parsed.protocol,
|
||||
loadingSemantics: parsed.loadingSemantics,
|
||||
queryParams: parsed.queryParams.getAll()
|
||||
});
|
||||
|
||||
result.sources = [resolvedPath];
|
||||
result.format = options.format || 'text';
|
||||
|
||||
logger.debug(`Resource resolved successfully`);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Failed to resolve resource ${resourceRef}:`, error.message);
|
||||
return ResourceResult.error(error, { originalRef: resourceRef });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解析多个资源
|
||||
* @param {string[]} resourceRefs - 资源引用列表
|
||||
* @param {object} options - 选项
|
||||
* @returns {Promise<ResourceResult[]>} 资源结果列表
|
||||
*/
|
||||
async resolveMultiple(resourceRefs, options = {}) {
|
||||
const results = [];
|
||||
|
||||
for (const ref of resourceRefs) {
|
||||
const result = await this.resolve(ref, options);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载单个资源
|
||||
* @param {string} resourcePath - 资源路径
|
||||
* @param {ParsedReference} parsed - 解析后的引用
|
||||
* @param {object} options - 选项
|
||||
* @returns {Promise<string>} 资源内容
|
||||
*/
|
||||
async loadResource(resourcePath, parsed, options = {}) {
|
||||
// 检查缓存
|
||||
const cacheKey = `${resourcePath}:${JSON.stringify(parsed.queryParams.getAll())}`;
|
||||
if (this.enableCache && this.cache.has(cacheKey)) {
|
||||
logger.debug(`Cache hit for: ${cacheKey}`);
|
||||
return this.cache.get(cacheKey);
|
||||
}
|
||||
|
||||
let content = '';
|
||||
|
||||
// 根据协议类型加载资源
|
||||
if (parsed.protocol === 'file' || resourcePath.startsWith('/') || resourcePath.includes('./')) {
|
||||
content = await this.loadFileResource(resourcePath, parsed.queryParams);
|
||||
} else if (parsed.protocol === 'http' || parsed.protocol === 'https') {
|
||||
content = await this.loadHttpResource(resourcePath, parsed.queryParams);
|
||||
} else if (parsed.protocol === 'prompt') {
|
||||
// prompt协议通过注册表已经解析为文件路径
|
||||
content = await this.loadFileResource(resourcePath, parsed.queryParams);
|
||||
} else {
|
||||
throw new Error(`Unsupported protocol: ${parsed.protocol}`);
|
||||
}
|
||||
|
||||
// 应用查询参数过滤
|
||||
content = this.applyQueryParams(content, parsed.queryParams);
|
||||
|
||||
// 缓存结果
|
||||
if (this.enableCache) {
|
||||
this.cache.set(cacheKey, content);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载文件资源
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 文件内容
|
||||
*/
|
||||
async loadFileResource(filePath, queryParams) {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// 处理相对路径
|
||||
let fullPath = filePath;
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
fullPath = path.resolve(this.workingDirectory, filePath);
|
||||
}
|
||||
|
||||
// 处理通配符
|
||||
if (fullPath.includes('*')) {
|
||||
return await this.loadGlobPattern(fullPath, queryParams);
|
||||
}
|
||||
|
||||
// 读取单个文件
|
||||
try {
|
||||
const content = await fs.readFile(fullPath, 'utf8');
|
||||
return content;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read file ${fullPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载通配符模式的文件
|
||||
* @param {string} pattern - 通配符模式
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 合并后的内容
|
||||
*/
|
||||
async loadGlobPattern(pattern, queryParams) {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { glob } = require('glob');
|
||||
|
||||
try {
|
||||
const files = await glob(pattern, { nodir: true });
|
||||
|
||||
if (files.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 排序文件
|
||||
files.sort();
|
||||
|
||||
const contents = [];
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await fs.readFile(file, 'utf8');
|
||||
const relativePath = path.relative(this.workingDirectory, file);
|
||||
|
||||
// 添加文件分隔符
|
||||
const separator = '='.repeat(80);
|
||||
const header = `### 文件: ${relativePath}`;
|
||||
contents.push(`${separator}\n${header}\n${separator}\n\n${content}`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to read file ${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return contents.join('\n\n');
|
||||
} catch (error) {
|
||||
throw new Error(`Glob pattern error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载HTTP资源
|
||||
* @param {string} url - URL地址
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {Promise<string>} 响应内容
|
||||
*/
|
||||
async loadHttpResource(url, queryParams) {
|
||||
// 简单实现,实际项目中可以使用axios等库
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = url.startsWith('https://') ? https : http;
|
||||
|
||||
client.get(url, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve(data);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
reject(new Error(`HTTP request failed: ${err.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用查询参数过滤
|
||||
* @param {string} content - 原始内容
|
||||
* @param {QueryParams} queryParams - 查询参数
|
||||
* @returns {string} 处理后的内容
|
||||
*/
|
||||
applyQueryParams(content, queryParams) {
|
||||
let result = content;
|
||||
|
||||
// 处理行范围过滤
|
||||
if (queryParams.line) {
|
||||
result = this.applyLineFilter(result, queryParams.line);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用行范围过滤
|
||||
* @param {string} content - 内容
|
||||
* @param {string} lineRange - 行范围 "5-10"
|
||||
* @returns {string} 过滤后的内容
|
||||
*/
|
||||
applyLineFilter(content, lineRange) {
|
||||
const lines = content.split('\n');
|
||||
|
||||
if (lineRange.includes('-')) {
|
||||
const [start, end] = lineRange.split('-').map(n => parseInt(n.trim()));
|
||||
if (!isNaN(start) && !isNaN(end)) {
|
||||
// 转换为0基索引,并确保范围有效
|
||||
const startIdx = Math.max(0, start - 1);
|
||||
const endIdx = Math.min(lines.length, end);
|
||||
return lines.slice(startIdx, endIdx).join('\n');
|
||||
}
|
||||
} else {
|
||||
const lineNum = parseInt(lineRange);
|
||||
if (!isNaN(lineNum) && lineNum > 0 && lineNum <= lines.length) {
|
||||
return lines[lineNum - 1];
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证资源引用
|
||||
* @param {string} resourceRef - 资源引用
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
isValidReference(resourceRef) {
|
||||
try {
|
||||
const parsed = this.parser.parse(resourceRef);
|
||||
return this.registry.validateReference(parsed.protocol, parsed.path);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册表信息
|
||||
* @param {string} protocol - 协议名(可选)
|
||||
* @returns {object} 注册表信息
|
||||
*/
|
||||
getRegistryInfo(protocol) {
|
||||
if (protocol) {
|
||||
return this.registry.getProtocolInfo(protocol);
|
||||
}
|
||||
return this.registry.getRegistryInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出可用协议
|
||||
* @returns {string[]} 协议列表
|
||||
*/
|
||||
listProtocols() {
|
||||
return this.registry.listProtocols();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
logger.debug('Resource cache cleared');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResourceManager;
|
||||
287
src/lib/core/resource/resourceProtocolParser.js
Normal file
287
src/lib/core/resource/resourceProtocolParser.js
Normal file
@ -0,0 +1,287 @@
|
||||
const {
|
||||
LoadingSemantics,
|
||||
ParsedReference,
|
||||
QueryParams,
|
||||
NestedReference
|
||||
} = require('./types');
|
||||
|
||||
/**
|
||||
* 资源协议解析器
|
||||
* 解析DPML资源引用语法:@protocol://path?params
|
||||
*/
|
||||
class ResourceProtocolParser {
|
||||
constructor() {
|
||||
// 资源引用正则表达式
|
||||
this.resourceRefRegex = /^(@[!?]?|@)([a-zA-Z][a-zA-Z0-9_-]*):(.+)$/;
|
||||
this.nestedRefRegex = /^(@[!?]?|@)([a-zA-Z][a-zA-Z0-9_-]*):(@[!?]?|@)?(.+)$/;
|
||||
this.queryParamsRegex = /^([^?]+)(?:\?(.+))?$/;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析资源引用
|
||||
* @param {string} resourceRef - 资源引用字符串
|
||||
* @returns {ParsedReference} 解析后的引用对象
|
||||
*/
|
||||
parse(resourceRef) {
|
||||
if (!resourceRef || typeof resourceRef !== 'string') {
|
||||
throw new Error('Invalid resource reference: must be a non-empty string');
|
||||
}
|
||||
|
||||
const trimmedRef = resourceRef.trim();
|
||||
if (!this.validateSyntax(trimmedRef)) {
|
||||
throw new Error(`Invalid resource reference syntax: ${trimmedRef}`);
|
||||
}
|
||||
|
||||
const parsed = new ParsedReference();
|
||||
parsed.originalRef = trimmedRef;
|
||||
|
||||
// 检查是否为嵌套引用
|
||||
if (this.isNestedReference(trimmedRef)) {
|
||||
return this.parseNestedReference(trimmedRef);
|
||||
}
|
||||
|
||||
// 解析基础引用
|
||||
return this.parseBasicReference(trimmedRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析基础资源引用
|
||||
* @param {string} ref - 基础引用
|
||||
* @returns {ParsedReference}
|
||||
*/
|
||||
parseBasicReference(ref) {
|
||||
const parsed = new ParsedReference();
|
||||
parsed.originalRef = ref;
|
||||
|
||||
// 解析加载语义
|
||||
parsed.loadingSemantics = this.parseLoadingSemantics(ref);
|
||||
|
||||
// 移除加载语义前缀
|
||||
const withoutSemantics = this.removeLoadingSemantics(ref);
|
||||
|
||||
// 匹配协议和路径
|
||||
const match = withoutSemantics.match(/^([a-zA-Z][a-zA-Z0-9_-]*):(.+)$/);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid protocol format: ${ref}`);
|
||||
}
|
||||
|
||||
parsed.protocol = match[1];
|
||||
let pathAndParams = match[2];
|
||||
|
||||
// 移除 :// 前缀(如果存在)
|
||||
if (pathAndParams.startsWith('//')) {
|
||||
pathAndParams = pathAndParams.substring(2);
|
||||
}
|
||||
|
||||
// 解析路径和查询参数
|
||||
const pathMatch = pathAndParams.match(this.queryParamsRegex);
|
||||
if (pathMatch) {
|
||||
parsed.path = pathMatch[1];
|
||||
if (pathMatch[2]) {
|
||||
parsed.queryParams = this.parseQueryParams(pathMatch[2]);
|
||||
}
|
||||
} else {
|
||||
parsed.path = pathAndParams;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析嵌套引用
|
||||
* @param {string} ref - 嵌套引用
|
||||
* @returns {ParsedReference}
|
||||
*/
|
||||
parseNestedReference(ref) {
|
||||
const parsed = new ParsedReference();
|
||||
parsed.originalRef = ref;
|
||||
parsed.isNested = true;
|
||||
|
||||
// 解析外层加载语义
|
||||
parsed.loadingSemantics = this.parseLoadingSemantics(ref);
|
||||
const withoutOuterSemantics = this.removeLoadingSemantics(ref);
|
||||
|
||||
// 匹配嵌套结构: protocol:@inner_protocol://path 或 protocol:inner_protocol://path
|
||||
const match = withoutOuterSemantics.match(/^([a-zA-Z][a-zA-Z0-9_-]*):(.+)$/);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid nested reference format: ${ref}`);
|
||||
}
|
||||
|
||||
parsed.protocol = match[1];
|
||||
let innerRef = match[2];
|
||||
|
||||
// 处理内层引用:移除可能的 :// 前缀,但保留 @ 前缀
|
||||
if (innerRef.startsWith('//')) {
|
||||
innerRef = innerRef.substring(2);
|
||||
}
|
||||
|
||||
// 确保内层引用有正确的格式
|
||||
if (!innerRef.startsWith('@')) {
|
||||
innerRef = '@' + innerRef;
|
||||
}
|
||||
|
||||
// 递归解析内层引用
|
||||
try {
|
||||
const innerParsed = this.parse(innerRef);
|
||||
|
||||
// 创建嵌套引用结构
|
||||
const nested = new NestedReference();
|
||||
nested.outer = parsed;
|
||||
nested.inner = innerParsed;
|
||||
nested.depth = this.calculateNestingDepth(innerParsed);
|
||||
|
||||
parsed.nestedRef = nested;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid nested inner reference: ${error.message}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析加载语义
|
||||
* @param {string} ref - 资源引用
|
||||
* @returns {string} 加载语义
|
||||
*/
|
||||
parseLoadingSemantics(ref) {
|
||||
if (ref.startsWith('@!')) {
|
||||
return LoadingSemantics.HOT_LOAD;
|
||||
} else if (ref.startsWith('@?')) {
|
||||
return LoadingSemantics.LAZY_LOAD;
|
||||
} else if (ref.startsWith('@')) {
|
||||
return LoadingSemantics.DEFAULT;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid loading semantics: ${ref}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除加载语义前缀
|
||||
* @param {string} ref - 资源引用
|
||||
* @returns {string} 移除前缀后的引用
|
||||
*/
|
||||
removeLoadingSemantics(ref) {
|
||||
if (ref.startsWith('@!') || ref.startsWith('@?')) {
|
||||
return ref.substring(2);
|
||||
} else if (ref.startsWith('@')) {
|
||||
return ref.substring(1);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析查询参数
|
||||
* @param {string} queryString - 查询字符串
|
||||
* @returns {QueryParams} 查询参数对象
|
||||
*/
|
||||
parseQueryParams(queryString) {
|
||||
const params = new QueryParams();
|
||||
|
||||
if (!queryString) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const pairs = queryString.split('&');
|
||||
for (const pair of pairs) {
|
||||
const [key, value] = pair.split('=').map(decodeURIComponent);
|
||||
|
||||
if (key) {
|
||||
// 处理特殊参数
|
||||
if (key === 'cache') {
|
||||
params.set(key, value === 'true' || value === '1');
|
||||
} else {
|
||||
params.set(key, value || '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证语法
|
||||
* @param {string} ref - 资源引用
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
validateSyntax(ref) {
|
||||
if (!ref) return false;
|
||||
|
||||
// 必须以@开头
|
||||
if (!ref.startsWith('@')) return false;
|
||||
|
||||
// 基本格式检查
|
||||
const withoutSemantics = this.removeLoadingSemantics(ref);
|
||||
return /^[a-zA-Z][a-zA-Z0-9_-]*:.+$/.test(withoutSemantics);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为嵌套引用
|
||||
* @param {string} ref - 资源引用
|
||||
* @returns {boolean} 是否为嵌套引用
|
||||
*/
|
||||
isNestedReference(ref) {
|
||||
const withoutSemantics = this.removeLoadingSemantics(ref);
|
||||
const colonIndex = withoutSemantics.indexOf(':');
|
||||
|
||||
if (colonIndex === -1) return false;
|
||||
|
||||
const afterColon = withoutSemantics.substring(colonIndex + 1);
|
||||
|
||||
// 检查是否包含内层引用 (@protocol: 或 protocol:)
|
||||
return afterColon.includes('@') || afterColon.includes('://');
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算嵌套深度
|
||||
* @param {ParsedReference} ref - 解析后的引用
|
||||
* @returns {number} 嵌套深度
|
||||
*/
|
||||
calculateNestingDepth(ref) {
|
||||
if (!ref.isNested) return 1;
|
||||
return 1 + this.calculateNestingDepth(ref.nestedRef.inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取协议名
|
||||
* @param {string} ref - 资源引用
|
||||
* @returns {string} 协议名
|
||||
*/
|
||||
extractProtocol(ref) {
|
||||
const withoutSemantics = this.removeLoadingSemantics(ref);
|
||||
const colonIndex = withoutSemantics.indexOf(':');
|
||||
return colonIndex > 0 ? withoutSemantics.substring(0, colonIndex) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取路径
|
||||
* @param {string} ref - 资源引用
|
||||
* @returns {string} 路径
|
||||
*/
|
||||
extractPath(ref) {
|
||||
const withoutSemantics = this.removeLoadingSemantics(ref);
|
||||
const colonIndex = withoutSemantics.indexOf(':');
|
||||
if (colonIndex === -1) return '';
|
||||
|
||||
let pathAndParams = withoutSemantics.substring(colonIndex + 1);
|
||||
|
||||
// 移除 :// 前缀(如果存在)
|
||||
if (pathAndParams.startsWith('//')) {
|
||||
pathAndParams = pathAndParams.substring(2);
|
||||
}
|
||||
|
||||
const queryIndex = pathAndParams.indexOf('?');
|
||||
return queryIndex > 0 ? pathAndParams.substring(0, queryIndex) : pathAndParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取查询参数字符串
|
||||
* @param {string} ref - 资源引用
|
||||
* @returns {string} 查询参数字符串
|
||||
*/
|
||||
extractParams(ref) {
|
||||
const queryIndex = ref.indexOf('?');
|
||||
return queryIndex > 0 ? ref.substring(queryIndex + 1) : '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResourceProtocolParser;
|
||||
248
src/lib/core/resource/resourceRegistry.js
Normal file
248
src/lib/core/resource/resourceRegistry.js
Normal file
@ -0,0 +1,248 @@
|
||||
const path = require('path');
|
||||
const { ProtocolInfo } = require('./types');
|
||||
|
||||
/**
|
||||
* 资源注册表管理器
|
||||
* 管理资源协议和ID到路径的映射
|
||||
*/
|
||||
class ResourceRegistry {
|
||||
constructor() {
|
||||
this.builtinRegistry = new Map();
|
||||
this.customRegistry = new Map();
|
||||
this.loadBuiltinRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载内置注册表
|
||||
*/
|
||||
loadBuiltinRegistry() {
|
||||
// PromptX 内置资源协议
|
||||
const promptProtocol = new ProtocolInfo();
|
||||
promptProtocol.name = 'prompt';
|
||||
promptProtocol.description = 'PromptX内置提示词资源协议';
|
||||
promptProtocol.location = 'prompt://{resource_id}';
|
||||
promptProtocol.registry = new Map([
|
||||
['protocols', '@package://prompt/protocol/**/*.md'],
|
||||
['core', '@package://prompt/core/**/*.md'],
|
||||
['domain', '@package://prompt/domain/**/*.md'],
|
||||
['resource', '@package://prompt/resource/**/*.md'],
|
||||
['bootstrap', '@package://bootstrap.md']
|
||||
]);
|
||||
this.builtinRegistry.set('prompt', promptProtocol);
|
||||
|
||||
// File 协议(标准协议,无需注册表)
|
||||
const fileProtocol = new ProtocolInfo();
|
||||
fileProtocol.name = 'file';
|
||||
fileProtocol.description = '文件系统资源协议';
|
||||
fileProtocol.location = 'file://{absolute_or_relative_path}';
|
||||
fileProtocol.params = {
|
||||
line: 'string - 行范围,如 "1-10"',
|
||||
encoding: 'string - 文件编码,默认 utf8'
|
||||
};
|
||||
this.builtinRegistry.set('file', fileProtocol);
|
||||
|
||||
// Memory 协议(项目记忆系统)
|
||||
const memoryProtocol = new ProtocolInfo();
|
||||
memoryProtocol.name = 'memory';
|
||||
memoryProtocol.description = '项目记忆系统协议';
|
||||
memoryProtocol.location = 'memory://{resource_id}';
|
||||
memoryProtocol.registry = new Map([
|
||||
['declarative', '@project://.promptx/memory/declarative.md'],
|
||||
['procedural', '@project://.promptx/memory/procedural.md'],
|
||||
['episodic', '@project://.promptx/memory/episodic.md'],
|
||||
['semantic', '@project://.promptx/memory/semantic.md']
|
||||
]);
|
||||
this.builtinRegistry.set('memory', memoryProtocol);
|
||||
|
||||
// HTTP/HTTPS 协议(标准协议)
|
||||
const httpProtocol = new ProtocolInfo();
|
||||
httpProtocol.name = 'http';
|
||||
httpProtocol.description = 'HTTP网络资源协议';
|
||||
httpProtocol.location = 'http://{url}';
|
||||
httpProtocol.params = {
|
||||
format: 'string - 响应格式,如 json, text',
|
||||
timeout: 'number - 超时时间(毫秒)',
|
||||
cache: 'boolean - 是否缓存响应'
|
||||
};
|
||||
this.builtinRegistry.set('http', httpProtocol);
|
||||
this.builtinRegistry.set('https', httpProtocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析资源ID到具体路径
|
||||
* @param {string} protocol - 协议名
|
||||
* @param {string} resourceId - 资源ID
|
||||
* @returns {string} 解析后的路径
|
||||
*/
|
||||
resolve(protocol, resourceId) {
|
||||
const protocolInfo = this.getProtocolInfo(protocol);
|
||||
|
||||
if (!protocolInfo) {
|
||||
throw new Error(`Unknown protocol: ${protocol}`);
|
||||
}
|
||||
|
||||
// 如果协议有注册表,尝试解析ID
|
||||
if (protocolInfo.registry && protocolInfo.registry.size > 0) {
|
||||
const resolvedPath = protocolInfo.registry.get(resourceId);
|
||||
if (resolvedPath) {
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
// 如果在注册表中找不到,但这是一个有注册表的协议,抛出错误
|
||||
throw new Error(`Resource ID '${resourceId}' not found in ${protocol} protocol registry`);
|
||||
}
|
||||
|
||||
// 对于没有注册表的协议(如file, http),直接返回资源ID作为路径
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册新的协议或更新现有协议
|
||||
* @param {string} protocolName - 协议名
|
||||
* @param {object} protocolDefinition - 协议定义
|
||||
*/
|
||||
register(protocolName, protocolDefinition) {
|
||||
const protocolInfo = new ProtocolInfo();
|
||||
protocolInfo.name = protocolName;
|
||||
protocolInfo.description = protocolDefinition.description || '';
|
||||
protocolInfo.location = protocolDefinition.location || '';
|
||||
protocolInfo.params = protocolDefinition.params || {};
|
||||
|
||||
// 设置注册表映射
|
||||
if (protocolDefinition.registry) {
|
||||
protocolInfo.registry = new Map();
|
||||
for (const [id, path] of Object.entries(protocolDefinition.registry)) {
|
||||
protocolInfo.registry.set(id, path);
|
||||
}
|
||||
}
|
||||
|
||||
this.customRegistry.set(protocolName, protocolInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议信息
|
||||
* @param {string} protocolName - 协议名
|
||||
* @returns {ProtocolInfo|null} 协议信息
|
||||
*/
|
||||
getProtocolInfo(protocolName) {
|
||||
return this.customRegistry.get(protocolName) ||
|
||||
this.builtinRegistry.get(protocolName) ||
|
||||
null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有可用协议
|
||||
* @returns {string[]} 协议名列表
|
||||
*/
|
||||
listProtocols() {
|
||||
const protocols = new Set();
|
||||
|
||||
for (const protocol of this.builtinRegistry.keys()) {
|
||||
protocols.add(protocol);
|
||||
}
|
||||
|
||||
for (const protocol of this.customRegistry.keys()) {
|
||||
protocols.add(protocol);
|
||||
}
|
||||
|
||||
return Array.from(protocols).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查协议是否存在
|
||||
* @param {string} protocolName - 协议名
|
||||
* @returns {boolean} 是否存在
|
||||
*/
|
||||
hasProtocol(protocolName) {
|
||||
return this.builtinRegistry.has(protocolName) ||
|
||||
this.customRegistry.has(protocolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议的注册表内容
|
||||
* @param {string} protocolName - 协议名
|
||||
* @returns {Map|null} 注册表映射
|
||||
*/
|
||||
getProtocolRegistry(protocolName) {
|
||||
const protocolInfo = this.getProtocolInfo(protocolName);
|
||||
return protocolInfo ? protocolInfo.registry : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出协议的所有可用资源ID
|
||||
* @param {string} protocolName - 协议名
|
||||
* @returns {string[]} 资源ID列表
|
||||
*/
|
||||
listProtocolResources(protocolName) {
|
||||
const registry = this.getProtocolRegistry(protocolName);
|
||||
return registry ? Array.from(registry.keys()) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开通配符模式
|
||||
* @param {string} pattern - 通配符模式
|
||||
* @returns {string[]} 展开后的路径列表
|
||||
*/
|
||||
expandWildcards(pattern) {
|
||||
// 这里暂时返回原样,实际实现需要结合文件系统
|
||||
// 在ResourceLocator中会有更详细的实现
|
||||
return [pattern];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证资源引用
|
||||
* @param {string} protocol - 协议名
|
||||
* @param {string} resourceId - 资源ID
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
validateReference(protocol, resourceId) {
|
||||
if (!this.hasProtocol(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const protocolInfo = this.getProtocolInfo(protocol);
|
||||
|
||||
// 如果有注册表,检查ID是否存在
|
||||
if (protocolInfo.registry && protocolInfo.registry.size > 0) {
|
||||
return protocolInfo.registry.has(resourceId);
|
||||
}
|
||||
|
||||
// 对于没有注册表的协议,只要协议存在就认为有效
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有注册表信息(用于调试)
|
||||
* @returns {object} 注册表信息
|
||||
*/
|
||||
getRegistryInfo() {
|
||||
const info = {
|
||||
builtin: {},
|
||||
custom: {}
|
||||
};
|
||||
|
||||
for (const [name, protocol] of this.builtinRegistry) {
|
||||
info.builtin[name] = {
|
||||
description: protocol.description,
|
||||
location: protocol.location,
|
||||
params: protocol.params,
|
||||
registrySize: protocol.registry ? protocol.registry.size : 0,
|
||||
resources: protocol.registry ? Array.from(protocol.registry.keys()) : []
|
||||
};
|
||||
}
|
||||
|
||||
for (const [name, protocol] of this.customRegistry) {
|
||||
info.custom[name] = {
|
||||
description: protocol.description,
|
||||
location: protocol.location,
|
||||
params: protocol.params,
|
||||
registrySize: protocol.registry ? protocol.registry.size : 0,
|
||||
resources: protocol.registry ? Array.from(protocol.registry.keys()) : []
|
||||
};
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResourceRegistry;
|
||||
236
src/lib/core/resource/types.js
Normal file
236
src/lib/core/resource/types.js
Normal file
@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 资源模块基础数据类型定义
|
||||
* 基于DPML资源协议标准
|
||||
*/
|
||||
|
||||
/**
|
||||
* 加载语义枚举
|
||||
*/
|
||||
const LoadingSemantics = {
|
||||
DEFAULT: 'default', // @ - AI自行决定加载时机
|
||||
HOT_LOAD: 'hot_load', // @! - 立即加载
|
||||
LAZY_LOAD: 'lazy_load' // @? - 懒加载
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析后的资源引用
|
||||
*/
|
||||
class ParsedReference {
|
||||
constructor() {
|
||||
this.loadingSemantics = LoadingSemantics.DEFAULT;
|
||||
this.protocol = '';
|
||||
this.path = '';
|
||||
this.queryParams = new QueryParams();
|
||||
this.isNested = false;
|
||||
this.nestedRef = null;
|
||||
this.originalRef = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数
|
||||
*/
|
||||
class QueryParams {
|
||||
constructor() {
|
||||
this.line = null; // 行范围 "5-10"
|
||||
this.format = null; // 输出格式 "json"
|
||||
this.cache = null; // 是否缓存,默认为null表示未设置
|
||||
this.params = new Map(); // 其他参数
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置参数
|
||||
*/
|
||||
set(key, value) {
|
||||
if (['line', 'format', 'cache'].includes(key)) {
|
||||
this[key] = value;
|
||||
} else {
|
||||
this.params.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数
|
||||
*/
|
||||
get(key) {
|
||||
if (['line', 'format', 'cache'].includes(key)) {
|
||||
return this[key];
|
||||
}
|
||||
return this.params.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有参数
|
||||
*/
|
||||
getAll() {
|
||||
const result = {};
|
||||
|
||||
// 只添加非null的内置参数
|
||||
if (this.line !== null) {
|
||||
result.line = this.line;
|
||||
}
|
||||
if (this.format !== null) {
|
||||
result.format = this.format;
|
||||
}
|
||||
if (this.cache !== null) {
|
||||
result.cache = this.cache;
|
||||
}
|
||||
|
||||
// 添加其他参数
|
||||
for (const [key, value] of this.params) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为字符串用于缓存键
|
||||
*/
|
||||
toString() {
|
||||
const params = [];
|
||||
|
||||
// 添加内置参数
|
||||
if (this.line !== null) {
|
||||
params.push(`line=${this.line}`);
|
||||
}
|
||||
if (this.format !== null) {
|
||||
params.push(`format=${this.format}`);
|
||||
}
|
||||
if (this.cache !== null) {
|
||||
params.push(`cache=${this.cache}`);
|
||||
}
|
||||
|
||||
// 添加其他参数(按键排序以确保一致性)
|
||||
const sortedParams = Array.from(this.params.entries()).sort();
|
||||
for (const [key, value] of sortedParams) {
|
||||
params.push(`${key}=${value}`);
|
||||
}
|
||||
|
||||
return params.join('&');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 嵌套引用
|
||||
*/
|
||||
class NestedReference {
|
||||
constructor() {
|
||||
this.outer = null; // 外层引用
|
||||
this.inner = null; // 内层引用
|
||||
this.depth = 0; // 嵌套深度
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源内容
|
||||
*/
|
||||
class ResourceContent {
|
||||
constructor(path, content, metadata = {}) {
|
||||
this.path = path;
|
||||
this.content = content;
|
||||
this.metadata = metadata;
|
||||
this.relativePath = '';
|
||||
this.lastModified = null;
|
||||
this.size = content ? content.length : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 懒加载资源
|
||||
*/
|
||||
class LazyResource {
|
||||
constructor(path, loader) {
|
||||
this.path = path;
|
||||
this.loader = loader;
|
||||
this.loaded = false;
|
||||
this._content = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载资源
|
||||
*/
|
||||
async load() {
|
||||
if (!this.loaded) {
|
||||
this._content = await this.loader(this.path);
|
||||
this.loaded = true;
|
||||
}
|
||||
return this._content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理后的结果
|
||||
*/
|
||||
class ProcessedResult {
|
||||
constructor() {
|
||||
this.content = '';
|
||||
this.metadata = {};
|
||||
this.format = 'text';
|
||||
this.sources = [];
|
||||
this.cached = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 最终资源结果
|
||||
*/
|
||||
class ResourceResult {
|
||||
constructor() {
|
||||
this.content = '';
|
||||
this.metadata = {};
|
||||
this.sources = [];
|
||||
this.format = 'text';
|
||||
this.cached = false;
|
||||
this.loadTime = Date.now();
|
||||
this.success = true;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功结果
|
||||
*/
|
||||
static success(content, metadata = {}) {
|
||||
const result = new ResourceResult();
|
||||
result.content = content;
|
||||
result.metadata = metadata;
|
||||
result.success = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误结果
|
||||
*/
|
||||
static error(error, metadata = {}) {
|
||||
const result = new ResourceResult();
|
||||
result.success = false;
|
||||
result.error = error;
|
||||
result.metadata = metadata;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源协议信息
|
||||
*/
|
||||
class ProtocolInfo {
|
||||
constructor() {
|
||||
this.name = '';
|
||||
this.description = '';
|
||||
this.location = ''; // EBNF路径定义
|
||||
this.params = {}; // 支持的参数
|
||||
this.registry = new Map(); // ID到路径的映射
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LoadingSemantics,
|
||||
ParsedReference,
|
||||
QueryParams,
|
||||
NestedReference,
|
||||
ResourceContent,
|
||||
LazyResource,
|
||||
ProcessedResult,
|
||||
ResourceResult,
|
||||
ProtocolInfo
|
||||
};
|
||||
42
src/lib/index.js
Normal file
42
src/lib/index.js
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* PromptX 核心库
|
||||
*
|
||||
* 提供AI prompt框架的核心功能,包括:
|
||||
* - 四大核心命令实现
|
||||
* - 资源管理和路径解析
|
||||
* - 格式化输出和用户交互
|
||||
* - 配置管理和错误处理
|
||||
*/
|
||||
|
||||
// 核心命令模块
|
||||
const commands = {
|
||||
hello: require('./commands/hello'),
|
||||
learn: require('./commands/learn'),
|
||||
recall: require('./commands/recall'),
|
||||
remember: require('./commands/remember')
|
||||
};
|
||||
|
||||
// 核心功能模块
|
||||
const core = {
|
||||
ResourceManager: require('./core/resource-manager'),
|
||||
PathResolver: require('./core/path-resolver'),
|
||||
OutputFormatter: require('./core/output-formatter')
|
||||
};
|
||||
|
||||
// 工具模块
|
||||
const utils = {
|
||||
logger: require('./utils/logger'),
|
||||
validator: require('./utils/validator'),
|
||||
config: require('./utils/config')
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
commands,
|
||||
core,
|
||||
utils,
|
||||
|
||||
// 便捷导出
|
||||
...commands,
|
||||
...core,
|
||||
...utils
|
||||
};
|
||||
90
src/lib/utils/logger.js
Normal file
90
src/lib/utils/logger.js
Normal file
@ -0,0 +1,90 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* 日志工具
|
||||
* 提供彩色和格式化的日志输出
|
||||
*/
|
||||
class Logger {
|
||||
constructor(options = {}) {
|
||||
this.silent = options.silent || false;
|
||||
this.prefix = options.prefix || 'PromptX';
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息日志
|
||||
*/
|
||||
info(message, ...args) {
|
||||
if (this.silent) return;
|
||||
console.log(chalk.blue('ℹ'), message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功日志
|
||||
*/
|
||||
success(message, ...args) {
|
||||
if (this.silent) return;
|
||||
console.log(chalk.green('✅'), message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 警告日志
|
||||
*/
|
||||
warn(message, ...args) {
|
||||
if (this.silent) return;
|
||||
console.log(chalk.yellow('⚠️'), chalk.yellow(message), ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误日志
|
||||
*/
|
||||
error(message, ...args) {
|
||||
if (this.silent) return;
|
||||
console.error(chalk.red('❌'), chalk.red(message), ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志
|
||||
*/
|
||||
debug(message, ...args) {
|
||||
if (this.silent || !process.env.DEBUG) return;
|
||||
console.log(chalk.gray('🐛'), chalk.gray(message), ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤日志(用于显示进度)
|
||||
*/
|
||||
step(message, ...args) {
|
||||
if (this.silent) return;
|
||||
console.log(chalk.cyan('▶️'), message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接输出(不带前缀)
|
||||
*/
|
||||
log(message, ...args) {
|
||||
if (this.silent) return;
|
||||
console.log(message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 空行
|
||||
*/
|
||||
newLine() {
|
||||
if (this.silent) return;
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 分隔线
|
||||
*/
|
||||
separator(char = '=', length = 80) {
|
||||
if (this.silent) return;
|
||||
console.log(chalk.gray(char.repeat(length)));
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
const logger = new Logger();
|
||||
|
||||
module.exports = logger;
|
||||
module.exports.Logger = Logger;
|
||||
237
src/tests/README.md
Normal file
237
src/tests/README.md
Normal file
@ -0,0 +1,237 @@
|
||||
# PromptX 测试指南
|
||||
|
||||
本文档介绍 PromptX 项目的测试规范、命名规则和执行方式。
|
||||
|
||||
## 测试文件命名规范
|
||||
|
||||
### 命名格式
|
||||
所有测试文件必须使用 **驼峰命名法(camelCase)** 并明确标识测试类型:
|
||||
|
||||
```
|
||||
{模块名}.{测试类型}.test.js
|
||||
```
|
||||
|
||||
### 测试类型
|
||||
- **unit**: 单元测试 - 测试单个函数或类的功能
|
||||
- **integration**: 集成测试 - 测试多个组件之间的协作
|
||||
- **e2e**: 端到端测试 - 测试完整的用户工作流
|
||||
|
||||
### 示例
|
||||
```
|
||||
resourceProtocolParser.unit.test.js
|
||||
resourceRegistry.unit.test.js
|
||||
resourceManager.integration.test.js
|
||||
promptxCli.e2e.test.js
|
||||
```
|
||||
|
||||
## 测试目录结构
|
||||
|
||||
```
|
||||
src/tests/
|
||||
├── setup.js # 全局测试配置
|
||||
├── fixtures/ # 测试固定数据
|
||||
│ └── testResources.js # 测试资源工厂
|
||||
├── __mocks__/ # 模拟对象
|
||||
├── core/
|
||||
│ └── resource/
|
||||
│ ├── resourceProtocolParser.unit.test.js
|
||||
│ ├── resourceRegistry.unit.test.js
|
||||
│ └── resourceManager.integration.test.js
|
||||
└── commands/
|
||||
└── promptxCli.e2e.test.js
|
||||
```
|
||||
|
||||
## 执行测试
|
||||
|
||||
### 运行所有测试
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### 分类运行测试
|
||||
```bash
|
||||
# 单元测试
|
||||
npm run test:unit
|
||||
|
||||
# 集成测试
|
||||
npm run test:integration
|
||||
|
||||
# 端到端测试
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
```bash
|
||||
# 监听模式运行所有测试
|
||||
npm run test:watch
|
||||
|
||||
# 监听模式运行单元测试
|
||||
npm run test:watchUnit
|
||||
|
||||
# 监听模式运行集成测试
|
||||
npm run test:watchIntegration
|
||||
```
|
||||
|
||||
### 覆盖率测试
|
||||
```bash
|
||||
# 生成覆盖率报告
|
||||
npm run test:coverage
|
||||
|
||||
# 分类生成覆盖率报告
|
||||
npm run test:coverageUnit
|
||||
npm run test:coverageIntegration
|
||||
npm run test:coverageE2e
|
||||
```
|
||||
|
||||
### CI/CD 测试
|
||||
```bash
|
||||
# 持续集成环境测试
|
||||
npm run test:ci
|
||||
```
|
||||
|
||||
### 调试测试
|
||||
```bash
|
||||
# 调试模式运行测试
|
||||
npm run test:debug
|
||||
```
|
||||
|
||||
## 测试编写规范
|
||||
|
||||
### 单元测试 (*.unit.test.js)
|
||||
- 测试单个函数、类或模块
|
||||
- 使用模拟(mock)隔离外部依赖
|
||||
- 快速执行,无外部资源依赖
|
||||
- 覆盖边界条件和错误场景
|
||||
|
||||
```javascript
|
||||
describe('ResourceProtocolParser - Unit Tests', () => {
|
||||
let parser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new ResourceProtocolParser();
|
||||
});
|
||||
|
||||
describe('基础语法解析', () => {
|
||||
test('应该解析基本的资源引用', () => {
|
||||
const result = parser.parse('@promptx://protocols');
|
||||
expect(result.protocol).toBe('promptx');
|
||||
expect(result.path).toBe('protocols');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 集成测试 (*.integration.test.js)
|
||||
- 测试多个组件之间的协作
|
||||
- 可使用真实的文件系统和临时资源
|
||||
- 测试完整的数据流和业务逻辑
|
||||
- 关注组件间接口和数据传递
|
||||
|
||||
```javascript
|
||||
describe('ResourceManager - Integration Tests', () => {
|
||||
let manager;
|
||||
let tempDir;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-test-'));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rmdir(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
test('应该解析并加载本地文件', async () => {
|
||||
const result = await manager.resolve('@file://test.md');
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 端到端测试 (*.e2e.test.js)
|
||||
- 测试完整的用户工作流
|
||||
- 通过CLI接口测试实际使用场景
|
||||
- 模拟真实的用户交互
|
||||
- 验证系统的整体行为
|
||||
|
||||
```javascript
|
||||
describe('PromptX CLI - E2E Tests', () => {
|
||||
function runCommand(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('node', [CLI_PATH, ...args]);
|
||||
// ... 实现命令执行逻辑
|
||||
});
|
||||
}
|
||||
|
||||
test('应该支持完整的AI认知循环', async () => {
|
||||
const helloResult = await runCommand(['hello']);
|
||||
expect(helloResult.code).toBe(0);
|
||||
|
||||
const learnResult = await runCommand(['learn', '@file://bootstrap.md']);
|
||||
expect(learnResult.code).toBe(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 测试工具和辅助函数
|
||||
|
||||
### 全局测试工具
|
||||
在 `setup.js` 中定义的全局工具:
|
||||
|
||||
```javascript
|
||||
// 等待函数
|
||||
await testUtils.sleep(1000);
|
||||
|
||||
// 延迟Promise
|
||||
const result = await testUtils.delayed('value', 100);
|
||||
|
||||
// 延迟拒绝
|
||||
await expect(testUtils.delayedReject(new Error('test'), 100)).rejects.toThrow();
|
||||
```
|
||||
|
||||
### 自定义断言
|
||||
```javascript
|
||||
// 验证DPML资源引用
|
||||
expect('@promptx://protocols').toBeValidDpmlReference();
|
||||
|
||||
// 验证对象属性
|
||||
expect(result).toHaveRequiredProperties(['protocol', 'path']);
|
||||
```
|
||||
|
||||
### 测试资源工厂
|
||||
使用 `TestResourceFactory` 创建测试数据:
|
||||
|
||||
```javascript
|
||||
const { createTestFactory } = require('../fixtures/testResources');
|
||||
|
||||
const factory = createTestFactory();
|
||||
const tempDir = await factory.createTempDir();
|
||||
const { structure, files } = await factory.createPromptXStructure(tempDir);
|
||||
```
|
||||
|
||||
## 覆盖率要求
|
||||
|
||||
项目设置了以下覆盖率阈值:
|
||||
- 分支覆盖率: 80%
|
||||
- 函数覆盖率: 80%
|
||||
- 行覆盖率: 80%
|
||||
- 语句覆盖率: 80%
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **命名清晰**: 测试名称应清楚描述测试的功能
|
||||
2. **独立性**: 每个测试应该独立运行,不依赖其他测试
|
||||
3. **快速执行**: 单元测试应该快速执行
|
||||
4. **完整清理**: 集成测试和E2E测试应清理临时资源
|
||||
5. **错误场景**: 不仅测试正常情况,也要测试错误和边界情况
|
||||
6. **文档化**: 复杂的测试逻辑应有适当的注释说明
|
||||
|
||||
## 持续集成
|
||||
|
||||
在 CI/CD 环境中,测试按以下顺序执行:
|
||||
1. 代码格式检查 (`npm run lint`)
|
||||
2. 单元测试 (`npm run test:unit`)
|
||||
3. 集成测试 (`npm run test:integration`)
|
||||
4. 端到端测试 (`npm run test:e2e`)
|
||||
5. 覆盖率检查
|
||||
|
||||
只有所有测试通过且覆盖率达标,才能合并代码或发布版本。
|
||||
313
src/tests/commands/promptxCli.e2e.test.js
Normal file
313
src/tests/commands/promptxCli.e2e.test.js
Normal file
@ -0,0 +1,313 @@
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
describe('PromptX CLI - E2E Tests', () => {
|
||||
const CLI_PATH = path.resolve(__dirname, '../../bin/promptx.js');
|
||||
let tempDir;
|
||||
|
||||
beforeAll(async () => {
|
||||
// 创建临时测试目录
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-e2e-'));
|
||||
|
||||
// 创建测试项目结构
|
||||
const promptDir = path.join(tempDir, 'prompt');
|
||||
await fs.mkdir(promptDir, { recursive: true });
|
||||
|
||||
const coreDir = path.join(promptDir, 'core');
|
||||
await fs.mkdir(coreDir, { recursive: true });
|
||||
|
||||
// 创建测试文件
|
||||
await fs.writeFile(
|
||||
path.join(coreDir, 'test-core.md'),
|
||||
'# Core Prompt\n\n这是核心提示词。'
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, 'bootstrap.md'),
|
||||
'# Bootstrap\n\n这是启动文件。'
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理临时目录
|
||||
await fs.rm(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* 运行CLI命令的辅助函数
|
||||
*/
|
||||
function runCommand(args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('node', [CLI_PATH, ...args], {
|
||||
cwd: options.cwd || tempDir,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env, ...options.env }
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({
|
||||
code,
|
||||
stdout,
|
||||
stderr
|
||||
});
|
||||
});
|
||||
|
||||
child.on('error', reject);
|
||||
|
||||
// 如果需要输入,发送输入数据
|
||||
if (options.input) {
|
||||
child.stdin.write(options.input);
|
||||
child.stdin.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('基础命令测试', () => {
|
||||
test('应该显示帮助信息', async () => {
|
||||
const result = await runCommand(['--help']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('PromptX CLI');
|
||||
expect(result.stdout).toContain('Usage:');
|
||||
expect(result.stdout).toContain('hello');
|
||||
expect(result.stdout).toContain('learn');
|
||||
expect(result.stdout).toContain('recall');
|
||||
expect(result.stdout).toContain('remember');
|
||||
});
|
||||
|
||||
test('应该显示版本信息', async () => {
|
||||
const result = await runCommand(['--version']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hello 命令 - 系统入口', () => {
|
||||
test('应该显示欢迎信息', async () => {
|
||||
const result = await runCommand(['hello']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('👋');
|
||||
expect(result.stdout).toContain('PromptX');
|
||||
expect(result.stdout).toContain('AI助手');
|
||||
});
|
||||
|
||||
test('应该支持个性化问候', async () => {
|
||||
const result = await runCommand(['hello', '--name', '张三']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('张三');
|
||||
});
|
||||
|
||||
test('应该显示系统状态', async () => {
|
||||
const result = await runCommand(['hello', '--status']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toMatch(/工作目录:/);
|
||||
expect(result.stdout).toMatch(/资源协议:/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('learn 命令 - 资源学习', () => {
|
||||
test('应该加载prompt协议资源', async () => {
|
||||
const result = await runCommand(['learn', '@prompt://bootstrap']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('学习资源');
|
||||
expect(result.stdout).toContain('@prompt://bootstrap');
|
||||
});
|
||||
|
||||
test('应该加载文件资源', async () => {
|
||||
const result = await runCommand(['learn', '@file://bootstrap.md']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('这是启动文件');
|
||||
});
|
||||
|
||||
test('应该支持带参数的资源加载', async () => {
|
||||
const result = await runCommand(['learn', '@file://bootstrap.md?line=1']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('# Bootstrap');
|
||||
expect(result.stdout).not.toContain('这是启动文件');
|
||||
});
|
||||
|
||||
test('应该处理无效资源引用', async () => {
|
||||
const result = await runCommand(['learn', 'invalid-reference']);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('资源引用格式错误');
|
||||
});
|
||||
|
||||
test('应该处理不存在的文件', async () => {
|
||||
const result = await runCommand(['learn', '@file://nonexistent.md']);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('Failed to read file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('recall 命令 - 记忆检索', () => {
|
||||
test('应该显示基本的记忆检索功能', async () => {
|
||||
const result = await runCommand(['recall', 'test']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('🔍 正在检索记忆');
|
||||
});
|
||||
|
||||
test('应该支持记忆类型指定', async () => {
|
||||
const result = await runCommand(['recall', 'test', '--type', 'semantic']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('semantic');
|
||||
});
|
||||
|
||||
test('应该支持模糊搜索', async () => {
|
||||
const result = await runCommand(['recall', 'test', '--fuzzy']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('模糊搜索');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remember 命令 - 记忆存储', () => {
|
||||
test('应该存储新的记忆', async () => {
|
||||
const result = await runCommand(['remember', 'test-memory', 'This is a test memory']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('🧠 正在存储记忆');
|
||||
expect(result.stdout).toContain('test-memory');
|
||||
});
|
||||
|
||||
test('应该支持记忆类型指定', async () => {
|
||||
const result = await runCommand([
|
||||
'remember',
|
||||
'procedure-test',
|
||||
'How to test',
|
||||
'--type',
|
||||
'procedural'
|
||||
]);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('procedural');
|
||||
});
|
||||
|
||||
test('应该支持标签添加', async () => {
|
||||
const result = await runCommand([
|
||||
'remember',
|
||||
'tagged-memory',
|
||||
'Tagged content',
|
||||
'--tags',
|
||||
'test,example'
|
||||
]);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('tags');
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理和边界情况', () => {
|
||||
test('应该处理无效命令', async () => {
|
||||
const result = await runCommand(['invalid-command']);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('Unknown command');
|
||||
});
|
||||
|
||||
test('应该处理缺少参数的情况', async () => {
|
||||
const result = await runCommand(['learn']);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('Missing required argument');
|
||||
});
|
||||
|
||||
test('应该处理权限错误', async () => {
|
||||
// 创建一个没有权限的文件
|
||||
const restrictedFile = path.join(tempDir, 'restricted.md');
|
||||
await fs.writeFile(restrictedFile, 'restricted content');
|
||||
await fs.chmod(restrictedFile, 0o000);
|
||||
|
||||
const result = await runCommand(['learn', '@file://restricted.md']);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('EACCES');
|
||||
|
||||
// 恢复权限以便清理
|
||||
await fs.chmod(restrictedFile, 0o644);
|
||||
});
|
||||
});
|
||||
|
||||
describe('工作流集成测试', () => {
|
||||
test('应该支持完整的AI认知循环', async () => {
|
||||
// 1. Hello - 建立连接
|
||||
const helloResult = await runCommand(['hello', '--name', 'E2E测试']);
|
||||
expect(helloResult.code).toBe(0);
|
||||
|
||||
// 2. Learn - 学习资源
|
||||
const learnResult = await runCommand(['learn', '@file://bootstrap.md']);
|
||||
expect(learnResult.code).toBe(0);
|
||||
|
||||
// 3. Remember - 存储记忆
|
||||
const rememberResult = await runCommand([
|
||||
'remember',
|
||||
'e2e-test',
|
||||
'E2E测试记忆',
|
||||
'--type',
|
||||
'episodic'
|
||||
]);
|
||||
expect(rememberResult.code).toBe(0);
|
||||
|
||||
// 4. Recall - 检索记忆
|
||||
const recallResult = await runCommand(['recall', 'e2e-test']);
|
||||
expect(recallResult.code).toBe(0);
|
||||
});
|
||||
|
||||
test('应该支持资源链式学习', async () => {
|
||||
// 创建链式引用文件
|
||||
const chainFile = path.join(tempDir, 'chain.md');
|
||||
await fs.writeFile(chainFile, '@file://bootstrap.md');
|
||||
|
||||
const result = await runCommand(['learn', '@file://chain.md']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('这是启动文件');
|
||||
});
|
||||
});
|
||||
|
||||
describe('输出格式和交互', () => {
|
||||
test('应该支持JSON输出格式', async () => {
|
||||
const result = await runCommand(['learn', '@file://bootstrap.md', '--format', 'json']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(() => JSON.parse(result.stdout)).not.toThrow();
|
||||
});
|
||||
|
||||
test('应该支持静默模式', async () => {
|
||||
const result = await runCommand(['hello', '--quiet']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout.trim()).toBe('');
|
||||
});
|
||||
|
||||
test('应该支持详细输出模式', async () => {
|
||||
const result = await runCommand(['learn', '@file://bootstrap.md', '--verbose']);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('DEBUG');
|
||||
});
|
||||
});
|
||||
});
|
||||
357
src/tests/core/resource/protocols/PackageProtocol.unit.test.js
Normal file
357
src/tests/core/resource/protocols/PackageProtocol.unit.test.js
Normal file
@ -0,0 +1,357 @@
|
||||
const PackageProtocol = require('../../../../lib/core/resource/protocols/PackageProtocol');
|
||||
const { QueryParams } = require('../../../../lib/core/resource/types');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
describe('PackageProtocol', () => {
|
||||
let packageProtocol;
|
||||
const originalEnv = process.env;
|
||||
const projectRoot = process.cwd(); // PromptX项目根目录
|
||||
|
||||
beforeEach(() => {
|
||||
packageProtocol = new PackageProtocol();
|
||||
// 重置环境变量
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
packageProtocol.clearCache();
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('基础功能', () => {
|
||||
test('应该正确初始化协议', () => {
|
||||
expect(packageProtocol.name).toBe('package');
|
||||
expect(packageProtocol.installModeCache).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
test('应该提供协议信息', () => {
|
||||
const info = packageProtocol.getProtocolInfo();
|
||||
expect(info.name).toBe('package');
|
||||
expect(info.description).toContain('包协议');
|
||||
expect(info.examples).toContain('@package://package.json');
|
||||
expect(info.examples).toContain('@package://src/index.js');
|
||||
expect(info.installModes).toContain('development');
|
||||
});
|
||||
|
||||
test('应该支持缓存', () => {
|
||||
expect(packageProtocol.enableCache).toBe(true);
|
||||
expect(packageProtocol.cache).toBeInstanceOf(Map);
|
||||
expect(packageProtocol.installModeCache).toBeInstanceOf(Map);
|
||||
});
|
||||
});
|
||||
|
||||
describe('安装模式检测', () => {
|
||||
test('应该检测开发模式', () => {
|
||||
// 设置开发环境
|
||||
process.env.NODE_ENV = 'development';
|
||||
packageProtocol.clearCache();
|
||||
|
||||
const mode = packageProtocol.detectInstallMode();
|
||||
expect(mode).toBe('development');
|
||||
});
|
||||
|
||||
test('应该检测npx执行模式', () => {
|
||||
// 模拟npx环境
|
||||
process.env.npm_execpath = '/usr/local/bin/npx';
|
||||
packageProtocol.clearCache();
|
||||
|
||||
const mode = packageProtocol.detectInstallMode();
|
||||
expect(mode).toBe('npx');
|
||||
});
|
||||
|
||||
test('应该缓存检测结果', () => {
|
||||
const mode1 = packageProtocol.detectInstallMode();
|
||||
const mode2 = packageProtocol.detectInstallMode();
|
||||
|
||||
expect(mode1).toBe(mode2);
|
||||
expect(packageProtocol.installModeCache.size).toBe(1);
|
||||
});
|
||||
|
||||
test('检测结果应该是有效的安装模式', () => {
|
||||
const mode = packageProtocol.detectInstallMode();
|
||||
const validModes = ['development', 'local', 'global', 'npx', 'monorepo', 'link'];
|
||||
expect(validModes).toContain(mode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NPX执行检测', () => {
|
||||
test('应该通过npm_execpath检测npx', () => {
|
||||
process.env.npm_execpath = '/path/to/npx';
|
||||
expect(packageProtocol._isNpxExecution()).toBe(true);
|
||||
});
|
||||
|
||||
test('应该通过npm_config_cache检测npx', () => {
|
||||
process.env.npm_config_cache = '/tmp/_npx/cache';
|
||||
expect(packageProtocol._isNpxExecution()).toBe(true);
|
||||
});
|
||||
|
||||
test('正常情况下应该返回false', () => {
|
||||
delete process.env.npm_execpath;
|
||||
delete process.env.npm_config_cache;
|
||||
expect(packageProtocol._isNpxExecution()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('全局安装检测', () => {
|
||||
test('应该检测常见的全局路径', () => {
|
||||
// 这个测试在实际环境中可能会失败,因为我们无法轻易改变__dirname
|
||||
const result = packageProtocol._isGlobalInstall();
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('开发模式检测', () => {
|
||||
test('应该通过NODE_ENV检测开发模式', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
expect(packageProtocol._isDevelopmentMode()).toBe(true);
|
||||
});
|
||||
|
||||
test('应该检测非node_modules目录', () => {
|
||||
// 当前测试环境应该不在node_modules中
|
||||
const result = packageProtocol._isDevelopmentMode();
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('包查找功能', () => {
|
||||
test('应该能找到package.json', () => {
|
||||
const packageJsonPath = packageProtocol.findPackageJson();
|
||||
expect(packageJsonPath).toBeTruthy();
|
||||
expect(packageJsonPath).toMatch(/package\.json$/);
|
||||
});
|
||||
|
||||
test('应该能找到根package.json', () => {
|
||||
const rootPackageJsonPath = packageProtocol.findRootPackageJson();
|
||||
expect(rootPackageJsonPath).toBeTruthy();
|
||||
expect(rootPackageJsonPath).toMatch(/package\.json$/);
|
||||
});
|
||||
|
||||
test('查找不存在的package.json应该返回null', () => {
|
||||
const result = packageProtocol.findPackageJson('/nonexistent/path');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('包根目录获取', () => {
|
||||
test('应该能获取包根目录', async () => {
|
||||
const packageRoot = await packageProtocol.getPackageRoot();
|
||||
expect(packageRoot).toBeTruthy();
|
||||
expect(typeof packageRoot).toBe('string');
|
||||
expect(path.isAbsolute(packageRoot)).toBe(true);
|
||||
});
|
||||
|
||||
test('项目根目录查找应该工作正常', () => {
|
||||
const root = packageProtocol._findProjectRoot();
|
||||
expect(root).toBeTruthy();
|
||||
expect(path.isAbsolute(root)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('路径解析', () => {
|
||||
test('应该解析package.json路径', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('package.json');
|
||||
expect(resolved).toMatch(/package\.json$/);
|
||||
expect(path.isAbsolute(resolved)).toBe(true);
|
||||
});
|
||||
|
||||
test('应该解析src目录路径', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('src/index.js');
|
||||
expect(resolved).toContain('src');
|
||||
expect(resolved).toMatch(/index\.js$/);
|
||||
});
|
||||
|
||||
test('应该解析prompt目录路径', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('prompt/core/thought.md');
|
||||
expect(resolved).toContain('prompt');
|
||||
expect(resolved).toContain('core');
|
||||
expect(resolved).toMatch(/thought\.md$/);
|
||||
});
|
||||
|
||||
test('空路径应该返回包根目录', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('');
|
||||
expect(path.isAbsolute(resolved)).toBe(true);
|
||||
expect(resolved).toBeTruthy();
|
||||
});
|
||||
|
||||
test('只有空格的路径应该返回包根目录', async () => {
|
||||
const resolved = await packageProtocol.resolvePath(' ');
|
||||
expect(path.isAbsolute(resolved)).toBe(true);
|
||||
expect(resolved).toBeTruthy();
|
||||
});
|
||||
|
||||
test('应该使用缓存', async () => {
|
||||
const path1 = await packageProtocol.resolvePath('package.json');
|
||||
const path2 = await packageProtocol.resolvePath('package.json');
|
||||
|
||||
expect(path1).toBe(path2);
|
||||
expect(packageProtocol.cache.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('路径安全检查', () => {
|
||||
test('应该阻止目录遍历攻击', async () => {
|
||||
await expect(
|
||||
packageProtocol.resolvePath('../../../etc/passwd')
|
||||
).rejects.toThrow('路径安全检查失败');
|
||||
});
|
||||
|
||||
test('正常的相对路径应该被允许', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('src/lib/utils.js');
|
||||
expect(resolved).toContain('src');
|
||||
expect(resolved).toContain('lib');
|
||||
expect(resolved).toMatch(/utils\.js$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('资源存在性检查', () => {
|
||||
test('存在的文件应该返回true', async () => {
|
||||
const exists = await packageProtocol.exists('package.json');
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
test('不存在的文件应该返回false', async () => {
|
||||
const exists = await packageProtocol.exists('nonexistent.txt');
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('内容加载', () => {
|
||||
test('应该能加载package.json内容', async () => {
|
||||
const result = await packageProtocol.loadContent('package.json');
|
||||
|
||||
expect(result).toHaveProperty('content');
|
||||
expect(result).toHaveProperty('path');
|
||||
expect(result).toHaveProperty('protocol', 'package');
|
||||
expect(result).toHaveProperty('installMode');
|
||||
expect(result).toHaveProperty('metadata');
|
||||
|
||||
expect(result.metadata).toHaveProperty('size');
|
||||
expect(result.metadata).toHaveProperty('lastModified');
|
||||
expect(result.metadata).toHaveProperty('absolutePath');
|
||||
expect(result.metadata).toHaveProperty('relativePath');
|
||||
|
||||
// 验证内容是有效的JSON
|
||||
expect(() => JSON.parse(result.content)).not.toThrow();
|
||||
});
|
||||
|
||||
test('加载不存在的文件应该抛出错误', async () => {
|
||||
await expect(
|
||||
packageProtocol.loadContent('nonexistent.txt')
|
||||
).rejects.toThrow('包资源不存在');
|
||||
});
|
||||
|
||||
test('返回的metadata应该包含正确信息', async () => {
|
||||
const result = await packageProtocol.loadContent('package.json');
|
||||
|
||||
expect(result.metadata.size).toBe(result.content.length);
|
||||
expect(result.metadata.lastModified.constructor.name).toBe('Date');
|
||||
expect(path.isAbsolute(result.metadata.absolutePath)).toBe(true);
|
||||
expect(result.metadata.relativePath).toBe('package.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('查询参数支持', () => {
|
||||
test('应该支持查询参数', async () => {
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('encoding', 'utf8');
|
||||
|
||||
const resolved = await packageProtocol.resolvePath('package.json', queryParams);
|
||||
expect(resolved).toMatch(/package\.json$/);
|
||||
});
|
||||
|
||||
test('相同路径但不同查询参数应该有不同的缓存', async () => {
|
||||
const queryParams1 = new QueryParams();
|
||||
queryParams1.set('test', 'value1');
|
||||
|
||||
const queryParams2 = new QueryParams();
|
||||
queryParams2.set('test', 'value2');
|
||||
|
||||
await packageProtocol.resolvePath('package.json', queryParams1);
|
||||
await packageProtocol.resolvePath('package.json', queryParams2);
|
||||
|
||||
expect(packageProtocol.cache.size).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('调试信息', () => {
|
||||
test('应该提供完整的调试信息', () => {
|
||||
const debugInfo = packageProtocol.getDebugInfo();
|
||||
|
||||
expect(debugInfo).toHaveProperty('protocol', 'package');
|
||||
expect(debugInfo).toHaveProperty('installMode');
|
||||
expect(debugInfo).toHaveProperty('packageRoot');
|
||||
expect(debugInfo).toHaveProperty('currentWorkingDirectory');
|
||||
expect(debugInfo).toHaveProperty('moduleDirectory');
|
||||
expect(debugInfo).toHaveProperty('environment');
|
||||
expect(debugInfo).toHaveProperty('cacheSize');
|
||||
|
||||
expect(debugInfo.environment).toHaveProperty('NODE_ENV');
|
||||
expect(debugInfo.environment).toHaveProperty('npm_execpath');
|
||||
expect(debugInfo.environment).toHaveProperty('npm_config_cache');
|
||||
});
|
||||
});
|
||||
|
||||
describe('缓存管理', () => {
|
||||
test('应该能清理所有缓存', async () => {
|
||||
// 生成一些缓存
|
||||
await packageProtocol.resolvePath('package.json');
|
||||
packageProtocol.detectInstallMode();
|
||||
|
||||
expect(packageProtocol.cache.size).toBeGreaterThan(0);
|
||||
expect(packageProtocol.installModeCache.size).toBeGreaterThan(0);
|
||||
|
||||
packageProtocol.clearCache();
|
||||
|
||||
expect(packageProtocol.cache.size).toBe(0);
|
||||
expect(packageProtocol.installModeCache.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
test('文件系统错误应该被正确处理', async () => {
|
||||
// 尝试访问一个权限不足的路径(如果存在的话)
|
||||
const result = await packageProtocol.exists('../../../root/.ssh/id_rsa');
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
|
||||
test('路径解析错误应该包含有用信息', async () => {
|
||||
try {
|
||||
await packageProtocol.resolvePath('../../../etc/passwd');
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('路径安全检查失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
test('深层嵌套路径应该正确处理', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('src/lib/core/resource/protocols/test.js');
|
||||
expect(resolved).toContain('src');
|
||||
expect(resolved).toContain('lib');
|
||||
expect(resolved).toContain('core');
|
||||
expect(resolved).toContain('resource');
|
||||
expect(resolved).toContain('protocols');
|
||||
expect(resolved).toMatch(/test\.js$/);
|
||||
});
|
||||
|
||||
test('特殊字符路径应该被正确处理', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('assets/images/logo-2024.png');
|
||||
expect(resolved).toContain('assets');
|
||||
expect(resolved).toContain('images');
|
||||
expect(resolved).toMatch(/logo-2024\.png$/);
|
||||
});
|
||||
|
||||
test('带有空格的路径应该被正确处理', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('docs/user guide.md');
|
||||
expect(resolved).toContain('docs');
|
||||
expect(resolved).toMatch(/user guide\.md$/);
|
||||
});
|
||||
|
||||
test('中文路径应该被正确处理', async () => {
|
||||
const resolved = await packageProtocol.resolvePath('文档/说明.md');
|
||||
expect(resolved).toContain('文档');
|
||||
expect(resolved).toMatch(/说明\.md$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
245
src/tests/core/resource/protocols/ProjectProtocol.unit.test.js
Normal file
245
src/tests/core/resource/protocols/ProjectProtocol.unit.test.js
Normal file
@ -0,0 +1,245 @@
|
||||
const ProjectProtocol = require('../../../../lib/core/resource/protocols/ProjectProtocol');
|
||||
const { QueryParams } = require('../../../../lib/core/resource/types');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
describe('ProjectProtocol', () => {
|
||||
let projectProtocol;
|
||||
const projectRoot = process.cwd(); // PromptX项目根目录
|
||||
const promptxPath = path.join(projectRoot, '.promptx');
|
||||
|
||||
beforeEach(() => {
|
||||
projectProtocol = new ProjectProtocol();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
projectProtocol.clearCache();
|
||||
});
|
||||
|
||||
describe('基础功能', () => {
|
||||
test('应该正确初始化协议', () => {
|
||||
expect(projectProtocol.name).toBe('project');
|
||||
expect(projectProtocol.projectDirs).toBeDefined();
|
||||
expect(Object.keys(projectProtocol.projectDirs)).toContain('root');
|
||||
expect(Object.keys(projectProtocol.projectDirs)).toContain('src');
|
||||
expect(Object.keys(projectProtocol.projectDirs)).toContain('lib');
|
||||
});
|
||||
|
||||
test('应该提供协议信息', () => {
|
||||
const info = projectProtocol.getProtocolInfo();
|
||||
expect(info.name).toBe('project');
|
||||
expect(info.description).toContain('项目协议');
|
||||
expect(info.projectMarker).toBe('.promptx');
|
||||
expect(info.supportedDirectories).toContain('src');
|
||||
expect(info.examples).toEqual(expect.arrayContaining([
|
||||
expect.stringContaining('project://src/')
|
||||
]));
|
||||
});
|
||||
|
||||
test('应该提供支持的查询参数', () => {
|
||||
const params = projectProtocol.getSupportedParams();
|
||||
expect(params.from).toContain('指定搜索起始目录');
|
||||
expect(params.create).toContain('如果目录不存在是否创建');
|
||||
expect(params.line).toContain('行范围');
|
||||
});
|
||||
});
|
||||
|
||||
describe('路径验证', () => {
|
||||
test('应该验证有效的项目路径', () => {
|
||||
expect(projectProtocol.validatePath('src/index.js')).toBe(true);
|
||||
expect(projectProtocol.validatePath('lib/utils')).toBe(true);
|
||||
expect(projectProtocol.validatePath('docs')).toBe(true);
|
||||
expect(projectProtocol.validatePath('root/package.json')).toBe(true);
|
||||
});
|
||||
|
||||
test('应该拒绝无效的项目路径', () => {
|
||||
expect(projectProtocol.validatePath('invalid/path')).toBe(false);
|
||||
expect(projectProtocol.validatePath('unknown')).toBe(false);
|
||||
expect(projectProtocol.validatePath('')).toBe(false);
|
||||
expect(projectProtocol.validatePath(null)).toBe(false);
|
||||
});
|
||||
|
||||
test('应该验证项目目录类型', () => {
|
||||
const supportedDirs = Object.keys(projectProtocol.projectDirs);
|
||||
supportedDirs.forEach(dir => {
|
||||
expect(projectProtocol.validatePath(`${dir}/test.js`)).toBe(true);
|
||||
expect(projectProtocol.validatePath(dir)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('项目根目录查找', () => {
|
||||
test('应该找到当前项目的根目录', async () => {
|
||||
const root = await projectProtocol.findProjectRoot();
|
||||
expect(root).toBe(projectRoot);
|
||||
});
|
||||
|
||||
test('应该从子目录找到项目根目录', async () => {
|
||||
const subDir = path.join(projectRoot, 'src', 'lib');
|
||||
const root = await projectProtocol.findProjectRoot(subDir);
|
||||
expect(root).toBe(projectRoot);
|
||||
});
|
||||
|
||||
test('应该缓存项目根目录结果', async () => {
|
||||
const root1 = await projectProtocol.findProjectRoot();
|
||||
const root2 = await projectProtocol.findProjectRoot();
|
||||
expect(root1).toBe(root2);
|
||||
expect(root1).toBe(projectRoot);
|
||||
});
|
||||
|
||||
test('应该处理未找到项目根目录的情况', async () => {
|
||||
// 使用系统临时目录测试
|
||||
const tempDir = '/tmp';
|
||||
const root = await projectProtocol.findProjectRoot(tempDir);
|
||||
expect(root).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('路径解析', () => {
|
||||
test('应该解析src目录路径', async () => {
|
||||
const resolvedPath = await projectProtocol.resolvePath('src/index.js');
|
||||
expect(resolvedPath).toBe(path.join(projectRoot, 'src', 'index.js'));
|
||||
});
|
||||
|
||||
test('应该解析lib目录路径', async () => {
|
||||
const resolvedPath = await projectProtocol.resolvePath('lib/core/resource');
|
||||
expect(resolvedPath).toBe(path.join(projectRoot, 'lib', 'core', 'resource'));
|
||||
});
|
||||
|
||||
test('应该解析根目录路径', async () => {
|
||||
const resolvedPath = await projectProtocol.resolvePath('root/package.json');
|
||||
expect(resolvedPath).toBe(path.join(projectRoot, 'package.json'));
|
||||
});
|
||||
|
||||
test('应该解析目录路径(无文件名)', async () => {
|
||||
const resolvedPath = await projectProtocol.resolvePath('src');
|
||||
expect(resolvedPath).toBe(path.join(projectRoot, 'src'));
|
||||
});
|
||||
|
||||
test('应该拒绝不支持的目录类型', async () => {
|
||||
await expect(projectProtocol.resolvePath('invalid/path')).rejects.toThrow('不支持的项目目录类型');
|
||||
});
|
||||
|
||||
test('应该处理安全路径检查', async () => {
|
||||
await expect(projectProtocol.resolvePath('src/../../../etc/passwd')).rejects.toThrow('安全错误');
|
||||
});
|
||||
|
||||
test('应该支持from参数指定起始目录', async () => {
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('from', projectRoot);
|
||||
|
||||
const resolvedPath = await projectProtocol.resolvePath('src/test.js', queryParams);
|
||||
expect(resolvedPath).toBe(path.join(projectRoot, 'src', 'test.js'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('内容加载', () => {
|
||||
test('应该加载存在的文件内容', async () => {
|
||||
const packageJsonPath = path.join(projectRoot, 'package.json');
|
||||
const content = await projectProtocol.loadFileContent(packageJsonPath);
|
||||
expect(content).toContain('promptx');
|
||||
});
|
||||
|
||||
test('应该加载目录内容', async () => {
|
||||
const srcPath = path.join(projectRoot, 'src');
|
||||
const content = await projectProtocol.loadDirectoryContent(srcPath);
|
||||
expect(content).toContain('[DIR]');
|
||||
});
|
||||
|
||||
test('应该支持JSON格式的目录列表', async () => {
|
||||
const srcPath = path.join(projectRoot, 'src');
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('format', 'json');
|
||||
|
||||
const content = await projectProtocol.loadDirectoryContent(srcPath, queryParams);
|
||||
const parsed = JSON.parse(content);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
|
||||
test('应该支持类型过滤', async () => {
|
||||
const rootPath = projectRoot;
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('type', 'file');
|
||||
|
||||
const content = await projectProtocol.loadDirectoryContent(rootPath, queryParams);
|
||||
expect(content).toContain('[FILE]');
|
||||
expect(content).not.toContain('[DIR]');
|
||||
});
|
||||
|
||||
test('应该处理不存在的文件', async () => {
|
||||
const nonExistentPath = path.join(projectRoot, 'nonexistent.txt');
|
||||
await expect(projectProtocol.loadContent(nonExistentPath)).rejects.toThrow('文件或目录不存在');
|
||||
});
|
||||
|
||||
test('应该支持exists=false参数', async () => {
|
||||
const nonExistentPath = path.join(projectRoot, 'nonexistent.txt');
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('exists', 'false');
|
||||
|
||||
const content = await projectProtocol.loadContent(nonExistentPath, queryParams);
|
||||
expect(content).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('完整协议解析', () => {
|
||||
test('应该完整解析project://协议', async () => {
|
||||
const content = await projectProtocol.resolve('root/package.json');
|
||||
expect(content).toContain('promptx');
|
||||
});
|
||||
|
||||
test('应该处理带查询参数的协议', async () => {
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('format', 'json');
|
||||
|
||||
const content = await projectProtocol.resolve('src', queryParams);
|
||||
const parsed = JSON.parse(content);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
|
||||
test('应该应用行过滤', async () => {
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('line', '1-3');
|
||||
|
||||
const content = await projectProtocol.resolve('root/package.json', queryParams);
|
||||
const lines = content.split('\n');
|
||||
expect(lines.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('项目信息', () => {
|
||||
test('应该获取项目信息', async () => {
|
||||
const info = await projectProtocol.getProjectInfo();
|
||||
expect(info.projectRoot).toBe(projectRoot);
|
||||
expect(info.promptxPath).toBe(promptxPath);
|
||||
expect(info.directories).toBeDefined();
|
||||
expect(info.directories.root.exists).toBe(true);
|
||||
expect(info.directories.src.exists).toBe(true);
|
||||
});
|
||||
|
||||
test('应该标识不存在的目录', async () => {
|
||||
const info = await projectProtocol.getProjectInfo();
|
||||
// 有些目录可能不存在,应该正确标识
|
||||
Object.values(info.directories).forEach(dir => {
|
||||
expect(dir).toHaveProperty('exists');
|
||||
expect(dir).toHaveProperty('path');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('缓存管理', () => {
|
||||
test('应该提供缓存统计', () => {
|
||||
const stats = projectProtocol.getCacheStats();
|
||||
expect(stats.protocol).toBe('project');
|
||||
expect(typeof stats.size).toBe('number');
|
||||
expect(typeof stats.enabled).toBe('boolean');
|
||||
});
|
||||
|
||||
test('应该能清除缓存', async () => {
|
||||
await projectProtocol.findProjectRoot(); // 填充缓存
|
||||
expect(projectProtocol.projectRootCache.size).toBeGreaterThan(0);
|
||||
|
||||
projectProtocol.clearCache();
|
||||
expect(projectProtocol.projectRootCache.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
232
src/tests/core/resource/protocols/UserProtocol.unit.test.js
Normal file
232
src/tests/core/resource/protocols/UserProtocol.unit.test.js
Normal file
@ -0,0 +1,232 @@
|
||||
const UserProtocol = require('../../../../lib/core/resource/protocols/UserProtocol');
|
||||
const { QueryParams } = require('../../../../lib/core/resource/types');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
describe('UserProtocol', () => {
|
||||
let userProtocol;
|
||||
|
||||
beforeEach(() => {
|
||||
userProtocol = new UserProtocol();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
userProtocol.clearCache();
|
||||
});
|
||||
|
||||
describe('基础功能', () => {
|
||||
test('应该正确初始化协议', () => {
|
||||
expect(userProtocol.name).toBe('user');
|
||||
expect(userProtocol.userDirs).toBeDefined();
|
||||
expect(Object.keys(userProtocol.userDirs)).toContain('home');
|
||||
expect(Object.keys(userProtocol.userDirs)).toContain('documents');
|
||||
expect(Object.keys(userProtocol.userDirs)).toContain('desktop');
|
||||
});
|
||||
|
||||
test('应该提供协议信息', () => {
|
||||
const info = userProtocol.getProtocolInfo();
|
||||
expect(info.name).toBe('user');
|
||||
expect(info.description).toBeDefined();
|
||||
expect(info.location).toBe('user://{directory}/{path}');
|
||||
expect(info.examples).toBeInstanceOf(Array);
|
||||
expect(info.supportedDirectories).toContain('home');
|
||||
});
|
||||
|
||||
test('应该提供支持的参数列表', () => {
|
||||
const params = userProtocol.getSupportedParams();
|
||||
expect(params.line).toBeDefined();
|
||||
expect(params.format).toBeDefined();
|
||||
expect(params.exists).toBeDefined();
|
||||
expect(params.type).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('路径验证', () => {
|
||||
test('应该验证有效的用户目录路径', () => {
|
||||
expect(userProtocol.validatePath('home')).toBe(true);
|
||||
expect(userProtocol.validatePath('documents/notes.txt')).toBe(true);
|
||||
expect(userProtocol.validatePath('desktop/readme.md')).toBe(true);
|
||||
expect(userProtocol.validatePath('downloads/')).toBe(true);
|
||||
});
|
||||
|
||||
test('应该拒绝无效的用户目录路径', () => {
|
||||
expect(userProtocol.validatePath('invalid')).toBe(false);
|
||||
expect(userProtocol.validatePath('unknown/path')).toBe(false);
|
||||
expect(userProtocol.validatePath('')).toBe(false);
|
||||
expect(userProtocol.validatePath(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('路径解析', () => {
|
||||
test('应该解析home目录', async () => {
|
||||
const resolved = await userProtocol.resolvePath('home');
|
||||
expect(resolved).toBe(os.homedir());
|
||||
});
|
||||
|
||||
test('应该解析documents目录', async () => {
|
||||
const resolved = await userProtocol.resolvePath('documents');
|
||||
expect(resolved).toContain('Documents');
|
||||
expect(path.isAbsolute(resolved)).toBe(true);
|
||||
});
|
||||
|
||||
test('应该解析带子路径的文件', async () => {
|
||||
const resolved = await userProtocol.resolvePath('documents/notes.txt');
|
||||
expect(resolved).toContain('Documents');
|
||||
expect(resolved).toContain('notes.txt');
|
||||
expect(path.isAbsolute(resolved)).toBe(true);
|
||||
});
|
||||
|
||||
test('应该拒绝不支持的目录类型', async () => {
|
||||
await expect(userProtocol.resolvePath('invalid/path'))
|
||||
.rejects.toThrow('不支持的用户目录类型');
|
||||
});
|
||||
|
||||
test('应该防止路径穿越攻击', async () => {
|
||||
await expect(userProtocol.resolvePath('documents/../../../etc/passwd'))
|
||||
.rejects.toThrow('安全错误:路径超出用户目录范围');
|
||||
});
|
||||
});
|
||||
|
||||
describe('用户目录获取', () => {
|
||||
test('应该获取所有支持的用户目录', async () => {
|
||||
const directories = await userProtocol.listUserDirectories();
|
||||
|
||||
expect(directories.home).toBeDefined();
|
||||
expect(directories.documents).toBeDefined();
|
||||
expect(directories.desktop).toBeDefined();
|
||||
expect(directories.downloads).toBeDefined();
|
||||
|
||||
// 检查路径是否为绝对路径
|
||||
expect(path.isAbsolute(directories.home)).toBe(true);
|
||||
});
|
||||
|
||||
test('应该缓存目录路径', async () => {
|
||||
// 第一次调用
|
||||
const dir1 = await userProtocol.getUserDirectory('home');
|
||||
expect(userProtocol.dirCache.has('home')).toBe(true);
|
||||
|
||||
// 第二次调用应该从缓存获取
|
||||
const dir2 = await userProtocol.getUserDirectory('home');
|
||||
expect(dir1).toBe(dir2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('内容加载', () => {
|
||||
test('应该加载目录内容', async () => {
|
||||
// 使用home目录进行测试(应该总是存在)
|
||||
const homePath = await userProtocol.resolvePath('home');
|
||||
const content = await userProtocol.loadContent(homePath);
|
||||
|
||||
expect(typeof content).toBe('string');
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该支持不同的目录格式化选项', async () => {
|
||||
const homePath = await userProtocol.resolvePath('home');
|
||||
const queryParams = new QueryParams();
|
||||
|
||||
// 测试json格式
|
||||
queryParams.set('format', 'json');
|
||||
const jsonContent = await userProtocol.loadContent(homePath, queryParams);
|
||||
expect(() => JSON.parse(jsonContent)).not.toThrow();
|
||||
|
||||
// 测试paths格式
|
||||
queryParams.set('format', 'paths');
|
||||
const pathsContent = await userProtocol.loadContent(homePath, queryParams);
|
||||
expect(typeof pathsContent).toBe('string');
|
||||
});
|
||||
|
||||
test('应该处理不存在的文件', async () => {
|
||||
const nonExistentPath = await userProtocol.resolvePath('documents/non-existent-file.txt');
|
||||
|
||||
// 默认情况下应该抛出错误
|
||||
await expect(userProtocol.loadContent(nonExistentPath))
|
||||
.rejects.toThrow('文件或目录不存在');
|
||||
|
||||
// 设置exists=false应该返回空字符串
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('exists', 'false');
|
||||
const content = await userProtocol.loadContent(nonExistentPath, queryParams);
|
||||
expect(content).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('查询参数处理', () => {
|
||||
test('应该应用行过滤', () => {
|
||||
const content = 'line1\nline2\nline3\nline4\nline5';
|
||||
|
||||
// 测试单行
|
||||
expect(userProtocol.applyLineFilter(content, '2')).toBe('line2');
|
||||
|
||||
// 测试范围
|
||||
expect(userProtocol.applyLineFilter(content, '2-4')).toBe('line2\nline3\nline4');
|
||||
|
||||
// 测试边界
|
||||
expect(userProtocol.applyLineFilter(content, '1-2')).toBe('line1\nline2');
|
||||
});
|
||||
|
||||
test('应该应用格式化', () => {
|
||||
const jsonContent = '{"name": "test", "value": 123}';
|
||||
|
||||
// 测试JSON格式化
|
||||
const formatted = userProtocol.applyFormat(jsonContent, 'json');
|
||||
expect(formatted).toContain('{\n "name"');
|
||||
|
||||
// 测试trim格式化
|
||||
const textContent = ' hello world ';
|
||||
expect(userProtocol.applyFormat(textContent, 'trim')).toBe('hello world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('缓存管理', () => {
|
||||
test('应该启用缓存', () => {
|
||||
expect(userProtocol.enableCache).toBe(true);
|
||||
});
|
||||
|
||||
test('应该提供缓存统计', () => {
|
||||
const stats = userProtocol.getCacheStats();
|
||||
expect(stats.protocol).toBe('user');
|
||||
expect(stats.enabled).toBe(true);
|
||||
expect(typeof stats.size).toBe('number');
|
||||
});
|
||||
|
||||
test('应该清除缓存', async () => {
|
||||
// 先缓存一些数据
|
||||
await userProtocol.getUserDirectory('home');
|
||||
expect(userProtocol.dirCache.size).toBeGreaterThan(0);
|
||||
|
||||
// 清除缓存
|
||||
userProtocol.clearCache();
|
||||
expect(userProtocol.dirCache.size).toBe(0);
|
||||
expect(userProtocol.cache.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('集成测试', () => {
|
||||
test('应该完整解析用户协议资源', async () => {
|
||||
const queryParams = new QueryParams();
|
||||
queryParams.set('format', 'json');
|
||||
|
||||
const content = await userProtocol.resolve('home', queryParams);
|
||||
|
||||
expect(typeof content).toBe('string');
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
|
||||
// 如果格式是json,应该能解析
|
||||
if (queryParams.get('format') === 'json') {
|
||||
expect(() => JSON.parse(content)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
test('应该处理嵌套路径', async () => {
|
||||
// 假设Documents目录存在
|
||||
try {
|
||||
const content = await userProtocol.resolve('documents');
|
||||
expect(typeof content).toBe('string');
|
||||
} catch (error) {
|
||||
// 如果Documents目录不存在,这是正常的
|
||||
expect(error.message).toContain('不存在');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
203
src/tests/core/resource/resourceManager.integration.test.js
Normal file
203
src/tests/core/resource/resourceManager.integration.test.js
Normal file
@ -0,0 +1,203 @@
|
||||
const ResourceManager = require('../../../lib/core/resource/resourceManager');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
describe('ResourceManager - Integration Tests', () => {
|
||||
let manager;
|
||||
let tempDir;
|
||||
|
||||
beforeAll(async () => {
|
||||
// 创建临时测试目录
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-test-'));
|
||||
|
||||
// 创建测试文件
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, 'test.md'),
|
||||
'# 测试文件\n\n这是一个测试文件。\n第三行内容。\n第四行内容。'
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, 'nested.md'),
|
||||
'nested content'
|
||||
);
|
||||
|
||||
// 创建子目录和更多测试文件
|
||||
const subDir = path.join(tempDir, 'subdir');
|
||||
await fs.mkdir(subDir);
|
||||
await fs.writeFile(
|
||||
path.join(subDir, 'sub-test.md'),
|
||||
'subdirectory content'
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理临时目录
|
||||
await fs.rm(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ResourceManager({
|
||||
workingDirectory: tempDir,
|
||||
enableCache: true
|
||||
});
|
||||
});
|
||||
|
||||
describe('完整的资源解析流程', () => {
|
||||
test('应该解析并加载本地文件', async () => {
|
||||
const result = await manager.resolve('@file://test.md');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('测试文件');
|
||||
expect(result.metadata.protocol).toBe('file');
|
||||
expect(result.sources).toContain('test.md');
|
||||
});
|
||||
|
||||
test('应该处理带查询参数的文件加载', async () => {
|
||||
const result = await manager.resolve('@file://test.md?line=2-3');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).not.toContain('# 测试文件');
|
||||
expect(result.content).toContain('这是一个测试文件');
|
||||
expect(result.content).not.toContain('第三行内容');
|
||||
expect(result.content).not.toContain('第四行内容');
|
||||
});
|
||||
|
||||
test('应该处理通配符文件模式', async () => {
|
||||
const result = await manager.resolve('@file://*.md');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('test.md');
|
||||
expect(result.content).toContain('nested.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('内置协议集成', () => {
|
||||
test('应该处理prompt协议的注册表解析', async () => {
|
||||
// 模拟prompt协议解析
|
||||
const mockProtocolFile = path.join(tempDir, 'protocols.md');
|
||||
await fs.writeFile(mockProtocolFile, '# PromptX 协议\n\nDPML协议说明');
|
||||
|
||||
// 注册测试协议
|
||||
manager.registry.register('test-prompt', {
|
||||
name: 'test-prompt',
|
||||
description: '测试提示词协议',
|
||||
registry: {
|
||||
'protocols': `@file://${mockProtocolFile}`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await manager.resolve('@test-prompt://protocols');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('PromptX 协议');
|
||||
expect(result.content).toContain('DPML协议说明');
|
||||
});
|
||||
|
||||
test('应该处理嵌套引用解析', async () => {
|
||||
// 创建指向嵌套文件的引用文件
|
||||
const refFile = path.join(tempDir, 'reference.md');
|
||||
await fs.writeFile(refFile, '@file://nested.md');
|
||||
|
||||
manager.registry.register('test-nested', {
|
||||
registry: {
|
||||
'ref': `@file://${refFile}`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await manager.resolve('@test-nested://ref');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toBe('nested content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('缓存机制', () => {
|
||||
test('应该缓存已加载的资源', async () => {
|
||||
const firstResult = await manager.resolve('@file://test.md');
|
||||
const secondResult = await manager.resolve('@file://test.md');
|
||||
|
||||
expect(firstResult.content).toBe(secondResult.content);
|
||||
expect(firstResult.success).toBe(true);
|
||||
expect(secondResult.success).toBe(true);
|
||||
});
|
||||
|
||||
test('应该清除缓存', async () => {
|
||||
await manager.resolve('@file://test.md');
|
||||
expect(manager.cache.size).toBeGreaterThan(0);
|
||||
|
||||
manager.clearCache();
|
||||
expect(manager.cache.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量资源解析', () => {
|
||||
test('应该批量解析多个资源', async () => {
|
||||
const refs = [
|
||||
'@file://test.md',
|
||||
'@file://nested.md'
|
||||
];
|
||||
|
||||
const results = await manager.resolveMultiple(refs);
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].success).toBe(true);
|
||||
expect(results[1].success).toBe(true);
|
||||
expect(results[0].content).toContain('测试文件');
|
||||
expect(results[1].content).toContain('nested content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
test('应该处理文件不存在的情况', async () => {
|
||||
const result = await manager.resolve('@file://nonexistent.md');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error.message).toContain('Failed to read file');
|
||||
});
|
||||
|
||||
test('应该处理无效的协议', async () => {
|
||||
const result = await manager.resolve('@unknown://test');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error.message).toContain('Unknown protocol');
|
||||
});
|
||||
|
||||
test('应该处理无效的资源引用语法', async () => {
|
||||
const result = await manager.resolve('invalid-reference');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error.message).toContain('Invalid resource reference syntax');
|
||||
});
|
||||
});
|
||||
|
||||
describe('验证功能', () => {
|
||||
test('应该验证有效的资源引用', () => {
|
||||
expect(manager.isValidReference('@file://test.md')).toBe(true);
|
||||
expect(manager.isValidReference('@prompt://protocols')).toBe(true);
|
||||
});
|
||||
|
||||
test('应该拒绝无效的资源引用', () => {
|
||||
expect(manager.isValidReference('invalid')).toBe(false);
|
||||
expect(manager.isValidReference('@unknown://test')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('工具功能', () => {
|
||||
test('应该列出可用协议', () => {
|
||||
const protocols = manager.listProtocols();
|
||||
|
||||
expect(protocols).toContain('file');
|
||||
expect(protocols).toContain('prompt');
|
||||
expect(protocols).toContain('memory');
|
||||
});
|
||||
|
||||
test('应该获取注册表信息', () => {
|
||||
const info = manager.getRegistryInfo('prompt');
|
||||
|
||||
expect(info).toBeDefined();
|
||||
expect(info.name).toBe('prompt');
|
||||
});
|
||||
});
|
||||
});
|
||||
133
src/tests/core/resource/resourceProtocolParser.unit.test.js
Normal file
133
src/tests/core/resource/resourceProtocolParser.unit.test.js
Normal file
@ -0,0 +1,133 @@
|
||||
const ResourceProtocolParser = require('../../../lib/core/resource/resourceProtocolParser');
|
||||
const {
|
||||
LoadingSemantics,
|
||||
ParsedReference,
|
||||
QueryParams
|
||||
} = require('../../../lib/core/resource/types');
|
||||
|
||||
describe('ResourceProtocolParser - Unit Tests', () => {
|
||||
let parser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new ResourceProtocolParser();
|
||||
});
|
||||
|
||||
describe('基础语法解析', () => {
|
||||
test('应该解析基本的资源引用', () => {
|
||||
const result = parser.parse('@prompt://protocols');
|
||||
|
||||
expect(result.protocol).toBe('prompt');
|
||||
expect(result.path).toBe('protocols');
|
||||
expect(result.loadingSemantics).toBe(LoadingSemantics.DEFAULT);
|
||||
expect(result.isNested).toBe(false);
|
||||
});
|
||||
|
||||
test('应该解析带查询参数的资源引用', () => {
|
||||
const result = parser.parse('@file://test.md?line=5-10&cache=true');
|
||||
|
||||
expect(result.protocol).toBe('file');
|
||||
expect(result.path).toBe('test.md');
|
||||
expect(result.queryParams.line).toBe('5-10');
|
||||
expect(result.queryParams.cache).toBe(true);
|
||||
});
|
||||
|
||||
test('应该解析热加载语义', () => {
|
||||
const result = parser.parse('@!prompt://core');
|
||||
|
||||
expect(result.protocol).toBe('prompt');
|
||||
expect(result.path).toBe('core');
|
||||
expect(result.loadingSemantics).toBe(LoadingSemantics.HOT_LOAD);
|
||||
});
|
||||
|
||||
test('应该解析懒加载语义', () => {
|
||||
const result = parser.parse('@?file://lazy-resource.md');
|
||||
|
||||
expect(result.protocol).toBe('file');
|
||||
expect(result.path).toBe('lazy-resource.md');
|
||||
expect(result.loadingSemantics).toBe(LoadingSemantics.LAZY_LOAD);
|
||||
});
|
||||
});
|
||||
|
||||
describe('嵌套引用解析', () => {
|
||||
test('应该解析简单嵌套引用', () => {
|
||||
const result = parser.parse('@prompt://@file://nested.md');
|
||||
|
||||
expect(result.protocol).toBe('prompt');
|
||||
expect(result.isNested).toBe(true);
|
||||
expect(result.nestedRef.inner.protocol).toBe('file');
|
||||
expect(result.nestedRef.inner.path).toBe('nested.md');
|
||||
});
|
||||
|
||||
test('应该解析多层嵌套引用', () => {
|
||||
const result = parser.parse('@prompt://@memory://@file://deep.md');
|
||||
|
||||
expect(result.protocol).toBe('prompt');
|
||||
expect(result.isNested).toBe(true);
|
||||
expect(result.nestedRef.inner.protocol).toBe('memory');
|
||||
expect(result.nestedRef.inner.isNested).toBe(true);
|
||||
expect(result.nestedRef.depth).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('查询参数解析', () => {
|
||||
test('应该解析多个查询参数', () => {
|
||||
const params = parser.parseQueryParams('line=1-10&format=json&cache=true');
|
||||
|
||||
expect(params.line).toBe('1-10');
|
||||
expect(params.format).toBe('json');
|
||||
expect(params.cache).toBe(true);
|
||||
});
|
||||
|
||||
test('应该处理空查询参数', () => {
|
||||
const params = parser.parseQueryParams('');
|
||||
|
||||
expect(params.getAll()).toEqual({});
|
||||
});
|
||||
|
||||
test('应该处理URL编码的参数', () => {
|
||||
const params = parser.parseQueryParams('query=%E4%B8%AD%E6%96%87');
|
||||
|
||||
expect(params.get('query')).toBe('中文');
|
||||
});
|
||||
});
|
||||
|
||||
describe('语法验证', () => {
|
||||
test('应该验证有效的语法', () => {
|
||||
expect(parser.validateSyntax('@prompt://protocols')).toBe(true);
|
||||
expect(parser.validateSyntax('@!file://test.md')).toBe(true);
|
||||
expect(parser.validateSyntax('@?memory://declarative')).toBe(true);
|
||||
});
|
||||
|
||||
test('应该拒绝无效的语法', () => {
|
||||
expect(parser.validateSyntax('prompt://protocols')).toBe(false); // 缺少@
|
||||
expect(parser.validateSyntax('@://test')).toBe(false); // 空协议
|
||||
expect(parser.validateSyntax('@123protocol://test')).toBe(false); // 协议名不能以数字开头
|
||||
expect(parser.validateSyntax('')).toBe(false); // 空字符串
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
test('应该抛出适当的错误信息', () => {
|
||||
expect(() => parser.parse('')).toThrow('Invalid resource reference');
|
||||
expect(() => parser.parse(null)).toThrow('Invalid resource reference');
|
||||
expect(() => parser.parse('invalid')).toThrow('Invalid resource reference syntax');
|
||||
});
|
||||
});
|
||||
|
||||
describe('工具方法', () => {
|
||||
test('应该正确提取协议名', () => {
|
||||
expect(parser.extractProtocol('@prompt://protocols')).toBe('prompt');
|
||||
expect(parser.extractProtocol('@!file://test.md')).toBe('file');
|
||||
});
|
||||
|
||||
test('应该正确提取路径', () => {
|
||||
expect(parser.extractPath('@prompt://protocols?format=json')).toBe('protocols');
|
||||
expect(parser.extractPath('@file://path/to/file.md')).toBe('path/to/file.md');
|
||||
});
|
||||
|
||||
test('应该正确提取查询参数', () => {
|
||||
expect(parser.extractParams('@file://test.md?line=5-10')).toBe('line=5-10');
|
||||
expect(parser.extractParams('@file://test.md')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
134
src/tests/core/resource/resourceRegistry.unit.test.js
Normal file
134
src/tests/core/resource/resourceRegistry.unit.test.js
Normal file
@ -0,0 +1,134 @@
|
||||
const ResourceRegistry = require('../../../lib/core/resource/resourceRegistry');
|
||||
const { ProtocolInfo } = require('../../../lib/core/resource/types');
|
||||
|
||||
describe('ResourceRegistry - Unit Tests', () => {
|
||||
let registry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new ResourceRegistry();
|
||||
});
|
||||
|
||||
describe('内置协议', () => {
|
||||
test('应该包含内置协议', () => {
|
||||
const protocols = registry.listProtocols();
|
||||
|
||||
expect(protocols).toContain('prompt');
|
||||
expect(protocols).toContain('file');
|
||||
expect(protocols).toContain('memory');
|
||||
});
|
||||
|
||||
test('应该正确获取prompt协议信息', () => {
|
||||
const protocolInfo = registry.getProtocolInfo('prompt');
|
||||
|
||||
expect(protocolInfo).toBeDefined();
|
||||
expect(protocolInfo.name).toBe('prompt');
|
||||
expect(protocolInfo.description).toContain('PromptX内置提示词资源协议');
|
||||
expect(protocolInfo.location).toContain('prompt://');
|
||||
});
|
||||
|
||||
test('应该为协议提供资源注册表', () => {
|
||||
const protocolInfo = registry.getProtocolInfo('memory');
|
||||
|
||||
expect(protocolInfo.registry).toBeDefined();
|
||||
expect(protocolInfo.registry.size).toBeGreaterThan(0);
|
||||
expect(protocolInfo.registry.has('declarative')).toBe(true);
|
||||
expect(protocolInfo.registry.has('procedural')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('资源解析', () => {
|
||||
test('应该解析prompt协议的资源ID', () => {
|
||||
const resolved = registry.resolve('prompt', 'protocols');
|
||||
|
||||
expect(resolved).toBe('@package://prompt/protocol/**/*.md');
|
||||
});
|
||||
|
||||
test('应该解析memory协议的资源ID', () => {
|
||||
const resolved = registry.resolve('memory', 'declarative');
|
||||
|
||||
expect(resolved).toBe('@project://.promptx/memory/declarative.md');
|
||||
});
|
||||
|
||||
test('应该解析未注册协议的资源路径', () => {
|
||||
const resolved = registry.resolve('file', 'any/path.md');
|
||||
|
||||
expect(resolved).toBe('any/path.md');
|
||||
});
|
||||
|
||||
test('应该在资源ID不存在时抛出错误', () => {
|
||||
expect(() => registry.resolve('prompt', 'nonexistent')).toThrow('Resource ID \'nonexistent\' not found in prompt protocol registry');
|
||||
});
|
||||
});
|
||||
|
||||
describe('自定义协议注册', () => {
|
||||
test('应该注册新的自定义协议', () => {
|
||||
const customProtocol = {
|
||||
description: '测试协议',
|
||||
location: 'test://{resource_id}',
|
||||
registry: {
|
||||
'test1': '@file://test1.md',
|
||||
'test2': '@file://test2.md'
|
||||
}
|
||||
};
|
||||
|
||||
registry.register('test', customProtocol);
|
||||
|
||||
expect(registry.hasProtocol('test')).toBe(true);
|
||||
expect(registry.resolve('test', 'test1')).toBe('@file://test1.md');
|
||||
});
|
||||
|
||||
test('应该列出自定义协议的资源', () => {
|
||||
const customProtocol = {
|
||||
registry: {
|
||||
'resource1': '@file://r1.md',
|
||||
'resource2': '@file://r2.md'
|
||||
}
|
||||
};
|
||||
|
||||
registry.register('custom', customProtocol);
|
||||
const resources = registry.listProtocolResources('custom');
|
||||
|
||||
expect(resources).toContain('resource1');
|
||||
expect(resources).toContain('resource2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('验证功能', () => {
|
||||
test('应该验证有效的协议和资源ID', () => {
|
||||
expect(registry.validateReference('prompt', 'protocols')).toBe(true);
|
||||
expect(registry.validateReference('file', 'any-path.md')).toBe(true);
|
||||
expect(registry.validateReference('memory', 'declarative')).toBe(true);
|
||||
});
|
||||
|
||||
test('应该拒绝无效的协议和资源ID', () => {
|
||||
expect(registry.validateReference('unknown', 'test')).toBe(false);
|
||||
expect(registry.validateReference('prompt', 'nonexistent')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('注册表信息', () => {
|
||||
test('应该返回完整的注册表信息', () => {
|
||||
const info = registry.getRegistryInfo();
|
||||
|
||||
expect(info.builtin).toHaveProperty('prompt');
|
||||
expect(info.builtin).toHaveProperty('file');
|
||||
expect(info.builtin).toHaveProperty('memory');
|
||||
expect(info.custom).toEqual({});
|
||||
});
|
||||
|
||||
test('应该返回协议的资源列表', () => {
|
||||
const resources = registry.listProtocolResources('prompt');
|
||||
|
||||
expect(resources).toContain('protocols');
|
||||
expect(resources).toContain('core');
|
||||
expect(resources).toContain('domain');
|
||||
expect(resources).toContain('bootstrap');
|
||||
});
|
||||
|
||||
test('应该为无注册表的协议返回空列表', () => {
|
||||
const resources = registry.listProtocolResources('file');
|
||||
|
||||
expect(resources).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
301
src/tests/fixtures/testResources.js
vendored
Normal file
301
src/tests/fixtures/testResources.js
vendored
Normal file
@ -0,0 +1,301 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* 测试资源工厂
|
||||
* 提供测试用的固定数据和辅助函数
|
||||
*/
|
||||
class TestResourceFactory {
|
||||
constructor() {
|
||||
this.tempDirs = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时测试目录
|
||||
* @returns {Promise<string>} 临时目录路径
|
||||
*/
|
||||
async createTempDir() {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-test-'));
|
||||
this.tempDirs.add(tempDir);
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有临时目录
|
||||
*/
|
||||
async cleanup() {
|
||||
for (const dir of this.tempDirs) {
|
||||
try {
|
||||
await fs.rmdir(dir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.warn(`Failed to cleanup temp dir ${dir}:`, error.message);
|
||||
}
|
||||
}
|
||||
this.tempDirs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建完整的PromptX测试项目结构
|
||||
* @param {string} baseDir - 基础目录
|
||||
* @returns {Promise<object>} 创建的文件路径映射
|
||||
*/
|
||||
async createPromptXStructure(baseDir) {
|
||||
const structure = {
|
||||
prompt: path.join(baseDir, 'prompt'),
|
||||
core: path.join(baseDir, 'prompt', 'core'),
|
||||
domain: path.join(baseDir, 'prompt', 'domain'),
|
||||
protocol: path.join(baseDir, 'prompt', 'protocol'),
|
||||
memory: path.join(baseDir, '.memory')
|
||||
};
|
||||
|
||||
// 创建目录结构
|
||||
for (const dir of Object.values(structure)) {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// 创建测试文件
|
||||
const files = {
|
||||
bootstrap: path.join(baseDir, 'bootstrap.md'),
|
||||
coreThought: path.join(structure.core, 'thought', 'critical-thinking.md'),
|
||||
coreExecution: path.join(structure.core, 'execution', 'problem-solving.md'),
|
||||
domainTest: path.join(structure.domain, 'test', 'unit-testing.md'),
|
||||
protocolDpml: path.join(structure.protocol, 'dpml.md'),
|
||||
memoryDeclarative: path.join(structure.memory, 'declarative.md'),
|
||||
memoryProcedural: path.join(structure.memory, 'procedural.md')
|
||||
};
|
||||
|
||||
// 创建core子目录
|
||||
await fs.mkdir(path.join(structure.core, 'thought'), { recursive: true });
|
||||
await fs.mkdir(path.join(structure.core, 'execution'), { recursive: true });
|
||||
await fs.mkdir(path.join(structure.domain, 'test'), { recursive: true });
|
||||
|
||||
// 写入测试文件内容
|
||||
await this.writeTestFiles(files);
|
||||
|
||||
return { structure, files };
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入测试文件内容
|
||||
* @param {object} files - 文件路径映射
|
||||
*/
|
||||
async writeTestFiles(files) {
|
||||
const contents = {
|
||||
[files.bootstrap]: `# PromptX Bootstrap
|
||||
|
||||
这是PromptX的启动文件,用于初始化AI助手。
|
||||
|
||||
## 核心功能
|
||||
- 资源加载
|
||||
- 协议解析
|
||||
- 角色初始化
|
||||
|
||||
## 使用方法
|
||||
通过 \`@promptx://bootstrap\` 引用此文件。`,
|
||||
|
||||
[files.coreThought]: `# 批判性思维模式
|
||||
|
||||
#思维模式 批判性思维
|
||||
|
||||
## 定义
|
||||
批判性思维是分析、评估和改进思维的过程。
|
||||
|
||||
## 原则
|
||||
1. 质疑假设
|
||||
2. 寻求证据
|
||||
3. 考虑多个角度
|
||||
4. 识别偏见
|
||||
|
||||
## 应用
|
||||
在分析问题时,始终保持批判性思维。`,
|
||||
|
||||
[files.coreExecution]: `# 问题解决执行模式
|
||||
|
||||
#执行模式 问题解决
|
||||
|
||||
## 流程
|
||||
1. 问题定义
|
||||
2. 分析阶段
|
||||
3. 解决方案设计
|
||||
4. 实施执行
|
||||
5. 结果评估
|
||||
|
||||
## 工具
|
||||
- 根因分析
|
||||
- 决策树
|
||||
- 头脑风暴
|
||||
- 原型验证`,
|
||||
|
||||
[files.domainTest]: `# 单元测试领域知识
|
||||
|
||||
#领域知识 软件测试
|
||||
|
||||
## 单元测试原则
|
||||
- 独立性
|
||||
- 可重复性
|
||||
- 快速执行
|
||||
- 自我验证
|
||||
|
||||
## 最佳实践
|
||||
- AAA模式(Arrange, Act, Assert)
|
||||
- 测试驱动开发(TDD)
|
||||
- 边界值测试
|
||||
- 异常情况覆盖`,
|
||||
|
||||
[files.protocolDpml]: `# DPML资源协议规范
|
||||
|
||||
## 语法格式
|
||||
\`@[!?]protocol://resource_id[?params]\`
|
||||
|
||||
## 加载语义
|
||||
- \`@\`: 默认加载
|
||||
- \`@!\`: 热加载
|
||||
- \`@?\`: 懒加载
|
||||
|
||||
## 支持的协议
|
||||
- promptx: PromptX内置资源
|
||||
- file: 文件系统资源
|
||||
- memory: 记忆系统资源
|
||||
- http/https: 网络资源`,
|
||||
|
||||
[files.memoryDeclarative]: `# 陈述性记忆
|
||||
|
||||
记录事实性知识和概念。
|
||||
|
||||
## 类型
|
||||
- 事实记忆
|
||||
- 概念记忆
|
||||
- 规则记忆
|
||||
|
||||
## 存储格式
|
||||
JSON结构化数据`,
|
||||
|
||||
[files.memoryProcedural]: `# 程序性记忆
|
||||
|
||||
记录如何执行特定任务的步骤。
|
||||
|
||||
## 类型
|
||||
- 技能记忆
|
||||
- 习惯记忆
|
||||
- 流程记忆
|
||||
|
||||
## 存储格式
|
||||
步骤化序列`
|
||||
};
|
||||
|
||||
for (const [filePath, content] of Object.entries(contents)) {
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试用的DPML资源引用
|
||||
*/
|
||||
getTestResourceRefs() {
|
||||
return {
|
||||
valid: [
|
||||
'@promptx://bootstrap',
|
||||
'@promptx://protocols',
|
||||
'@promptx://core',
|
||||
'@file://test.md',
|
||||
'@!file://hot-reload.md',
|
||||
'@?memory://lazy-load',
|
||||
'@file://data.json?format=json',
|
||||
'@file://content.md?line=5-10',
|
||||
'@memory://declarative',
|
||||
'@memory://procedural',
|
||||
'@http://example.com/api',
|
||||
'@https://api.example.com/data.json'
|
||||
],
|
||||
invalid: [
|
||||
'promptx://missing-at',
|
||||
'@://empty-protocol',
|
||||
'@123invalid://numeric-start',
|
||||
'@file://',
|
||||
'',
|
||||
null,
|
||||
undefined,
|
||||
'@unknown://invalid-protocol'
|
||||
],
|
||||
nested: [
|
||||
'@promptx://@file://nested.md',
|
||||
'@memory://@promptx://bootstrap',
|
||||
'@file://@memory://declarative',
|
||||
'@promptx://@memory://@file://deep-nested.md'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试用的查询参数
|
||||
*/
|
||||
getTestQueryParams() {
|
||||
return {
|
||||
line: ['1', '5-10', '1-', '-10'],
|
||||
format: ['json', 'text', 'xml'],
|
||||
cache: ['true', 'false', '1', '0'],
|
||||
encoding: ['utf8', 'gbk', 'ascii'],
|
||||
timeout: ['5000', '10000'],
|
||||
custom: ['value1', 'value2']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟的协议注册表
|
||||
*/
|
||||
getMockProtocolRegistry() {
|
||||
return {
|
||||
'test-protocol': {
|
||||
description: '测试协议',
|
||||
location: 'test://{resource_id}',
|
||||
registry: {
|
||||
'resource1': '@file://test1.md',
|
||||
'resource2': '@file://test2.md',
|
||||
'nested': '@test-protocol://resource1'
|
||||
}
|
||||
},
|
||||
'mock-http': {
|
||||
description: '模拟HTTP协议',
|
||||
location: 'mock-http://{url}',
|
||||
params: {
|
||||
timeout: 'number',
|
||||
format: 'string'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误场景测试数据
|
||||
*/
|
||||
getErrorScenarios() {
|
||||
return {
|
||||
fileNotFound: '@file://nonexistent.md',
|
||||
permissionDenied: '@file://restricted.md',
|
||||
invalidProtocol: '@unknown://test',
|
||||
malformedUrl: '@file://',
|
||||
networkTimeout: '@http://timeout.example.com',
|
||||
parseError: 'invalid-syntax'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TestResourceFactory,
|
||||
|
||||
// 便捷工厂函数
|
||||
createTestFactory: () => new TestResourceFactory(),
|
||||
|
||||
// 常用测试数据
|
||||
SAMPLE_DPML_REFS: [
|
||||
'@promptx://protocols',
|
||||
'@file://test.md?line=1-10',
|
||||
'@!memory://hot-memory',
|
||||
'@?file://lazy-file.md'
|
||||
],
|
||||
|
||||
SAMPLE_PROTOCOLS: ['promptx', 'file', 'memory', 'http', 'https'],
|
||||
|
||||
SAMPLE_LOADING_SEMANTICS: ['@', '@!', '@?']
|
||||
};
|
||||
123
src/tests/setup.js
Normal file
123
src/tests/setup.js
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Jest测试环境设置
|
||||
*/
|
||||
|
||||
// 设置测试超时时间
|
||||
jest.setTimeout(30000);
|
||||
|
||||
// 全局变量设置
|
||||
global.TEST_ENV = 'test';
|
||||
|
||||
// 模拟console.log以减少测试输出噪音
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
// 在测试环境中静默一些不必要的日志
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
console.log = (...args) => {
|
||||
// 只有在明确需要时才输出
|
||||
if (args.some(arg => typeof arg === 'string' && arg.includes('TEST_OUTPUT'))) {
|
||||
originalConsoleLog(...args);
|
||||
}
|
||||
};
|
||||
|
||||
console.warn = (...args) => {
|
||||
// 保留警告信息
|
||||
if (args.some(arg => typeof arg === 'string' && arg.includes('TEST_WARN'))) {
|
||||
originalConsoleWarn(...args);
|
||||
}
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
// 保留错误信息
|
||||
originalConsoleError(...args);
|
||||
};
|
||||
}
|
||||
|
||||
// 测试结束后恢复console
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
// 全局测试工具函数
|
||||
global.testUtils = {
|
||||
/**
|
||||
* 等待一段时间
|
||||
* @param {number} ms - 毫秒数
|
||||
*/
|
||||
sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
|
||||
|
||||
/**
|
||||
* 创建延迟Promise
|
||||
* @param {any} value - 返回值
|
||||
* @param {number} delay - 延迟时间
|
||||
*/
|
||||
delayed: (value, delay = 100) =>
|
||||
new Promise(resolve => setTimeout(() => resolve(value), delay)),
|
||||
|
||||
/**
|
||||
* 创建拒绝的Promise
|
||||
* @param {any} error - 错误对象
|
||||
* @param {number} delay - 延迟时间
|
||||
*/
|
||||
delayedReject: (error, delay = 100) =>
|
||||
new Promise((_, reject) => setTimeout(() => reject(error), delay))
|
||||
};
|
||||
|
||||
// 全局断言扩展
|
||||
expect.extend({
|
||||
/**
|
||||
* 检查是否为有效的DPML资源引用
|
||||
*/
|
||||
toBeValidDpmlReference(received) {
|
||||
const dpmlPattern = /^@[!?]?[a-zA-Z][a-zA-Z0-9_-]*:\/\/.+/;
|
||||
const pass = typeof received === 'string' && dpmlPattern.test(received);
|
||||
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected ${received} not to be a valid DPML reference`,
|
||||
pass: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected ${received} to be a valid DPML reference`,
|
||||
pass: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查对象是否包含必需的属性
|
||||
*/
|
||||
toHaveRequiredProperties(received, properties) {
|
||||
const missingProps = properties.filter(prop => !(prop in received));
|
||||
const pass = missingProps.length === 0;
|
||||
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected object not to have properties ${properties.join(', ')}`,
|
||||
pass: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected object to have properties ${missingProps.join(', ')}`,
|
||||
pass: false
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 处理未捕获的Promise拒绝
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
// 处理未捕获的异常
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught Exception:', error);
|
||||
});
|
||||
|
||||
console.log('🧪 Jest测试环境已初始化');
|
||||
Reference in New Issue
Block a user