Files
metona-ai-desktop/electron/services/workspace.service.ts
T
thzxx 97a3d3d53b fix: 全面修复审计问题并优化系统提示词
**崩溃/挂死修复 (5):**
- 统一 TrayManager.isQuitting 变量,修复 Cmd+Q 无法退出
- useAgentStream 闭包过期快照 → 每次 getState()
- Agnes chatStream 添加 AbortSignal.timeout
- SSE JSON.parse 添加 try-catch 保护
- Orchestrator setTools 污染 → save/restore 模式

**功能修复 (14):**
- 上下文压缩实现 (每5轮 COMPRESSING 状态)
- 修复 requestId 硬编码空串
- ConfigService.set() 保留已有 category
- MemoryManager 新增 working 类型搜索
- PromptInjectionDefender 补全 sanitize()
- Ollama: 补全 dynamicReminders + reasoningContent
- openai-format: 所有 assistant 消息保留 reasoningContent
- SSE: finish_reason 时提前 flush tool_calls
- DeepSeek thinking effort 映射注释
- Ollama done_reason load→stop
- RateLimitHook >= 边界修复
- WorkspaceService isValid 首次启动修复
- sessions:archive IPC handler
- 托盘/窗口图标路径生产环境修复

**系统提示词优化:**
- SOUL.md 存在时不显示兜底身份,原文放最前
- 兜底身份改为中文 (MetonaAI 自身描述)
- 用户文本在前,附件内容在后

**文件上传:**
- 非图片文件不再 base64 编码,保留 JSON 结构
- 用户文本优先于文件内容

**UI 修复:**
- 首页 Logo 路径修复 (public/ + 相对路径)
- TokenUsage contextWindow 动态计算 (Provider 感知)
- 切换 Provider 同步 contextWindow
- 托盘图标始终显示 Logo (状态由右键菜单展示)
2026-06-30 23:17:38 +08:00

331 lines
9.6 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 持久记忆
#
# 格式版本: 1.0
# 创建时间: ${new Date().toISOString()}
# 最后更新: ${new Date().toISOString()}
# 工作空间: __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 content = MEMORY_TEMPLATE.replace('__WORKSPACE_PATH__', this.workspacePath);
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 '';
}
}
}