feat: v0.7.3 成本收口 · 状态一致 · 死账清理 — Prompt Cache 根治 + SSRF DNS Pinning + 87 用例扩充全量回归
CI / 类型检查 + Lint + 单元测试 (push) Failing after 5m47s
CI / 全量测试 (Electron ABI) (push) Failing after 5m19s
CI / 产物编译验证 (push) Successful in 9m55s

P1 修复面收口: Prompt Cache 根治(日期/记忆/附件三类易变内容出 system 入用户消息
  前置块 user-context.ts, system 跨 run 字节级稳定; Anthropic system 块数组化 +
  cache_control ephemeral 断言, DeepSeek 自动缓存前缀命中 — 多轮对话输入 token
  成本降数量级); 编辑重发/重新生成幽灵 Trace 双侧根治(DB truncateMessagesAfter
  同步过滤 metadata.traceSteps + 前端 trimTraceStepsByAnchor 镜像, 严格小于锚点
  时间戳, 同毫秒等值判废); sessions:deleteMessage 死通道全链路删除(渲染层零调用
  + message_count 漂移面); Ollama vision 能力门控全链路(MetonaModelInfo
  .supportsVision 贯穿 adapter/IPC/store/UI, model-capabilities.ts 三道判定纯函数,
  未知保守放行); 记忆固化节流(consolidation-policy 纯函数: 总开关 + 内容门控
  [回答>=200字符或存在成功工具调用] + 会话级 10 分钟频率窗口, 三 memory.* 配置键)

P2 安全纵深: SSRF DNS Pinning 关闭 rebinding 窗口(ssrf-guard 重构
  resolvePublicAddresses 单源; ssrf-dispatcher 以 undici Agent.connect.lookup
  钉死校验 IP, TLS SNI 保持原域名, 一次性 dispatcher 用后即毁; 代理激活显式
  退化为仅入口校验); web_fetch 重写手动逐跳重定向循环(每跳先校验后连接,
  替代 redirect:follow 内核跟跳的中间跳裸奔, 上限 5 跳); http_request 换用
  pinned fetch; web_search 可达性预检加固(私有 URL 零请求 + 不跟跳, 3xx 视为
  可达); Agent 浏览器 CORS 通配收紧为 Origin 回显 + Vary: Origin;
  ConfirmationHook.forgetSession 会话终态清理(会话删除/abort 联动/SubAgent
  终结三处接线, 根治 rememberedDecisions 泄漏)

P3 架构还债: agent.enableReflection 死配置全链路接线(main→shared→引擎→
  Orchestrator→设置开关, REFLECTING 状态真实可达); AgentLoopConfig.timeoutMs
  死字段删除; MemoryManager.cleanupExpired 挂入健康检查周期(expires_at 回收
  管道真实化); buildSafeEnv 收敛 utils/safe-env.ts 单源(run_command 与 MCP
  stdio 共用, 终结双实现漂移); Trace 生命周期治理(metadata 只保留最近 20 个
  run — keepRecentRuns 纯函数; JSONL 录制启动自动清理保留 200 个 + 设置页
  手动清理); SLO/健康快照可视化(app:healthSnapshot IPC + 设置页只读卡片 +
  审计链一键校验)

P4 能力演进: 会话标题 LLM 自动生成(TitleGenerator — 每会话幂等/并发重入复用
  同一 Promise/自定义标题不覆盖/失败静默回退, Sidebar 经 config:changed 实时
  刷新); MCP 自动重连(5s/15s/60s 退避最多 3 次, reconnecting 状态机,
  teardownConnection 内部拆除保留簿记 — 用户断开/开关关闭即时取消, 设置页
  显示第 N/3 次); 死循环检测 ABAB 乒乓模式(最近4轮 A→B→A→B 交替判定, 补齐
  docs 第五章"两状态反复切换"检测契约); i18n 第三阶段(ChatInput/LLMSettings/
  OnboardingWizard/MemoryViewer 主链路文案出层, zh-CN + en-US 双字典补齐)

