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.
537 lines (457 loc) • 18.5 kB
JavaScript
const fs = require('fs-extra')
const path = require('path')
const os = require('os')
const crypto = require('crypto')
const { getGlobalServerEnvironment } = require('./ServerEnvironment')
const logger = require('./logger')
/**
* 统一项目管理器 - 新架构
* 核心原则:一次设置,全程使用
* 负责当前项目状态管理和多项目配置持久化
*/
class ProjectManager {
constructor() {
this.promptxHomeDir = path.join(os.homedir(), '.promptx')
this.projectsDir = path.join(this.promptxHomeDir, 'project')
}
// 🎯 新架构:当前项目状态管理
static currentProject = {
workingDirectory: null,
mcpId: null,
ideType: null,
transport: null,
initialized: false
}
/**
* 设置当前项目(init时调用)
* @param {string} workingDirectory - 项目工作目录绝对路径
* @param {string} mcpId - MCP进程ID
* @param {string} ideType - IDE类型
* @param {string} transport - 传输协议类型
*/
static setCurrentProject(workingDirectory, mcpId, ideType, transport) {
this.currentProject = {
workingDirectory: path.resolve(workingDirectory),
mcpId,
ideType,
transport,
initialized: true
}
}
/**
* 获取当前项目路径(@project协议使用)
* @returns {string} 当前项目工作目录
*/
static getCurrentProjectPath() {
logger.debug(`[ProjectManager DEBUG] getCurrentProjectPath被调用`)
logger.debug(`[ProjectManager DEBUG] currentProject.initialized: ${this.currentProject.initialized}`)
logger.debug(`[ProjectManager DEBUG] currentProject状态:`, JSON.stringify(this.currentProject, null, 2))
// 输出完整的调用栈,包含文件名和行号
const stack = new Error().stack
const stackLines = stack.split('\n').slice(1, 8) // 取前7层调用栈
logger.error(`[ProjectManager DEBUG] 完整调用栈:`)
stackLines.forEach((line, index) => {
logger.error(`[ProjectManager DEBUG] ${index + 1}. ${line.trim()}`)
})
if (!this.currentProject.initialized) {
logger.error(`[ProjectManager DEBUG] ❌ 项目未初始化,将抛出错误`)
throw new Error('项目未初始化,请先调用 init 命令')
}
logger.debug(`[ProjectManager DEBUG] ✅ 返回项目路径: ${this.currentProject.workingDirectory}`)
return this.currentProject.workingDirectory
}
/**
* 获取当前项目信息
* @returns {Object} 当前项目完整信息
*/
static getCurrentProject() {
logger.debug(`[ProjectManager DEBUG] getCurrentProject被调用`)
logger.debug(`[ProjectManager DEBUG] currentProject.initialized: ${this.currentProject.initialized}`)
logger.debug(`[ProjectManager DEBUG] currentProject状态:`, JSON.stringify(this.currentProject, null, 2))
if (!this.currentProject.initialized) {
logger.error(`[ProjectManager DEBUG] ❌ 项目未初始化,将抛出错误`)
throw new Error('项目未初始化,请先调用 init 命令')
}
logger.debug(`[ProjectManager DEBUG] ✅ 返回项目信息`)
return { ...this.currentProject }
}
/**
* 检查项目是否已初始化
* @returns {boolean} 是否已初始化
*/
static isInitialized() {
return this.currentProject.initialized
}
/**
* 注册项目到MCP实例 - 使用Hash目录结构
* @param {string} projectPath - 项目绝对路径
* @param {string} mcpId - MCP进程ID
* @param {string} ideType - IDE类型(cursor/vscode等)
* @param {string} transport - 传输协议类型(stdio/http/sse)
* @returns {Promise<Object>} 项目配置对象
*/
async registerProject(projectPath, mcpId, ideType, transport = 'stdio') {
// 验证项目路径
if (!await this.validateProjectPath(projectPath)) {
throw new Error(`无效的项目路径: ${projectPath}`)
}
// 生成项目配置
const projectConfig = {
mcpId: mcpId,
ideType: ideType.toLowerCase(),
transport: transport.toLowerCase(),
projectPath: path.resolve(projectPath),
projectHash: this.generateProjectHash(projectPath)
}
// 生成项目Hash目录
const projectHash = this.generateProjectHash(projectPath)
const projectConfigDir = path.join(this.projectsDir, projectHash)
// 🎯 确保Hash目录和.promptx子目录存在
await fs.ensureDir(projectConfigDir)
await fs.ensureDir(path.join(projectConfigDir, '.promptx'))
await fs.ensureDir(path.join(projectConfigDir, '.promptx', 'memory'))
await fs.ensureDir(path.join(projectConfigDir, '.promptx', 'resource'))
// 生成配置文件名并保存到Hash目录下
const fileName = this.generateConfigFileName(mcpId, ideType, transport, projectPath)
const configPath = path.join(projectConfigDir, fileName)
await fs.writeJson(configPath, projectConfig, { spaces: 2 })
return projectConfig
}
/**
* 根据MCP ID获取单个项目配置(假设只有一个项目)
* @param {string} mcpId - MCP进程ID
* @returns {Promise<Object|null>} 项目配置对象
*/
async getProjectByMcpId(mcpId) {
const projects = await this.getProjectsByMcpId(mcpId)
return projects.length > 0 ? projects[0] : null
}
/**
* 根据MCP ID获取所有绑定的项目配置 - 支持Hash目录结构
* @param {string} mcpId - MCP进程ID
* @returns {Promise<Array>} 项目配置数组
*/
async getProjectsByMcpId(mcpId) {
if (!await fs.pathExists(this.projectsDir)) {
return []
}
const hashDirs = await fs.readdir(this.projectsDir)
const projects = []
for (const hashDir of hashDirs) {
const hashDirPath = path.join(this.projectsDir, hashDir)
// 🎯 只处理Hash目录(忽略旧的平铺文件)
if (!(await fs.stat(hashDirPath)).isDirectory()) {
continue
}
try {
const configFiles = await fs.readdir(hashDirPath)
for (const file of configFiles) {
// 查找MCP配置文件
if (file.startsWith('mcp-') && file.endsWith('.json')) {
try {
const configPath = path.join(hashDirPath, file)
const config = await fs.readJson(configPath)
if (config.mcpId === mcpId) {
projects.push(config)
}
} catch (error) {
// 忽略损坏的配置文件
logger.warn(`跳过损坏的配置文件: ${file}`)
}
}
}
} catch (error) {
// 忽略无法读取的目录
logger.warn(`跳过无法读取的目录: ${hashDir}`)
}
}
return projects
}
/**
* 获取特定项目的所有实例(不同IDE/MCP的绑定) - 支持Hash目录结构
* @param {string} projectPath - 项目路径
* @returns {Promise<Array>} 项目实例数组
*/
async getProjectInstances(projectPath) {
if (!await fs.pathExists(this.projectsDir)) {
return []
}
const projectHash = this.generateProjectHash(projectPath)
const projectConfigDir = path.join(this.projectsDir, projectHash)
// 检查Hash目录是否存在
if (!await fs.pathExists(projectConfigDir)) {
return []
}
const instances = []
try {
const configFiles = await fs.readdir(projectConfigDir)
for (const file of configFiles) {
// 查找MCP配置文件
if (file.startsWith('mcp-') && file.endsWith('.json')) {
try {
const configPath = path.join(projectConfigDir, file)
const config = await fs.readJson(configPath)
if (config.projectHash === projectHash) {
instances.push(config)
}
} catch (error) {
logger.warn(`跳过损坏的配置文件: ${file}`)
}
}
}
} catch (error) {
logger.warn(`无法读取项目配置目录: ${projectConfigDir}`)
}
return instances
}
/**
* 删除项目绑定 - 支持Hash目录结构
* @param {string} mcpId - MCP进程ID
* @param {string} ideType - IDE类型
* @param {string} transport - 传输协议类型
* @param {string} projectPath - 项目路径
* @returns {Promise<boolean>} 是否删除成功
*/
async removeProject(mcpId, ideType, transport, projectPath) {
const projectHash = this.generateProjectHash(projectPath)
const projectConfigDir = path.join(this.projectsDir, projectHash)
const fileName = this.generateConfigFileName(mcpId, ideType, transport, projectPath)
const configPath = path.join(projectConfigDir, fileName)
if (await fs.pathExists(configPath)) {
await fs.remove(configPath)
// 🎯 检查Hash目录是否为空,如果为空则删除整个目录
try {
const remainingFiles = await fs.readdir(projectConfigDir)
const mcpConfigFiles = remainingFiles.filter(file => file.startsWith('mcp-') && file.endsWith('.json'))
if (mcpConfigFiles.length === 0) {
// 没有其他MCP配置文件,删除整个Hash目录
await fs.remove(projectConfigDir)
}
} catch (error) {
// 目录可能已经被删除,忽略错误
}
return true
}
return false
}
/**
* 清理过期的项目配置 - 支持Hash目录结构
* @returns {Promise<number>} 清理的配置文件数量
*/
async cleanupExpiredProjects() {
if (!await fs.pathExists(this.projectsDir)) {
return 0
}
const hashDirs = await fs.readdir(this.projectsDir)
let cleanedCount = 0
for (const hashDir of hashDirs) {
const hashDirPath = path.join(this.projectsDir, hashDir)
// 只处理Hash目录
if (!(await fs.stat(hashDirPath)).isDirectory()) {
continue
}
try {
const configFiles = await fs.readdir(hashDirPath)
let hasValidConfig = false
for (const file of configFiles) {
if (file.startsWith('mcp-') && file.endsWith('.json')) {
try {
const configPath = path.join(hashDirPath, file)
const config = await fs.readJson(configPath)
// 检查项目路径是否仍然存在
if (!await fs.pathExists(config.projectPath)) {
await fs.remove(configPath)
cleanedCount++
logger.info(`清理过期项目配置: ${file}`)
} else {
hasValidConfig = true
}
} catch (error) {
// 清理损坏的配置文件
await fs.remove(path.join(hashDirPath, file))
cleanedCount++
logger.info(`清理损坏配置文件: ${file}`)
}
}
}
// 如果Hash目录中没有有效的配置文件,删除整个目录
if (!hasValidConfig) {
await fs.remove(hashDirPath)
logger.info(`清理空的项目Hash目录: ${hashDir}`)
}
} catch (error) {
// 清理无法访问的目录
await fs.remove(hashDirPath)
cleanedCount++
logger.info(`清理无法访问的目录: ${hashDir}`)
}
}
return cleanedCount
}
/**
* 生成多项目环境下的AI提示词
* @param {string} contextType - 上下文类型:'list'/'action'/'learn'
* @param {string} mcpId - MCP进程ID
* @param {string} ideType - IDE类型
* @returns {Promise<string>} 格式化的AI提示词
*/
async generateTopLevelProjectPrompt(contextType = 'list', mcpId, ideType) {
const projects = await this.getProjectsByMcpId(mcpId)
if (projects.length === 0) {
// 未注册任何项目
return `🛑 **项目环境未初始化** 🛑
⚠️ **当前MCP实例(${mcpId})尚未绑定任何项目**
💢 **立即执行**:
1. 调用 \`promptx_init\` 工具注册当前项目
2. 提供正确的 workingDirectory 参数
3. 确认项目绑定后重新开始
⛔ **严禁继续**:未初始化环境中的任何操作都可能失败!`
}
if (projects.length === 1) {
// 单项目环境(保持现有体验)
const project = projects[0]
const basePrompt = `🛑 **项目环境验证** 🛑
📍 当前绑定项目: ${project.projectPath}
🔗 MCP实例: ${mcpId} (${ideType})
⚠️ **执行前确认**:上述路径是否为你当前工作的项目?`
switch (contextType) {
case 'action':
return `${basePrompt}
如不一致,立即停止所有操作并使用 \`promptx_init\` 更新!
💥 **严重警告**:在错误项目路径下操作将导致不可预知的错误!`
case 'learn':
return `${basePrompt}
错误环境将导致知识关联失效!
💥 **严重警告**:项目环境不匹配将影响学习效果!`
default:
return `${basePrompt}
如不一致,必须使用 \`promptx_init\` 更新正确路径!
💥 **严重警告**:错误的项目环境将导致服务异常!`
}
}
// 多项目环境
const projectList = projects.map((proj, index) =>
`${index + 1}. ${path.basename(proj.projectPath)} (${proj.projectPath})`
).join('\n')
return `🎯 **多项目环境检测** 🎯
📍 当前MCP实例(${mcpId})已绑定 ${projects.length} 个项目:
${projectList}
⚠️ **请明确指定**:你要在哪个项目中执行操作?
💡 **建议**:在对话中明确说明项目名称或路径`
}
/**
* 验证路径是否为有效的项目目录
* @param {string} projectPath - 要验证的路径
* @returns {Promise<boolean>} 是否为有效项目目录
*/
async validateProjectPath(projectPath) {
try {
// 基础检查:路径存在且为目录
const stat = await fs.stat(projectPath)
if (!stat.isDirectory()) {
return false
}
// 简单检查:避免明显错误的路径
const resolved = path.resolve(projectPath)
const homeDir = os.homedir()
// 不允许是用户主目录
if (resolved === homeDir) {
return false
}
return true
} catch (error) {
return false
}
}
/**
* 生成配置文件名
* @param {string} mcpId - MCP进程ID
* @param {string} ideType - IDE类型
* @param {string} transport - 传输协议类型
* @param {string} projectPath - 项目路径
* @returns {string} 配置文件名
*/
generateConfigFileName(mcpId, ideType, transport, projectPath) {
const projectHash = this.generateProjectHash(projectPath)
const projectName = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9-]/g, '-')
const ideTypeSafe = ideType.replace(/[^a-z0-9-]/g, '').toLowerCase() || 'unknown'
const transportSafe = transport.replace(/[^a-z0-9-]/g, '').toLowerCase() || 'unknown'
// 格式:mcp-transport-id-idetype-projectname-hash.json
return `mcp-${transportSafe}-${mcpId.replace('mcp-', '')}-${ideTypeSafe}-${projectName}-${projectHash}.json`
}
/**
* 生成项目路径的Hash值
* @param {string} projectPath - 项目路径
* @returns {string} 8位Hash值
*/
generateProjectHash(projectPath) {
return crypto.createHash('md5').update(path.resolve(projectPath)).digest('hex').substr(0, 8)
}
/**
* 从配置文件中获取IDE类型
* @param {string} mcpId - MCP进程ID
* @returns {Promise<string>} IDE类型
*/
async getIdeType(mcpId) {
const project = await this.getProjectByMcpId(mcpId)
return project ? project.ideType : 'unknown'
}
/**
* 生成MCP进程ID - 基于进程ID确保实例唯一
* @param {string} ideType - IDE类型(保留参数兼容性,实际不使用)
* @returns {string} MCP进程ID
*/
static generateMcpId(ideType = 'unknown') {
const serverEnv = getGlobalServerEnvironment()
if (serverEnv.isInitialized()) {
return serverEnv.getMcpId()
}
// fallback到原逻辑
return `mcp-${process.pid}`
}
/**
* 统一项目注册方法 - 新架构:设置当前项目并持久化配置
* @param {string} workingDirectory - 项目工作目录
* @param {string} ideType - IDE类型(可选,默认'unknown')
* @returns {Promise<Object>} 项目配置对象
*/
static async registerCurrentProject(workingDirectory, ideType = 'unknown') {
logger.debug(`[ProjectManager DEBUG] ======= registerCurrentProject开始 =======`)
logger.debug(`[ProjectManager DEBUG] 参数 - workingDirectory: ${workingDirectory}`)
logger.debug(`[ProjectManager DEBUG] 参数 - ideType: ${ideType}`)
logger.debug(`[ProjectManager DEBUG] 注册前 currentProject状态:`, JSON.stringify(this.currentProject, null, 2))
const serverEnv = getGlobalServerEnvironment()
if (!serverEnv.isInitialized()) {
logger.error(`[ProjectManager DEBUG] ❌ ServerEnvironment未初始化`)
throw new Error('ServerEnvironment not initialized')
}
const mcpId = serverEnv.getMcpId()
const transport = serverEnv.getTransport()
logger.debug(`[ProjectManager DEBUG] ServerEnvironment信息 - mcpId: ${mcpId}, transport: ${transport}`)
// 🎯 新架构:设置当前项目状态
logger.debug(`[ProjectManager DEBUG] 调用 setCurrentProject...`)
this.setCurrentProject(workingDirectory, mcpId, ideType, transport)
logger.debug(`[ProjectManager DEBUG] setCurrentProject完成后 currentProject状态:`, JSON.stringify(this.currentProject, null, 2))
// 持久化项目配置(保持多项目管理功能)
logger.debug(`[ProjectManager DEBUG] 开始持久化项目配置...`)
const projectManager = getGlobalProjectManager()
const result = await projectManager.registerProject(workingDirectory, mcpId, ideType, transport)
logger.debug(`[ProjectManager DEBUG] 项目配置持久化完成:`, JSON.stringify(result, null, 2))
logger.debug(`[ProjectManager DEBUG] ======= registerCurrentProject结束 =======`)
return result
}
}
// 创建全局单例实例
let globalProjectManager = null
/**
* 获取全局ProjectManager单例
* @returns {ProjectManager} 全局ProjectManager实例
*/
function getGlobalProjectManager() {
if (!globalProjectManager) {
globalProjectManager = new ProjectManager()
}
return globalProjectManager
}
module.exports = ProjectManager
module.exports.getGlobalProjectManager = getGlobalProjectManager