1 Commits
Author SHA1 Message Date
thzxx 516d8a728f 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
2026-07-21 21:05:50 +08:00
12 changed files with 318 additions and 33 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
> 生产级通用 AI Agent 智能体桌面应用 > 生产级通用 AI Agent 智能体桌面应用
[![Version](https://img.shields.io/badge/version-0.3.14-blue)](./package.json) [![Version](https://img.shields.io/badge/version-0.3.15-blue)](./package.json)
[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE) [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
[![Electron](https://img.shields.io/badge/Electron-35-47848F)](https://www.electronjs.org/) [![Electron](https://img.shields.io/badge/Electron-35-47848F)](https://www.electronjs.org/)
[![React](https://img.shields.io/badge/React-19-61DAFB)](https://react.dev/) [![React](https://img.shields.io/badge/React-19-61DAFB)](https://react.dev/)
+145
View File
@@ -132,6 +132,7 @@
<a href="#tools">🔧 工具调用 (Function Calling)</a> <a href="#tools">🔧 工具调用 (Function Calling)</a>
<a href="#websearch">🌐 联网搜索</a> <a href="#websearch">🌐 联网搜索</a>
<a href="#tts">🎙️ 语音合成 (TTS)</a> <a href="#tts">🎙️ 语音合成 (TTS)</a>
<a href="#multimodal">🖼️ 多模态输入</a>
<a href="#examples">💻 代码示例</a> <a href="#examples">💻 代码示例</a>
<a href="#errors">⚠️ 错误码说明</a> <a href="#errors">⚠️ 错误码说明</a>
</div> </div>
@@ -331,6 +332,56 @@ Content-Type: application/json</pre>
</div> </div>
</section> </section>
<!-- ===== MULTIMODAL ===== -->
<section id="multimodal">
<h2>🖼️ 多模态输入</h2>
<p><code>messages[].content</code> 为数组时,可混合传入文本、图像内容。采用 OpenAI 兼容的 content parts 数组格式。</p>
<h3>content parts 字段</h3>
<table>
<tr><th width="200">字段名</th><th width="90">类型</th><th>描述</th></tr>
<tr><td><code>content</code></td><td><span class="type-tag">string | array</span></td><td>纯文本字符串,或 content parts 数组</td></tr>
<tr><td><code>content[].type</code></td><td><span class="type-tag">string</span></td><td>part 类型:<code>"text"</code> / <code>"image_url"</code></td></tr>
<tr><td><code>content[].text</code></td><td><span class="type-tag">string</span></td><td>文本内容(type 为 <code>"text"</code> 时必填)</td></tr>
<tr><td><code>content[].image_url</code></td><td><span class="type-tag">object</span></td><td>图像对象(type 为 <code>"image_url"</code> 时必填)</td></tr>
<tr><td><code>content[].image_url.url</code></td><td><span class="type-tag">string</span></td><td>图像公网 URL 或 base64 data URI(格式:<code>data:image/png;base64,...</code></td></tr>
<tr><td><code>content[].image_url.detail</code></td><td><span class="type-tag">string</span></td><td>可选,清晰度:<code>"low"</code> / <code>"high"</code> / <code>"auto"</code>(默认 <code>"auto"</code></td></tr>
</table>
<div class="info-box">
<strong>📌 支持的图像格式:</strong>
<ul style="padding-left:20px;margin:8px 0 0;">
<li><strong>URL</strong>:公网可访问的 HTTPS 图片地址</li>
<li><strong>Base64 data URI</strong><code>data:image/{format};base64,{base64_data}</code>,支持 png / jpeg / gif / webp</li>
</ul>
</div>
<h3>多模态消息示例</h3>
<pre><span class="code-lang">json</span>{
"role": "user",
"content": [
{ "type": "text", "text": "请描述这张图片的内容" },
{
"type": "image_url",
"image_url": {
"url": "https://example.com/image.jpg",
"detail": "auto"
}
}
]
}</pre>
<div class="warn-box">
<strong>⚠️ 多模态限制:</strong>
<ul style="padding-left:20px;margin:8px 0 0;">
<li><code>mimo-v2.5-pro</code> / <code>mimo-v2.5</code> 文本模型支持图像输入</li>
<li>TTS 系列模型(<code>mimo-v2.5-tts*</code>)不支持多模态图像输入</li>
<li>历史消息中的图片 content 数组应原样保留,不建议在多轮对话中删除</li>
<li>base64 data URI 会显著增加请求体大小,建议对大图先压缩或使用公网 URL</li>
</ul>
</div>
</section>
<!-- ===== RESPONSE ===== --> <!-- ===== RESPONSE ===== -->
<section id="response"> <section id="response">
<h2>📤 响应对象(非流式输出)</h2> <h2>📤 响应对象(非流式输出)</h2>
@@ -746,6 +797,100 @@ audio_bytes = base64.b64decode(audio_data)
with output("output.wav", "wb") as f: with output("output.wav", "wb") as f:
f.write(audio_bytes) f.write(audio_bytes)
print("音频已保存到 output.wav")</pre> print("音频已保存到 output.wav")</pre>
<h3>多模态图像输入</h3>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab(event,'tab-mm-curl')">curl</button>
<button class="tab-btn" onclick="switchTab(event,'tab-mm-python')">Python (OpenAI SDK)</button>
<button class="tab-btn" onclick="switchTab(event,'tab-mm-base64')">Python (Base64 本地图片)</button>
</div>
<div id="tab-mm-curl" class="tab-content active">
<pre><span class="code-lang">bash</span>curl --location --request POST 'https://api.xiaomimimo.com/v1/chat/completions' \
--header "api-key: $MIMO_API_KEY" \
--header "Content-Type: application/json" \
--data-raw '{
"model": "mimo-v2.5-pro",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "请描述这张图片的内容"},
{
"type": "image_url",
"image_url": {
"url": "https://example.com/image.jpg",
"detail": "auto"
}
}
]
}
],
"max_completion_tokens": 1024,
"thinking": {"type": "disabled"}
}'</pre>
</div>
<div id="tab-mm-python" class="tab-content">
<pre><span class="code-lang">python</span>from openai import OpenAI
client = OpenAI(api_key="$MIMO_API_KEY", base_url="https://api.xiaomimimo.com/v1")
response = client.chat.completions.create(
model="mimo-v2.5-pro",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "请描述这张图片的内容"},
{
"type": "image_url",
"image_url": {
"url": "https://example.com/image.jpg",
"detail": "auto"
}
}
]
}
],
max_completion_tokens=1024,
thinking={"type": "disabled"}
)
print(response.choices[0].message.content)
print(f"图像 token: {response.usage.prompt_tokens_details.image_tokens}")</pre>
</div>
<div id="tab-mm-base64" class="tab-content">
<pre><span class="code-lang">python</span>import base64
from openai import OpenAI
client = OpenAI(api_key="$MIMO_API_KEY", base_url="https://api.xiaomimimo.com/v1")
# 读取本地图片并转为 base64 data URI
with open("local_image.jpg", "rb") as f:
image_data = base64.b64encode(f.read()).decode("utf-8")
image_data_uri = f"data:image/jpeg;base64,{image_data}"
response = client.chat.completions.create(
model="mimo-v2.5-pro",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "这张图里有什么?"},
{
"type": "image_url",
"image_url": {"url": image_data_uri}
}
]
}
],
max_completion_tokens=1024
)
print(response.choices[0].message.content)</pre>
</div>
</section> </section>
<!-- ===== ERRORS ===== --> <!-- ===== ERRORS ===== -->
+27
View File
@@ -152,6 +152,7 @@ export class MimoAdapter extends BaseAdapter {
* 构建 MiMo 原生请求体 * 构建 MiMo 原生请求体
* *
* MiMo 特有参数: * MiMo 特有参数:
* - 多模态图片:user 消息的 images[] → OpenAI content 数组 [{type:"text"}, {type:"image_url"}]
* - thinking: { type: "enabled" / "disabled" } — 与 DeepSeek 一致 * - thinking: { type: "enabled" / "disabled" } — 与 DeepSeek 一致
* - max_completion_tokens — 非 max_tokensMiMo 使用新字段名) * - max_completion_tokens — 非 max_tokensMiMo 使用新字段名)
* - stream_options: { include_usage: true } — 流式返回 usage * - stream_options: { include_usage: true } — 流式返回 usage
@@ -163,6 +164,32 @@ export class MimoAdapter extends BaseAdapter {
const messages = buildOpenAICompatibleMessages(request); const messages = buildOpenAICompatibleMessages(request);
const tools = buildOpenAICompatibleTools(request.tools); 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> = { const body: Record<string, unknown> = {
model: this.config.defaultModel, model: this.config.defaultModel,
messages, messages,
+60 -11
View File
@@ -1132,25 +1132,74 @@ export function registerAllIPCHandlers(
mkdirSync(resolvedTarget, { recursive: true }); 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 inherited: string[] = [];
const failed: Array<{ file: string; error: string }> = []; const failed: Array<{ file: string; error: string }> = [];
for (const fileName of files) { for (const fileName of files) {
// 仅允许继承白名单文件(防止路径遍历)— v0.3.14: 仅 SOUL.md const pathSegments = INHERIT_WHITELIST[fileName];
if (!['SOUL.md'].includes(fileName)) { // 不在白名单中:拒绝(防止路径遍历)
if (!pathSegments) {
failed.push({ file: fileName, error: '不在继承白名单中' }); failed.push({ file: fileName, error: '不在继承白名单中' });
continue; continue;
} }
const srcFile = join(resolvedSource, fileName); const srcFile = join(resolvedSource, ...pathSegments);
const dstFile = join(resolvedTarget, fileName); const dstFile = join(resolvedTarget, ...pathSegments);
try { try {
if (existsSync(srcFile)) { // 确保目标文件的父目录存在(如 .metona/)
copyFileSync(srcFile, dstFile); const dstDir = join(resolvedTarget, ...pathSegments.slice(0, -1));
inherited.push(fileName); if (!existsSync(dstDir)) {
log.info(`[WORKSPACE] Inherited ${fileName}: ${resolvedSource}${resolvedTarget}`); mkdirSync(dstDir, { recursive: true });
} else {
failed.push({ file: fileName, error: '源文件不存在' });
} }
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) { } catch (err) {
failed.push({ file: fileName, error: (err as Error).message }); failed.push({ file: fileName, error: (err as Error).message });
} }
@@ -1166,7 +1215,7 @@ export function registerAllIPCHandlers(
const files = workspaceService.reload(); // 同步外部可能的手动修改 const files = workspaceService.reload(); // 同步外部可能的手动修改
const REQUIRED = ['SOUL.md', 'MEMORY.md'] as const; 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 fileInfos = REQUIRED.map((name) => {
const filePath = join(workspacePath, name); const filePath = join(workspacePath, name);
+2 -2
View File
@@ -35,7 +35,7 @@ export interface WorkspaceInfo {
const REQUIRED_FILES = ['SOUL.md', 'MEMORY.md'] as const; 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 持久记忆 const MEMORY_TEMPLATE = `# MEMORY.md — AI 持久记忆
@@ -77,7 +77,7 @@ export class WorkspaceService {
// 创建工作空间目录 // 创建工作空间目录
this.ensureDirectory(this.workspacePath); this.ensureDirectory(this.workspacePath);
// 创建自动目录(logs/, traces/, .metona/ // 创建自动目录(logs/, .metona/
for (const dir of AUTO_CREATED_DIRS) { for (const dir of AUTO_CREATED_DIRS) {
this.ensureDirectory(join(this.workspacePath, dir)); this.ensureDirectory(join(this.workspacePath, dir));
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.14", "version": "0.3.15",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.14", "version": "0.3.15",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.14", "version": "0.3.15",
"description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用", "description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用",
"main": "dist-electron/main/main.js", "main": "dist-electron/main/main.js",
"author": "Metona Team", "author": "Metona Team",
+47 -1
View File
@@ -20,9 +20,24 @@ export function OnboardingWizard(): React.JSX.Element | null {
const [apiKey, setApiKey] = useState(''); const [apiKey, setApiKey] = useState('');
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const [workspacePath, setWorkspacePath] = useState(''); const [workspacePath, setWorkspacePath] = useState('');
// 上下文窗口(联动 Provider:ollama 可空=由模型决定;其他 provider 默认值见 DEFAULT_CTX
const [contextWindow, setContextWindow] = useState<number | null>(null);
if (onboardingCompleted) return null; if (onboardingCompleted) return null;
// 各 Provider 上下文窗口默认值(与 SettingsModal 保持一致)
// ollama 返回 null(由模型决定),其他 provider 返回正整数
const DEFAULT_CTX: Record<string, number | null> = {
deepseek: 1_000_000,
agnes: 1_000_000,
mimo: 131_072,
ollama: null,
};
// 上下文窗口校验:ollama 允许空,最小 512;其他 provider 最小 4096
const ctxMin = provider === 'ollama' ? 512 : 4096;
const ctxError =
contextWindow != null && (!Number.isFinite(contextWindow) || contextWindow < ctxMin);
const handleNext = async () => { const handleNext = async () => {
if (step < STEPS.length - 1) { setStep(step + 1); return; } if (step < STEPS.length - 1) { setStep(step + 1); return; }
try { try {
@@ -39,6 +54,14 @@ export function OnboardingWizard(): React.JSX.Element | null {
if (model.trim()) entries.push({ key: 'llm.model', value: model.trim() }); if (model.trim()) entries.push({ key: 'llm.model', value: model.trim() });
if (apiKey.trim()) entries.push({ key: 'llm.apiKey', value: apiKey.trim() }); if (apiKey.trim()) entries.push({ key: 'llm.apiKey', value: apiKey.trim() });
if (workspacePath.trim()) entries.push({ key: 'workspace.path', value: workspacePath.trim() }); if (workspacePath.trim()) entries.push({ key: 'workspace.path', value: workspacePath.trim() });
// 上下文窗口:根据 Provider 落库到对应 key
// - ollama: ollama.numCtx(允许 null=由模型决定)
// - deepseek/agnes/mimo: {provider}.contextWindow(必须有值且 >= 4096
if (provider === 'ollama') {
entries.push({ key: 'ollama.numCtx', value: contextWindow });
} else if (provider && contextWindow != null && contextWindow >= 4096) {
entries.push({ key: `${provider}.contextWindow`, value: contextWindow });
}
entries.push({ key: 'onboarding.completed', value: true }); entries.push({ key: 'onboarding.completed', value: true });
const r = await window.metona.config.setBatch(entries); const r = await window.metona.config.setBatch(entries);
@@ -48,6 +71,10 @@ export function OnboardingWizard(): React.JSX.Element | null {
return; return;
} }
useAgentStore.getState().setProvider(provider.trim() || 'deepseek', model.trim() || ''); useAgentStore.getState().setProvider(provider.trim() || 'deepseek', model.trim() || '');
// 同步 contextWindow 到 Agent Store(与 SettingsModal 行为一致)
if (contextWindow != null && contextWindow >= ctxMin) {
useAgentStore.setState({ contextWindow });
}
// P1-6 修复: Onboarding 完成后标记配置已加载 // P1-6 修复: Onboarding 完成后标记配置已加载
useAgentStore.getState().setConfigLoaded(true); useAgentStore.getState().setConfigLoaded(true);
} }
@@ -81,7 +108,12 @@ export function OnboardingWizard(): React.JSX.Element | null {
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 2 }}> Provider API Base URL </Typography> <Typography variant="body2" sx={{ color: 'text.secondary', mb: 2 }}> Provider API Base URL </Typography>
<Stack spacing={2}> <Stack spacing={2}>
<FormControl size="small"><InputLabel>Provider</InputLabel> <FormControl size="small"><InputLabel>Provider</InputLabel>
<Select value={provider} label="Provider" onChange={(e) => setProvider(e.target.value)}> <Select value={provider} label="Provider" onChange={(e) => {
const v = e.target.value;
setProvider(v);
// 联动默认上下文窗口(与 SettingsModal 默认值一致)
setContextWindow(DEFAULT_CTX[v] ?? null);
}}>
<MenuItem value="deepseek">DeepSeek</MenuItem> <MenuItem value="deepseek">DeepSeek</MenuItem>
<MenuItem value="agnes">Agnes AI</MenuItem> <MenuItem value="agnes">Agnes AI</MenuItem>
<MenuItem value="mimo">MiMo ()</MenuItem> <MenuItem value="mimo">MiMo ()</MenuItem>
@@ -93,6 +125,20 @@ export function OnboardingWizard(): React.JSX.Element | null {
<TextField size="small" label="API Key" type={showKey ? 'text' : 'password'} value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="sk-...(本地模型可留空)" <TextField size="small" label="API Key" type={showKey ? 'text' : 'password'} value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="sk-...(本地模型可留空)"
slotProps={{ input: { endAdornment: <InputAdornment position="end"><IconButton size="small" onClick={() => setShowKey(!showKey)}>{showKey ? <EyeOff size={14} /> : <Eye size={14} />}</IconButton></InputAdornment> } }} slotProps={{ input: { endAdornment: <InputAdornment position="end"><IconButton size="small" onClick={() => setShowKey(!showKey)}>{showKey ? <EyeOff size={14} /> : <Eye size={14} />}</IconButton></InputAdornment> } }}
/> />
<TextField
size="small"
label={provider === 'ollama' ? '上下文长度 (num_ctx)' : '上下文窗口 (contextWindow)'}
type="number"
value={contextWindow ?? ''}
onChange={(e) => {
const v = e.target.value;
setContextWindow(v === '' ? null : Number(v));
}}
placeholder={provider === 'ollama' ? '默认由模型决定(如 2048、4096、128000' : '如 64000、128000、1000000'}
slotProps={{ htmlInput: { min: ctxMin, step: ctxMin } }}
error={ctxError}
helperText={ctxError ? `最小值为 ${ctxMin}` : (provider === 'ollama' ? ' ' : '用于上下文压缩判断,不传给 API')}
/>
</Stack> </Stack>
</Box> </Box>
)} )}
+19 -3
View File
@@ -116,6 +116,8 @@ function WorkspaceSettings() {
const [checkResult, setCheckResult] = useState<MetonaWorkspaceCheckResult | null>(null); const [checkResult, setCheckResult] = useState<MetonaWorkspaceCheckResult | null>(null);
const [checking, setChecking] = useState(false); const [checking, setChecking] = useState(false);
const [inheritSoul, setInheritSoul] = useState(true); const [inheritSoul, setInheritSoul] = useState(true);
// 数据库继承:默认勾选(历史会话/消息/记忆/Trace 丢失不可逆,默认带过去更安全)
const [inheritDatabase, setInheritDatabase] = useState(true);
const [applying, setApplying] = useState(false); const [applying, setApplying] = useState(false);
const [showRestartDialog, setShowRestartDialog] = useState(false); const [showRestartDialog, setShowRestartDialog] = useState(false);
@@ -145,9 +147,12 @@ function WorkspaceSettings() {
if (!pendingPath || !checkResult?.valid) return; if (!pendingPath || !checkResult?.valid) return;
setApplying(true); setApplying(true);
try { try {
// 如果勾选了继承文件,从旧工作空间复制到新工作空间v0.3.14: 仅 SOUL.md // 如果勾选了继承文件,从旧工作空间复制到新工作空间
// - SOUL.md: Agent 身份定义
// - .metona/agent.db: 数据库(会话/消息/记忆/Trace),后端白名单校验
const filesToInherit: string[] = []; const filesToInherit: string[] = [];
if (inheritSoul) filesToInherit.push('SOUL.md'); if (inheritSoul) filesToInherit.push('SOUL.md');
if (inheritDatabase) filesToInherit.push('.metona/agent.db');
if (filesToInherit.length > 0 && currentPath) { if (filesToInherit.length > 0 && currentPath) {
try { try {
@@ -260,6 +265,18 @@ function WorkspaceSettings() {
control={<Checkbox size="small" checked={inheritSoul} onChange={(e) => setInheritSoul(e.target.checked)} />} control={<Checkbox size="small" checked={inheritSoul} onChange={(e) => setInheritSoul(e.target.checked)} />}
label={<Typography variant="caption">SOUL.md</Typography>} label={<Typography variant="caption">SOUL.md</Typography>}
/> />
{/* 数据库继承:仅在新工作空间(空目录)时显示,避免覆盖已有工作空间的数据 */}
{checkResult.isNewWorkspace && (
<>
<FormControlLabel
control={<Checkbox size="small" checked={inheritDatabase} onChange={(e) => setInheritDatabase(e.target.checked)} />}
label={<Typography variant="caption"> agent.db///Trace </Typography>}
/>
<Typography variant="caption" sx={{ color: 'text.disabled', fontSize: 10, pl: 3 }}>
SQLite backup API
</Typography>
</>
)}
<Typography variant="caption" sx={{ color: 'text.disabled', fontSize: 10, pl: 3 }}> <Typography variant="caption" sx={{ color: 'text.disabled', fontSize: 10, pl: 3 }}>
MEMORY.md MEMORY.md
</Typography> </Typography>
@@ -1288,8 +1305,7 @@ function LogsSettings() {
fullWidth fullWidth
value={logFilePath} value={logFilePath}
placeholder={logPathLoading ? '正在获取路径...' : '路径不可用'} placeholder={logPathLoading ? '正在获取路径...' : '路径不可用'}
readOnly slotProps={{ input: { readOnly: true, sx: { fontSize: 11, fontFamily: 'monospace' } } }}
slotProps={{ input: { sx: { fontSize: 11, fontFamily: 'monospace' } } }}
/> />
<Stack direction="row" spacing={1} sx={{ mt: 1 }}> <Stack direction="row" spacing={1} sx={{ mt: 1 }}>
<Button <Button
+2 -2
View File
@@ -3,8 +3,8 @@
* *
* 展示当前工作空间的: * 展示当前工作空间的:
* - 根路径(可在文件管理器中打开) * - 根路径(可在文件管理器中打开)
* - 4 个核心文件(SOUL/AGENTS/MEMORY/USERS):状态、大小、修改时间、内容预览 * - 2 个核心文件(SOUL/MEMORY):状态、大小、修改时间、内容预览
* - 自动目录(logs/traces/.metona):存在性和文件数 * - 自动目录(logs/.metona):存在性和文件数
* *
* Agent 完成任务后自动刷新(thinking/executing → idle)。 * Agent 完成任务后自动刷新(thinking/executing → idle)。
*/ */
+2
View File
@@ -124,6 +124,8 @@ interface AgentState {
updateLastAssistantMessage: (delta: string) => void; updateLastAssistantMessage: (delta: string) => void;
setAgentStatus: (status: AgentStatus) => void; setAgentStatus: (status: AgentStatus) => void;
setStreaming: (streaming: boolean) => void; setStreaming: (streaming: boolean) => void;
// P1-6 修复: 配置加载完成标志的 setter
setConfigLoaded: (loaded: boolean) => void;
setCurrentRunId: (runId: string | null) => void; setCurrentRunId: (runId: string | null) => void;
addTraceStep: (step: TraceStep) => void; addTraceStep: (step: TraceStep) => void;
updateTraceStep: (iteration: number, updates: Partial<TraceStep>) => void; updateTraceStep: (iteration: number, updates: Partial<TraceStep>) => void;
+10 -10
View File
@@ -85,8 +85,8 @@ interface MetonaSessionInfo {
interface MetonaSessionsAPI { interface MetonaSessionsAPI {
list: () => Promise<MetonaSessionInfo[]>; list: () => Promise<MetonaSessionInfo[]>;
create: (title?: string) => Promise<MetonaSessionInfo>; create: (title?: string) => Promise<MetonaSessionInfo>;
rename: (sessionId: string, title: string) => Promise<{ success: boolean }>; rename: (sessionId: string, title: string) => Promise<{ success: boolean; error?: string }>;
delete: (sessionId: string) => Promise<{ success: boolean }>; delete: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
getMessages: (sessionId: string) => Promise<Array<{ getMessages: (sessionId: string) => Promise<Array<{
id: string; id: string;
role: string; role: string;
@@ -98,8 +98,8 @@ interface MetonaSessionsAPI {
iteration?: number; iteration?: number;
timestamp: number; timestamp: number;
}>>; }>>;
pin: (sessionId: string, pinned: boolean) => Promise<{ success: boolean }>; pin: (sessionId: string, pinned: boolean) => Promise<{ success: boolean; error?: string }>;
archive: (sessionId: string, archived: boolean) => Promise<{ success: boolean }>; archive: (sessionId: string, archived: boolean) => Promise<{ success: boolean; error?: string }>;
deleteMessage: (messageId: string) => Promise<{ success: boolean }>; deleteMessage: (messageId: string) => Promise<{ success: boolean }>;
clearMessages: (sessionId: string) => Promise<{ success: boolean }>; clearMessages: (sessionId: string) => Promise<{ success: boolean }>;
saveTrace: (sessionId: string, data: { traceSteps: unknown[]; tokenUsage: unknown }) => Promise<{ success: boolean }>; saveTrace: (sessionId: string, data: { traceSteps: unknown[]; tokenUsage: unknown }) => Promise<{ success: boolean }>;
@@ -126,9 +126,9 @@ interface MetonaMCPServerStatus {
interface MetonaMCPAPI { interface MetonaMCPAPI {
listServers: () => Promise<MetonaMCPServerStatus[]>; listServers: () => Promise<MetonaMCPServerStatus[]>;
addServer: (config: MetonaMCPServerConfig) => Promise<{ success: boolean }>; addServer: (config: MetonaMCPServerConfig) => Promise<{ success: boolean; error?: string }>;
removeServer: (name: string) => Promise<{ success: boolean }>; removeServer: (name: string) => Promise<{ success: boolean; error?: string }>;
toggleServer: (name: string, enabled: boolean) => Promise<{ success: boolean }>; toggleServer: (name: string, enabled: boolean) => Promise<{ success: boolean; error?: string }>;
} }
// ===== Memory API ===== // ===== Memory API =====
@@ -161,9 +161,9 @@ interface MetonaMemoryAPI {
interface MetonaConfigAPI { interface MetonaConfigAPI {
get: (key: string) => Promise<unknown>; get: (key: string) => Promise<unknown>;
set: (key: string, value: unknown) => Promise<{ success: boolean }>; set: (key: string, value: unknown) => Promise<{ success: boolean; error?: string }>;
// v0.3.9: 批量保存配置,避免串行保存中间态触发 reloadAdapter 失败 // v0.3.9: 批量保存配置,避免串行保存中间态触发 reloadAdapter 失败
setBatch: (entries: Array<{ key: string; value: unknown }>) => Promise<{ success: boolean }>; setBatch: (entries: Array<{ key: string; value: unknown }>) => Promise<{ success: boolean; error?: string }>;
} }
// ===== App API ===== // ===== App API =====
@@ -247,7 +247,7 @@ interface MetonaToolInfo {
interface MetonaToolsAPI { interface MetonaToolsAPI {
list: () => Promise<MetonaToolInfo[]>; list: () => Promise<MetonaToolInfo[]>;
toggle: (toolName: string, enabled: boolean) => Promise<{ success: boolean }>; toggle: (toolName: string, enabled: boolean) => Promise<{ success: boolean; error?: string }>;
} }
// ===== SearXNG API ===== // ===== SearXNG API =====