测试: 737 → 824 用例(+87, 新增 8 个测试文件 + 扩展 3 个)。新覆盖: user-context
  分组/空值收缩/拼接契约、context-builder 字节级稳定性、Anthropic cache_control
  四态、consolidation-policy 九路判定矩阵、ssrf-dispatcher(pinned lookup/重定向
  解析/IP 校验)、forget-session 会话隔离、trace-lifecycle run 淘汰、
  trace-trim 严格小于边界、safe-env 净化矩阵、mcp-reconnect 退避状态机
  (fake timers)、title-generator 并发重入、SQLite 侧 truncate×TRACE 联动
  (Electron ABI)。测试驱动修复: GIT_*/ 注释终止块注释、重连计数被自身重试
  前置断开重置(拆 teardownConnection 保留簿记)、TitleGenerator 幂等占位与
  并发去重的检查顺序竞态(去重先于幂等)

版本: 0.7.3; README 同步(配置表新增 agent.enableReflection/memory.*/mcp.autoReconnect)

回归: typecheck 双端 0 错误; ESLint 0/0; 系统 Node 771 通过 53 跳过
  (better-sqlite3 ABI); Electron ABI 全量 824/824 零跳过
This commit is contained in:
2026-08-30 09:44:43 +08:00
parent 26169b7be4
commit ebe45482b0
68 changed files with 4568 additions and 664 deletions
+283
View File
@@ -103,6 +103,144 @@ registerTranslations('zh-CN', {
'monitor.sub.running': '运行中',
'monitor.sub.completed': '已完成',
'monitor.sub.error': '失败',
// ===== 输入框(ChatInput)—— v0.7.3 P4-3 =====
'input.placeholder.ready': '输入消息... (Cmd/Ctrl+Enter 发送, Cmd/Ctrl+Shift+Enter 换行, / 命令)',
'input.placeholder.toolsLoading': '工具加载中...',
'input.placeholder.configLoading': '正在加载配置...',
'input.send': '发送',
'input.abort': '中断',
'input.attach.image': '附加文件(图片/文本/代码)',
'input.attach.textOnly.model': '附加文件(文本/代码)— 当前模型不支持图片',
'input.attach.textOnly.toggle': '附加文件(文本/代码)— 多模态未开启(设置 → LLM 配置)',
'input.image.skipped.model':
'当前模型不支持图片({{provider}} 需多模态模型),已跳过 {{count}} 个图片文件',
'input.image.skipped.toggle': '多模态未开启(设置 → LLM 配置),已跳过 {{count}} 个图片文件',
'input.image.skipped.probe': '当前模型不支持图片(能力探测),已跳过 {{count}} 个图片文件',
'input.file.readFailed': '文件读取失败,请检查文件是否损坏或被锁定',
'input.file.partialFailed': '{{count}} 个文件读取失败,已跳过',
'input.text.truncated': '文本附件 "{{name}}" 超过 512KB,已截断为前 512KB',
'input.text.truncatedNote':
'\n\n[... 文件内容已截断:原始大小 {{size}},仅包含前 {{limit}}。如需完整内容请分段处理 ...]',
'input.toast.toolsLoading': '工具正在加载中,请稍候...',
'input.slash.tool': '选择工具',
'input.slash.memory': '搜索记忆',
'input.slash.clear': '清空会话',
'input.slash.export': '导出 Markdown',
// ===== LLM 配置(LLMSettings)—— v0.7.3 P4-3 =====
'llm.title': 'LLM 配置',
'llm.loading': '加载中...',
'llm.provider.label': 'Provider',
'llm.baseURL': 'API Base URL',
'llm.baseURL.error': '需以 http:// 或 https:// 开头',
'llm.model.label': '模型名称',
'llm.model.placeholder': '如 deepseek-v4-pro、gpt-4o、claude-sonnet-4-5',
'llm.model.space': '模型名称不能包含空格',
'llm.models.load': '获取模型列表',
'llm.models.hint': '基于「已保存」的配置查询(修改后请先保存)',
'llm.models.hint.unloaded': '可手动输入,或点击下方按钮获取模型列表',
'llm.models.failed': '获取模型列表失败',
'llm.multimodal.label': '启用多模态(图片输入)',
'llm.multimodal.helper':
'开启后可在输入框上传图片;DeepSeek 需 vision 系列模型。历史会话图片会随上下文回传(最近 10 张)',
'llm.apiKey.label': 'API Key',
'llm.apiKey.required': '必填,未填 {{provider}} 的 API Key 会 401',
'llm.numCtx.label': '上下文长度 (num_ctx)',
'llm.numCtx.placeholder': '默认由模型决定(如 2048、4096、128000',
'llm.numCtx.error': '最小值为 512',
'llm.ctxWindow.label': '上下文窗口 (contextWindow)',
'llm.ctxWindow.placeholder': '如 64000、128000、1000000',
'llm.ctxWindow.minError': '最小值为 4096',
'llm.ctxWindow.helper': '用于上下文压缩判断,不传给 API',
'llm.ctxWindow.mimoHelper': '默认 10000001M),用于上下文压缩判断',
'llm.ctxWindow.oaHelper': 'gpt-4o 默认 128Kgpt-4.1 默认 1M',
'llm.ctxWindow.anthropicHelper': 'Claude 默认 200K',
'llm.pull.title': '下载 Ollama 模型',
'llm.pull.placeholder': '模型名,如 qwen3:8b',
'llm.pull.download': '下载',
'llm.pull.cancel': '取消',
'llm.pull.helper':
'从 Ollama 服务拉取模型(大模型下载耗时取决于网络,可随时取消);完成后获取模型列表即可见',
'llm.pull.done': '模型 {{name}} 下载完成',
'llm.pull.failed': '模型下载失败:{{message}}',
'llm.balance.label': '账户余额',
'llm.balance.querying': '查询中...',
'llm.balance.unqueried': '未查询(需已保存 API Key',
'llm.balance.granted': '(赠送 {{granted}} + 充值 {{topped}}',
'llm.balance.queryFailed': '查询失败',
'llm.fallback.title': '故障转移(可选)',
'llm.fallback.helper':
'主 Provider 请求失败(重试耗尽或密钥失效)时自动切换到备用 Provider 重发。留空禁用。',
'llm.fallback.provider': '备用 Provider',
'llm.fallback.disabled': '禁用',
'llm.fallback.baseURL': '备用 Base URL',
'llm.fallback.model': '备用模型名称',
'llm.fallback.apiKey': '备用 API Key',
'llm.save': '保存配置',
'llm.saving': '保存中...',
'llm.save.blocked': '请修正表单错误后再保存',
'llm.save.blockedToast': '请修正表单中的错误后再保存',
'llm.save.success': '配置已保存',
'llm.save.failed': '保存失败:{{message}}',
'llm.api.unavailable': '配置 API 不可用',
'llm.save.failed.generic': '配置保存失败',
// ===== 记忆库(MemoryViewer)—— v0.7.3 P4-3 =====
'memory.search.title': '记忆搜索',
'memory.list.title': '记忆库',
'memory.results': '{{count}} 条结果',
'memory.total': '{{count}} 条',
'memory.search.placeholder': '搜索记忆...',
'memory.search.clear': '清除',
'memory.searching': '搜索中...',
'memory.search.empty': '无匹配结果',
'memory.empty': '暂无记忆数据',
'memory.loadFailed': '加载记忆失败',
'memory.deleteFailed': '删除失败',
'memory.type.episodic': '情景记忆',
'memory.type.semantic': '语义记忆',
'memory.type.working': '工作记忆',
// ===== 引导向导(OnboardingWizard)—— v0.7.3 P4-3 =====
'onboarding.step.welcome': '欢迎',
'onboarding.step.llm': '配置 LLM',
'onboarding.step.agent': '自定义 Agent',
'onboarding.step.workspace': '工作空间',
'onboarding.step.done': '开始使用',
'onboarding.prev': '上一步',
'onboarding.next': '下一步',
'onboarding.finish': '开始使用',
'onboarding.welcome.title': '欢迎使用 MetonaAI Desktop',
'onboarding.welcome.body':
'生产级通用 AI Agent 智能体桌面应用,支持多轮对话、工具调用、记忆系统和 MCP 协议集成。',
'onboarding.welcome.hint': '让我们花 1 分钟完成初始配置。',
'onboarding.llm.title': '配置 LLM Provider',
'onboarding.llm.body': '选择 Provider 并填写 API 信息。Base URL 和模型名称支持任意输入。',
'onboarding.llm.multimodal.label': '启用多模态(图片输入)',
'onboarding.llm.multimodal.helper': '开启后可在输入框上传图片;DeepSeek 需 vision 系列模型',
'onboarding.llm.apiKey.placeholder': 'sk-...(本地模型可留空)',
'onboarding.llm.ctx.ollama': '上下文长度 (num_ctx)',
'onboarding.llm.ctx.window': '上下文窗口 (contextWindow)',
'onboarding.llm.ctx.ollamaPlaceholder': '默认由模型决定(如 2048、4096、128000',
'onboarding.llm.ctx.placeholder': '如 64000、128000、1000000',
'onboarding.llm.ctx.helper': '用于上下文压缩判断,不传给 API',
'onboarding.llm.ctx.minError': '最小值为 {{min}}',
'onboarding.agent.title': '自定义 Agent',
'onboarding.agent.body': '编辑工作空间中的 SOUL.md 文件来定义 Agent 的身份和性格。',
'onboarding.agent.soulLabel': 'SOUL.md — 定义 Agent 的身份、性格、核心价值观',
'onboarding.agent.soulHint': '此步骤可稍后在工作空间目录中完成。',
'onboarding.workspace.title': '工作空间',
'onboarding.workspace.body': '选择工作空间目录,或使用默认路径。',
'onboarding.workspace.pick': '选择文件夹',
'onboarding.workspace.placeholder': '~/MetonaWorkspaces/default/',
'onboarding.workspace.helper': '包含 SOUL.md、MEMORY.md 两个必需文件,首次打开时自动创建。',
'onboarding.done.title': '配置完成!',
'onboarding.done.body': 'MetonaAI Desktop 已准备就绪。开始与你的 AI Agent 对话吧!',
'onboarding.done.hint': '按 Ctrl+Enter 发送消息,输入 / 查看命令列表',
'onboarding.toast.saveFailed': '配置保存失败,请重试',
'onboarding.toast.saveFailedWithReason': '保存配置失败:{{message}}',
'onboarding.toast.folderFailed': '选择文件夹失败',
});
registerTranslations('en-US', {
@@ -195,4 +333,149 @@ registerTranslations('en-US', {
'monitor.sub.running': 'Running',
'monitor.sub.completed': 'Completed',
'monitor.sub.error': 'Failed',
// ===== 输入框(ChatInput=====
'input.placeholder.ready':
'Type a message... (Cmd/Ctrl+Enter to send, Cmd/Ctrl+Shift+Enter for newline, / for commands)',
'input.placeholder.toolsLoading': 'Loading tools...',
'input.placeholder.configLoading': 'Loading configuration...',
'input.send': 'Send',
'input.abort': 'Stop',
'input.attach.image': 'Attach files (images/text/code)',
'input.attach.textOnly.model': 'Attach files (text/code) — current model has no image support',
'input.attach.textOnly.toggle': 'Attach files (text/code) — multimodal disabled (Settings → LLM)',
'input.image.skipped.model':
'Current model has no image support ({{provider}} requires a multimodal model); {{count}} image file(s) skipped',
'input.image.skipped.toggle':
'Multimodal is disabled (Settings → LLM); {{count}} image file(s) skipped',
'input.image.skipped.probe':
'Current model has no image support (capability probe); {{count}} image file(s) skipped',
'input.file.readFailed': 'Failed to read files — check whether they are corrupted or locked',
'input.file.partialFailed': '{{count}} file(s) failed to read and were skipped',
'input.text.truncated':
'Text attachment "{{name}}" exceeds 512KB and was truncated to the first 512KB',
'input.text.truncatedNote':
'\n\n[... File content truncated: original size {{size}}, only the first {{limit}} included. Process in segments if you need the full content ...]',
'input.toast.toolsLoading': 'Tools are still loading, please wait...',
'input.slash.tool': 'Pick a tool',
'input.slash.memory': 'Search memory',
'input.slash.clear': 'Clear session',
'input.slash.export': 'Export Markdown',
// ===== LLM 配置(LLMSettings=====
'llm.title': 'LLM Configuration',
'llm.loading': 'Loading...',
'llm.provider.label': 'Provider',
'llm.baseURL': 'API Base URL',
'llm.baseURL.error': 'Must start with http:// or https://',
'llm.model.label': 'Model Name',
'llm.model.placeholder': 'e.g. deepseek-v4-pro, gpt-4o, claude-sonnet-4-5',
'llm.model.space': 'Model name must not contain spaces',
'llm.models.load': 'Fetch model list',
'llm.models.hint': 'Queries the saved configuration (save changes first)',
'llm.models.hint.unloaded': 'Type manually, or fetch the model list below',
'llm.models.failed': 'Failed to fetch model list',
'llm.multimodal.label': 'Enable multimodal (image input)',
'llm.multimodal.helper':
'Allows uploading images; DeepSeek requires a vision model. Images from history are replayed (last 10)',
'llm.apiKey.label': 'API Key',
'llm.apiKey.required': 'Required — missing {{provider}} API Key will result in 401',
'llm.numCtx.label': 'Context length (num_ctx)',
'llm.numCtx.placeholder': 'Default decided by the model (e.g. 2048, 4096, 128000)',
'llm.numCtx.error': 'Minimum is 512',
'llm.ctxWindow.label': 'Context window (contextWindow)',
'llm.ctxWindow.placeholder': 'e.g. 64000, 128000, 1000000',
'llm.ctxWindow.minError': 'Minimum is 4096',
'llm.ctxWindow.helper': 'Used for context-compression decisions; not sent to the API',
'llm.ctxWindow.mimoHelper': 'Default 1000000 (1M), used for compression decisions',
'llm.ctxWindow.oaHelper': 'gpt-4o defaults to 128K, gpt-4.1 to 1M',
'llm.ctxWindow.anthropicHelper': 'Claude defaults to 200K',
'llm.pull.title': 'Download Ollama model',
'llm.pull.placeholder': 'Model name, e.g. qwen3:8b',
'llm.pull.download': 'Download',
'llm.pull.cancel': 'Cancel',
'llm.pull.helper':
'Pull a model from the Ollama server (large downloads depend on network; cancellable). Fetch the list afterwards',
'llm.pull.done': 'Model {{name}} downloaded',
'llm.pull.failed': 'Model download failed: {{message}}',
'llm.balance.label': 'Account Balance',
'llm.balance.querying': 'Querying...',
'llm.balance.unqueried': 'Not queried (requires a saved API Key)',
'llm.balance.granted': '(granted {{granted}} + topped up {{topped}})',
'llm.balance.queryFailed': 'Query failed',
'llm.fallback.title': 'Failover (optional)',
'llm.fallback.helper':
'Switch to the fallback provider when the primary fails (retries exhausted or invalid key). Leave empty to disable.',
'llm.fallback.provider': 'Fallback Provider',
'llm.fallback.disabled': 'Disabled',
'llm.fallback.baseURL': 'Fallback Base URL',
'llm.fallback.model': 'Fallback Model Name',
'llm.fallback.apiKey': 'Fallback API Key',
'llm.save': 'Save Configuration',
'llm.saving': 'Saving...',
'llm.save.blocked': 'Fix form errors before saving',
'llm.save.blockedToast': 'Please fix the form errors before saving',
'llm.save.success': 'Configuration saved',
'llm.save.failed': 'Save failed: {{message}}',
'llm.api.unavailable': 'Configuration API unavailable',
'llm.save.failed.generic': 'Failed to save configuration',
// ===== 记忆库(MemoryViewer=====
'memory.search.title': 'Memory Search',
'memory.list.title': 'Memory Store',
'memory.results': '{{count}} results',
'memory.total': '{{count}} items',
'memory.search.placeholder': 'Search memories...',
'memory.search.clear': 'Clear',
'memory.searching': 'Searching...',
'memory.search.empty': 'No matches',
'memory.empty': 'No memories yet',
'memory.loadFailed': 'Failed to load memories',
'memory.deleteFailed': 'Delete failed',
'memory.type.episodic': 'Episodic',
'memory.type.semantic': 'Semantic',
'memory.type.working': 'Working',
// ===== 引导向导(OnboardingWizard=====
'onboarding.step.welcome': 'Welcome',
'onboarding.step.llm': 'Configure LLM',
'onboarding.step.agent': 'Customize Agent',
'onboarding.step.workspace': 'Workspace',
'onboarding.step.done': 'Get Started',
'onboarding.prev': 'Back',
'onboarding.next': 'Next',
'onboarding.finish': 'Get Started',
'onboarding.welcome.title': 'Welcome to MetonaAI Desktop',
'onboarding.welcome.body':
'A production-grade AI Agent desktop app with multi-turn chat, tool calling, memory, and MCP integration.',
'onboarding.welcome.hint': "Let's spend 1 minute on initial setup.",
'onboarding.llm.title': 'Configure LLM Provider',
'onboarding.llm.body':
'Pick a provider and fill in API details. Base URL and model accept any input.',
'onboarding.llm.multimodal.label': 'Enable multimodal (image input)',
'onboarding.llm.multimodal.helper': 'Allows uploading images; DeepSeek requires a vision model',
'onboarding.llm.apiKey.placeholder': 'sk-... (leave empty for local models)',
'onboarding.llm.ctx.ollama': 'Context length (num_ctx)',
'onboarding.llm.ctx.window': 'Context window (contextWindow)',
'onboarding.llm.ctx.ollamaPlaceholder': 'Default decided by the model (e.g. 2048, 4096, 128000)',
'onboarding.llm.ctx.placeholder': 'e.g. 64000, 128000, 1000000',
'onboarding.llm.ctx.helper': 'Used for context-compression decisions; not sent to the API',
'onboarding.llm.ctx.minError': 'Minimum is {{min}}',
'onboarding.agent.title': 'Customize Agent',
'onboarding.agent.body':
'Edit SOUL.md in the workspace to define the agent identity and personality.',
'onboarding.agent.soulLabel': 'SOUL.md — define the agent identity, personality, and core values',
'onboarding.agent.soulHint': 'You can do this later in the workspace directory.',
'onboarding.workspace.title': 'Workspace',
'onboarding.workspace.body': 'Choose a workspace directory, or keep the default.',
'onboarding.workspace.pick': 'Choose Folder',
'onboarding.workspace.placeholder': '~/MetonaWorkspaces/default/',
'onboarding.workspace.helper':
'Contains the two required files SOUL.md and MEMORY.md, auto-created on first open.',
'onboarding.done.title': 'All set!',
'onboarding.done.body': 'MetonaAI Desktop is ready. Start chatting with your AI agent!',
'onboarding.done.hint': 'Press Ctrl+Enter to send; type / to see commands',
'onboarding.toast.saveFailed': 'Failed to save configuration, please retry',
'onboarding.toast.saveFailedWithReason': 'Failed to save configuration: {{message}}',
'onboarding.toast.folderFailed': 'Failed to choose folder',
});