feat: 升级至 v0.3.15 — MiMo 多模态 + 工作空间数据库继承 + 引导窗口上下文配置 + TS 错误修复
- feat(mimo): 适配器支持图片输入,将 images 转为 OpenAI 兼容 content parts 数组 - feat(workspace): 切换工作空间时支持继承数据库 agent.db(SQLite backup API 原子导出,自动 checkpoint WAL) - feat(onboarding): 引导弹窗 LLM 配置加上下文长度字段(DeepSeek/Agnes=1000000、MiMo=131072、Ollama=null) - fix(types): 修复 16 个 TypeScript 编译错误(setConfigLoaded 接口声明、10 个 IPC 返回类型加 error 字段、MUI TextField readOnly slot 迁移) - chore(workspace): 移除死代码 traces 目录(trace 数据实际存储于数据库 sessions.metadata 字段,traces 目录从未被任何代码读写) - docs(mimo): API 文档补充多模态输入章节和 curl/Python 代码示例 - chore: 版本号 0.3.14 → 0.3.15
This commit is contained in:
@@ -152,6 +152,7 @@ export class MimoAdapter extends BaseAdapter {
|
||||
* 构建 MiMo 原生请求体
|
||||
*
|
||||
* MiMo 特有参数:
|
||||
* - 多模态图片:user 消息的 images[] → OpenAI content 数组 [{type:"text"}, {type:"image_url"}]
|
||||
* - thinking: { type: "enabled" / "disabled" } — 与 DeepSeek 一致
|
||||
* - max_completion_tokens — 非 max_tokens(MiMo 使用新字段名)
|
||||
* - stream_options: { include_usage: true } — 流式返回 usage
|
||||
@@ -163,6 +164,32 @@ export class MimoAdapter extends BaseAdapter {
|
||||
const messages = buildOpenAICompatibleMessages(request);
|
||||
const tools = buildOpenAICompatibleTools(request.tools);
|
||||
|
||||
// === MiMo 多模态:将 images 转为 OpenAI content 数组 ===
|
||||
// buildOpenAICompatibleMessages 不处理图片(各 Provider 自行处理)
|
||||
// MiMo 是 OpenAI 兼容 API,多模态格式与 Agnes AI 一致
|
||||
const nonSystemMsgs = request.messages.filter((m) => m.role !== 'system');
|
||||
let imageCount = 0;
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
// messages[0] 是 system,非 system 消息从 messages[1] 开始
|
||||
if (i === 0) continue;
|
||||
const origMsg = nonSystemMsgs[i - 1];
|
||||
if (!origMsg?.images?.length) continue;
|
||||
|
||||
imageCount += origMsg.images.length;
|
||||
|
||||
const contentParts: Array<Record<string, unknown>> = [];
|
||||
if (origMsg.content) {
|
||||
contentParts.push({ type: 'text', text: origMsg.content });
|
||||
}
|
||||
for (const img of origMsg.images) {
|
||||
contentParts.push({
|
||||
type: 'image_url',
|
||||
image_url: { url: img.url },
|
||||
});
|
||||
}
|
||||
messages[i].content = contentParts;
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model: this.config.defaultModel,
|
||||
messages,
|
||||
|
||||
+60
-11
@@ -1132,25 +1132,74 @@ export function registerAllIPCHandlers(
|
||||
mkdirSync(resolvedTarget, { recursive: true });
|
||||
}
|
||||
|
||||
// 继承白名单:键为前端传入的标识,值为实际相对路径分段
|
||||
// - SOUL.md: 工作空间根目录的 Agent 身份定义
|
||||
// - .metona/agent.db: 工作空间内的 SQLite 数据库(会话/消息/记忆/Trace)
|
||||
// 白名单映射防止路径遍历攻击(不直接使用用户传入的路径拼接到 fs 调用)
|
||||
const INHERIT_WHITELIST: Record<string, string[]> = {
|
||||
'SOUL.md': ['SOUL.md'],
|
||||
'.metona/agent.db': ['.metona', 'agent.db'],
|
||||
};
|
||||
|
||||
const inherited: string[] = [];
|
||||
const failed: Array<{ file: string; error: string }> = [];
|
||||
|
||||
for (const fileName of files) {
|
||||
// 仅允许继承白名单文件(防止路径遍历)— v0.3.14: 仅 SOUL.md
|
||||
if (!['SOUL.md'].includes(fileName)) {
|
||||
const pathSegments = INHERIT_WHITELIST[fileName];
|
||||
// 不在白名单中:拒绝(防止路径遍历)
|
||||
if (!pathSegments) {
|
||||
failed.push({ file: fileName, error: '不在继承白名单中' });
|
||||
continue;
|
||||
}
|
||||
const srcFile = join(resolvedSource, fileName);
|
||||
const dstFile = join(resolvedTarget, fileName);
|
||||
const srcFile = join(resolvedSource, ...pathSegments);
|
||||
const dstFile = join(resolvedTarget, ...pathSegments);
|
||||
try {
|
||||
if (existsSync(srcFile)) {
|
||||
copyFileSync(srcFile, dstFile);
|
||||
inherited.push(fileName);
|
||||
log.info(`[WORKSPACE] Inherited ${fileName}: ${resolvedSource} → ${resolvedTarget}`);
|
||||
} else {
|
||||
failed.push({ file: fileName, error: '源文件不存在' });
|
||||
// 确保目标文件的父目录存在(如 .metona/)
|
||||
const dstDir = join(resolvedTarget, ...pathSegments.slice(0, -1));
|
||||
if (!existsSync(dstDir)) {
|
||||
mkdirSync(dstDir, { recursive: true });
|
||||
}
|
||||
if (!existsSync(srcFile)) {
|
||||
failed.push({ file: fileName, error: '源文件不存在' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// === 数据库文件特殊处理 ===
|
||||
// agent.db 启用了 WAL 模式(database.service.ts:53),直接 copyFileSync 存在两个问题:
|
||||
// 1. WAL 中未 checkpoint 的事务会丢失(最近会话/消息可能没了)
|
||||
// 2. 主进程可能仍持有 db 句柄,Windows 上文件锁定会导致复制失败或不一致
|
||||
// 解决方案:用 better-sqlite3 的 backup API(自带 checkpoint + 一致性快照)
|
||||
if (fileName === '.metona/agent.db') {
|
||||
try {
|
||||
const Database = (await import('better-sqlite3')).default;
|
||||
// 以只读方式打开源库(如果主进程已持有句柄,better-sqlite3 会共享锁)
|
||||
const srcDb = new Database(srcFile, { readonly: true, fileMustExist: true });
|
||||
try {
|
||||
// backup API 原子性导出完整快照,自动合并 WAL 内容
|
||||
srcDb.backup(dstFile);
|
||||
inherited.push(fileName);
|
||||
log.info(`[WORKSPACE] Inherited ${fileName} (via SQLite backup): ${resolvedSource} → ${resolvedTarget}`);
|
||||
} finally {
|
||||
srcDb.close();
|
||||
}
|
||||
} catch (err) {
|
||||
// backup 失败时回退到 copyFileSync(至少保证基本可用)
|
||||
log.warn(`[WORKSPACE] SQLite backup failed, fallback to copyFileSync: ${(err as Error).message}`);
|
||||
try {
|
||||
copyFileSync(srcFile, dstFile);
|
||||
inherited.push(fileName);
|
||||
log.info(`[WORKSPACE] Inherited ${fileName} (fallback copyFileSync): ${resolvedSource} → ${resolvedTarget}`);
|
||||
} catch (err2) {
|
||||
failed.push({ file: fileName, error: `backup 和 fallback 均失败: ${(err2 as Error).message}` });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// === 普通文件直接复制 ===
|
||||
copyFileSync(srcFile, dstFile);
|
||||
inherited.push(fileName);
|
||||
log.info(`[WORKSPACE] Inherited ${fileName}: ${resolvedSource} → ${resolvedTarget}`);
|
||||
} catch (err) {
|
||||
failed.push({ file: fileName, error: (err as Error).message });
|
||||
}
|
||||
@@ -1166,7 +1215,7 @@ export function registerAllIPCHandlers(
|
||||
const files = workspaceService.reload(); // 同步外部可能的手动修改
|
||||
|
||||
const REQUIRED = ['SOUL.md', 'MEMORY.md'] as const;
|
||||
const AUTO_DIRS = ['logs', 'traces', '.metona'] as const;
|
||||
const AUTO_DIRS = ['logs', '.metona'] as const;
|
||||
|
||||
const fileInfos = REQUIRED.map((name) => {
|
||||
const filePath = join(workspacePath, name);
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface WorkspaceInfo {
|
||||
|
||||
const REQUIRED_FILES = ['SOUL.md', 'MEMORY.md'] as const;
|
||||
|
||||
const AUTO_CREATED_DIRS = ['logs', 'traces', '.metona'] as const;
|
||||
const AUTO_CREATED_DIRS = ['logs', '.metona'] as const;
|
||||
|
||||
const MEMORY_TEMPLATE = `# MEMORY.md — AI 持久记忆
|
||||
|
||||
@@ -77,7 +77,7 @@ export class WorkspaceService {
|
||||
// 创建工作空间目录
|
||||
this.ensureDirectory(this.workspacePath);
|
||||
|
||||
// 创建自动目录(logs/, traces/, .metona/)
|
||||
// 创建自动目录(logs/, .metona/)
|
||||
for (const dir of AUTO_CREATED_DIRS) {
|
||||
this.ensureDirectory(join(this.workspacePath, dir));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user