Files
metona-ai-desktop/electron/services/workspace.service.ts
T
thzxx 025f00171b feat: 升级至 v0.3.0 — 安全增强、死循环检测、六轮全面审计修复
大版本迭代,新增安全增强、Agent Loop 增强、UI/UX 增强,经六轮全面审计修复所有问题。

新增功能:
- OutputValidator 事实一致性检查 + 幻觉检测
- PromptInjectionDefender 语义级检测(指令性动词密度、角色边界、分隔符嵌套)
- DeadLoopError 死循环检测(连续3轮相同工具调用自动终止)
- PolicyEngine maxFrequency 滑动窗口频率限制
- MemoryManager TF-IDF 语义检索 + IDF 缓存原子替换
- 斜杠命令(/tool /memory /clear /export)+ 快捷键(Ctrl+B/J/Shift+F/N/[/])
- 专注模式(Ctrl+Shift+F)带面板状态快照保存/恢复

六轮审计修复(共修复 3 CRITICAL + 8 HIGH + 13 MEDIUM + 11 LOW):

CRITICAL:
- main.ts 传入 createAdapter 而非 reloadAdapter,导致 Provider 切换完全失效
- Ctrl+N/Ctrl+[/Ctrl+] 不同步 agent-store,导致消息发到错误会话
- engine.ts emit('error') 无监听器导致 DONE 事件丢失、前端卡死

HIGH:
- 死循环检测在工具执行之后(移入 PARSING 后 EXECUTING 前)
- deadLoop 事件前端未处理
- ConfirmationHook.clearPending 从未调用导致定时器泄漏
- engine.ts retry abort listener 未移除导致监听器堆积
- MCPManager JSON.parse 无 try-catch 导致初始化崩溃
- sendMessage 自动创建会话不同步 session-store
- setCurrentSession 竞态导致旧请求覆盖新会话数据

MEDIUM:
- toggleFocusMode 覆盖用户原有面板状态
- 正则检测可被常见词绕过
- 频率限制内存泄漏 + customPolicies 覆盖
- LIKE 回退转义未包含反斜杠
- OutputValidator 新功能未传入 toolResults/context
- MemoryConsolidator LLM 调用无超时保护
- AuditService 每次 log 都查询数据库
- browser-window-manager 超时后未停止页面加载
- output-validator URL 比较大小写敏感
- FileReader 无 onerror 导致 Promise 永久挂起
- /clear /export 不关闭斜杠菜单
- 专注模式下 toggleSidebar/toggleDetail 未恢复另一面板快照

LOW:
- abortPromise 事件监听器堆积
- RateLimitHook Map 内存泄漏
- memory.ts await 同步方法
- PolicyEngine 死代码清理
- Orchestrator sessionDepth 会话结束不清理
- workspace.service.ts 元数据插入边界问题
2026-07-12 19:46:47 +08:00

326 lines
9.2 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. 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 持久记忆
> 创建时间: __CREATED_AT__
> 最后更新: __UPDATED_AT__
> 工作空间: __WORKSPACE_PATH__
## 用户偏好
## 项目上下文
## 重要决策
## 待办事项
## 已知问题
`;
// ===== 服务类 =====
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 格式
*
* 简化规则:
* 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;
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 '';
}
}
}