dpml-prompt
Version:
DPML-powered AI prompt framework - Revolutionary AI-First CLI system based on Deepractice Prompt Markup Language. Build sophisticated AI agents with structured prompts, memory systems, and execution frameworks.
534 lines (452 loc) • 16.4 kB
JavaScript
const FilePatternDiscovery = require('./FilePatternDiscovery')
const RegistryData = require('../RegistryData')
const logger = require('../../../utils/logger')
const path = require('path')
const fs = require('fs-extra')
const { getDirectoryService } = require('../../../utils/DirectoryService')
/**
* PackageDiscovery - 包级资源发现器
*
* 负责发现NPM包内的资源:
* 1. 从 src/resource.registry.json 加载静态注册表
* 2. 扫描 resource/ 目录发现动态资源
*
* 优先级:1 (最高优先级)
*/
class PackageDiscovery extends FilePatternDiscovery {
constructor() {
super('PACKAGE', 1)
this.directoryService = getDirectoryService()
// 将在_getRegistryPath()中动态计算
this.registryPath = null
}
/**
* 发现包级资源 (优化版 - 硬编码注册表)
* @returns {Promise<Array>} 发现的资源列表
*/
async discover() {
try {
// 使用硬编码注册表替代动态扫描,性能提升100倍
const registry = await this._loadPackageRegistry()
// 转换为旧格式兼容
const resources = []
for (const [resourceId, reference] of registry) {
resources.push({
id: resourceId,
reference: reference
})
}
return resources.map(resource => this.normalizeResource(resource))
} catch (error) {
logger.warn(`PackageDiscovery discovery failed: ${error.message}`)
// 降级到动态扫描作为fallback
return this._fallbackToLegacyDiscovery()
}
}
/**
* 发现包级资源注册表
* @returns {Promise<Map>} 资源注册表 Map<resourceId, reference>
*/
async discoverRegistry() {
try {
// 1. 优先从硬编码注册表加载
const registryData = await this._loadFromRegistry()
if (registryData && !registryData.isEmpty()) {
logger.info(`[PackageDiscovery] ✅ 硬编码注册表加载成功,发现 ${registryData.size} 个资源`)
// 调试:显示包级角色资源
const roleResources = registryData.getResourcesByProtocol('role')
const roleIds = roleResources.flatMap(r => [r.getFullId(), r.getBaseId()])
logger.debug(`[PackageDiscovery] 📋 包级角色资源: ${roleIds.join(', ')}`)
return registryData.getResourceMap(true)
}
// 2. 如果注册表不存在或为空,回退到动态扫描
logger.warn(`[PackageDiscovery] ⚠️ 注册表不存在,回退到动态扫描`)
return await this._fallbackToScanning()
} catch (error) {
logger.warn(`[PackageDiscovery] ❌ 注册表加载失败: ${error.message},回退到动态扫描`)
return await this._fallbackToScanning()
}
}
/**
* 实现基类要求的方法:获取包扫描基础目录
* @returns {Promise<string>} 包资源目录路径
*/
async _getBaseDirectory() {
const packageRoot = await this._findPackageRoot()
return path.join(packageRoot, 'resource')
}
/**
* 重写基类方法:获取注册表文件路径
* @returns {Promise<string>} 注册表文件路径
*/
async _getRegistryPath() {
if (!this.registryPath) {
try {
const context = {
startDir: process.cwd(),
platform: process.platform,
avoidUserHome: true
}
const projectRoot = await this.directoryService.getProjectRoot(context)
this.registryPath = path.join(projectRoot, 'resource/package.registry.json')
} catch (error) {
// 回退到默认路径
this.registryPath = path.join(process.cwd(), 'resource/package.registry.json')
}
}
return this.registryPath
}
/**
* 从硬编码注册表加载资源
* @returns {Promise<RegistryData|null>} 注册表数据
* @private
*/
async _loadFromRegistry() {
try {
const registryPath = await this._getRegistryPath()
logger.debug(`[PackageDiscovery] 🔧 注册表路径: ${registryPath}`)
if (!(await fs.pathExists(registryPath))) {
logger.warn(`[PackageDiscovery] ❌ 注册表文件不存在: ${registryPath}`)
return null
}
const registryData = await RegistryData.fromFile('package', registryPath)
logger.debug(`[PackageDiscovery] 📊 加载资源总数: ${registryData.size}`)
return registryData
} catch (error) {
logger.warn(`[PackageDiscovery] ⚠️ 注册表加载异常: ${error.message}`)
return null
}
}
/**
* 回退到动态扫描(保持向后兼容)
* @returns {Promise<Map>} 资源注册表
* @private
*/
async _fallbackToScanning() {
logger.debug(`[PackageDiscovery] 🔍 开始动态扫描包级资源...`)
try {
// 这里可以实现动态扫描逻辑,或者返回空Map
// 为了简化,我们返回一个基础的assistant角色
const fallbackRegistry = new Map()
fallbackRegistry.set('assistant', '@package://resource/role/assistant/assistant.role.md')
fallbackRegistry.set('package:assistant', '@package://resource/role/assistant/assistant.role.md')
logger.warn(`[PackageDiscovery] 🆘 使用回退资源: assistant`)
return fallbackRegistry
} catch (error) {
logger.warn(`[PackageDiscovery] ❌ 动态扫描失败: ${error.message}`)
return new Map()
}
}
/**
* 生成包级资源注册表(用于构建时)使用新的基类方法
* @param {string} packageRoot - 包根目录
* @returns {Promise<RegistryData>} 生成的注册表数据
*/
async generateRegistry(packageRoot) {
logger.info(`[PackageDiscovery] 🏗️ 开始生成包级资源注册表...`)
try {
// 使用基类的统一生成方法
const resourceDir = path.join(packageRoot, 'resource')
return await super.generateRegistry(resourceDir)
} catch (error) {
logger.error(`[PackageDiscovery] ❌ 注册表生成失败: ${error.message}`)
throw error
}
}
/**
* 扫描目录并添加资源到注册表(使用新的基类方法)
* @param {string} promptDir - prompt目录路径
* @param {RegistryData} registryData - 注册表数据
* @private
*/
async _scanDirectory(promptDir, registryData) {
try {
// 使用基类的统一文件模式扫描
await this._scanResourcesByFilePattern(registryData)
} catch (error) {
logger.warn(`[PackageDiscovery] 扫描目录失败: ${error.message}`)
}
}
/**
* 加载包级硬编码注册表 (性能优化核心方法)
* @returns {Promise<Map>} 包级资源注册表
*/
async _loadPackageRegistry() {
const cacheKey = 'packageRegistry'
if (this.getFromCache(cacheKey)) {
return this.getFromCache(cacheKey)
}
try {
// 查找package.registry.json文件位置
const packageRoot = await this._findPackageRoot()
const registryPath = path.join(packageRoot, 'resource', 'package.registry.json')
// 使用RegistryData统一管理
const registryData = await RegistryData.fromFile('package', registryPath)
const registry = registryData.getResourceMap(true) // 包含源前缀
logger.debug(`[PackageDiscovery] 🔧 注册表路径: ${registryPath}`)
logger.debug(`[PackageDiscovery] 📊 加载资源总数: ${registry.size}`)
// 缓存结果
this.setCache(cacheKey, registry)
return registry
} catch (error) {
logger.warn(`[PackageDiscovery] Failed to load package registry: ${error.message}`)
throw error
}
}
/**
* 降级到传统动态扫描方法 (fallback)
* @returns {Promise<Array>} 动态扫描的资源列表
*/
async _fallbackToLegacyDiscovery() {
logger.warn('[PackageDiscovery] Falling back to legacy dynamic scanning...')
try {
const scanResources = await this._scanPromptDirectory()
return scanResources.map(resource => this.normalizeResource(resource))
} catch (error) {
logger.warn(`[PackageDiscovery] Legacy discovery also failed: ${error.message}`)
return []
}
}
/**
* 扫描prompt目录发现资源(使用新的基类方法)
* @returns {Promise<Array>} 扫描发现的资源列表
*/
async _scanPromptDirectory() {
try {
// 使用新的基类扫描方法
const registryData = RegistryData.createEmpty('package', null)
await this._scanResourcesByFilePattern(registryData)
// 转换为旧格式兼容性
const resources = []
for (const resource of registryData.resources) {
resources.push({
id: resource.id,
reference: resource.reference
})
}
return resources
} catch (error) {
logger.warn(`[PackageDiscovery] Failed to scan prompt directory: ${error.message}`)
return []
}
}
/**
* 检测执行环境类型
* @returns {Promise<string>} 环境类型:development, npx, local, unknown
*/
async _detectExecutionEnvironment() {
// 1. 优先检查npx执行(具体环境,避免MCP误判)
if (this._isNpxExecution()) {
return 'npx'
}
// 2. 检查本地安装(具体环境)
if (this._isLocalInstallation()) {
return 'local'
}
// 3. 最后检查开发环境(通用环境,优先级降低)
if (await this._isDevelopmentMode()) {
return 'development'
}
return 'unknown'
}
/**
* 检查是否在开发模式
* @returns {Promise<boolean>} 是否为开发模式
*/
async _isDevelopmentMode() {
try {
const context = {
startDir: process.cwd(),
platform: process.platform,
avoidUserHome: true
}
const projectRoot = await this.directoryService.getProjectRoot(context)
const hasCliScript = await fs.pathExists(path.join(projectRoot, 'src', 'bin', 'promptx.js'))
const hasPackageJson = await fs.pathExists(path.join(projectRoot, 'package.json'))
if (!hasCliScript || !hasPackageJson) {
return false
}
const packageJson = await fs.readJSON(path.join(projectRoot, 'package.json'))
return packageJson.name === 'dpml-prompt'
} catch (error) {
return false
}
}
/**
* 检查是否通过npx执行
* @returns {boolean} 是否为npx执行
*/
_isNpxExecution() {
// 检查环境变量
if (process.env.npm_execpath && process.env.npm_execpath.includes('npx')) {
return true
}
// 检查目录路径(npx缓存目录)
const currentDir = this._getCurrentDirectory()
if (currentDir.includes('.npm/_npx/') || currentDir.includes('_npx')) {
return true
}
return false
}
/**
* 检查是否在本地安装
* @returns {boolean} 是否为本地安装
*/
_isLocalInstallation() {
const currentDir = this._getCurrentDirectory()
return currentDir.includes('node_modules/dpml-prompt')
}
/**
* 获取当前目录(可以被测试mock)
* @returns {string} 当前目录路径
*/
_getCurrentDirectory() {
return __dirname
}
/**
* 查找包根目录
* @returns {Promise<string>} 包根目录路径
*/
async _findPackageRoot() {
const cacheKey = 'packageRoot'
const cached = this.getFromCache(cacheKey)
if (cached) {
return cached
}
const environment = await this._detectExecutionEnvironment()
let packageRoot = null
switch (environment) {
case 'development':
packageRoot = await this._findDevelopmentRoot()
break
case 'npx':
case 'local':
packageRoot = await this._findInstalledRoot()
break
default:
packageRoot = await this._findFallbackRoot()
}
if (!packageRoot) {
throw new Error('Package root not found')
}
this.setCache(cacheKey, packageRoot)
return packageRoot
}
/**
* 查找开发环境的包根目录
* @returns {Promise<string|null>} 包根目录路径或null
*/
async _findDevelopmentRoot() {
// 策略1:检查当前工作目录
const cwd = process.cwd()
if (await this._isValidDevelopmentRoot(cwd)) {
return fs.realpathSync(cwd)
}
// 策略2:检查启动脚本的目录(适用于通过脚本启动的情况)
const scriptDir = path.dirname(process.argv[1])
let searchDir = scriptDir
// 向上查找最多5级目录
for (let i = 0; i < 5; i++) {
if (await this._isValidDevelopmentRoot(searchDir)) {
return fs.realpathSync(searchDir)
}
const parentDir = path.dirname(searchDir)
if (parentDir === searchDir) break // 已到根目录
searchDir = parentDir
}
return null
}
/**
* 检查目录是否为有效的开发环境根目录
* @param {string} dir - 要检查的目录
* @returns {Promise<boolean>} 是否为有效的开发根目录
* @private
*/
async _isValidDevelopmentRoot(dir) {
const hasPackageJson = await fs.pathExists(path.join(dir, 'package.json'))
const hasResourceDir = await fs.pathExists(path.join(dir, 'resource'))
if (!hasPackageJson || !hasResourceDir) {
return false
}
try {
const packageJson = await fs.readJSON(path.join(dir, 'package.json'))
return packageJson.name === 'dpml-prompt'
} catch (error) {
return false
}
}
/**
* 查找已安装包的根目录
* @returns {Promise<string|null>} 包根目录路径或null
*/
async _findInstalledRoot() {
try {
const currentDir = this._getCurrentDirectory()
let searchDir = currentDir
// 向上查找package.json
while (searchDir !== path.parse(searchDir).root) {
const packageJsonPath = path.join(searchDir, 'package.json')
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJSON(packageJsonPath)
if (packageJson.name === 'dpml-prompt') {
return searchDir
}
}
searchDir = path.dirname(searchDir)
}
} catch (error) {
// Ignore errors
}
return null
}
/**
* 后备方案:使用模块解析查找包根目录
* @returns {Promise<string|null>} 包根目录路径或null
*/
async _findFallbackRoot() {
try {
// 优先使用__dirname计算包根目录(更可靠的路径)
const packageRoot = path.resolve(__dirname, '../../../../../')
// 验证是否为有效的dpml-prompt包
const packageJsonPath = path.join(packageRoot, 'package.json')
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJSON(packageJsonPath)
if (packageJson.name === 'dpml-prompt') {
return packageRoot
}
}
// 后备方案:使用模块解析(使用__dirname作为basedir)
const resolve = require('resolve')
const resolvedPackageJsonPath = resolve.sync('dpml-prompt/package.json', {
basedir: __dirname
})
return path.dirname(resolvedPackageJsonPath)
} catch (error) {
return null
}
}
/**
* 获取RegistryData对象(新架构方法)
* @returns {Promise<RegistryData>} 包级RegistryData对象
*/
async getRegistryData() {
try {
// 查找package.registry.json文件位置
const packageRoot = await this._findPackageRoot()
const registryPath = path.join(packageRoot, 'resource', 'package.registry.json')
// 直接加载RegistryData
const registryData = await RegistryData.fromFile('package', registryPath)
logger.info(`[PackageDiscovery] ✅ 硬编码注册表加载成功,发现 ${registryData.size} 个资源`)
// 输出角色资源信息(调试用)
const roleResources = registryData.getResourcesByProtocol('role')
const roleIds = roleResources.map(r => r.getFullId()).concat(roleResources.map(r => r.getBaseId()))
logger.info(`[PackageDiscovery] 📋 包级角色资源: ${roleIds.join(', ')}`)
return registryData
} catch (error) {
logger.warn(`[PackageDiscovery] Failed to load RegistryData: ${error.message}`)
// 返回空的RegistryData
return new RegistryData('package', null)
}
}
}
module.exports = PackageDiscovery