refector:@ refrence 架构重构
This commit is contained in:
475
docs/resource-referrence-architech.md
Normal file
475
docs/resource-referrence-architech.md
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
# PromptX 统一资源协议架构设计文档
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
- [1. 概述](#1-概述)
|
||||||
|
- [2. 核心设计理念](#2-核心设计理念)
|
||||||
|
- [3. DPML协议体系设计](#3-dpml协议体系设计)
|
||||||
|
- [4. 多层级资源发现架构](#4-多层级资源发现架构)
|
||||||
|
- [5. 统一注册表设计](#5-统一注册表设计)
|
||||||
|
- [6. 协议解析执行流程](#6-协议解析执行流程)
|
||||||
|
- [7. 资源生命周期管理](#7-资源生命周期管理)
|
||||||
|
- [8. 扩展性设计](#8-扩展性设计)
|
||||||
|
- [9. 错误处理与容错机制](#9-错误处理与容错机制)
|
||||||
|
- [10. 性能优化策略](#10-性能优化策略)
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
PromptX采用统一资源协议架构,支持多层级资源发现与动态注册机制。该架构通过DPML(Domain-Specific Prompt Markup Language)协议实现资源的统一标识、发现、注册和解析。
|
||||||
|
|
||||||
|
## 2. 核心设计理念
|
||||||
|
|
||||||
|
### 2.1 分层架构原则
|
||||||
|
|
||||||
|
- **协议层**: 统一的@协议标识体系
|
||||||
|
- **发现层**: 多源资源发现机制
|
||||||
|
- **注册层**: 统一的资源注册表
|
||||||
|
- **解析层**: 协议到文件系统的映射
|
||||||
|
|
||||||
|
### 2.2 职责分离原则
|
||||||
|
|
||||||
|
- **Registry**: 专注资源映射存储和查询
|
||||||
|
- **Discovery**: 专注特定来源的资源发现
|
||||||
|
- **Protocol**: 专注协议解析和路径转换
|
||||||
|
- **Manager**: 专注流程编排和生命周期管理
|
||||||
|
|
||||||
|
## 3. DPML协议体系设计
|
||||||
|
|
||||||
|
### 3.1 协议语法结构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["@[semantic][protocol]://[resource_path]"] --> B[加载语义]
|
||||||
|
A --> C[协议类型]
|
||||||
|
A --> D[资源路径]
|
||||||
|
|
||||||
|
B --> B1["@ - 默认加载"]
|
||||||
|
B --> B2["@! - 热加载"]
|
||||||
|
B --> B3["@? - 懒加载"]
|
||||||
|
|
||||||
|
C --> C1["package - NPM包资源"]
|
||||||
|
C --> C2["project - 项目本地资源"]
|
||||||
|
C --> C3["user - 用户全局资源"]
|
||||||
|
C --> C4["internet - 网络资源"]
|
||||||
|
C --> C5["file - 文件系统"]
|
||||||
|
C --> C6["thought - 思维模式"]
|
||||||
|
C --> C7["execution - 执行模式"]
|
||||||
|
C --> C8["role - AI角色"]
|
||||||
|
C --> C9["knowledge - 知识库"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 协议层级设计
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class ProtocolSystem {
|
||||||
|
<<interface>>
|
||||||
|
+parseReference(ref: string)
|
||||||
|
+resolve(ref: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BasicProtocol {
|
||||||
|
+package
|
||||||
|
+project
|
||||||
|
+user
|
||||||
|
+internet
|
||||||
|
+file
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogicalProtocol {
|
||||||
|
+thought
|
||||||
|
+execution
|
||||||
|
+role
|
||||||
|
+knowledge
|
||||||
|
+memory
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtocolSystem <|-- BasicProtocol
|
||||||
|
ProtocolSystem <|-- LogicalProtocol
|
||||||
|
|
||||||
|
BasicProtocol : +直接文件映射
|
||||||
|
BasicProtocol : +路径解析
|
||||||
|
LogicalProtocol : +注册表查询
|
||||||
|
LogicalProtocol : +逻辑资源映射
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 多层级资源发现架构
|
||||||
|
|
||||||
|
### 4.1 发现层级结构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[资源发现生态系统] --> B[包级发现 PackageDiscovery]
|
||||||
|
A --> C[项目级发现 ProjectDiscovery]
|
||||||
|
A --> D[用户级发现 UserDiscovery]
|
||||||
|
A --> E[网络级发现 InternetDiscovery]
|
||||||
|
|
||||||
|
B --> B1[NPM包内置资源]
|
||||||
|
B --> B2[src/resource.registry.json]
|
||||||
|
B --> B3[prompt/ 目录结构]
|
||||||
|
|
||||||
|
C --> C1[项目本地资源]
|
||||||
|
C --> C2[.promptx/resource/]
|
||||||
|
C --> C3[动态角色发现]
|
||||||
|
|
||||||
|
D --> D1[用户全局资源]
|
||||||
|
D --> D2[~/promptx/resource/]
|
||||||
|
D --> D3[用户自定义角色]
|
||||||
|
|
||||||
|
E --> E1[远程资源库]
|
||||||
|
E --> E2[Git仓库资源]
|
||||||
|
E --> E3[在线角色市场]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 发现优先级机制
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant RM as ResourceManager
|
||||||
|
participant PD as PackageDiscovery
|
||||||
|
participant PRD as ProjectDiscovery
|
||||||
|
participant UD as UserDiscovery
|
||||||
|
participant ID as InternetDiscovery
|
||||||
|
participant RR as ResourceRegistry
|
||||||
|
|
||||||
|
RM->>+PD: discover() [优先级: 1]
|
||||||
|
PD->>-RM: 系统内置资源列表
|
||||||
|
|
||||||
|
RM->>+PRD: discover() [优先级: 2]
|
||||||
|
PRD->>-RM: 项目本地资源列表
|
||||||
|
|
||||||
|
RM->>+UD: discover() [优先级: 3]
|
||||||
|
UD->>-RM: 用户全局资源列表
|
||||||
|
|
||||||
|
RM->>+ID: discover() [优先级: 4]
|
||||||
|
ID->>-RM: 网络资源列表
|
||||||
|
|
||||||
|
Note over RM: 按优先级合并,后发现的覆盖先发现的
|
||||||
|
|
||||||
|
RM->>RR: 批量注册所有发现的资源
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 统一注册表设计
|
||||||
|
|
||||||
|
### 5.1 注册表核心结构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class ResourceRegistry {
|
||||||
|
-Map~string,string~ index
|
||||||
|
-Map~string,ResourceMeta~ metadata
|
||||||
|
+register(id: string, reference: string)
|
||||||
|
+resolve(id: string): string
|
||||||
|
+merge(other: ResourceRegistry)
|
||||||
|
+has(id: string): boolean
|
||||||
|
+list(protocol?: string): string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResourceMeta {
|
||||||
|
+source: DiscoverySource
|
||||||
|
+priority: number
|
||||||
|
+timestamp: Date
|
||||||
|
+metadata: object
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscoverySource {
|
||||||
|
<<enumeration>>
|
||||||
|
PACKAGE
|
||||||
|
PROJECT
|
||||||
|
USER
|
||||||
|
INTERNET
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceRegistry --> ResourceMeta
|
||||||
|
ResourceMeta --> DiscoverySource
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 注册表合并策略
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[多源资源发现] --> B{资源ID冲突检测}
|
||||||
|
|
||||||
|
B -->|无冲突| C[直接注册]
|
||||||
|
B -->|有冲突| D[优先级比较]
|
||||||
|
|
||||||
|
D --> E{优先级策略}
|
||||||
|
E -->|用户 > 项目 > 包 > 网络| F[高优先级覆盖]
|
||||||
|
E -->|同优先级| G[时间戳比较]
|
||||||
|
|
||||||
|
F --> H[更新注册表]
|
||||||
|
G --> H
|
||||||
|
C --> H
|
||||||
|
|
||||||
|
H --> I[生成统一资源索引]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 协议解析执行流程
|
||||||
|
|
||||||
|
### 6.1 完整解析序列
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as User
|
||||||
|
participant RM as ResourceManager
|
||||||
|
participant RR as ResourceRegistry
|
||||||
|
participant PR as ProtocolResolver
|
||||||
|
participant PP as PackageProtocol
|
||||||
|
participant PRP as ProjectProtocol
|
||||||
|
participant FS as FileSystem
|
||||||
|
|
||||||
|
U->>RM: loadResource("role:xiaohongshu-copywriter")
|
||||||
|
|
||||||
|
Note over RM: 1. 资源ID解析阶段
|
||||||
|
RM->>RR: resolve("role:xiaohongshu-copywriter")
|
||||||
|
RR->>RM: "@project://.promptx/resource/domain/xiaohongshu-copywriter/xiaohongshu-copywriter.role.md"
|
||||||
|
|
||||||
|
Note over RM: 2. 协议解析阶段
|
||||||
|
RM->>PR: parseReference("@project://...")
|
||||||
|
PR->>RM: {semantic: "@", protocol: "project", path: "..."}
|
||||||
|
|
||||||
|
Note over RM: 3. 路径解析阶段
|
||||||
|
RM->>PRP: resolve(".promptx/resource/domain/...")
|
||||||
|
PRP->>FS: 定位项目根目录
|
||||||
|
FS->>PRP: "/user/project/"
|
||||||
|
PRP->>RM: "/user/project/.promptx/resource/domain/xiaohongshu-copywriter/xiaohongshu-copywriter.role.md"
|
||||||
|
|
||||||
|
Note over RM: 4. 文件加载阶段
|
||||||
|
RM->>FS: readFile(absolutePath)
|
||||||
|
FS->>RM: 文件内容
|
||||||
|
|
||||||
|
RM->>U: {success: true, content: "...", path: "..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 嵌套引用解析
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant SR as SemanticRenderer
|
||||||
|
participant RM as ResourceManager
|
||||||
|
participant RR as ResourceRegistry
|
||||||
|
|
||||||
|
Note over SR: 发现嵌套引用 @!thought://remember
|
||||||
|
|
||||||
|
SR->>RM: resolve("@!thought://remember")
|
||||||
|
|
||||||
|
Note over RM: 1. 解析加载语义 @!
|
||||||
|
RM->>RM: 识别热加载模式
|
||||||
|
|
||||||
|
Note over RM: 2. 解析逻辑协议 thought
|
||||||
|
RM->>RR: resolve("thought:remember")
|
||||||
|
RR->>RM: "@package://prompt/core/thought/remember.thought.md"
|
||||||
|
|
||||||
|
Note over RM: 3. 解析基础协议 @package
|
||||||
|
RM->>RM: 委托给PackageProtocol
|
||||||
|
|
||||||
|
Note over RM: 4. 返回解析结果
|
||||||
|
RM->>SR: 思维模式内容
|
||||||
|
|
||||||
|
Note over SR: 5. 渲染到最终输出
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 资源生命周期管理
|
||||||
|
|
||||||
|
### 7.1 初始化流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Uninitialized
|
||||||
|
|
||||||
|
Uninitialized --> Discovering: initialize()
|
||||||
|
|
||||||
|
Discovering --> PackageDiscovery: 发现包资源
|
||||||
|
PackageDiscovery --> ProjectDiscovery: 发现项目资源
|
||||||
|
ProjectDiscovery --> UserDiscovery: 发现用户资源
|
||||||
|
UserDiscovery --> InternetDiscovery: 发现网络资源
|
||||||
|
|
||||||
|
InternetDiscovery --> Registering: 开始注册
|
||||||
|
|
||||||
|
Registering --> Merging: 合并资源
|
||||||
|
Merging --> Indexing: 建立索引
|
||||||
|
Indexing --> Ready: 完成初始化
|
||||||
|
|
||||||
|
Ready --> Resolving: resolve()
|
||||||
|
Resolving --> Ready: 返回结果
|
||||||
|
|
||||||
|
Ready --> Refreshing: refresh()
|
||||||
|
Refreshing --> Discovering: 重新发现
|
||||||
|
|
||||||
|
Ready --> [*]: destroy()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 缓存与更新策略
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[资源访问请求] --> B{缓存检查}
|
||||||
|
|
||||||
|
B -->|命中| C[检查加载语义]
|
||||||
|
B -->|未命中| D[触发发现流程]
|
||||||
|
|
||||||
|
C --> E{加载语义判断}
|
||||||
|
E -->|at 默认| F[返回缓存内容]
|
||||||
|
E -->|a! 热加载| G[强制重新加载]
|
||||||
|
E -->|a? 懒加载| H[延迟加载策略]
|
||||||
|
|
||||||
|
D --> I[执行Discovery]
|
||||||
|
G --> I
|
||||||
|
H --> I
|
||||||
|
|
||||||
|
I --> J[更新注册表]
|
||||||
|
J --> K[更新缓存]
|
||||||
|
K --> L[返回最新内容]
|
||||||
|
|
||||||
|
F --> M[返回给用户]
|
||||||
|
L --> M
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 扩展性设计
|
||||||
|
|
||||||
|
### 8.1 新协议扩展机制
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class ProtocolExtension {
|
||||||
|
<<interface>>
|
||||||
|
+name: string
|
||||||
|
+resolve(path: string): string
|
||||||
|
+validate(path: string): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseProtocol {
|
||||||
|
+name: "database"
|
||||||
|
+resolve(path): string
|
||||||
|
+validate(path): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitProtocol {
|
||||||
|
+name: "git"
|
||||||
|
+resolve(path): string
|
||||||
|
+validate(path): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class S3Protocol {
|
||||||
|
+name: "s3"
|
||||||
|
+resolve(path): string
|
||||||
|
+validate(path): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtocolExtension <|-- DatabaseProtocol
|
||||||
|
ProtocolExtension <|-- GitProtocol
|
||||||
|
ProtocolExtension <|-- S3Protocol
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 新发现源扩展
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class DiscoveryExtension {
|
||||||
|
<<interface>>
|
||||||
|
+source: DiscoverySource
|
||||||
|
+priority: number
|
||||||
|
+discover(): Promise~Resource[]~
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitDiscovery {
|
||||||
|
+source: GIT
|
||||||
|
+priority: 5
|
||||||
|
+discover(): Promise~Resource[]~
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseDiscovery {
|
||||||
|
+source: DATABASE
|
||||||
|
+priority: 6
|
||||||
|
+discover(): Promise~Resource[]~
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarketplaceDiscovery {
|
||||||
|
+source: MARKETPLACE
|
||||||
|
+priority: 7
|
||||||
|
+discover(): Promise~Resource[]~
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoveryExtension <|-- GitDiscovery
|
||||||
|
DiscoveryExtension <|-- DatabaseDiscovery
|
||||||
|
DiscoveryExtension <|-- MarketplaceDiscovery
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 错误处理与容错机制
|
||||||
|
|
||||||
|
### 9.1 分层错误处理
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[用户请求] --> B[ResourceManager]
|
||||||
|
|
||||||
|
B --> C{Discovery阶段}
|
||||||
|
C -->|发现失败| D[降级到缓存]
|
||||||
|
C -->|部分失败| E[记录错误,继续处理]
|
||||||
|
C -->|完全失败| F[返回空注册表]
|
||||||
|
|
||||||
|
B --> G{Registry阶段}
|
||||||
|
G -->|注册失败| H[跳过失败项]
|
||||||
|
G -->|冲突解决失败| I[使用默认策略]
|
||||||
|
|
||||||
|
B --> J{Protocol阶段}
|
||||||
|
J -->|协议不支持| K[尝试文件协议]
|
||||||
|
J -->|路径解析失败| L[返回详细错误]
|
||||||
|
|
||||||
|
B --> M{FileSystem阶段}
|
||||||
|
M -->|文件不存在| N[返回空内容标记]
|
||||||
|
M -->|权限错误| O[尝试替代路径]
|
||||||
|
|
||||||
|
D --> P[用户获得部分功能]
|
||||||
|
E --> P
|
||||||
|
H --> P
|
||||||
|
I --> P
|
||||||
|
K --> P
|
||||||
|
|
||||||
|
F --> Q[用户获得错误信息]
|
||||||
|
L --> Q
|
||||||
|
N --> Q
|
||||||
|
O --> Q
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 性能优化策略
|
||||||
|
|
||||||
|
### 10.1 并行发现优化
|
||||||
|
|
||||||
|
| 模式 | PackageDiscovery | ProjectDiscovery | UserDiscovery | InternetDiscovery | 合并注册 |
|
||||||
|
|------|-----------------|------------------|---------------|-------------------|---------|
|
||||||
|
| 串行模式 | 0-200ms | 200-400ms | 400-600ms | 600-1000ms | - |
|
||||||
|
| 并行模式 | 0-200ms | 0-250ms | 0-180ms | 0-800ms | 800-850ms |
|
||||||
|
|
||||||
|
### 10.2 缓存分层策略
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[L1: 内存缓存] --> B[L2: 文件缓存]
|
||||||
|
B --> C[L3: 远程缓存]
|
||||||
|
|
||||||
|
A --> A1[热点资源]
|
||||||
|
A --> A2[最近访问]
|
||||||
|
|
||||||
|
B --> B1[用户资源]
|
||||||
|
B --> B2[项目资源]
|
||||||
|
|
||||||
|
C --> C1[网络资源]
|
||||||
|
C --> C2[共享资源]
|
||||||
|
|
||||||
|
D[缓存失效策略] --> D1[TTL过期]
|
||||||
|
D --> D2[版本变更]
|
||||||
|
D --> D3[手动刷新]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
这个架构设计确保了PromptX资源系统的高度可扩展性、跨平台兼容性和优秀的性能表现,同时保持了清晰的职责分离和统一的访问接口。
|
||||||
|
|
||||||
|
### 核心优势
|
||||||
|
|
||||||
|
1. **统一协议体系**: 通过DPML协议实现资源的标准化访问
|
||||||
|
2. **多层级发现**: 支持包、项目、用户、网络多个层级的资源发现
|
||||||
|
3. **智能注册表**: 自动处理资源冲突和优先级管理
|
||||||
|
4. **高性能设计**: 并行发现和多层缓存策略
|
||||||
|
5. **强容错性**: 完善的错误处理和降级机制
|
||||||
|
6. **易扩展性**: 支持新协议和新发现源的动态扩展
|
||||||
292
src/lib/core/resource/EnhancedResourceRegistry.js
Normal file
292
src/lib/core/resource/EnhancedResourceRegistry.js
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
/**
|
||||||
|
* EnhancedResourceRegistry - 增强的资源注册表
|
||||||
|
*
|
||||||
|
* 按照DPML协议架构文档设计,支持:
|
||||||
|
* 1. 资源元数据管理(source, priority, timestamp)
|
||||||
|
* 2. 智能合并策略(优先级和时间戳)
|
||||||
|
* 3. 发现源优先级管理
|
||||||
|
* 4. 批量操作支持
|
||||||
|
*/
|
||||||
|
class EnhancedResourceRegistry {
|
||||||
|
constructor() {
|
||||||
|
// 主索引:resourceId -> reference
|
||||||
|
this.index = new Map()
|
||||||
|
|
||||||
|
// 元数据索引:resourceId -> metadata
|
||||||
|
this.metadata = new Map()
|
||||||
|
|
||||||
|
// 发现源优先级映射
|
||||||
|
this.sourcePriority = {
|
||||||
|
'USER': 1, // 最高优先级
|
||||||
|
'PROJECT': 2,
|
||||||
|
'PACKAGE': 3,
|
||||||
|
'INTERNET': 4 // 最低优先级
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册单个资源
|
||||||
|
* @param {Object} resource - 资源对象
|
||||||
|
* @param {string} resource.id - 资源ID
|
||||||
|
* @param {string} resource.reference - 资源引用
|
||||||
|
* @param {Object} resource.metadata - 资源元数据
|
||||||
|
*/
|
||||||
|
register(resource) {
|
||||||
|
this._validateResource(resource)
|
||||||
|
|
||||||
|
const { id, reference, metadata } = resource
|
||||||
|
|
||||||
|
// 如果资源已存在,检查是否应该覆盖
|
||||||
|
if (this.has(id)) {
|
||||||
|
const existingMetadata = this.metadata.get(id)
|
||||||
|
if (!this._shouldOverride(existingMetadata, metadata)) {
|
||||||
|
return // 不覆盖,保持现有资源
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册资源
|
||||||
|
this.index.set(id, reference)
|
||||||
|
this.metadata.set(id, { ...metadata })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量注册资源
|
||||||
|
* @param {Array} resources - 资源数组
|
||||||
|
*/
|
||||||
|
registerBatch(resources) {
|
||||||
|
if (!Array.isArray(resources)) {
|
||||||
|
throw new Error('Resources must be an array')
|
||||||
|
}
|
||||||
|
|
||||||
|
resources.forEach(resource => {
|
||||||
|
try {
|
||||||
|
if (resource && typeof resource === 'object') {
|
||||||
|
this.register(resource)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[EnhancedResourceRegistry] Failed to register resource: ${error.message}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并另一个注册表
|
||||||
|
* @param {EnhancedResourceRegistry} otherRegistry - 另一个注册表实例
|
||||||
|
*/
|
||||||
|
merge(otherRegistry) {
|
||||||
|
if (!(otherRegistry instanceof EnhancedResourceRegistry)) {
|
||||||
|
throw new Error('Can only merge with another EnhancedResourceRegistry instance')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有资源并批量注册(会自动处理优先级)
|
||||||
|
const otherResources = otherRegistry.list().map(id => ({
|
||||||
|
id,
|
||||||
|
reference: otherRegistry.resolve(id),
|
||||||
|
metadata: otherRegistry.getMetadata(id)
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.registerBatch(otherResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析资源ID到引用
|
||||||
|
* @param {string} resourceId - 资源ID
|
||||||
|
* @returns {string} 资源引用
|
||||||
|
*/
|
||||||
|
resolve(resourceId) {
|
||||||
|
// 1. 直接查找
|
||||||
|
if (this.index.has(resourceId)) {
|
||||||
|
return this.index.get(resourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 向后兼容:尝试添加协议前缀
|
||||||
|
const protocols = ['role', 'thought', 'execution', 'memory']
|
||||||
|
|
||||||
|
for (const protocol of protocols) {
|
||||||
|
const fullId = `${protocol}:${resourceId}`
|
||||||
|
if (this.index.has(fullId)) {
|
||||||
|
return this.index.get(fullId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Resource '${resourceId}' not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查资源是否存在
|
||||||
|
* @param {string} resourceId - 资源ID
|
||||||
|
* @returns {boolean} 是否存在
|
||||||
|
*/
|
||||||
|
has(resourceId) {
|
||||||
|
try {
|
||||||
|
this.resolve(resourceId)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源元数据
|
||||||
|
* @param {string} resourceId - 资源ID
|
||||||
|
* @returns {Object|null} 元数据对象或null
|
||||||
|
*/
|
||||||
|
getMetadata(resourceId) {
|
||||||
|
// 直接查找
|
||||||
|
if (this.metadata.has(resourceId)) {
|
||||||
|
return { ...this.metadata.get(resourceId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向后兼容查找
|
||||||
|
const protocols = ['role', 'thought', 'execution', 'memory']
|
||||||
|
|
||||||
|
for (const protocol of protocols) {
|
||||||
|
const fullId = `${protocol}:${resourceId}`
|
||||||
|
if (this.metadata.has(fullId)) {
|
||||||
|
return { ...this.metadata.get(fullId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出所有资源ID
|
||||||
|
* @param {string} [protocol] - 可选的协议过滤器
|
||||||
|
* @returns {Array<string>} 资源ID列表
|
||||||
|
*/
|
||||||
|
list(protocol = null) {
|
||||||
|
const allIds = Array.from(this.index.keys())
|
||||||
|
|
||||||
|
if (!protocol) {
|
||||||
|
return allIds
|
||||||
|
}
|
||||||
|
|
||||||
|
return allIds.filter(id => id.startsWith(`${protocol}:`))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取注册表大小
|
||||||
|
* @returns {number} 资源数量
|
||||||
|
*/
|
||||||
|
size() {
|
||||||
|
return this.index.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空注册表
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.index.clear()
|
||||||
|
this.metadata.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除资源
|
||||||
|
* @param {string} resourceId - 资源ID
|
||||||
|
*/
|
||||||
|
remove(resourceId) {
|
||||||
|
// 尝试直接移除
|
||||||
|
if (this.index.has(resourceId)) {
|
||||||
|
this.index.delete(resourceId)
|
||||||
|
this.metadata.delete(resourceId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向后兼容移除
|
||||||
|
const protocols = ['role', 'thought', 'execution', 'memory']
|
||||||
|
|
||||||
|
for (const protocol of protocols) {
|
||||||
|
const fullId = `${protocol}:${resourceId}`
|
||||||
|
if (this.index.has(fullId)) {
|
||||||
|
this.index.delete(fullId)
|
||||||
|
this.metadata.delete(fullId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从发现管理器结果加载资源
|
||||||
|
* @param {Array} discoveryResults - 发现器返回的资源数组
|
||||||
|
*/
|
||||||
|
loadFromDiscoveryResults(discoveryResults) {
|
||||||
|
if (!Array.isArray(discoveryResults)) {
|
||||||
|
console.warn('[EnhancedResourceRegistry] Discovery results must be an array')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerBatch(discoveryResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证资源对象
|
||||||
|
* @param {Object} resource - 资源对象
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_validateResource(resource) {
|
||||||
|
if (!resource || typeof resource !== 'object') {
|
||||||
|
throw new Error('Resource must be an object')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resource.id || !resource.reference) {
|
||||||
|
throw new Error('Resource must have id and reference')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resource.metadata || typeof resource.metadata !== 'object') {
|
||||||
|
throw new Error('Resource must have metadata with source and priority')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resource.metadata.source || typeof resource.metadata.priority !== 'number') {
|
||||||
|
throw new Error('Resource must have metadata with source and priority')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证ID格式
|
||||||
|
if (typeof resource.id !== 'string' || !resource.id.includes(':')) {
|
||||||
|
throw new Error('Resource id must be in format "protocol:resourcePath"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证引用格式
|
||||||
|
if (typeof resource.reference !== 'string' || !resource.reference.startsWith('@')) {
|
||||||
|
throw new Error('Resource reference must be in DPML format "@protocol://path"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否应该覆盖现有资源
|
||||||
|
* @param {Object} existingMetadata - 现有资源元数据
|
||||||
|
* @param {Object} newMetadata - 新资源元数据
|
||||||
|
* @returns {boolean} 是否应该覆盖
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_shouldOverride(existingMetadata, newMetadata) {
|
||||||
|
// 1. 按发现源优先级比较
|
||||||
|
const existingSourcePriority = this.sourcePriority[existingMetadata.source] || 999
|
||||||
|
const newSourcePriority = this.sourcePriority[newMetadata.source] || 999
|
||||||
|
|
||||||
|
if (newSourcePriority < existingSourcePriority) {
|
||||||
|
return true // 新资源优先级更高
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSourcePriority > existingSourcePriority) {
|
||||||
|
return false // 现有资源优先级更高
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 相同优先级,按数字优先级比较
|
||||||
|
if (newMetadata.priority < existingMetadata.priority) {
|
||||||
|
return true // 数字越小优先级越高
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newMetadata.priority > existingMetadata.priority) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 相同优先级,按时间戳比较(新的覆盖旧的)
|
||||||
|
const existingTime = existingMetadata.timestamp ? new Date(existingMetadata.timestamp).getTime() : 0
|
||||||
|
const newTime = newMetadata.timestamp ? new Date(newMetadata.timestamp).getTime() : 0
|
||||||
|
|
||||||
|
return newTime >= existingTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EnhancedResourceRegistry
|
||||||
127
src/lib/core/resource/discovery/BaseDiscovery.js
Normal file
127
src/lib/core/resource/discovery/BaseDiscovery.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* BaseDiscovery - 资源发现基础抽象类
|
||||||
|
*
|
||||||
|
* 按照DPML协议架构文档设计,提供统一的资源发现接口
|
||||||
|
* 所有具体的Discovery实现都应该继承这个基类
|
||||||
|
*/
|
||||||
|
class BaseDiscovery {
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param {string} source - 发现源类型 (PACKAGE, PROJECT, USER, INTERNET)
|
||||||
|
* @param {number} priority - 优先级,数字越小优先级越高
|
||||||
|
*/
|
||||||
|
constructor(source, priority = 0) {
|
||||||
|
if (!source) {
|
||||||
|
throw new Error('Discovery source is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.source = source
|
||||||
|
this.priority = priority
|
||||||
|
this.cache = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象方法:发现资源
|
||||||
|
* 子类必须实现此方法
|
||||||
|
* @returns {Promise<Array>} 发现的资源列表
|
||||||
|
*/
|
||||||
|
async discover() {
|
||||||
|
throw new Error('discover method must be implemented by subclass')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取发现器信息
|
||||||
|
* @returns {Object} 发现器元数据
|
||||||
|
*/
|
||||||
|
getDiscoveryInfo() {
|
||||||
|
return {
|
||||||
|
source: this.source,
|
||||||
|
priority: this.priority,
|
||||||
|
description: `${this.source} resource discovery`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证资源结构
|
||||||
|
* @param {Object} resource - 待验证的资源对象
|
||||||
|
* @throws {Error} 如果资源结构无效
|
||||||
|
*/
|
||||||
|
validateResource(resource) {
|
||||||
|
if (!resource || typeof resource !== 'object') {
|
||||||
|
throw new Error('Resource must be an object')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resource.id || !resource.reference) {
|
||||||
|
throw new Error('Resource must have id and reference')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证ID格式 (protocol:resourcePath)
|
||||||
|
if (typeof resource.id !== 'string' || !resource.id.includes(':')) {
|
||||||
|
throw new Error('Resource id must be in format "protocol:resourcePath"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证引用格式 (@protocol://path)
|
||||||
|
if (typeof resource.reference !== 'string' || !resource.reference.startsWith('@')) {
|
||||||
|
throw new Error('Resource reference must be in DPML format "@protocol://path"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化资源对象,添加元数据
|
||||||
|
* @param {Object} resource - 原始资源对象
|
||||||
|
* @returns {Object} 规范化后的资源对象
|
||||||
|
*/
|
||||||
|
normalizeResource(resource) {
|
||||||
|
// 验证资源结构
|
||||||
|
this.validateResource(resource)
|
||||||
|
|
||||||
|
// 创建规范化的资源对象
|
||||||
|
const normalizedResource = {
|
||||||
|
id: resource.id,
|
||||||
|
reference: resource.reference,
|
||||||
|
metadata: {
|
||||||
|
source: this.source,
|
||||||
|
priority: this.priority,
|
||||||
|
timestamp: new Date(),
|
||||||
|
...resource.metadata // 保留现有元数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedResource
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理缓存
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.cache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存大小
|
||||||
|
* @returns {number} 缓存条目数量
|
||||||
|
*/
|
||||||
|
getCacheSize() {
|
||||||
|
return this.cache.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从缓存获取资源
|
||||||
|
* @param {string} key - 缓存键
|
||||||
|
* @returns {*} 缓存的值或undefined
|
||||||
|
*/
|
||||||
|
getFromCache(key) {
|
||||||
|
return this.cache.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存
|
||||||
|
* @param {string} key - 缓存键
|
||||||
|
* @param {*} value - 缓存值
|
||||||
|
*/
|
||||||
|
setCache(key, value) {
|
||||||
|
this.cache.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BaseDiscovery
|
||||||
178
src/lib/core/resource/discovery/CrossPlatformFileScanner.js
Normal file
178
src/lib/core/resource/discovery/CrossPlatformFileScanner.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CrossPlatformFileScanner - 跨平台文件扫描器
|
||||||
|
*
|
||||||
|
* 替代glob库,使用Node.js原生fs API实现跨平台文件扫描
|
||||||
|
* 避免glob在Windows上的兼容性问题
|
||||||
|
*/
|
||||||
|
class CrossPlatformFileScanner {
|
||||||
|
/**
|
||||||
|
* 递归扫描目录,查找匹配的文件
|
||||||
|
* @param {string} baseDir - 基础目录
|
||||||
|
* @param {Object} options - 扫描选项
|
||||||
|
* @param {Array<string>} options.extensions - 文件扩展名列表,如 ['.role.md', '.execution.md']
|
||||||
|
* @param {Array<string>} options.subdirs - 限制扫描的子目录,如 ['domain', 'execution']
|
||||||
|
* @param {number} options.maxDepth - 最大扫描深度,默认10
|
||||||
|
* @returns {Promise<Array<string>>} 匹配的文件路径列表
|
||||||
|
*/
|
||||||
|
async scanFiles(baseDir, options = {}) {
|
||||||
|
const {
|
||||||
|
extensions = [],
|
||||||
|
subdirs = null,
|
||||||
|
maxDepth = 10
|
||||||
|
} = options
|
||||||
|
|
||||||
|
if (!await fs.pathExists(baseDir)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
await this._scanRecursive(baseDir, baseDir, extensions, subdirs, maxDepth, 0, results)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描特定类型的资源文件
|
||||||
|
* @param {string} baseDir - 基础目录
|
||||||
|
* @param {string} resourceType - 资源类型 ('role', 'execution', 'thought')
|
||||||
|
* @returns {Promise<Array<string>>} 匹配的文件路径列表
|
||||||
|
*/
|
||||||
|
async scanResourceFiles(baseDir, resourceType) {
|
||||||
|
const resourceConfig = {
|
||||||
|
role: {
|
||||||
|
extensions: ['.role.md'],
|
||||||
|
subdirs: ['domain'] // 角色文件通常在domain目录下
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
extensions: ['.execution.md'],
|
||||||
|
subdirs: ['execution'] // 执行模式文件在execution目录下
|
||||||
|
},
|
||||||
|
thought: {
|
||||||
|
extensions: ['.thought.md'],
|
||||||
|
subdirs: ['thought'] // 思维模式文件在thought目录下
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = resourceConfig[resourceType]
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Unsupported resource type: ${resourceType}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.scanFiles(baseDir, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归扫描目录的内部实现
|
||||||
|
* @param {string} currentDir - 当前扫描目录
|
||||||
|
* @param {string} baseDir - 基础目录
|
||||||
|
* @param {Array<string>} extensions - 文件扩展名列表
|
||||||
|
* @param {Array<string>|null} subdirs - 限制扫描的子目录
|
||||||
|
* @param {number} maxDepth - 最大深度
|
||||||
|
* @param {number} currentDepth - 当前深度
|
||||||
|
* @param {Array<string>} results - 结果数组
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _scanRecursive(currentDir, baseDir, extensions, subdirs, maxDepth, currentDepth, results) {
|
||||||
|
if (currentDepth >= maxDepth) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(currentDir, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(currentDir, entry.name)
|
||||||
|
|
||||||
|
if (entry.isFile()) {
|
||||||
|
// 检查文件扩展名
|
||||||
|
if (this._matchesExtensions(entry.name, extensions)) {
|
||||||
|
results.push(fullPath)
|
||||||
|
}
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
// 检查是否应该扫描这个子目录
|
||||||
|
if (this._shouldScanDirectory(entry.name, subdirs, currentDepth)) {
|
||||||
|
await this._scanRecursive(
|
||||||
|
fullPath,
|
||||||
|
baseDir,
|
||||||
|
extensions,
|
||||||
|
subdirs,
|
||||||
|
maxDepth,
|
||||||
|
currentDepth + 1,
|
||||||
|
results
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略权限错误或其他文件系统错误
|
||||||
|
console.warn(`[CrossPlatformFileScanner] Failed to scan directory ${currentDir}: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件名是否匹配指定扩展名
|
||||||
|
* @param {string} fileName - 文件名
|
||||||
|
* @param {Array<string>} extensions - 扩展名列表
|
||||||
|
* @returns {boolean} 是否匹配
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_matchesExtensions(fileName, extensions) {
|
||||||
|
if (!extensions || extensions.length === 0) {
|
||||||
|
return true // 如果没有指定扩展名,匹配所有文件
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions.some(ext => fileName.endsWith(ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否应该扫描指定目录
|
||||||
|
* @param {string} dirName - 目录名
|
||||||
|
* @param {Array<string>|null} subdirs - 允许扫描的子目录列表
|
||||||
|
* @param {number} currentDepth - 当前深度
|
||||||
|
* @returns {boolean} 是否应该扫描
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_shouldScanDirectory(dirName, subdirs, currentDepth) {
|
||||||
|
// 跳过隐藏目录和node_modules
|
||||||
|
if (dirName.startsWith('.') || dirName === 'node_modules') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有指定子目录限制,扫描所有目录
|
||||||
|
if (!subdirs || subdirs.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在根级别,只扫描指定的子目录
|
||||||
|
if (currentDepth === 0) {
|
||||||
|
return subdirs.includes(dirName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在更深层级,扫描所有目录
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化路径,确保跨平台兼容性
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @returns {string} 规范化后的路径
|
||||||
|
*/
|
||||||
|
normalizePath(filePath) {
|
||||||
|
return path.normalize(filePath).replace(/\\/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成相对路径,确保跨平台兼容性
|
||||||
|
* @param {string} from - 起始路径
|
||||||
|
* @param {string} to - 目标路径
|
||||||
|
* @returns {string} 规范化的相对路径
|
||||||
|
*/
|
||||||
|
getRelativePath(from, to) {
|
||||||
|
const relativePath = path.relative(from, to)
|
||||||
|
return relativePath.replace(/\\/g, '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CrossPlatformFileScanner
|
||||||
159
src/lib/core/resource/discovery/DiscoveryManager.js
Normal file
159
src/lib/core/resource/discovery/DiscoveryManager.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
const PackageDiscovery = require('./PackageDiscovery')
|
||||||
|
const ProjectDiscovery = require('./ProjectDiscovery')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DiscoveryManager - 资源发现管理器
|
||||||
|
*
|
||||||
|
* 统一管理多个资源发现器,按照文档架构设计:
|
||||||
|
* 1. 按优先级排序发现器 (数字越小优先级越高)
|
||||||
|
* 2. 并行执行资源发现
|
||||||
|
* 3. 收集并合并所有发现的资源
|
||||||
|
* 4. 提供容错机制,单个发现器失败不影响整体
|
||||||
|
*/
|
||||||
|
class DiscoveryManager {
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param {Array} discoveries - 自定义发现器列表,如果不提供则使用默认配置
|
||||||
|
*/
|
||||||
|
constructor(discoveries = null) {
|
||||||
|
if (discoveries) {
|
||||||
|
this.discoveries = [...discoveries]
|
||||||
|
} else {
|
||||||
|
// 默认发现器配置:只包含包级和项目级发现
|
||||||
|
this.discoveries = [
|
||||||
|
new PackageDiscovery(), // 优先级: 1
|
||||||
|
new ProjectDiscovery() // 优先级: 2
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按优先级排序
|
||||||
|
this._sortDiscoveriesByPriority()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加发现器
|
||||||
|
* @param {Object} discovery - 实现了发现器接口的对象
|
||||||
|
*/
|
||||||
|
addDiscovery(discovery) {
|
||||||
|
if (!discovery || typeof discovery.discover !== 'function') {
|
||||||
|
throw new Error('Discovery must implement discover method')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.discoveries.push(discovery)
|
||||||
|
this._sortDiscoveriesByPriority()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除发现器
|
||||||
|
* @param {string} source - 发现器源类型
|
||||||
|
*/
|
||||||
|
removeDiscovery(source) {
|
||||||
|
this.discoveries = this.discoveries.filter(discovery => discovery.source !== source)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发现所有资源(并行模式)
|
||||||
|
* @returns {Promise<Array>} 所有发现的资源列表
|
||||||
|
*/
|
||||||
|
async discoverAll() {
|
||||||
|
const discoveryPromises = this.discoveries.map(async (discovery) => {
|
||||||
|
try {
|
||||||
|
const resources = await discovery.discover()
|
||||||
|
return Array.isArray(resources) ? resources : []
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[DiscoveryManager] ${discovery.source} discovery failed: ${error.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 并行执行所有发现器
|
||||||
|
const discoveryResults = await Promise.allSettled(discoveryPromises)
|
||||||
|
|
||||||
|
// 收集所有成功的结果
|
||||||
|
const allResources = []
|
||||||
|
discoveryResults.forEach((result, index) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
allResources.push(...result.value)
|
||||||
|
} else {
|
||||||
|
console.warn(`[DiscoveryManager] ${this.discoveries[index].source} discovery rejected: ${result.reason}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return allResources
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按源类型发现资源
|
||||||
|
* @param {string} source - 发现器源类型
|
||||||
|
* @returns {Promise<Array>} 指定源的资源列表
|
||||||
|
*/
|
||||||
|
async discoverBySource(source) {
|
||||||
|
const discovery = this._findDiscoveryBySource(source)
|
||||||
|
if (!discovery) {
|
||||||
|
throw new Error(`Discovery source ${source} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await discovery.discover()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有发现器信息
|
||||||
|
* @returns {Array} 发现器信息列表
|
||||||
|
*/
|
||||||
|
getDiscoveryInfo() {
|
||||||
|
return this.discoveries.map(discovery => {
|
||||||
|
if (typeof discovery.getDiscoveryInfo === 'function') {
|
||||||
|
return discovery.getDiscoveryInfo()
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
source: discovery.source || 'UNKNOWN',
|
||||||
|
priority: discovery.priority || 0,
|
||||||
|
description: 'No description available'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理所有发现器缓存
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.discoveries.forEach(discovery => {
|
||||||
|
if (typeof discovery.clearCache === 'function') {
|
||||||
|
discovery.clearCache()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取发现器数量
|
||||||
|
* @returns {number} 注册的发现器数量
|
||||||
|
*/
|
||||||
|
getDiscoveryCount() {
|
||||||
|
return this.discoveries.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按优先级排序发现器
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_sortDiscoveriesByPriority() {
|
||||||
|
this.discoveries.sort((a, b) => {
|
||||||
|
const priorityA = a.priority || 0
|
||||||
|
const priorityB = b.priority || 0
|
||||||
|
return priorityA - priorityB // 升序排序,数字越小优先级越高
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据源类型查找发现器
|
||||||
|
* @param {string} source - 发现器源类型
|
||||||
|
* @returns {Object|undefined} 找到的发现器或undefined
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_findDiscoveryBySource(source) {
|
||||||
|
return this.discoveries.find(discovery => discovery.source === source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DiscoveryManager
|
||||||
228
src/lib/core/resource/discovery/PackageDiscovery.js
Normal file
228
src/lib/core/resource/discovery/PackageDiscovery.js
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
const BaseDiscovery = require('./BaseDiscovery')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
const CrossPlatformFileScanner = require('./CrossPlatformFileScanner')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PackageDiscovery - 包级资源发现器
|
||||||
|
*
|
||||||
|
* 负责发现NPM包内的资源:
|
||||||
|
* 1. 从 src/resource.registry.json 加载静态注册表
|
||||||
|
* 2. 扫描 prompt/ 目录发现动态资源
|
||||||
|
*
|
||||||
|
* 优先级:1 (最高优先级)
|
||||||
|
*/
|
||||||
|
class PackageDiscovery extends BaseDiscovery {
|
||||||
|
constructor() {
|
||||||
|
super('PACKAGE', 1)
|
||||||
|
this.fileScanner = new CrossPlatformFileScanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发现包级资源
|
||||||
|
* @returns {Promise<Array>} 发现的资源列表
|
||||||
|
*/
|
||||||
|
async discover() {
|
||||||
|
const resources = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 加载静态注册表资源
|
||||||
|
const registryResources = await this._loadStaticRegistryResources()
|
||||||
|
resources.push(...registryResources)
|
||||||
|
|
||||||
|
// 2. 扫描prompt目录资源
|
||||||
|
const scanResources = await this._scanPromptDirectory()
|
||||||
|
resources.push(...scanResources)
|
||||||
|
|
||||||
|
// 3. 规范化所有资源
|
||||||
|
return resources.map(resource => this.normalizeResource(resource))
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[PackageDiscovery] Discovery failed: ${error.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从静态注册表加载资源
|
||||||
|
* @returns {Promise<Array>} 注册表中的资源列表
|
||||||
|
*/
|
||||||
|
async _loadStaticRegistryResources() {
|
||||||
|
try {
|
||||||
|
const registry = await this._loadStaticRegistry()
|
||||||
|
const resources = []
|
||||||
|
|
||||||
|
if (registry.protocols) {
|
||||||
|
// 遍历所有协议
|
||||||
|
for (const [protocol, protocolInfo] of Object.entries(registry.protocols)) {
|
||||||
|
if (protocolInfo.registry) {
|
||||||
|
// 遍历协议下的所有资源
|
||||||
|
for (const [resourceId, resourceInfo] of Object.entries(protocolInfo.registry)) {
|
||||||
|
const reference = typeof resourceInfo === 'string'
|
||||||
|
? resourceInfo
|
||||||
|
: resourceInfo.file
|
||||||
|
|
||||||
|
if (reference) {
|
||||||
|
resources.push({
|
||||||
|
id: `${protocol}:${resourceId}`,
|
||||||
|
reference: reference
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[PackageDiscovery] Failed to load static registry: ${error.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载静态注册表文件
|
||||||
|
* @returns {Promise<Object>} 注册表内容
|
||||||
|
*/
|
||||||
|
async _loadStaticRegistry() {
|
||||||
|
const packageRoot = await this._findPackageRoot()
|
||||||
|
const registryPath = path.join(packageRoot, 'src', 'resource.registry.json')
|
||||||
|
|
||||||
|
if (!await fs.pathExists(registryPath)) {
|
||||||
|
throw new Error('Static registry file not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fs.readJSON(registryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描prompt目录发现资源
|
||||||
|
* @returns {Promise<Array>} 扫描发现的资源列表
|
||||||
|
*/
|
||||||
|
async _scanPromptDirectory() {
|
||||||
|
try {
|
||||||
|
const packageRoot = await this._findPackageRoot()
|
||||||
|
const promptDir = path.join(packageRoot, 'prompt')
|
||||||
|
|
||||||
|
if (!await fs.pathExists(promptDir)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = []
|
||||||
|
|
||||||
|
// 定义要扫描的资源类型
|
||||||
|
const resourceTypes = ['role', 'execution', 'thought']
|
||||||
|
|
||||||
|
// 并行扫描所有资源类型
|
||||||
|
for (const resourceType of resourceTypes) {
|
||||||
|
const files = await this.fileScanner.scanResourceFiles(promptDir, resourceType)
|
||||||
|
|
||||||
|
for (const filePath of files) {
|
||||||
|
const suffix = `.${resourceType}.md`
|
||||||
|
const id = this._extractResourceId(filePath, resourceType, suffix)
|
||||||
|
const reference = this._generatePackageReference(filePath, packageRoot)
|
||||||
|
|
||||||
|
resources.push({
|
||||||
|
id: id,
|
||||||
|
reference: reference
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[PackageDiscovery] Failed to scan prompt directory: ${error.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件扫描(可以被测试mock)
|
||||||
|
* @param {string} baseDir - 基础目录
|
||||||
|
* @param {string} resourceType - 资源类型
|
||||||
|
* @returns {Promise<Array>} 匹配的文件路径列表
|
||||||
|
*/
|
||||||
|
async _scanFiles(baseDir, resourceType) {
|
||||||
|
return await this.fileScanner.scanResourceFiles(baseDir, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找包根目录
|
||||||
|
* @returns {Promise<string>} 包根目录路径
|
||||||
|
*/
|
||||||
|
async _findPackageRoot() {
|
||||||
|
const cacheKey = 'packageRoot'
|
||||||
|
const cached = this.getFromCache(cacheKey)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageRoot = await this._findPackageJsonWithPrompt()
|
||||||
|
if (!packageRoot) {
|
||||||
|
throw new Error('Package root with prompt directory not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setCache(cacheKey, packageRoot)
|
||||||
|
return packageRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找包含prompt目录的package.json
|
||||||
|
* @returns {Promise<string|null>} 包根目录路径或null
|
||||||
|
*/
|
||||||
|
async _findPackageJsonWithPrompt() {
|
||||||
|
let currentDir = __dirname
|
||||||
|
|
||||||
|
while (currentDir !== path.parse(currentDir).root) {
|
||||||
|
const packageJsonPath = path.join(currentDir, 'package.json')
|
||||||
|
const promptDirPath = path.join(currentDir, 'prompt')
|
||||||
|
|
||||||
|
// 检查是否同时存在package.json和prompt目录
|
||||||
|
const [hasPackageJson, hasPromptDir] = await Promise.all([
|
||||||
|
fs.pathExists(packageJsonPath),
|
||||||
|
fs.pathExists(promptDirPath)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (hasPackageJson && hasPromptDir) {
|
||||||
|
// 验证是否是PromptX包
|
||||||
|
try {
|
||||||
|
const packageJson = await fs.readJSON(packageJsonPath)
|
||||||
|
if (packageJson.name === 'promptx' || packageJson.name === 'dpml-prompt') {
|
||||||
|
return currentDir
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略package.json读取错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDir = path.dirname(currentDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成包引用路径
|
||||||
|
* @param {string} filePath - 文件绝对路径
|
||||||
|
* @param {string} packageRoot - 包根目录
|
||||||
|
* @returns {string} @package://相对路径
|
||||||
|
*/
|
||||||
|
_generatePackageReference(filePath, packageRoot) {
|
||||||
|
const relativePath = this.fileScanner.getRelativePath(packageRoot, filePath)
|
||||||
|
return `@package://${relativePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取资源ID
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @param {string} protocol - 协议类型
|
||||||
|
* @param {string} suffix - 文件后缀
|
||||||
|
* @returns {string} 资源ID (protocol:resourceName)
|
||||||
|
*/
|
||||||
|
_extractResourceId(filePath, protocol, suffix) {
|
||||||
|
const fileName = path.basename(filePath, suffix)
|
||||||
|
return `${protocol}:${fileName}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PackageDiscovery
|
||||||
223
src/lib/core/resource/discovery/ProjectDiscovery.js
Normal file
223
src/lib/core/resource/discovery/ProjectDiscovery.js
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
const BaseDiscovery = require('./BaseDiscovery')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
const CrossPlatformFileScanner = require('./CrossPlatformFileScanner')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectDiscovery - 项目级资源发现器
|
||||||
|
*
|
||||||
|
* 负责发现项目本地的资源:
|
||||||
|
* 1. 扫描 .promptx/resource/ 目录
|
||||||
|
* 2. 发现用户自定义的角色、执行模式、思维模式等
|
||||||
|
*
|
||||||
|
* 优先级:2
|
||||||
|
*/
|
||||||
|
class ProjectDiscovery extends BaseDiscovery {
|
||||||
|
constructor() {
|
||||||
|
super('PROJECT', 2)
|
||||||
|
this.fileScanner = new CrossPlatformFileScanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发现项目级资源
|
||||||
|
* @returns {Promise<Array>} 发现的资源列表
|
||||||
|
*/
|
||||||
|
async discover() {
|
||||||
|
try {
|
||||||
|
// 1. 查找项目根目录
|
||||||
|
const projectRoot = await this._findProjectRoot()
|
||||||
|
|
||||||
|
// 2. 检查.promptx目录是否存在
|
||||||
|
const hasPrompxDir = await this._checkPrompxDirectory(projectRoot)
|
||||||
|
if (!hasPrompxDir) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 扫描项目资源
|
||||||
|
const resources = await this._scanProjectResources(projectRoot)
|
||||||
|
|
||||||
|
// 4. 规范化所有资源
|
||||||
|
return resources.map(resource => this.normalizeResource(resource))
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[ProjectDiscovery] Discovery failed: ${error.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找项目根目录
|
||||||
|
* @returns {Promise<string>} 项目根目录路径
|
||||||
|
*/
|
||||||
|
async _findProjectRoot() {
|
||||||
|
const cacheKey = 'projectRoot'
|
||||||
|
const cached = this.getFromCache(cacheKey)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentDir = process.cwd()
|
||||||
|
|
||||||
|
// 向上查找包含package.json的目录
|
||||||
|
while (currentDir !== path.dirname(currentDir)) {
|
||||||
|
const packageJsonPath = path.join(currentDir, 'package.json')
|
||||||
|
|
||||||
|
if (await this._fsExists(packageJsonPath)) {
|
||||||
|
this.setCache(cacheKey, currentDir)
|
||||||
|
return currentDir
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDir = path.dirname(currentDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没找到package.json,返回当前工作目录
|
||||||
|
const fallbackRoot = process.cwd()
|
||||||
|
this.setCache(cacheKey, fallbackRoot)
|
||||||
|
return fallbackRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查.promptx目录是否存在
|
||||||
|
* @param {string} projectRoot - 项目根目录
|
||||||
|
* @returns {Promise<boolean>} 是否存在.promptx/resource目录
|
||||||
|
*/
|
||||||
|
async _checkPrompxDirectory(projectRoot) {
|
||||||
|
const promptxResourcePath = path.join(projectRoot, '.promptx', 'resource')
|
||||||
|
return await this._fsExists(promptxResourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描项目资源
|
||||||
|
* @param {string} projectRoot - 项目根目录
|
||||||
|
* @returns {Promise<Array>} 扫描发现的资源列表
|
||||||
|
*/
|
||||||
|
async _scanProjectResources(projectRoot) {
|
||||||
|
try {
|
||||||
|
const resourcesDir = path.join(projectRoot, '.promptx', 'resource')
|
||||||
|
const resources = []
|
||||||
|
|
||||||
|
// 定义要扫描的资源类型
|
||||||
|
const resourceTypes = ['role', 'execution', 'thought']
|
||||||
|
|
||||||
|
// 并行扫描所有资源类型
|
||||||
|
for (const resourceType of resourceTypes) {
|
||||||
|
try {
|
||||||
|
const files = await this.fileScanner.scanResourceFiles(resourcesDir, resourceType)
|
||||||
|
|
||||||
|
for (const filePath of files) {
|
||||||
|
// 验证文件内容
|
||||||
|
const isValid = await this._validateResourceFile(filePath, resourceType)
|
||||||
|
if (!isValid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = `.${resourceType}.md`
|
||||||
|
const id = this._extractResourceId(filePath, resourceType, suffix)
|
||||||
|
const reference = this._generateProjectReference(filePath, projectRoot)
|
||||||
|
|
||||||
|
resources.push({
|
||||||
|
id: id,
|
||||||
|
reference: reference
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[ProjectDiscovery] Failed to scan ${resourceType} resources: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[ProjectDiscovery] Failed to scan project resources: ${error.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件扫描(可以被测试mock)
|
||||||
|
* @param {string} baseDir - 基础目录
|
||||||
|
* @param {string} resourceType - 资源类型
|
||||||
|
* @returns {Promise<Array>} 匹配的文件路径列表
|
||||||
|
*/
|
||||||
|
async _scanFiles(baseDir, resourceType) {
|
||||||
|
return await this.fileScanner.scanResourceFiles(baseDir, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件系统存在性检查(可以被测试mock)
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @returns {Promise<boolean>} 文件是否存在
|
||||||
|
*/
|
||||||
|
async _fsExists(filePath) {
|
||||||
|
return await fs.pathExists(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取文件内容(可以被测试mock)
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @returns {Promise<string>} 文件内容
|
||||||
|
*/
|
||||||
|
async _readFile(filePath) {
|
||||||
|
return await fs.readFile(filePath, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证资源文件格式
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @param {string} protocol - 协议类型
|
||||||
|
* @returns {Promise<boolean>} 是否是有效的资源文件
|
||||||
|
*/
|
||||||
|
async _validateResourceFile(filePath, protocol) {
|
||||||
|
try {
|
||||||
|
const content = await this._readFile(filePath)
|
||||||
|
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedContent = content.trim()
|
||||||
|
if (trimmedContent.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据协议类型验证DPML标签
|
||||||
|
switch (protocol) {
|
||||||
|
case 'role':
|
||||||
|
return trimmedContent.includes('<role>') && trimmedContent.includes('</role>')
|
||||||
|
case 'execution':
|
||||||
|
return trimmedContent.includes('<execution>') && trimmedContent.includes('</execution>')
|
||||||
|
case 'thought':
|
||||||
|
return trimmedContent.includes('<thought>') && trimmedContent.includes('</thought>')
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[ProjectDiscovery] Failed to validate ${filePath}: ${error.message}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成项目引用路径
|
||||||
|
* @param {string} filePath - 文件绝对路径
|
||||||
|
* @param {string} projectRoot - 项目根目录
|
||||||
|
* @returns {string} @project://相对路径
|
||||||
|
*/
|
||||||
|
_generateProjectReference(filePath, projectRoot) {
|
||||||
|
const relativePath = this.fileScanner.getRelativePath(projectRoot, filePath)
|
||||||
|
return `@project://${relativePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取资源ID
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @param {string} protocol - 协议类型
|
||||||
|
* @param {string} suffix - 文件后缀
|
||||||
|
* @returns {string} 资源ID (protocol:resourceName)
|
||||||
|
*/
|
||||||
|
_extractResourceId(filePath, protocol, suffix) {
|
||||||
|
const fileName = path.basename(filePath, suffix)
|
||||||
|
return `${protocol}:${fileName}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ProjectDiscovery
|
||||||
420
src/tests/core/resource/EnhancedResourceRegistry.unit.test.js
Normal file
420
src/tests/core/resource/EnhancedResourceRegistry.unit.test.js
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
const EnhancedResourceRegistry = require('../../../lib/core/resource/EnhancedResourceRegistry')
|
||||||
|
|
||||||
|
describe('EnhancedResourceRegistry', () => {
|
||||||
|
let registry
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
registry = new EnhancedResourceRegistry()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
test('should initialize with empty registry', () => {
|
||||||
|
expect(registry.size()).toBe(0)
|
||||||
|
expect(registry.list()).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('register', () => {
|
||||||
|
test('should register resource with metadata', () => {
|
||||||
|
const resource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: {
|
||||||
|
source: 'PACKAGE',
|
||||||
|
priority: 1,
|
||||||
|
timestamp: new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(resource)
|
||||||
|
|
||||||
|
expect(registry.has('role:test')).toBe(true)
|
||||||
|
expect(registry.resolve('role:test')).toBe('@package://test.md')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error for invalid resource', () => {
|
||||||
|
expect(() => {
|
||||||
|
registry.register({ id: 'test' }) // missing reference
|
||||||
|
}).toThrow('Resource must have id and reference')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error for missing metadata', () => {
|
||||||
|
expect(() => {
|
||||||
|
registry.register({
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md'
|
||||||
|
// missing metadata
|
||||||
|
})
|
||||||
|
}).toThrow('Resource must have metadata with source and priority')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('registerBatch', () => {
|
||||||
|
test('should register multiple resources at once', () => {
|
||||||
|
const resources = [
|
||||||
|
{
|
||||||
|
id: 'role:test1',
|
||||||
|
reference: '@package://test1.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:test2',
|
||||||
|
reference: '@project://test2.md',
|
||||||
|
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
registry.registerBatch(resources)
|
||||||
|
|
||||||
|
expect(registry.size()).toBe(2)
|
||||||
|
expect(registry.has('role:test1')).toBe(true)
|
||||||
|
expect(registry.has('role:test2')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle batch registration failures gracefully', () => {
|
||||||
|
const resources = [
|
||||||
|
{
|
||||||
|
id: 'role:valid',
|
||||||
|
reference: '@package://valid.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:invalid'
|
||||||
|
// missing reference and metadata
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Should register valid resources and skip invalid ones
|
||||||
|
registry.registerBatch(resources)
|
||||||
|
|
||||||
|
expect(registry.size()).toBe(1)
|
||||||
|
expect(registry.has('role:valid')).toBe(true)
|
||||||
|
expect(registry.has('role:invalid')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('merge', () => {
|
||||||
|
test('should merge with another registry using priority rules', () => {
|
||||||
|
// Setup first registry with package resources
|
||||||
|
const resource1 = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date('2023-01-01') }
|
||||||
|
}
|
||||||
|
registry.register(resource1)
|
||||||
|
|
||||||
|
// Create second registry with project resources (higher priority)
|
||||||
|
const otherRegistry = new EnhancedResourceRegistry()
|
||||||
|
const resource2 = {
|
||||||
|
id: 'role:test', // same ID, should override
|
||||||
|
reference: '@project://test.md',
|
||||||
|
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date('2023-01-02') }
|
||||||
|
}
|
||||||
|
otherRegistry.register(resource2)
|
||||||
|
|
||||||
|
// Merge - PROJECT should override PACKAGE due to higher priority
|
||||||
|
registry.merge(otherRegistry)
|
||||||
|
|
||||||
|
expect(registry.resolve('role:test')).toBe('@project://test.md')
|
||||||
|
const metadata = registry.getMetadata('role:test')
|
||||||
|
expect(metadata.source).toBe('PROJECT')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle same priority by timestamp (newer wins)', () => {
|
||||||
|
const older = new Date('2023-01-01')
|
||||||
|
const newer = new Date('2023-01-02')
|
||||||
|
|
||||||
|
// Setup first registry
|
||||||
|
const resource1 = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://old.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: older }
|
||||||
|
}
|
||||||
|
registry.register(resource1)
|
||||||
|
|
||||||
|
// Create second registry with same priority but newer timestamp
|
||||||
|
const otherRegistry = new EnhancedResourceRegistry()
|
||||||
|
const resource2 = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://new.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: newer }
|
||||||
|
}
|
||||||
|
otherRegistry.register(resource2)
|
||||||
|
|
||||||
|
registry.merge(otherRegistry)
|
||||||
|
|
||||||
|
expect(registry.resolve('role:test')).toBe('@package://new.md')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle discovery source priority correctly', () => {
|
||||||
|
// Test priority order: USER > PROJECT > PACKAGE > INTERNET
|
||||||
|
const resources = [
|
||||||
|
{
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@internet://remote.md',
|
||||||
|
metadata: { source: 'INTERNET', priority: 4, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://builtin.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@project://project.md',
|
||||||
|
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@user://custom.md',
|
||||||
|
metadata: { source: 'USER', priority: 3, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Register in random order
|
||||||
|
registry.register(resources[0]) // INTERNET
|
||||||
|
registry.register(resources[1]) // PACKAGE (should override INTERNET)
|
||||||
|
registry.register(resources[2]) // PROJECT (should override PACKAGE)
|
||||||
|
registry.register(resources[3]) // USER (should override PROJECT)
|
||||||
|
|
||||||
|
expect(registry.resolve('role:test')).toBe('@user://custom.md')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resolve', () => {
|
||||||
|
test('should resolve resource by exact ID', () => {
|
||||||
|
const resource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
registry.register(resource)
|
||||||
|
|
||||||
|
expect(registry.resolve('role:test')).toBe('@package://test.md')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should support backwards compatibility lookup', () => {
|
||||||
|
const resource = {
|
||||||
|
id: 'role:java-developer',
|
||||||
|
reference: '@package://java.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
registry.register(resource)
|
||||||
|
|
||||||
|
// Should find by bare name if no exact match
|
||||||
|
expect(registry.resolve('java-developer')).toBe('@package://java.md')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error if resource not found', () => {
|
||||||
|
expect(() => {
|
||||||
|
registry.resolve('non-existent')
|
||||||
|
}).toThrow('Resource \'non-existent\' not found')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
test('should list all resources', () => {
|
||||||
|
const resources = [
|
||||||
|
{
|
||||||
|
id: 'role:test1',
|
||||||
|
reference: '@package://test1.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'execution:test2',
|
||||||
|
reference: '@project://test2.md',
|
||||||
|
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
registry.registerBatch(resources)
|
||||||
|
|
||||||
|
const list = registry.list()
|
||||||
|
expect(list).toHaveLength(2)
|
||||||
|
expect(list).toContain('role:test1')
|
||||||
|
expect(list).toContain('execution:test2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should filter by protocol', () => {
|
||||||
|
const resources = [
|
||||||
|
{
|
||||||
|
id: 'role:test1',
|
||||||
|
reference: '@package://test1.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'execution:test2',
|
||||||
|
reference: '@project://test2.md',
|
||||||
|
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:test3',
|
||||||
|
reference: '@user://test3.md',
|
||||||
|
metadata: { source: 'USER', priority: 3, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
registry.registerBatch(resources)
|
||||||
|
|
||||||
|
const roleList = registry.list('role')
|
||||||
|
expect(roleList).toHaveLength(2)
|
||||||
|
expect(roleList).toContain('role:test1')
|
||||||
|
expect(roleList).toContain('role:test3')
|
||||||
|
|
||||||
|
const executionList = registry.list('execution')
|
||||||
|
expect(executionList).toHaveLength(1)
|
||||||
|
expect(executionList).toContain('execution:test2')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getMetadata', () => {
|
||||||
|
test('should return resource metadata', () => {
|
||||||
|
const timestamp = new Date()
|
||||||
|
const resource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: timestamp }
|
||||||
|
}
|
||||||
|
registry.register(resource)
|
||||||
|
|
||||||
|
const metadata = registry.getMetadata('role:test')
|
||||||
|
expect(metadata).toEqual({
|
||||||
|
source: 'PACKAGE',
|
||||||
|
priority: 1,
|
||||||
|
timestamp: timestamp
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return null for non-existent resource', () => {
|
||||||
|
const metadata = registry.getMetadata('non-existent')
|
||||||
|
expect(metadata).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
test('should clear all resources', () => {
|
||||||
|
const resource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
registry.register(resource)
|
||||||
|
|
||||||
|
expect(registry.size()).toBe(1)
|
||||||
|
|
||||||
|
registry.clear()
|
||||||
|
|
||||||
|
expect(registry.size()).toBe(0)
|
||||||
|
expect(registry.list()).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('size', () => {
|
||||||
|
test('should return number of registered resources', () => {
|
||||||
|
expect(registry.size()).toBe(0)
|
||||||
|
|
||||||
|
const resources = [
|
||||||
|
{
|
||||||
|
id: 'role:test1',
|
||||||
|
reference: '@package://test1.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:test2',
|
||||||
|
reference: '@project://test2.md',
|
||||||
|
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
registry.registerBatch(resources)
|
||||||
|
|
||||||
|
expect(registry.size()).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('has', () => {
|
||||||
|
test('should check if resource exists', () => {
|
||||||
|
const resource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
registry.register(resource)
|
||||||
|
|
||||||
|
expect(registry.has('role:test')).toBe(true)
|
||||||
|
expect(registry.has('non-existent')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('remove', () => {
|
||||||
|
test('should remove resource', () => {
|
||||||
|
const resource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
registry.register(resource)
|
||||||
|
|
||||||
|
expect(registry.has('role:test')).toBe(true)
|
||||||
|
|
||||||
|
registry.remove('role:test')
|
||||||
|
|
||||||
|
expect(registry.has('role:test')).toBe(false)
|
||||||
|
expect(registry.size()).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should do nothing if resource does not exist', () => {
|
||||||
|
registry.remove('non-existent') // Should not throw
|
||||||
|
expect(registry.size()).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('loadFromDiscoveryResults', () => {
|
||||||
|
test('should load resources from discovery manager results', () => {
|
||||||
|
const discoveryResults = [
|
||||||
|
{
|
||||||
|
id: 'role:test1',
|
||||||
|
reference: '@package://test1.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:test2',
|
||||||
|
reference: '@project://test2.md',
|
||||||
|
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
registry.loadFromDiscoveryResults(discoveryResults)
|
||||||
|
|
||||||
|
expect(registry.size()).toBe(2)
|
||||||
|
expect(registry.resolve('role:test1')).toBe('@package://test1.md')
|
||||||
|
expect(registry.resolve('role:test2')).toBe('@project://test2.md')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle empty discovery results', () => {
|
||||||
|
registry.loadFromDiscoveryResults([])
|
||||||
|
expect(registry.size()).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle invalid discovery results gracefully', () => {
|
||||||
|
const discoveryResults = [
|
||||||
|
{
|
||||||
|
id: 'role:valid',
|
||||||
|
reference: '@package://valid.md',
|
||||||
|
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role:invalid'
|
||||||
|
// missing reference and metadata
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
undefined
|
||||||
|
]
|
||||||
|
|
||||||
|
registry.loadFromDiscoveryResults(discoveryResults)
|
||||||
|
|
||||||
|
expect(registry.size()).toBe(1)
|
||||||
|
expect(registry.has('role:valid')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
99
src/tests/core/resource/discovery/BaseDiscovery.unit.test.js
Normal file
99
src/tests/core/resource/discovery/BaseDiscovery.unit.test.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
const BaseDiscovery = require('../../../../lib/core/resource/discovery/BaseDiscovery')
|
||||||
|
|
||||||
|
describe('BaseDiscovery', () => {
|
||||||
|
let discovery
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
discovery = new BaseDiscovery('test', 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
test('should initialize with source and priority', () => {
|
||||||
|
expect(discovery.source).toBe('test')
|
||||||
|
expect(discovery.priority).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error if source is not provided', () => {
|
||||||
|
expect(() => new BaseDiscovery()).toThrow('Discovery source is required')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should use default priority if not provided', () => {
|
||||||
|
const defaultDiscovery = new BaseDiscovery('test')
|
||||||
|
expect(defaultDiscovery.priority).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('discover method', () => {
|
||||||
|
test('should throw error for abstract method', async () => {
|
||||||
|
await expect(discovery.discover()).rejects.toThrow('discover method must be implemented by subclass')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getDiscoveryInfo', () => {
|
||||||
|
test('should return discovery metadata', () => {
|
||||||
|
const info = discovery.getDiscoveryInfo()
|
||||||
|
expect(info).toEqual({
|
||||||
|
source: 'test',
|
||||||
|
priority: 1,
|
||||||
|
description: expect.any(String)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validateResource', () => {
|
||||||
|
test('should validate resource structure', () => {
|
||||||
|
const validResource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: {
|
||||||
|
source: 'test',
|
||||||
|
priority: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => discovery.validateResource(validResource)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error for invalid resource', () => {
|
||||||
|
const invalidResource = { id: 'test' } // missing reference
|
||||||
|
|
||||||
|
expect(() => discovery.validateResource(invalidResource)).toThrow('Resource must have id and reference')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('normalizeResource', () => {
|
||||||
|
test('should add metadata to resource', () => {
|
||||||
|
const resource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = discovery.normalizeResource(resource)
|
||||||
|
|
||||||
|
expect(normalized).toEqual({
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: {
|
||||||
|
source: 'test',
|
||||||
|
priority: 1,
|
||||||
|
timestamp: expect.any(Date)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should preserve existing metadata', () => {
|
||||||
|
const resource = {
|
||||||
|
id: 'role:test',
|
||||||
|
reference: '@package://test.md',
|
||||||
|
metadata: {
|
||||||
|
customField: 'value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = discovery.normalizeResource(resource)
|
||||||
|
|
||||||
|
expect(normalized.metadata.customField).toBe('value')
|
||||||
|
expect(normalized.metadata.source).toBe('test')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
185
src/tests/core/resource/discovery/DiscoveryManager.unit.test.js
Normal file
185
src/tests/core/resource/discovery/DiscoveryManager.unit.test.js
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
const DiscoveryManager = require('../../../../lib/core/resource/discovery/DiscoveryManager')
|
||||||
|
const PackageDiscovery = require('../../../../lib/core/resource/discovery/PackageDiscovery')
|
||||||
|
const ProjectDiscovery = require('../../../../lib/core/resource/discovery/ProjectDiscovery')
|
||||||
|
|
||||||
|
describe('DiscoveryManager', () => {
|
||||||
|
let manager
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new DiscoveryManager()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
test('should initialize with default discoveries', () => {
|
||||||
|
expect(manager.discoveries).toHaveLength(2)
|
||||||
|
expect(manager.discoveries[0]).toBeInstanceOf(PackageDiscovery)
|
||||||
|
expect(manager.discoveries[1]).toBeInstanceOf(ProjectDiscovery)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should allow custom discoveries in constructor', () => {
|
||||||
|
const customDiscovery = new PackageDiscovery()
|
||||||
|
const customManager = new DiscoveryManager([customDiscovery])
|
||||||
|
|
||||||
|
expect(customManager.discoveries).toHaveLength(1)
|
||||||
|
expect(customManager.discoveries[0]).toBe(customDiscovery)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addDiscovery', () => {
|
||||||
|
test('should add discovery and sort by priority', () => {
|
||||||
|
const customDiscovery = { source: 'CUSTOM', priority: 0, discover: jest.fn() }
|
||||||
|
|
||||||
|
manager.addDiscovery(customDiscovery)
|
||||||
|
|
||||||
|
expect(manager.discoveries).toHaveLength(3)
|
||||||
|
expect(manager.discoveries[0].source).toBe('CUSTOM') // priority 0 comes first
|
||||||
|
expect(manager.discoveries[1].source).toBe('PACKAGE') // priority 1
|
||||||
|
expect(manager.discoveries[2].source).toBe('PROJECT') // priority 2
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('removeDiscovery', () => {
|
||||||
|
test('should remove discovery by source', () => {
|
||||||
|
manager.removeDiscovery('PACKAGE')
|
||||||
|
|
||||||
|
expect(manager.discoveries).toHaveLength(1)
|
||||||
|
expect(manager.discoveries[0].source).toBe('PROJECT')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should do nothing if discovery not found', () => {
|
||||||
|
manager.removeDiscovery('NON_EXISTENT')
|
||||||
|
|
||||||
|
expect(manager.discoveries).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('discoverAll', () => {
|
||||||
|
test('should discover resources from all discoveries in parallel', async () => {
|
||||||
|
// Mock discovery methods
|
||||||
|
const packageResources = [
|
||||||
|
{ id: 'role:java-developer', reference: '@package://test1.md', metadata: { source: 'PACKAGE', priority: 1 } }
|
||||||
|
]
|
||||||
|
const projectResources = [
|
||||||
|
{ id: 'role:custom-role', reference: '@project://test2.md', metadata: { source: 'PROJECT', priority: 2 } }
|
||||||
|
]
|
||||||
|
|
||||||
|
manager.discoveries[0].discover = jest.fn().mockResolvedValue(packageResources)
|
||||||
|
manager.discoveries[1].discover = jest.fn().mockResolvedValue(projectResources)
|
||||||
|
|
||||||
|
const allResources = await manager.discoverAll()
|
||||||
|
|
||||||
|
expect(allResources).toHaveLength(2)
|
||||||
|
expect(allResources[0].id).toBe('role:java-developer')
|
||||||
|
expect(allResources[1].id).toBe('role:custom-role')
|
||||||
|
expect(manager.discoveries[0].discover).toHaveBeenCalled()
|
||||||
|
expect(manager.discoveries[1].discover).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle discovery failures gracefully', async () => {
|
||||||
|
const packageResources = [
|
||||||
|
{ id: 'role:java-developer', reference: '@package://test1.md', metadata: { source: 'PACKAGE', priority: 1 } }
|
||||||
|
]
|
||||||
|
|
||||||
|
manager.discoveries[0].discover = jest.fn().mockResolvedValue(packageResources)
|
||||||
|
manager.discoveries[1].discover = jest.fn().mockRejectedValue(new Error('Project discovery failed'))
|
||||||
|
|
||||||
|
const allResources = await manager.discoverAll()
|
||||||
|
|
||||||
|
expect(allResources).toHaveLength(1)
|
||||||
|
expect(allResources[0].id).toBe('role:java-developer')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return empty array if all discoveries fail', async () => {
|
||||||
|
manager.discoveries[0].discover = jest.fn().mockRejectedValue(new Error('Package discovery failed'))
|
||||||
|
manager.discoveries[1].discover = jest.fn().mockRejectedValue(new Error('Project discovery failed'))
|
||||||
|
|
||||||
|
const allResources = await manager.discoverAll()
|
||||||
|
|
||||||
|
expect(allResources).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('discoverBySource', () => {
|
||||||
|
test('should discover resources from specific source', async () => {
|
||||||
|
const packageResources = [
|
||||||
|
{ id: 'role:java-developer', reference: '@package://test1.md', metadata: { source: 'PACKAGE', priority: 1 } }
|
||||||
|
]
|
||||||
|
|
||||||
|
manager.discoveries[0].discover = jest.fn().mockResolvedValue(packageResources)
|
||||||
|
|
||||||
|
const resources = await manager.discoverBySource('PACKAGE')
|
||||||
|
|
||||||
|
expect(resources).toEqual(packageResources)
|
||||||
|
expect(manager.discoveries[0].discover).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error if source not found', async () => {
|
||||||
|
await expect(manager.discoverBySource('NON_EXISTENT')).rejects.toThrow('Discovery source NON_EXISTENT not found')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getDiscoveryInfo', () => {
|
||||||
|
test('should return info for all discoveries', () => {
|
||||||
|
// Mock getDiscoveryInfo methods
|
||||||
|
manager.discoveries[0].getDiscoveryInfo = jest.fn().mockReturnValue({
|
||||||
|
source: 'PACKAGE',
|
||||||
|
priority: 1,
|
||||||
|
description: 'Package discovery'
|
||||||
|
})
|
||||||
|
manager.discoveries[1].getDiscoveryInfo = jest.fn().mockReturnValue({
|
||||||
|
source: 'PROJECT',
|
||||||
|
priority: 2,
|
||||||
|
description: 'Project discovery'
|
||||||
|
})
|
||||||
|
|
||||||
|
const info = manager.getDiscoveryInfo()
|
||||||
|
|
||||||
|
expect(info).toHaveLength(2)
|
||||||
|
expect(info[0].source).toBe('PACKAGE')
|
||||||
|
expect(info[1].source).toBe('PROJECT')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('clearCache', () => {
|
||||||
|
test('should clear cache for all discoveries', () => {
|
||||||
|
// Mock clearCache methods
|
||||||
|
manager.discoveries[0].clearCache = jest.fn()
|
||||||
|
manager.discoveries[1].clearCache = jest.fn()
|
||||||
|
|
||||||
|
manager.clearCache()
|
||||||
|
|
||||||
|
expect(manager.discoveries[0].clearCache).toHaveBeenCalled()
|
||||||
|
expect(manager.discoveries[1].clearCache).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_sortDiscoveriesByPriority', () => {
|
||||||
|
test('should sort discoveries by priority ascending', () => {
|
||||||
|
const discovery1 = { source: 'A', priority: 3 }
|
||||||
|
const discovery2 = { source: 'B', priority: 1 }
|
||||||
|
const discovery3 = { source: 'C', priority: 2 }
|
||||||
|
|
||||||
|
manager.discoveries = [discovery1, discovery2, discovery3]
|
||||||
|
manager._sortDiscoveriesByPriority()
|
||||||
|
|
||||||
|
expect(manager.discoveries[0].source).toBe('B') // priority 1
|
||||||
|
expect(manager.discoveries[1].source).toBe('C') // priority 2
|
||||||
|
expect(manager.discoveries[2].source).toBe('A') // priority 3
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_findDiscoveryBySource', () => {
|
||||||
|
test('should find discovery by source', () => {
|
||||||
|
const packageDiscovery = manager._findDiscoveryBySource('PACKAGE')
|
||||||
|
|
||||||
|
expect(packageDiscovery).toBeDefined()
|
||||||
|
expect(packageDiscovery.source).toBe('PACKAGE')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return undefined if source not found', () => {
|
||||||
|
const discovery = manager._findDiscoveryBySource('NON_EXISTENT')
|
||||||
|
|
||||||
|
expect(discovery).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
177
src/tests/core/resource/discovery/PackageDiscovery.unit.test.js
Normal file
177
src/tests/core/resource/discovery/PackageDiscovery.unit.test.js
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
const PackageDiscovery = require('../../../../lib/core/resource/discovery/PackageDiscovery')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
describe('PackageDiscovery', () => {
|
||||||
|
let discovery
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
discovery = new PackageDiscovery()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
test('should initialize with PACKAGE source and priority 1', () => {
|
||||||
|
expect(discovery.source).toBe('PACKAGE')
|
||||||
|
expect(discovery.priority).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('discover', () => {
|
||||||
|
test('should discover package resources from static registry', async () => {
|
||||||
|
// Mock registry file content
|
||||||
|
jest.spyOn(discovery, '_loadStaticRegistry').mockResolvedValue({
|
||||||
|
protocols: {
|
||||||
|
role: {
|
||||||
|
registry: {
|
||||||
|
'java-backend-developer': '@package://prompt/domain/java-backend-developer/java-backend-developer.role.md',
|
||||||
|
'product-manager': '@package://prompt/domain/product-manager/product-manager.role.md'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
thought: {
|
||||||
|
registry: {
|
||||||
|
'remember': '@package://prompt/core/thought/remember.thought.md'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock scan to return empty array to isolate static registry test
|
||||||
|
jest.spyOn(discovery, '_scanPromptDirectory').mockResolvedValue([])
|
||||||
|
|
||||||
|
const resources = await discovery.discover()
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(3)
|
||||||
|
expect(resources[0]).toMatchObject({
|
||||||
|
id: 'role:java-backend-developer',
|
||||||
|
reference: '@package://prompt/domain/java-backend-developer/java-backend-developer.role.md',
|
||||||
|
metadata: {
|
||||||
|
source: 'PACKAGE',
|
||||||
|
priority: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should discover resources from prompt directory scan', async () => {
|
||||||
|
// Mock file system operations
|
||||||
|
jest.spyOn(discovery, '_scanPromptDirectory').mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'role:assistant',
|
||||||
|
reference: '@package://prompt/domain/assistant/assistant.role.md'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest.spyOn(discovery, '_loadStaticRegistry').mockResolvedValue({})
|
||||||
|
|
||||||
|
const resources = await discovery.discover()
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(1)
|
||||||
|
expect(resources[0].id).toBe('role:assistant')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle registry loading failures gracefully', async () => {
|
||||||
|
jest.spyOn(discovery, '_loadStaticRegistry').mockRejectedValue(new Error('Registry not found'))
|
||||||
|
jest.spyOn(discovery, '_scanPromptDirectory').mockResolvedValue([])
|
||||||
|
|
||||||
|
const resources = await discovery.discover()
|
||||||
|
|
||||||
|
expect(resources).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_loadStaticRegistry', () => {
|
||||||
|
test('should load registry from default path', async () => {
|
||||||
|
// This would be mocked in real tests
|
||||||
|
expect(typeof discovery._loadStaticRegistry).toBe('function')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_scanPromptDirectory', () => {
|
||||||
|
test('should scan for role, execution, thought files', async () => {
|
||||||
|
// Mock package root and prompt directory
|
||||||
|
jest.spyOn(discovery, '_findPackageRoot').mockResolvedValue('/mock/package/root')
|
||||||
|
|
||||||
|
// Mock fs.pathExists to return true for prompt directory
|
||||||
|
const mockPathExists = jest.spyOn(require('fs-extra'), 'pathExists').mockResolvedValue(true)
|
||||||
|
|
||||||
|
// Mock file scanner
|
||||||
|
const mockScanResourceFiles = jest.fn()
|
||||||
|
.mockResolvedValueOnce(['/mock/package/root/prompt/domain/test/test.role.md']) // roles
|
||||||
|
.mockResolvedValueOnce(['/mock/package/root/prompt/core/execution/test.execution.md']) // executions
|
||||||
|
.mockResolvedValueOnce(['/mock/package/root/prompt/core/thought/test.thought.md']) // thoughts
|
||||||
|
|
||||||
|
discovery.fileScanner.scanResourceFiles = mockScanResourceFiles
|
||||||
|
|
||||||
|
const resources = await discovery._scanPromptDirectory()
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(3)
|
||||||
|
expect(resources[0].id).toBe('role:test')
|
||||||
|
expect(resources[1].id).toBe('execution:test')
|
||||||
|
expect(resources[2].id).toBe('thought:test')
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
mockPathExists.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_findPackageRoot', () => {
|
||||||
|
test('should find package root containing prompt directory', async () => {
|
||||||
|
// Mock file system check
|
||||||
|
jest.spyOn(discovery, '_findPackageJsonWithPrompt').mockResolvedValue('/mock/package/root')
|
||||||
|
|
||||||
|
const root = await discovery._findPackageRoot()
|
||||||
|
|
||||||
|
expect(root).toBe('/mock/package/root')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error if package root not found', async () => {
|
||||||
|
jest.spyOn(discovery, '_findPackageJsonWithPrompt').mockResolvedValue(null)
|
||||||
|
|
||||||
|
await expect(discovery._findPackageRoot()).rejects.toThrow('Package root with prompt directory not found')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_generatePackageReference', () => {
|
||||||
|
test('should generate @package:// reference', () => {
|
||||||
|
const filePath = '/mock/package/root/prompt/domain/test/test.role.md'
|
||||||
|
const packageRoot = '/mock/package/root'
|
||||||
|
|
||||||
|
// Mock the fileScanner.getRelativePath method
|
||||||
|
discovery.fileScanner.getRelativePath = jest.fn().mockReturnValue('prompt/domain/test/test.role.md')
|
||||||
|
|
||||||
|
const reference = discovery._generatePackageReference(filePath, packageRoot)
|
||||||
|
|
||||||
|
expect(reference).toBe('@package://prompt/domain/test/test.role.md')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle Windows paths correctly', () => {
|
||||||
|
const filePath = 'C:\\mock\\package\\root\\prompt\\domain\\test\\test.role.md'
|
||||||
|
const packageRoot = 'C:\\mock\\package\\root'
|
||||||
|
|
||||||
|
// Mock the fileScanner.getRelativePath method
|
||||||
|
discovery.fileScanner.getRelativePath = jest.fn().mockReturnValue('prompt/domain/test/test.role.md')
|
||||||
|
|
||||||
|
const reference = discovery._generatePackageReference(filePath, packageRoot)
|
||||||
|
|
||||||
|
expect(reference).toBe('@package://prompt/domain/test/test.role.md')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_extractResourceId', () => {
|
||||||
|
test('should extract role id from path', () => {
|
||||||
|
const filePath = '/mock/package/root/prompt/domain/test/test.role.md'
|
||||||
|
const protocol = 'role'
|
||||||
|
|
||||||
|
const id = discovery._extractResourceId(filePath, protocol, '.role.md')
|
||||||
|
|
||||||
|
expect(id).toBe('role:test')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should extract execution id from path', () => {
|
||||||
|
const filePath = '/mock/package/root/prompt/core/execution/memory-trigger.execution.md'
|
||||||
|
const protocol = 'execution'
|
||||||
|
|
||||||
|
const id = discovery._extractResourceId(filePath, protocol, '.execution.md')
|
||||||
|
|
||||||
|
expect(id).toBe('execution:memory-trigger')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
227
src/tests/core/resource/discovery/ProjectDiscovery.unit.test.js
Normal file
227
src/tests/core/resource/discovery/ProjectDiscovery.unit.test.js
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
const ProjectDiscovery = require('../../../../lib/core/resource/discovery/ProjectDiscovery')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
describe('ProjectDiscovery', () => {
|
||||||
|
let discovery
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
discovery = new ProjectDiscovery()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
test('should initialize with PROJECT source and priority 2', () => {
|
||||||
|
expect(discovery.source).toBe('PROJECT')
|
||||||
|
expect(discovery.priority).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('discover', () => {
|
||||||
|
test('should discover project resources from .promptx directory', async () => {
|
||||||
|
// Mock project root and .promptx directory
|
||||||
|
jest.spyOn(discovery, '_findProjectRoot').mockResolvedValue('/mock/project/root')
|
||||||
|
jest.spyOn(discovery, '_checkPrompxDirectory').mockResolvedValue(true)
|
||||||
|
|
||||||
|
jest.spyOn(discovery, '_scanProjectResources').mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'role:custom-role',
|
||||||
|
reference: '@project://.promptx/resource/domain/custom-role/custom-role.role.md'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'execution:custom-execution',
|
||||||
|
reference: '@project://.promptx/resource/execution/custom-execution.execution.md'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const resources = await discovery.discover()
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(2)
|
||||||
|
expect(resources[0]).toMatchObject({
|
||||||
|
id: 'role:custom-role',
|
||||||
|
reference: '@project://.promptx/resource/domain/custom-role/custom-role.role.md',
|
||||||
|
metadata: {
|
||||||
|
source: 'PROJECT',
|
||||||
|
priority: 2
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return empty array if .promptx directory does not exist', async () => {
|
||||||
|
jest.spyOn(discovery, '_findProjectRoot').mockResolvedValue('/mock/project/root')
|
||||||
|
jest.spyOn(discovery, '_checkPrompxDirectory').mockResolvedValue(false)
|
||||||
|
|
||||||
|
const resources = await discovery.discover()
|
||||||
|
|
||||||
|
expect(resources).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle project root not found', async () => {
|
||||||
|
jest.spyOn(discovery, '_findProjectRoot').mockRejectedValue(new Error('Project root not found'))
|
||||||
|
|
||||||
|
const resources = await discovery.discover()
|
||||||
|
|
||||||
|
expect(resources).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_findProjectRoot', () => {
|
||||||
|
test('should find project root containing package.json', async () => {
|
||||||
|
const mockFsExists = jest.fn()
|
||||||
|
.mockResolvedValueOnce(false) // /current/dir/package.json
|
||||||
|
.mockResolvedValueOnce(true) // /current/package.json
|
||||||
|
|
||||||
|
discovery._fsExists = mockFsExists
|
||||||
|
|
||||||
|
// Mock process.cwd()
|
||||||
|
const originalCwd = process.cwd
|
||||||
|
process.cwd = jest.fn().mockReturnValue('/current/dir')
|
||||||
|
|
||||||
|
const root = await discovery._findProjectRoot()
|
||||||
|
|
||||||
|
expect(root).toBe('/current')
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
process.cwd = originalCwd
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return current directory if no package.json found', async () => {
|
||||||
|
const mockFsExists = jest.fn().mockResolvedValue(false)
|
||||||
|
discovery._fsExists = mockFsExists
|
||||||
|
|
||||||
|
const originalCwd = process.cwd
|
||||||
|
process.cwd = jest.fn().mockReturnValue('/current/dir')
|
||||||
|
|
||||||
|
const root = await discovery._findProjectRoot()
|
||||||
|
|
||||||
|
expect(root).toBe('/current/dir')
|
||||||
|
|
||||||
|
process.cwd = originalCwd
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_checkPrompxDirectory', () => {
|
||||||
|
test('should check if .promptx/resource directory exists', async () => {
|
||||||
|
const mockFsExists = jest.fn().mockResolvedValue(true)
|
||||||
|
discovery._fsExists = mockFsExists
|
||||||
|
|
||||||
|
const exists = await discovery._checkPrompxDirectory('/mock/project/root')
|
||||||
|
|
||||||
|
expect(exists).toBe(true)
|
||||||
|
expect(mockFsExists).toHaveBeenCalledWith('/mock/project/root/.promptx/resource')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_scanProjectResources', () => {
|
||||||
|
test('should scan for role, execution, thought files in .promptx', async () => {
|
||||||
|
const projectRoot = '/mock/project/root'
|
||||||
|
|
||||||
|
// Mock file scanner
|
||||||
|
const mockScanResourceFiles = jest.fn()
|
||||||
|
.mockResolvedValueOnce([`${projectRoot}/.promptx/resource/domain/test/test.role.md`]) // roles
|
||||||
|
.mockResolvedValueOnce([`${projectRoot}/.promptx/resource/execution/test.execution.md`]) // executions
|
||||||
|
.mockResolvedValueOnce([`${projectRoot}/.promptx/resource/thought/test.thought.md`]) // thoughts
|
||||||
|
|
||||||
|
discovery.fileScanner.scanResourceFiles = mockScanResourceFiles
|
||||||
|
|
||||||
|
// Mock file validation
|
||||||
|
discovery._validateResourceFile = jest.fn().mockResolvedValue(true)
|
||||||
|
|
||||||
|
const resources = await discovery._scanProjectResources(projectRoot)
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(3)
|
||||||
|
expect(resources[0].id).toBe('role:test')
|
||||||
|
expect(resources[1].id).toBe('execution:test')
|
||||||
|
expect(resources[2].id).toBe('thought:test')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle scan errors gracefully', async () => {
|
||||||
|
const projectRoot = '/mock/project/root'
|
||||||
|
|
||||||
|
const mockScanResourceFiles = jest.fn().mockRejectedValue(new Error('Scan failed'))
|
||||||
|
discovery.fileScanner.scanResourceFiles = mockScanResourceFiles
|
||||||
|
|
||||||
|
const resources = await discovery._scanProjectResources(projectRoot)
|
||||||
|
|
||||||
|
expect(resources).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_generateProjectReference', () => {
|
||||||
|
test('should generate @project:// reference', () => {
|
||||||
|
const filePath = '/mock/project/root/.promptx/resource/domain/test/test.role.md'
|
||||||
|
const projectRoot = '/mock/project/root'
|
||||||
|
|
||||||
|
// Mock the fileScanner.getRelativePath method
|
||||||
|
discovery.fileScanner.getRelativePath = jest.fn().mockReturnValue('.promptx/resource/domain/test/test.role.md')
|
||||||
|
|
||||||
|
const reference = discovery._generateProjectReference(filePath, projectRoot)
|
||||||
|
|
||||||
|
expect(reference).toBe('@project://.promptx/resource/domain/test/test.role.md')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle Windows paths correctly', () => {
|
||||||
|
const filePath = 'C:\\mock\\project\\root\\.promptx\\resource\\domain\\test\\test.role.md'
|
||||||
|
const projectRoot = 'C:\\mock\\project\\root'
|
||||||
|
|
||||||
|
// Mock the fileScanner.getRelativePath method
|
||||||
|
discovery.fileScanner.getRelativePath = jest.fn().mockReturnValue('.promptx/resource/domain/test/test.role.md')
|
||||||
|
|
||||||
|
const reference = discovery._generateProjectReference(filePath, projectRoot)
|
||||||
|
|
||||||
|
expect(reference).toBe('@project://.promptx/resource/domain/test/test.role.md')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_extractResourceId', () => {
|
||||||
|
test('should extract resource id with protocol prefix', () => {
|
||||||
|
const filePath = '/mock/project/root/.promptx/resource/domain/test/test.role.md'
|
||||||
|
const protocol = 'role'
|
||||||
|
|
||||||
|
const id = discovery._extractResourceId(filePath, protocol, '.role.md')
|
||||||
|
|
||||||
|
expect(id).toBe('role:test')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_validateResourceFile', () => {
|
||||||
|
test('should validate role file contains required DPML tags', async () => {
|
||||||
|
const filePath = '/mock/test.role.md'
|
||||||
|
const mockContent = `
|
||||||
|
# Test Role
|
||||||
|
|
||||||
|
<role>
|
||||||
|
Test role content
|
||||||
|
</role>
|
||||||
|
`
|
||||||
|
|
||||||
|
const mockReadFile = jest.fn().mockResolvedValue(mockContent)
|
||||||
|
discovery._readFile = mockReadFile
|
||||||
|
|
||||||
|
const isValid = await discovery._validateResourceFile(filePath, 'role')
|
||||||
|
|
||||||
|
expect(isValid).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for invalid role file', async () => {
|
||||||
|
const filePath = '/mock/test.role.md'
|
||||||
|
const mockContent = 'Invalid content without DPML tags'
|
||||||
|
|
||||||
|
const mockReadFile = jest.fn().mockResolvedValue(mockContent)
|
||||||
|
discovery._readFile = mockReadFile
|
||||||
|
|
||||||
|
const isValid = await discovery._validateResourceFile(filePath, 'role')
|
||||||
|
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle file read errors', async () => {
|
||||||
|
const filePath = '/mock/test.role.md'
|
||||||
|
|
||||||
|
const mockReadFile = jest.fn().mockRejectedValue(new Error('File not found'))
|
||||||
|
discovery._readFile = mockReadFile
|
||||||
|
|
||||||
|
const isValid = await discovery._validateResourceFile(filePath, 'role')
|
||||||
|
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user