Files
metona-ai-desktop/electron/services/workspace.service.ts
T

318 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Workspace Service — 工作空间管理
*
* 负责:
* 1. 工作空间目录的创建和验证
* 2. 2 个必需磁盘文件的加载和自动创建(SOUL.md + MEMORY.md
* 3. MEMORY.md 格式校验和自动修正
*
* v0.3.14: 移除 AGENTS.md 和 USERS.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;
memory: string;
}
export interface WorkspaceInfo {
path: string;
files: WorkspaceFiles;
isValid: boolean;
missingFiles: string[];
}
// ===== 常量 =====
const REQUIRED_FILES = ['SOUL.md', 'MEMORY.md'] as const;
const AUTO_CREATED_DIRS = ['logs', 'traces', '.metona'] as const;
const MEMORY_TEMPLATE = `# MEMORY.md — AI 持久记忆
> 创建时间: __CREATED_AT__
> 最后更新: __UPDATED_AT__
> 工作空间: __WORKSPACE_PATH__
## 用户偏好
## 项目上下文
## 重要决策
## 待办事项
## 已知问题
`;
// ===== 服务类 =====
export class WorkspaceService {
private workspacePath: string;
private files: WorkspaceFiles = { soul: '', memory: '' };
constructor(workspacePath?: string) {
this.workspacePath = workspacePath ?? join(app.getPath('userData'), 'MetonaWorkspaces', 'default');
}
/**
* 初始化工作空间
*
* 1. 创建目录结构
* 2. 验证/创建 2 个必需文件(SOUL.md + MEMORY.md
* 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));
}
// 验证/创建必需文件(SOUL.md + MEMORY.md
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 格式
*
* 简化规则:
* 1. 元数据头 — 用 > 引用语法,包含创建时间、最后更新、工作空间
* 2. 分区结构 — 包含用户偏好、项目上下文、重要决策(其余可选)
* 3. 时间戳自动更新
*/
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: Array<{ key: string; line: string }> = [
{ key: '> 创建时间', line: `> 创建时间: ${new Date().toISOString()}` },
{ key: '> 最后更新', line: `> 最后更新: ${new Date().toISOString()}` },
{ key: '> 工作空间', line: `> 工作空间: ${this.workspacePath}` },
];
for (const meta of requiredMeta) {
if (!content.includes(meta.key)) {
// v0.3.0 修复: 检查 # MEMORY 标题是否存在,不存在则在文件开头插入
const memoryTitleIdx = content.indexOf('# MEMORY');
let titleEnd: number;
if (memoryTitleIdx === -1) {
// 标题不存在,在开头插入
titleEnd = 0;
} else {
titleEnd = content.indexOf('\n', memoryTitleIdx) + 1;
// 如果没有换行符(文件只有一行),titleEnd 为 0,在末尾插入
if (titleEnd === 0) titleEnd = content.length;
}
content = content.slice(0, titleEnd) + meta.line + '\n' + content.slice(titleEnd);
modified = true;
log.info(`MEMORY.md: auto-added missing metadata "${meta.key}"`);
}
}
// 规则 2: 核心分区
const requiredSections = ['## 用户偏好', '## 项目上下文', '## 重要决策'];
for (const section of requiredSections) {
if (!content.includes(section)) {
content += `\n${section}\n`;
modified = true;
log.info(`MEMORY.md: auto-added missing section "${section}"`);
}
}
// 规则 3: 时间戳更新
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;
}
log.info(`Auto-created file: ${fileName}`);
}
/**
* 加载所有文件内容(SOUL.md + MEMORY.md
*/
private loadFiles(): void {
this.files.soul = this.readFile('SOUL.md');
this.files.memory = this.readFile('MEMORY.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 '';
}
}
}