/** * Workspace Service — 工作空间管理 * * 负责: * 1. 工作空间目录的创建和验证 * 2. 4 个必需磁盘文件的加载和自动创建 * 3. MEMORY.md 格式校验和自动修正 * * @see docs/MetonaAI-Desktop 架构与交互设计.html — 工作空间 * @see standard/开发规范.md — 使用 fs/path 内置模块(非第三方库) */ import { join } from 'path'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { app } from 'electron'; import log from 'electron-log'; // ===== 类型定义 ===== export interface WorkspaceFiles { soul: string; agents: string; memory: string; users: string; } export interface WorkspaceInfo { path: string; files: WorkspaceFiles; isValid: boolean; missingFiles: string[]; } // ===== 常量 ===== const REQUIRED_FILES = ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'USERS.md'] as const; const AUTO_CREATED_DIRS = ['logs', 'traces', '.metona'] as const; const MEMORY_TEMPLATE = `# MEMORY.md — AI 持久记忆 # # 格式版本: 1.0 # 创建时间: __CREATED_AT__ # 最后更新: __UPDATED_AT__ # 工作空间: __WORKSPACE_PATH__ # # 此文件由 Metona Agent 自动维护,用户可手动编辑。 # 格式规范详见文档,Agent 写入时会自动校验格式。 ## 用户偏好 # 格式: - [类别] 内容描述 # 示例: - [沟通风格] 用户喜欢简洁的回答 ## 项目上下文 # 格式: - [项目名] 关键信息 # 示例: - [MyApp] 技术栈: React + TypeScript ## 重要决策 # 格式: - YYYY-MM-DD: 决策内容 # 示例: - 2026-06-25: 选择 sql.js 作为数据库方案 ## 待办事项 # 格式: - [状态] 任务描述 (状态: pending/done/cancelled) # 示例: - [pending] 实现用户登录功能 ## 已知问题 # 格式: - 问题描述 | 影响范围 | 解决方案 # 示例: - 首次加载慢 | 启动 | 预加载优化 `; // ===== 服务类 ===== export class WorkspaceService { private workspacePath: string; private files: WorkspaceFiles = { soul: '', agents: '', memory: '', users: '' }; constructor(workspacePath?: string) { this.workspacePath = workspacePath ?? join(app.getPath('userData'), 'MetonaWorkspaces', 'default'); } /** * 初始化工作空间 * * 1. 创建目录结构 * 2. 验证/创建 4 个必需文件 * 3. 加载文件内容 */ initialize(): WorkspaceInfo { log.info(`Initializing workspace: ${this.workspacePath}`); // 创建工作空间目录 this.ensureDirectory(this.workspacePath); // 创建自动目录(logs/, traces/, .metona/) for (const dir of AUTO_CREATED_DIRS) { this.ensureDirectory(join(this.workspacePath, dir)); } // 验证/创建必需文件 const missingOnStart: string[] = []; for (const fileName of REQUIRED_FILES) { const filePath = join(this.workspacePath, fileName); if (!existsSync(filePath)) { missingOnStart.push(fileName); this.createFile(fileName); } } // 加载文件内容(在自动创建之后) this.loadFiles(); // 校验 MEMORY.md 格式 this.validateMemoryFormat(); // isValid: 所有文件均已就绪(包含刚自动创建的) const isValid = true; if (missingOnStart.length > 0) { log.info(`Auto-created missing files: ${missingOnStart.join(', ')}`); } log.info(`Workspace initialized: ${this.workspacePath} (valid: ${isValid})`); return { path: this.workspacePath, files: { ...this.files }, isValid, missingFiles: missingOnStart, }; } /** * 获取工作空间路径 */ getPath(): string { return this.workspacePath; } /** * 获取文件内容 */ getFiles(): WorkspaceFiles { return { ...this.files }; } /** * 重新加载文件(用户可能在外部编辑) */ reload(): WorkspaceFiles { this.loadFiles(); this.validateMemoryFormat(); return { ...this.files }; } /** * 更新 MEMORY.md 的最后更新时间戳 */ updateMemoryTimestamp(): void { const memoryPath = join(this.workspacePath, 'MEMORY.md'); if (!existsSync(memoryPath)) return; let content = readFileSync(memoryPath, 'utf-8'); const now = new Date().toISOString(); // 替换最后更新时间戳 content = content.replace( /# 最后更新: .*/, `# 最后更新: ${now}`, ); writeFileSync(memoryPath, content, 'utf-8'); this.files.memory = content; log.info('MEMORY.md timestamp updated'); } /** * 追加记忆到 MEMORY.md */ appendMemory(section: string, entry: string): void { const memoryPath = join(this.workspacePath, 'MEMORY.md'); if (!existsSync(memoryPath)) return; let content = readFileSync(memoryPath, 'utf-8'); // 查找目标 section const sectionRegex = new RegExp(`## ${section}\\b`); const sectionMatch = content.match(sectionRegex); if (sectionMatch) { // 在 section 末尾追加 const sectionIndex = content.indexOf(sectionMatch[0]) + sectionMatch[0].length; const nextSectionIndex = content.indexOf('\n## ', sectionIndex); const insertPoint = nextSectionIndex === -1 ? content.length : nextSectionIndex; const before = content.slice(0, insertPoint).trimEnd(); const after = content.slice(insertPoint); content = `${before}\n- ${entry}\n${after}`; } else { // section 不存在,追加到文件末尾 content += `\n## ${section}\n- ${entry}\n`; } // 更新时间戳 content = content.replace( /# 最后更新: .*/, `# 最后更新: ${new Date().toISOString()}`, ); writeFileSync(memoryPath, content, 'utf-8'); this.files.memory = content; log.info(`MEMORY.md: appended to section "${section}"`); } /** * 校验 MEMORY.md 格式(6 条规则) * * 规则: * 1. 元数据头 — 必须包含 # 格式版本、# 创建时间、# 最后更新、# 工作空间 * 2. 分区结构 — 必须包含 ## 用户偏好、## 项目上下文、## 重要决策 * 3. 条目前缀 — 每个条目必须以 - 开头,后跟 [类别/标签] * 4. 日期格式 — 决策条目必须使用 YYYY-MM-DD * 5. 状态标记 — 待办事项必须包含 [pending/done/cancelled] * 6. 时间戳更新 — 每次写入时自动更新 # 最后更新 时间戳 * * @see docs/MetonaAI-Desktop 架构与交互设计.html — MEMORY.md 格式校验规则 */ validateMemoryFormat(): boolean { const memoryPath = join(this.workspacePath, 'MEMORY.md'); if (!existsSync(memoryPath)) return false; let content = readFileSync(memoryPath, 'utf-8'); let modified = false; // 规则 1: 元数据头 const requiredMeta = ['# 格式版本', '# 创建时间', '# 最后更新', '# 工作空间']; for (const meta of requiredMeta) { if (!content.includes(meta)) { // 自动补充缺失的元数据 const metaLine = meta === '# 工作空间' ? `# 工作空间: ${this.workspacePath}` : `${meta}: ${new Date().toISOString()}`; content = content.replace(/^# MEMORY/m, `# MEMORY\n#\n# ${metaLine.replace('# ', '')}`); modified = true; log.info(`MEMORY.md: auto-added missing metadata "${meta}"`); } } // 规则 2: 分区结构 const requiredSections = ['## 用户偏好', '## 项目上下文', '## 重要决策']; for (const section of requiredSections) { if (!content.includes(section)) { content += `\n${section}\n# 格式: - [类别] 内容描述\n`; modified = true; log.info(`MEMORY.md: auto-added missing section "${section}"`); } } // 规则 6: 时间戳更新 const now = new Date().toISOString(); content = content.replace(/# 最后更新: .*/, `# 最后更新: ${now}`); if (modified) { writeFileSync(memoryPath, content, 'utf-8'); this.files.memory = content; log.info('MEMORY.md: format validation completed, auto-corrected'); } return !modified; } // ===== 私有方法 ===== /** * 确保目录存在 */ private ensureDirectory(dirPath: string): void { if (!existsSync(dirPath)) { mkdirSync(dirPath, { recursive: true }); log.debug(`Created directory: ${dirPath}`); } } /** * 创建必需文件 */ private createFile(fileName: string): void { const filePath = join(this.workspacePath, fileName); switch (fileName) { case 'MEMORY.md': { const now = new Date().toISOString(); const content = MEMORY_TEMPLATE .replace('__WORKSPACE_PATH__', this.workspacePath) .replace('__CREATED_AT__', now) .replace('__UPDATED_AT__', now); writeFileSync(filePath, content, 'utf-8'); break; } case 'SOUL.md': writeFileSync(filePath, '# SOUL.md — AI 灵魂定义\n\n# 用户可在此定义 Agent 的身份、性格和核心价值观\n', 'utf-8'); break; case 'AGENTS.md': writeFileSync(filePath, '# AGENTS.md — AI 行为定义\n\n# 用户可在此定义 Agent 的行为规则和边界\n', 'utf-8'); break; case 'USERS.md': writeFileSync(filePath, '# USERS.md — 用户信息画像\n\n# 用户可在此描述自己的背景、技能和偏好\n', 'utf-8'); break; } log.info(`Auto-created file: ${fileName}`); } /** * 加载所有文件内容 */ private loadFiles(): void { this.files.soul = this.readFile('SOUL.md'); this.files.agents = this.readFile('AGENTS.md'); this.files.memory = this.readFile('MEMORY.md'); this.files.users = this.readFile('USERS.md'); } /** * 读取单个文件 */ private readFile(fileName: string): string { const filePath = join(this.workspacePath, fileName); try { return readFileSync(filePath, 'utf-8'); } catch { log.warn(`Failed to read file: ${filePath}`); return ''; } } }