Files
metona-ai-desktop/docs/MetonaAI-Desktop 内部API请求与响应标准.html
T
thzxx 1d185db6b3 feat: MetonaAI Desktop 初始项目
- Electron + React + TypeScript 架构
- 三栏布局: Sidebar | ChatPanel | DetailPanel
- 9 个内置工具 (文件系统/网络/记忆/命令)
- SQLite 持久化 (better-sqlite3)
- MUI 暗色/亮色主题系统
- Agent Loop ReAct 状态机引擎
- DeepSeek / Agnes AI / Ollama Provider 适配器
- MCP 协议集成
- 系统托盘 + 全局快捷键
- Tailwind CSS v4 + Tailwind Merge
- 修复: Sidebar 缺失 TextField 导入导致黑屏
2026-06-27 21:33:27 +08:00

1262 lines
75 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Metona 内部 API 请求与响应标准 | v1.0</title>
<style>
:root {
--bg: #0f1117;
--bg-card: #1a1d27;
--bg-code: #12141c;
--bg-nav: #141620;
--text: #e1e4ed;
--text-dim: #8b8fa7;
--accent: #f59e0b;
--accent2: #fbbf24;
--green: #34d399;
--orange: #fb923c;
--red: #f87171;
--cyan: #22d3ee;
--border: #2a2d3a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
display: flex;
scroll-behavior: smooth;
}
.sidebar {
position: fixed;
top: 0; left: 0;
width: 290px;
height: 100vh;
background: var(--bg-nav);
border-right: 1px solid var(--border);
overflow-y: auto;
z-index: 100;
padding: 24px 0;
}
.sidebar-logo {
padding: 0 20px 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 16px;
}
.sidebar-logo h2 {
font-size: 20px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-logo .badge {
font-size: 11px;
background: var(--accent);
color: #000;
padding: 2px 8px;
border-radius: 10px;
-webkit-text-fill-color: #000;
font-weight: 700;
}
.sidebar-section {
padding: 8px 20px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-dim);
font-weight: 600;
}
.sidebar a {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 20px;
color: var(--text-dim);
text-decoration: none;
font-size: 13px;
transition: all 0.15s;
}
.sidebar a:hover, .sidebar a.active {
color: var(--text);
background: rgba(245, 158, 11, 0.06);
}
.sidebar a.active {
border-right: 2px solid var(--accent);
}
.main {
margin-left: 290px;
flex: 1;
min-height: 100vh;
}
.hero {
background: linear-gradient(135deg, rgba(245,158,11,0.08), rgba(251,191,36,0.06));
border-bottom: 1px solid var(--border);
padding: 60px 60px 50px;
}
.hero h1 {
font-size: 32px;
font-weight: 800;
background: linear-gradient(135deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 12px;
}
.hero p {
color: var(--text-dim);
font-size: 15px;
max-width: 720px;
}
.hero p a { color: var(--accent); text-decoration: none; }
.hero-meta {
display: flex; gap: 24px; margin-top: 20px; flex-wrap: wrap;
}
.hero-meta span {
font-size: 13px; color: var(--text-dim);
display: flex; align-items: center; gap: 6px;
}
.hero-meta .dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.dot-green { background: var(--green); }
.dot-accent { background: var(--accent); }
.dot-orange { background: var(--orange); }
.content { padding: 40px 60px 100px; max-width: 1050px; }
.api-section { margin-bottom: 60px; scroll-margin-top: 20px; }
.api-section h2 {
font-size: 22px; font-weight: 700; margin-bottom: 8px;
display: flex; align-items: center; gap: 10px;
}
.api-section h3 {
font-size: 17px; font-weight: 600; margin: 32px 0 12px;
color: var(--accent);
}
.api-section .desc {
color: var(--text-dim); font-size: 14.5px; margin-bottom: 20px;
}
.api-section .desc code {
color: var(--cyan); background: var(--bg-code);
padding: 2px 6px; border-radius: 4px; font-size: 13px;
}
/* TABLES */
table.spec {
width: 100%; border-collapse: collapse;
margin-bottom: 24px; font-size: 13.5px;
}
table.spec th {
text-align: left; padding: 10px 14px;
background: var(--bg-card); border-bottom: 1px solid var(--border);
color: var(--text-dim); font-weight: 600;
font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
}
table.spec td {
padding: 10px 14px; border-bottom: 1px solid rgba(42,45,58,0.5);
vertical-align: top;
transition: background 0.15s;
}
table.spec tr:hover td { background: rgba(245,158,11,0.03); }
table.spec tr:last-child td { border-bottom: none; }
.field-name {
font-family: 'SF Mono', 'Fira Code', monospace;
color: var(--cyan); font-weight: 600; font-size: 13px;
}
.field-type {
font-family: 'SF Mono', 'Fira Code', monospace;
color: var(--accent2); font-size: 11.5px;
}
.field-req { color: var(--red); font-size: 11px; font-weight: 600; }
.field-opt { color: var(--text-dim); font-size: 11px; }
.field-note { color: var(--text-dim); font-size: 11.5px; }
.field-num { color: var(--accent); font-size: 11px; font-weight: 600; }
/* CODE BLOCKS */
.code-block {
background: var(--bg-code); border: 1px solid var(--border);
border-radius: 10px; padding: 20px; overflow-x: auto;
margin-bottom: 24px;
transition: border-color 0.2s;
}
.code-block:hover {
border-color: rgba(245,158,11,0.3);
}
.code-block pre {
margin: 0; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 13px; line-height: 1.6; color: var(--text);
}
.hl-kw { color: var(--accent2); }
.hl-str { color: var(--green); }
.hl-num { color: var(--orange); }
.hl-cm { color: var(--text-dim); font-style: italic; }
.hl-fn { color: var(--accent); }
.hl-prop { color: var(--cyan); }
.hl-type { color: #fbbf24; }
.hl-null { color: var(--text-dim); }
.section-divider { border: none; height: 1px; background: var(--border); margin: 56px 0; }
.note-box {
background: rgba(245,158,11,0.05); border-left: 3px solid var(--accent);
border-radius: 0 8px 8px 0; padding: 14px 18px; margin-bottom: 24px;
font-size: 13.5px; color: var(--text-dim);
transition: border-color 0.2s;
}
.note-box:hover {
border-left-color: var(--accent2);
}
.note-box strong { color: var(--accent); }
.note-box code {
color: var(--cyan); background: var(--bg-code);
padding: 1px 5px; border-radius: 3px; font-size: 12.5px;
}
/* ARCHITECTURE DIAGRAM */
.arch-diagram {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 10px; padding: 28px;
margin: 20px 0 30px; overflow-x: auto;
}
.arch-diagram pre {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 12.5px; line-height: 1.55;
color: var(--text); margin: 0;
}
.arch-diagram .hl-box { color: var(--accent); }
.arch-diagram .hl-arrow { color: var(--text-dim); }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
@media (max-width: 900px) {
.sidebar { display: none; }
.main { margin-left: 0; }
.hero { padding: 30px 24px; }
.content { padding: 24px; }
}
</style>
</head>
<body>
<nav class="sidebar">
<div class="sidebar-logo">
<h2>&#x2699;&#xFE0F; Metona IR <span class="badge">v1.0</span></h2>
</div>
<div class="sidebar-section">设计理念</div>
<a href="#philosophy">&#x1F3AF; 核心思想</a>
<a href="#architecture">&#x1F3D7;&#xFE0F; 架构概览</a>
<div class="sidebar-section">请求标准</div>
<a href="#request">&#x1F4E4; MetonaRequest</a>
<a href="#request-messages">&#x1F4AC; 消息格式</a>
<a href="#request-tools">&#x1F527; 工具定义</a>
<div class="sidebar-section">响应标准</div>
<a href="#response">&#x1F4E5; MetonaResponse</a>
<a href="#response-stream">&#x26A1; 流式响应</a>
<a href="#response-thinking">&#x1F9E0; 思考内容</a>
<a href="#response-toolcall">&#x1F50C; 工具调用</a>
<a href="#response-error">&#x274C; 错误格式</a>
<div class="sidebar-section">上下文 &amp; 记忆</div>
<a href="#context">&#x1F4CB; 上下文标准</a>
<a href="#memory">&#x1F9E9; 记忆格式</a>
<div class="sidebar-section">工程规范</div>
<a href="#adapter">&#x1F517; Provider Adapter</a>
<a href="#examples">&#x1F4D6; 完整示例</a>
<a href="#migration">&#x1F504; 迁移指南</a>
</nav>
<div class="main">
<div class="hero">
<h1>Metona 内部 API 请求与响应标准</h1>
<p>Metona Internal Representation (IR) —— 项目内所有 AI 交互的统一数据格式。无论底层对接 DeepSeek、Ollama、Agnes 还是其他 LLMAgent Loop、UI、记忆系统均读写此标准格式。Provider Adapter 负责外部 API 与此 IR 之间的双向转换。</p>
<div class="hero-meta">
<span><span class="dot dot-accent"></span> 版本: <strong>v1.0.0</strong></span>
<span><span class="dot dot-green"></span> 适用范围: <strong>Electron 主进程 / IPC / 渲染进程</strong></span>
<span><span class="dot dot-orange"></span> 更新日期: <strong>2026-06-26</strong></span>
</div>
<div class="note-box" style="margin-top:16px">
<strong>&#x1F4CB; 文档层级:</strong>本文档是 <strong>类型系统与数据格式的权威定义</strong>,与《构建指南》第四章(ReAct)、第五章(Harness)对应。冲突时以本文档为准。
</div>
</div>
<div class="content">
<!-- ===== 核心思想 ===== -->
<section class="api-section" id="philosophy">
<h2>&#x1F3AF; 核心思想</h2>
<p class="desc">
Metona 面向多 LLM Provider,每个外部 API 的请求/响应格式各不相同(OpenAI 格式、Anthropic 格式、Ollama 原生格式等)。
如果项目各处代码直接依赖外部格式,切换 Provider 或新增模型将导致大规模改动。
</p>
<p class="desc">
<strong>Metona IR</strong> 在项目内部建立一道抽象边界:
</p>
<div class="arch-diagram">
<pre>
<span class="hl-box">┌──────────────────────────┐</span>
<span class="hl-box">│ Agent Loop / IPC / UI │</span> ← 只读写 Metona IR
<span class="hl-box">└────────────┬─────────────┘</span>
<span class="hl-box">┌────────────▼─────────────┐</span>
<span class="hl-box">│ Metona IR Standard │</span> ← 项目内唯一标准
<span class="hl-box">└────────────┬─────────────┘</span>
┌────────┼────────┬────────┐
│ │ │ │
<span class="hl-box">┌───▼──┐ ┌──▼───┐ ┌─▼───┐ ┌─▼─────┐</span>
<span class="hl-box">│DeepSeek│ │Agnes│ │Ollama│ │Anthropic│</span> ← Adapter 层
<span class="hl-box">└───────┘ └─────┘ └──────┘ └────────┘</span>
</pre>
</div>
<p class="desc">
<strong>铁律</strong><code>electron/harness/</code> 下的所有代码、<code>src/</code> 下的所有 UI 代码、IPC 通道传输的数据,<strong>只能使用 Metona IR 定义的类型</strong>
任何外部 API 的原始类型不得穿透到这些层。
</p>
</section>
<hr class="section-divider">
<!-- ===== 架构概览 ===== -->
<section class="api-section" id="architecture">
<h2>&#x1F3D7;&#xFE0F; 架构概览</h2>
<p class="desc">一次完整的用户请求经过以下数据流转:</p>
<div class="arch-diagram">
<pre>
<span class="hl-box">1. UI (Renderer)</span>
│ 构造 MetonaRequest,通过 IPC 发送到主进程
<span class="hl-box">2. IPC Bridge</span>
│ 传输 MetonaRequest JSON
<span class="hl-box">3. Context Builder</span>
│ 注入 System Prompt、会话历史、检索记忆、可用工具列表
│ 输出 MetonaContext
<span class="hl-box">4. Provider Adapter</span>
│ 将 MetonaContext 转换为目标 Provider 的原生请求格式
│ 调用外部 API
│ 将原生响应转换为 MetonaResponse / MetonaStreamEvent
<span class="hl-box">5. Agent Loop Engine</span>
│ 按 ReAct 状态机解析 MetonaResponse
│ 提取 Thought → 执行 ToolCall → 收集 Observation
│ 构建下一轮的 MetonaRequest
<span class="hl-box">6. IPC → UI</span>
│ 将 MetonaStreamEvent / MetonaResponse 推送回渲染进程
</pre>
</div>
</section>
<hr class="section-divider">
<!-- ===== 请求标准 ===== -->
<section class="api-section" id="request">
<h2>&#x1F4E4; 请求标准:MetonaRequest</h2>
<p class="desc">Agent Loop 向 Provider Adapter 发出的统一请求。每次 ReAct 迭代构造一个新的 MetonaRequest。</p>
<h3>完整类型定义</h3>
<div class="code-block">
<pre><span class="hl-cm">// ====== electron/harness/types/metona-request.ts ======</span>
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaRequest</span> {
<span class="hl-cm">/** 请求元信息 */</span>
<span class="hl-prop">meta</span>: <span class="hl-type">MetonaRequestMeta</span>;
<span class="hl-cm">/** System Prompt(行为宪法) */</span>
<span class="hl-prop">systemPrompt</span>: <span class="hl-type">MetonaSystemPrompt</span>;
<span class="hl-cm">/** 消息列表(含历史 + 当前用户输入 + 工具结果) */</span>
<span class="hl-prop">messages</span>: <span class="hl-type">MetonaMessage</span>[];
<span class="hl-cm">/** 本轮可用的工具定义列表 */</span>
<span class="hl-prop">tools</span>?: <span class="hl-type">MetonaToolDef</span>[];
<span class="hl-cm">/** 生成参数 */</span>
<span class="hl-prop">params</span>: <span class="hl-type">MetonaGenerationParams</span>;
<span class="hl-cm">/** 安全约束 */</span>
<span class="hl-prop">constraints</span>?: <span class="hl-type">MetonaConstraints</span>;
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaRequestMeta</span> {
<span class="hl-prop">sessionId</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 会话 ID</span>
<span class="hl-prop">iteration</span>: <span class="hl-type">number</span>; <span class="hl-cm">// 当前 ReAct 迭代轮次(从 1 开始)</span>
<span class="hl-prop">requestId</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 本次请求的唯一 ID</span>
<span class="hl-prop">timestamp</span>: <span class="hl-type">number</span>; <span class="hl-cm">// Unix 毫秒时间戳</span>
<span class="hl-prop">agentVersion</span>: <span class="hl-type">string</span>; <span class="hl-cm">// Agent 引擎版本</span>
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaSystemPrompt</span> {
<span class="hl-cm">/** 角色定义(静态区,利用 LLM 缓存) */</span>
<span class="hl-prop">roleDefinition</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** 输出格式约束 */</span>
<span class="hl-prop">outputConstraints</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** 安全准则 */</span>
<span class="hl-prop">safetyGuidelines</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** 动态注入的尾部提醒 */</span>
<span class="hl-prop">dynamicReminders</span>?: <span class="hl-type">string</span>;
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaGenerationParams</span> {
<span class="hl-prop">maxTokens</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 最大生成 token 数</span>
<span class="hl-prop">temperature</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 温度(默认 0.0,Agent 需要确定性)</span>
<span class="hl-prop">topP</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 核采样</span>
<span class="hl-prop">stream</span>?: <span class="hl-type">boolean</span>; <span class="hl-cm">// 是否流式输出</span>
<span class="hl-prop">stopSequences</span>?: <span class="hl-type">string</span>[]; <span class="hl-cm">// 停止序列</span>
<span class="hl-prop">thinkingEnabled</span>?: <span class="hl-type">boolean</span>; <span class="hl-cm">// 是否启用思考模式</span>
<span class="hl-prop">thinkingEffort</span>?: <span class="hl-str">'low'</span> | <span class="hl-str">'medium'</span> | <span class="hl-str">'high'</span> | <span class="hl-str">'max'</span>; <span class="hl-cm">// 思考强度(替代 thinkingBudget,各 Provider 映射见 Adapter 规范)</span>
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaConstraints</span> {
<span class="hl-prop">allowedTools</span>?: <span class="hl-type">string</span>[]; <span class="hl-cm">// 本迭代允许使用的工具白名单</span>
<span class="hl-prop">maxToolCalls</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 单轮最大工具调用数</span>
<span class="hl-prop">timeoutMs</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 本请求整体超时</span>
}</pre>
</div>
<h3>字段说明</h3>
<table class="spec">
<tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td class="field-name">meta</td><td class="field-type">MetonaRequestMeta</td><td class="field-req">必填</td><td>请求元信息,含 sessionId、iteration、requestId、timestamp</td></tr>
<tr><td class="field-name">systemPrompt</td><td class="field-type">MetonaSystemPrompt</td><td class="field-req">必填</td><td>分区化的 System PromptAdaper 负责拼接为 Provider 格式</td></tr>
<tr><td class="field-name">messages</td><td class="field-type">MetonaMessage[]</td><td class="field-req">必填</td><td>统一消息列表,见下方消息格式</td></tr>
<tr><td class="field-name">tools</td><td class="field-type">MetonaToolDef[]</td><td class="field-opt">可选</td><td>本轮可用工具列表</td></tr>
<tr><td class="field-name">params</td><td class="field-type">MetonaGenerationParams</td><td class="field-req">必填</td><td>生成参数</td></tr>
<tr><td class="field-name">constraints</td><td class="field-type">MetonaConstraints</td><td class="field-opt">可选</td><td>安全约束和限流参数</td></tr>
</table>
</section>
<!-- ===== 消息格式 ===== -->
<section class="api-section" id="request-messages">
<h2>&#x1F4AC; 消息格式:MetonaMessage</h2>
<p class="desc">Metona 统一消息结构。所有角色(system / user / assistant / tool)共用同一结构,通过 <code>role</code> 区分。</p>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">MetonaMessage</span> {
<span class="hl-prop">role</span>: <span class="hl-str">'system'</span> | <span class="hl-str">'user'</span> | <span class="hl-str">'assistant'</span> | <span class="hl-str">'tool'</span>;
<span class="hl-cm">/** 文本内容(纯文本或 Markdown */</span>
<span class="hl-prop">content</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** (仅 assistant)思考/推理内容 */</span>
<span class="hl-prop">reasoningContent</span>?: <span class="hl-type">string</span>;
<span class="hl-cm">/** (仅 assistant)工具调用请求 */</span>
<span class="hl-prop">toolCalls</span>?: <span class="hl-type">MetonaToolCall</span>[];
<span class="hl-cm">/** (仅 tool)工具执行结果 */</span>
<span class="hl-prop">toolResult</span>?: <span class="hl-type">MetonaToolResult</span>;
<span class="hl-cm">/** 时间戳 */</span>
<span class="hl-prop">timestamp</span>: <span class="hl-type">number</span>;
<span class="hl-cm">/** 所属迭代轮次 */</span>
<span class="hl-prop">iteration</span>?: <span class="hl-type">number</span>;
<span class="hl-cm">/** 图片内容(可选,用于多模态) */</span>
<span class="hl-prop">images</span>?: <span class="hl-type">MetonaImageContent</span>[];
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaImageContent</span> {
<span class="hl-prop">url</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 图片公网 URL 或 base64 data URI</span>
<span class="hl-prop">detail</span>?: <span class="hl-str">'low'</span> | <span class="hl-str">'high'</span> | <span class="hl-str">'auto'</span>;
}</pre>
</div>
<div class="note-box">
<strong>&#x1F4A1; 设计要点:</strong>部分 LLM 的 <code>reasoning_content</code> 需要伴随 <code>toolCalls</code> 回传上下文。
MetonaMessage 统一携带 <code>reasoningContent</code>,由 Adapter 决定是否需要回传。
</div>
<div class="note-box">
<strong>&#x1F5BC; 多模态消息转换:</strong><code>MetonaMessage.content</code>string+ <code>images</code>(数组)的分离设计比 OpenAI 的 content 数组更清晰。Adapter 负责转换:
<br><strong>DeepSeek/Agnes (OpenAI 兼容)</strong><code>content</code> 数组 = <code>[{type:"text", text: content}, ...images.map(i =&gt; ({type:"image_url", image_url:{url: i.url}}))]</code>
<br><strong>Ollama</strong><code>content</code> 保持 string<code>images</code> 作为消息的独立字段传入 base64 数组(Adapter 需将 URL 下载为 base64
<br><strong>纯文本消息</strong>(无 images):所有 Provider 的 <code>content</code> 直接传 string
</div>
</section>
<!-- ===== 工具定义 ===== -->
<section class="api-section" id="request-tools">
<h2>&#x1F527; 工具定义:MetonaToolDef</h2>
<p class="desc">统一的工具描述格式。内置工具和 MCP 动态工具都使用此结构。</p>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">MetonaToolDef</span> {
<span class="hl-prop">name</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 工具唯一名称</span>
<span class="hl-prop">description</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 功能描述(供 LLM 阅读)</span>
<span class="hl-prop">parameters</span>: <span class="hl-type">MetonaToolParams</span>; <span class="hl-cm">// 参数 JSON Schema</span>
<span class="hl-prop">category</span>: <span class="hl-type">MetonaToolCategory</span>; <span class="hl-cm">// 分类</span>
<span class="hl-prop">riskLevel</span>: <span class="hl-type">MetonaRiskLevel</span>; <span class="hl-cm">// 风险等级</span>
<span class="hl-prop">requiresPermission</span>: <span class="hl-type">boolean</span>; <span class="hl-cm">// 是否需要用户授权</span>
<span class="hl-prop">timeoutMs</span>: <span class="hl-type">number</span>; <span class="hl-cm">// 超时时间</span>
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaToolParams</span> {
<span class="hl-prop">type</span>: <span class="hl-str">'object'</span>;
<span class="hl-prop">properties</span>: <span class="hl-type">Record</span>&lt;<span class="hl-type">string</span>, <span class="hl-type">MetonaParamField</span>&gt;;
<span class="hl-prop">required</span>?: <span class="hl-type">string</span>[];
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaParamField</span> {
<span class="hl-prop">type</span>: <span class="hl-str">'string'</span> | <span class="hl-str">'number'</span> | <span class="hl-str">'boolean'</span> | <span class="hl-str">'object'</span> | <span class="hl-str">'array'</span>;
<span class="hl-prop">description</span>: <span class="hl-type">string</span>;
<span class="hl-prop">enum</span>?: <span class="hl-type">string</span>[];
<span class="hl-prop">items</span>?: <span class="hl-type">MetonaParamField</span>;
}
<span class="hl-kw">export enum</span> <span class="hl-type">MetonaToolCategory</span> {
<span class="hl-prop">FILESYSTEM</span> = <span class="hl-str">'filesystem'</span>,
<span class="hl-prop">SEARCH</span> = <span class="hl-str">'search'</span>,
<span class="hl-prop">CALCULATION</span> = <span class="hl-str">'calculation'</span>,
<span class="hl-prop">CODE_EXECUTION</span> = <span class="hl-str">'code_execution'</span>,
<span class="hl-prop">NETWORK</span> = <span class="hl-str">'network'</span>,
<span class="hl-prop">DATABASE</span> = <span class="hl-str">'database'</span>,
<span class="hl-prop">MCP</span> = <span class="hl-str">'mcp'</span>,
<span class="hl-prop">CUSTOM</span> = <span class="hl-str">'custom'</span>,
}
<span class="hl-kw">export enum</span> <span class="hl-type">MetonaRiskLevel</span> {
<span class="hl-prop">SAFE</span> = <span class="hl-str">'safe'</span>,
<span class="hl-prop">LOW</span> = <span class="hl-str">'low'</span>,
<span class="hl-prop">MEDIUM</span> = <span class="hl-str">'medium'</span>,
<span class="hl-prop">HIGH</span> = <span class="hl-str">'high'</span>,
<span class="hl-prop">CRITICAL</span> = <span class="hl-str">'critical'</span>,
}</pre>
</div>
<h3>MetonaToolDef 与内部 ToolDefinition 的关系</h3>
<p class="desc">项目内部存在两种工具类型:<code>MetonaToolDef</code>IR 标准类型)和 <code>ToolDefinition</code>(内部实现类型,使用 Zod Schema 进行运行时参数校验)。两者的关系如下:</p>
<table class="spec">
<tr><th>维度</th><th>MetonaToolDefIR 标准)</th><th>ToolDefinition(内部实现)</th></tr>
<tr><td class="field-name">用途</td><td>跨进程传输、LLM 可读描述、IPC 通信</td><td>运行时参数校验、工具注册、安全检查</td></tr>
<tr><td class="field-name">参数格式</td><td>JSON Schema<code>MetonaToolParams</code></td><td>Zod Schema(支持类型推断和运行时校验)</td></tr>
<tr><td class="field-name">定义位置</td><td>本文档(IR 标准)</td><td><code>electron/harness/tools/base-tool.ts</code></td></tr>
</table>
<div class="note-box">
<strong>&#x1F517; 转换规则:</strong><code>BaseTool.getDescriptionForLLM()</code> 负责将 Zod Schema → JSON Schema → <code>MetonaToolDef</code>
<br><code>ToolDefinition.name</code><code>MetonaToolDef.name</code>
<br><code>ToolDefinition.parameters</code>Zod)→ <code>MetonaToolDef.parameters</code>JSON Schema),使用 <code>zod-to-json-schema</code> 库转换
<br><code>ToolDefinition.category</code><code>MetonaToolDef.category</code>(枚举值一致)
<br><code>ToolDefinition.riskLevel</code><code>MetonaToolDef.riskLevel</code>(枚举值一致)
<br>• IPC 通道和 Agent Loop <strong>只使用 MetonaToolDef</strong>,不接触 ToolDefinition/Zod
<br>• 工具注册表(<code>ToolRegistry</code>)内部使用 <code>IBaseTool</code>,对外暴露时转换为 <code>MetonaToolDef</code>
</div>
<div class="note-box">
<strong>&#x26A0;&#xFE0F; 强制规则:</strong>架构文档中 9 个基础工具的参数定义必须以 <code>MetonaToolDef</code> 格式为准(JSON Schema),不再使用自然语言描述。内部实现时使用 Zod Schema 做运行时校验,通过 <code>zod-to-json-schema</code> 转换后对外暴露。
</div>
<hr class="section-divider">
<!-- ===== 响应标准 ===== -->
<section class="api-section" id="response">
<h2>&#x1F4E5; 响应标准:MetonaResponse</h2>
<p class="desc">Provider Adapter 将外部 API 的原始响应转换为 MetonaResponse 后返回给 Agent Loop。</p>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">MetonaResponse</span> {
<span class="hl-cm">/** 响应元信息 */</span>
<span class="hl-prop">meta</span>: <span class="hl-type">MetonaResponseMeta</span>;
<span class="hl-cm">/** 模型输出(完整文本) */</span>
<span class="hl-prop">content</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** 思考/推理内容(Thinking 模式) */</span>
<span class="hl-prop">reasoningContent</span>?: <span class="hl-type">string</span>;
<span class="hl-cm">/** 结构化输出(如果模型原生支持 JSON Schema */</span>
<span class="hl-prop">structuredOutput</span>?: <span class="hl-type">unknown</span>;
<span class="hl-cm">/** 工具调用请求列表 */</span>
<span class="hl-prop">toolCalls</span>?: <span class="hl-type">MetonaToolCall</span>[];
<span class="hl-cm">/** Token 使用统计 */</span>
<span class="hl-prop">usage</span>: <span class="hl-type">MetonaTokenUsage</span>;
<span class="hl-cm">/** 停止原因 */</span>
<span class="hl-prop">finishReason</span>: <span class="hl-type">MetonaFinishReason</span>;
<span class="hl-cm">/** 错误信息(如果出错) */</span>
<span class="hl-prop">error</span>?: <span class="hl-type">MetonaError</span>;
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaResponseMeta</span> {
<span class="hl-prop">requestId</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 对应的请求 ID</span>
<span class="hl-prop">provider</span>: <span class="hl-type">string</span>; <span class="hl-cm">// Provider 标识(如 'deepseek'</span>
<span class="hl-prop">model</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 实际使用的模型名称</span>
<span class="hl-prop">latencyMs</span>: <span class="hl-type">number</span>; <span class="hl-cm">// 端到端延迟</span>
<span class="hl-prop">timestamp</span>: <span class="hl-type">number</span>; <span class="hl-cm">// 响应时间戳</span>
<span class="hl-cm">/** Provider 原生性能统计(可选,主要用于 Ollama) */</span>
<span class="hl-prop">perfStats</span>?: {
<span class="hl-prop">loadDurationMs</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 模型加载耗时</span>
<span class="hl-prop">promptEvalDurationMs</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// Prompt 评估耗时</span>
<span class="hl-prop">evalDurationMs</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 生成耗时</span>
<span class="hl-prop">tokensPerSecond</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 生成速率</span>
};
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaTokenUsage</span> {
<span class="hl-prop">inputTokens</span>: <span class="hl-type">number</span>;
<span class="hl-prop">outputTokens</span>: <span class="hl-type">number</span>;
<span class="hl-prop">totalTokens</span>: <span class="hl-type">number</span>;
<span class="hl-prop">reasoningTokens</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// Thinking 模式专用</span>
<span class="hl-prop">cacheHitTokens</span>?: <span class="hl-type">number</span>;
<span class="hl-prop">cacheMissTokens</span>?: <span class="hl-type">number</span>;
}
<span class="hl-kw">export enum</span> <span class="hl-type">MetonaFinishReason</span> {
<span class="hl-prop">STOP</span> = <span class="hl-str">'stop'</span>, <span class="hl-cm">// 自然结束</span>
<span class="hl-prop">LENGTH</span> = <span class="hl-str">'length'</span>, <span class="hl-cm">// 达到长度上限</span>
<span class="hl-prop">TOOL_CALLS</span> = <span class="hl-str">'tool_calls'</span>, <span class="hl-cm">// 因工具调用而停止</span>
<span class="hl-prop">CONTENT_FILTER</span> = <span class="hl-str">'content_filter'</span>, <span class="hl-cm">// 内容过滤</span>
<span class="hl-prop">ERROR</span> = <span class="hl-str">'error'</span>, <span class="hl-cm">// 错误终止</span>
}</pre>
</div>
</section>
<!-- ===== 流式响应 ===== -->
<section class="api-section" id="response-stream">
<h2>&#x26A1; 流式响应:MetonaStreamEvent</h2>
<p class="desc">流式输出使用统一的事件类型,Agent Loop 和 UI 均可订阅。</p>
<div class="code-block">
<pre><span class="hl-cm">/** 流式事件类型枚举 */</span>
<span class="hl-kw">export enum</span> <span class="hl-type">MetonaStreamEventType</span> {
<span class="hl-prop">TEXT_DELTA</span> = <span class="hl-str">'text_delta'</span>, <span class="hl-cm">// 文本增量</span>
<span class="hl-prop">REASONING_DELTA</span> = <span class="hl-str">'reasoning_delta'</span>, <span class="hl-cm">// 推理内容增量</span>
<span class="hl-prop">TOOL_CALL_DELTA</span> = <span class="hl-str">'tool_call_delta'</span>, <span class="hl-cm">// 工具调用增量</span>
<span class="hl-prop">TOOL_CALL_COMPLETE</span> = <span class="hl-str">'tool_call_complete'</span>,
<span class="hl-prop">THINKING_START</span> = <span class="hl-str">'thinking_start'</span>, <span class="hl-cm">// 思考开始</span>
<span class="hl-prop">THINKING_END</span> = <span class="hl-str">'thinking_end'</span>, <span class="hl-cm">// 思考结束</span>
<span class="hl-prop">ERROR</span> = <span class="hl-str">'error'</span>, <span class="hl-cm">// 流中错误</span>
<span class="hl-prop">DONE</span> = <span class="hl-str">'done'</span>, <span class="hl-cm">// 流结束</span>
<span class="hl-prop">USAGE</span> = <span class="hl-str">'usage'</span>, <span class="hl-cm">// Token 统计(通常在 DONE 前)</span>
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaStreamEvent</span> {
<span class="hl-prop">type</span>: <span class="hl-type">MetonaStreamEventType</span>;
<span class="hl-prop">requestId</span>: <span class="hl-type">string</span>;
<span class="hl-prop">sessionId</span>: <span class="hl-type">string</span>;
<span class="hl-prop">iteration</span>: <span class="hl-type">number</span>;
<span class="hl-prop">seq</span>: <span class="hl-type">number</span>; <span class="hl-cm">// 序列号</span>
<span class="hl-prop">timestamp</span>: <span class="hl-type">number</span>;
<span class="hl-cm">/** 根据 type 使用不同字段 */</span>
<span class="hl-prop">delta</span>?: <span class="hl-type">string</span>; <span class="hl-cm">// TEXT_DELTA / REASONING_DELTA</span>
<span class="hl-prop">toolCallDelta</span>?: {
<span class="hl-prop">index</span>: <span class="hl-type">number</span>; <span class="hl-cm">// 工具调用索引(同一轮可能有多个)</span>
<span class="hl-prop">name</span>?: <span class="hl-type">string</span>; <span class="hl-cm">// 工具名称片段(首个事件携带)</span>
<span class="hl-prop">argsDelta</span>?: <span class="hl-type">string</span>; <span class="hl-cm">// 参数 JSON 增量片段</span>
}; <span class="hl-cm">// TOOL_CALL_DELTA</span>
<span class="hl-prop">toolCall</span>?: <span class="hl-type">MetonaToolCall</span>; <span class="hl-cm">// TOOL_CALL_COMPLETE(拼接完成后的完整调用)</span>
<span class="hl-prop">usage</span>?: <span class="hl-type">MetonaTokenUsage</span>; <span class="hl-cm">// USAGE</span>
<span class="hl-prop">error</span>?: <span class="hl-type">MetonaError</span>; <span class="hl-cm">// ERROR</span>
}</pre>
</div>
<h3>流式传输协议</h3>
<p class="desc">IPC 通道使用 <strong>SSE-like</strong> 格式(Server-Sent Events),每条事件为一行 JSON:</p>
<div class="code-block">
<pre><span class="hl-cm">// 实际传输格式(IPC 通道内,每行一条事件)</span>
{"type":"thinking_start","requestId":"r_1","sessionId":"s_1","iteration":1,"seq":0,"timestamp":1719000000000}
{"type":"reasoning_delta","requestId":"r_1","sessionId":"s_1","iteration":1,"seq":1,"timestamp":1719000000100,"delta":"让我先分析问题的关键点..."}
{"type":"thinking_end","requestId":"r_1","sessionId":"s_1","iteration":1,"seq":2,"timestamp":1719000000500}
{"type":"text_delta","requestId":"r_1","sessionId":"s_1","iteration":1,"seq":3,"timestamp":1719000000600,"delta":"根据分析,"}
{"type":"text_delta","requestId":"r_1","sessionId":"s_1","iteration":1,"seq":4,"timestamp":1719000000650,"delta":"答案是..."}
{"type":"tool_call_complete","requestId":"r_1","sessionId":"s_1","iteration":1,"seq":5,"timestamp":1719000000700,"toolCall":{...}}
{"type":"usage","requestId":"r_1","sessionId":"s_1","iteration":1,"seq":6,"timestamp":1719000000800,"usage":{"inputTokens":120,"outputTokens":45,"totalTokens":165}}
{"type":"done","requestId":"r_1","sessionId":"s_1","iteration":1,"seq":7,"timestamp":1719000000800}</pre>
</div>
<h3>流式工具调用拼接策略</h3>
<p class="desc">不同 Provider 的流式工具调用返回方式不同,Adapter 负责统一处理:</p>
<table class="spec">
<tr><th>Provider</th><th>流式工具调用格式</th><th>Adapter 处理方式</th></tr>
<tr>
<td class="field-name">DeepSeek / Agnes</td>
<td class="field-type">分片返回:<code>delta.tool_calls[i]</code> 先返回 <code>index</code> + <code>function.name</code> 片段,再返回 <code>function.arguments</code> 片段</td>
<td>Adapter 缓冲拼接:每个 <code>delta.tool_calls</code> 片段转换为 <code>TOOL_CALL_DELTA</code> 事件;拼接完成后发 <code>TOOL_CALL_COMPLETE</code></td>
</tr>
<tr>
<td class="field-name">Ollama</td>
<td class="field-type">整块返回:<code>message.tool_calls</code> 在最后一个 chunk 中一次性返回</td>
<td>Adapter 直接转换为 <code>TOOL_CALL_COMPLETE</code> 事件(无 <code>TOOL_CALL_DELTA</code></td>
</tr>
</table>
<div class="note-box">
<strong>&#x1F4A1; 拼接规则:</strong>Adapter 维护一个 <code>Map&lt;number, {name: string, argsBuffer: string}&gt;</code> 缓冲区。
<br>• 收到 <code>TOOL_CALL_DELTA</code> 时:如果 <code>name</code> 不为空,初始化缓冲区;将 <code>argsDelta</code> 追加到 <code>argsBuffer</code>
<br>• 收到流结束(<code>done</code>)或 <code>finish_reason: "tool_calls"</code> 时:遍历缓冲区,对每个拼接完整的工具调用发 <code>TOOL_CALL_COMPLETE</code> 事件(<code>JSON.parse(argsBuffer)</code> 作为 <code>args</code>
<br>• UI 端可以选择忽略 <code>TOOL_CALL_DELTA</code> 事件,只监听 <code>TOOL_CALL_COMPLETE</code>(简化实现)
</div>
<!-- ===== 思考内容 ===== -->
<section class="api-section" id="response-thinking">
<h2>&#x1F9E0; 思考内容:MetonaThinking</h2>
<p class="desc">各 Provider 的思考/推理内容格式不同(DeepSeek 用 <code>reasoning_content</code>、Anthropic 用 <code>thinking</code> block、Ollama 在 <code>think</code> 标签内),统一为 MetonaThinking。</p>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">MetonaThinking</span> {
<span class="hl-cm">/** 思考内容文本 */</span>
<span class="hl-prop">content</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** 思考状态 */</span>
<span class="hl-prop">status</span>: <span class="hl-str">'thinking'</span> | <span class="hl-str">'complete'</span>;
<span class="hl-cm">/** 思考耗时 (ms) */</span>
<span class="hl-prop">durationMs</span>: <span class="hl-type">number</span>;
<span class="hl-cm">/** 思考消耗的 token 数 */</span>
<span class="hl-prop">tokensUsed</span>: <span class="hl-type">number</span>;
}</pre>
</div>
<div class="note-box">
<strong>&#x1F4A1; 流式思考:</strong>流式输出时,通过 <code>thinking_start</code> 事件开始,一系列 <code>reasoning_delta</code> 传输增量,最后 <code>thinking_end</code> 结束。
最终在 MetonaResponse 中合并为完整的 <code>reasoningContent</code> 字符串。
</div>
</section>
<!-- ===== 工具调用 ===== -->
<section class="api-section" id="response-toolcall">
<h2>&#x1F50C; 工具调用:MetonaToolCall</h2>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">MetonaToolCall</span> {
<span class="hl-prop">id</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 工具调用唯一 ID</span>
<span class="hl-prop">name</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 工具名称</span>
<span class="hl-prop">args</span>: <span class="hl-type">Record</span>&lt;<span class="hl-type">string</span>, <span class="hl-type">unknown</span>&gt;; <span class="hl-cm">// 调用参数</span>
<span class="hl-prop">iteration</span>: <span class="hl-type">number</span>; <span class="hl-cm">// 所属 ReAct 迭代</span>
<span class="hl-prop">timestamp</span>: <span class="hl-type">number</span>;
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaToolResult</span> {
<span class="hl-prop">toolCallId</span>: <span class="hl-type">string</span>;
<span class="hl-prop">toolName</span>: <span class="hl-type">string</span>;
<span class="hl-prop">result</span>: <span class="hl-type">unknown</span>; <span class="hl-cm">// 工具原始返回值</span>
<span class="hl-prop">summary</span>?: <span class="hl-type">string</span>; <span class="hl-cm">// 人工可读摘要(用于 LLM 上下文注入)</span>
<span class="hl-prop">success</span>: <span class="hl-type">boolean</span>;
<span class="hl-prop">error</span>?: <span class="hl-type">string</span>;
<span class="hl-prop">durationMs</span>: <span class="hl-type">number</span>;
<span class="hl-prop">timestamp</span>: <span class="hl-type">number</span>;
}</pre>
</div>
<p class="desc" style="margin-top:16px;">
<strong>工具结果注入规则:</strong>工具执行完毕后,构造 <code>role='tool'</code> 的 MetonaMessage 追加到 messages 数组中。
<code>summary</code> 字段供 LLM 阅读(精简后),<code>result</code> 保留原始值供审计和调试。
</p>
<div class="note-box">
<strong>&#x1F527; Tool Calling 模式(首选):</strong>当使用 Tool Calling 时,Provider Adapter 在 <code>MetonaRequest.tools</code> 中传入工具列表,LLM 原生返回 <code>tool_calls</code> 结构化数据。Adapter 直接映射为 <code>MetonaResponse.toolCalls</code><strong>无需文本解析</strong>
<br><strong>DeepSeek</strong>:请求参数 <code>tools</code> + <code>tool_choice</code>,响应 <code>choices[0].message.tool_calls</code>
<br><strong>Agnes</strong>:同 DeepSeekOpenAI 兼容)
<br><strong>Ollama</strong>:请求参数 <code>tools</code>,响应 <code>message.tool_calls</code>
<br>三个 Provider 均原生支持 Tool Calling,正则解析仅作为 Provider 不支持时的降级方案。
</div>
<div class="note-box">
<strong>&#x1F9E0; 解析策略优先级:</strong>
<br><strong>1. Tool Calling(首选)</strong>:利用 Provider 原生的 <code>tools</code> + <code>tool_choice</code> 参数,LLM 直接返回结构化 <code>tool_calls</code>
<br><strong>2. Structured Output(备选)</strong>:当不需要工具调用但需要结构化输出时,使用 <code>response_format: json_object</code>
<br><strong>3. 正则降级(仅兜底)</strong>:仅在 Provider 不支持 Tool Calling 时使用(当前三个 Provider 全部支持,实际不触发)。
</div>
</section>
<!-- ===== 错误格式 ===== -->
<section class="api-section" id="response-error">
<h2>&#x274C; 错误格式:MetonaError</h2>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">MetonaError</span> {
<span class="hl-prop">code</span>: <span class="hl-type">MetonaErrorCode</span>; <span class="hl-cm">// 错误码</span>
<span class="hl-prop">message</span>: <span class="hl-type">string</span>; <span class="hl-cm">// 人类可读的错误描述</span>
<span class="hl-prop">provider</span>?: <span class="hl-type">string</span>; <span class="hl-cm">// 出错的 Provider</span>
<span class="hl-prop">providerCode</span>?: <span class="hl-type">string</span>; <span class="hl-cm">// Provider 原始错误码</span>
<span class="hl-prop">retryable</span>: <span class="hl-type">boolean</span>; <span class="hl-cm">// 是否可重试</span>
<span class="hl-prop">retryAfterMs</span>?: <span class="hl-type">number</span>; <span class="hl-cm">// 建议重试等待时间</span>
}
<span class="hl-kw">export enum</span> <span class="hl-type">MetonaErrorCode</span> {
<span class="hl-cm">// 网络层</span>
<span class="hl-prop">NETWORK_TIMEOUT</span> = <span class="hl-str">'network_timeout'</span>,
<span class="hl-prop">NETWORK_ERROR</span> = <span class="hl-str">'network_error'</span>,
<span class="hl-cm">// 认证层</span>
<span class="hl-prop">AUTH_INVALID</span> = <span class="hl-str">'auth_invalid'</span>,
<span class="hl-prop">AUTH_EXPIRED</span> = <span class="hl-str">'auth_expired'</span>,
<span class="hl-cm">// 频率限制</span>
<span class="hl-prop">RATE_LIMITED</span> = <span class="hl-str">'rate_limited'</span>,
<span class="hl-prop">QUOTA_EXCEEDED</span> = <span class="hl-str">'quota_exceeded'</span>,
<span class="hl-cm">// 模型层</span>
<span class="hl-prop">MODEL_OVERLOADED</span> = <span class="hl-str">'model_overloaded'</span>,
<span class="hl-prop">MODEL_NOT_FOUND</span> = <span class="hl-str">'model_not_found'</span>,
<span class="hl-prop">CONTEXT_LENGTH_EXCEEDED</span> = <span class="hl-str">'context_length_exceeded'</span>,
<span class="hl-prop">OUTPUT_LENGTH_EXCEEDED</span> = <span class="hl-str">'output_length_exceeded'</span>,
<span class="hl-cm">// 内容层</span>
<span class="hl-prop">CONTENT_FILTERED</span> = <span class="hl-str">'content_filtered'</span>,
<span class="hl-cm">// 解析层</span>
<span class="hl-prop">PARSE_ERROR</span> = <span class="hl-str">'parse_error'</span>,
<span class="hl-prop">INVALID_RESPONSE</span> = <span class="hl-str">'invalid_response'</span>,
<span class="hl-cm">// Agent 层</span>
<span class="hl-prop">MAX_ITERATIONS</span> = <span class="hl-str">'max_iterations'</span>,
<span class="hl-prop">USER_ABORTED</span> = <span class="hl-str">'user_aborted'</span>,
<span class="hl-prop">TIMEOUT</span> = <span class="hl-str">'timeout'</span>,
<span class="hl-prop">UNKNOWN</span> = <span class="hl-str">'unknown'</span>,
}</pre>
</div>
</section>
<hr class="section-divider">
<!-- ===== 上下文标准 ===== -->
<section class="api-section" id="context">
<h2>&#x1F4CB; 上下文标准:MetonaContext</h2>
<p class="desc">Context Builder 的输出,包含了完整的上下文信息,供 Agent Loop 和 Adapter 使用。</p>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">MetonaContext</span> {
<span class="hl-cm">/** 上下文唯一标识 */</span>
<span class="hl-prop">id</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** 关联的会话 */</span>
<span class="hl-prop">sessionId</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** System Prompt 分区 */</span>
<span class="hl-prop">systemPrompt</span>: <span class="hl-type">MetonaSystemPrompt</span>;
<span class="hl-cm">/** 会话历史(最近 N 轮) */</span>
<span class="hl-prop">history</span>: <span class="hl-type">MetonaMessage</span>[];
<span class="hl-cm">/** 检索到的相关记忆 */</span>
<span class="hl-prop">relevantMemories</span>: <span class="hl-type">MetonaMemoryItem</span>[];
<span class="hl-cm">/** 当前任务信息 */</span>
<span class="hl-prop">currentTask</span>: {
<span class="hl-prop">userInput</span>: <span class="hl-type">string</span>;
<span class="hl-prop">iteration</span>: <span class="hl-type">number</span>;
<span class="hl-prop">taskGoal</span>?: <span class="hl-type">string</span>;
};
<span class="hl-cm">/** 可用工具列表 */</span>
<span class="hl-prop">availableTools</span>: <span class="hl-type">MetonaToolDef</span>[];
<span class="hl-cm">/** 预估 Token 数 */</span>
<span class="hl-prop">estimatedTokens</span>: <span class="hl-type">number</span>;
<span class="hl-cm">/** 上下文使用率(estimatedTokens / contextWindow */</span>
<span class="hl-prop">usageRatio</span>: <span class="hl-type">number</span>;
<span class="hl-cm">/** 是否需要压缩 */</span>
<span class="hl-prop">needsCompression</span>: <span class="hl-type">boolean</span>;
}</pre>
</div>
<div class="note-box">
<strong>&#x1F4A1; 上下文压缩策略(Context Compression):</strong>
<br><code>usageRatio &gt; 0.8</code><code>needsCompression = true</code>Agent Loop 进入 <code>COMPRESSING</code> 状态。
<br><strong>压缩流程</strong>(由 ContextBuilder 负责):
<br>1. 保留最近 N 轮对话原文(N 由配置决定,默认 5)
<br>2. 将更早的对话轮次用 LLM 摘要为一组 "对话摘要" 消息插入上下文
<br>3. 保留所有 <code>tool_calls</code><code>tool_results</code> 的精简版(只保留工具名 + 结果状态,省略完整输出)
<br>4. 保留 System Prompt 和 MEMORY.md 注入内容不变
<br>5. 压缩后重新计算 <code>estimatedTokens</code>,确保 <code>usageRatio &lt; 0.5</code>
<br><strong>注意区分</strong>:上下文压缩(压缩 LLM 对话窗口)≠ 记忆压缩(清理 SQLite 记忆库),两者由不同组件负责。
</div>
</section>
<!-- ===== 记忆格式 ===== -->
<section class="api-section" id="memory">
<h2>&#x1F9E9; 记忆格式:MetonaMemoryItem</h2>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">MetonaMemoryItem</span> {
<span class="hl-prop">id</span>: <span class="hl-type">string</span>;
<span class="hl-prop">type</span>: <span class="hl-str">'episodic'</span> | <span class="hl-str">'semantic'</span> | <span class="hl-str">'working'</span>;
<span class="hl-cm">/** 可被 LLM 阅读的记忆内容 */</span>
<span class="hl-prop">content</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** 精简摘要(上下文紧张时使用) */</span>
<span class="hl-prop">summary</span>?: <span class="hl-type">string</span>;
<span class="hl-cm">/** 来源 */</span>
<span class="hl-prop">source</span>: <span class="hl-str">'user_input'</span> | <span class="hl-str">'tool_result'</span> | <span class="hl-str">'agent_thought'</span> | <span class="hl-str">'imported'</span>;
<span class="hl-cm">/** 重要程度 0-1 */</span>
<span class="hl-prop">importance</span>: <span class="hl-type">number</span>;
<span class="hl-cm">/** 检索相关性分数(仅在检索结果中出现) */</span>
<span class="hl-prop">relevanceScore</span>?: <span class="hl-type">number</span>;
<span class="hl-prop">sessionId</span>?: <span class="hl-type">string</span>;
<span class="hl-prop">createdAt</span>: <span class="hl-type">number</span>;
<span class="hl-prop">expiresAt</span>?: <span class="hl-type">number</span>;
}</pre>
</div>
</section>
<hr class="section-divider">
<!-- ===== Provider Adapter ===== -->
<section class="api-section" id="adapter">
<h2>&#x1F517; Provider Adapter 规范</h2>
<p class="desc">每个 LLM Provider 必须实现一个 Adapter,负责 Metona IR 和外部 API 格式之间的双向转换。</p>
<div class="code-block">
<pre><span class="hl-kw">export interface</span> <span class="hl-type">IMetonaProviderAdapter</span> {
<span class="hl-cm">/** Provider 标识 */</span>
<span class="hl-kw">readonly</span> <span class="hl-prop">providerId</span>: <span class="hl-type">string</span>;
<span class="hl-cm">/** 支持的模型列表 */</span>
<span class="hl-kw">readonly</span> <span class="hl-prop">supportedModels</span>: <span class="hl-type">string</span>[];
<span class="hl-cm">/** 上下文窗口大小 */</span>
<span class="hl-fn">getContextWindow</span>(<span class="hl-prop">model</span>: <span class="hl-type">string</span>): <span class="hl-type">number</span>;
<span class="hl-cm">/** 健康检查 */</span>
<span class="hl-fn">healthCheck</span>(): <span class="hl-type">Promise</span>&lt;<span class="hl-type">boolean</span>&gt;;
<span class="hl-cm">/**
* 核心方法:发送请求
* @param request - Metona 标准请求
* @returns Metona 标准响应
*/</span>
<span class="hl-fn">send</span>(<span class="hl-prop">request</span>: <span class="hl-type">MetonaRequest</span>): <span class="hl-type">Promise</span>&lt;<span class="hl-type">MetonaResponse</span>&gt;;
<span class="hl-cm">/**
* 核心方法:发送流式请求
* @param request - Metona 标准请求
* @param onEvent - 流式事件回调
* @returns 完整响应(流结束后返回)
*/</span>
<span class="hl-fn">sendStream</span>(
<span class="hl-prop">request</span>: <span class="hl-type">MetonaRequest</span>,
<span class="hl-prop">onEvent</span>: (<span class="hl-prop">event</span>: <span class="hl-type">MetonaStreamEvent</span>) => <span class="hl-type">void</span>
): <span class="hl-type">Promise</span>&lt;<span class="hl-type">MetonaResponse</span>&gt;;
<span class="hl-cm">/** 获取可用模型列表 */</span>
<span class="hl-fn">listModels</span>(): <span class="hl-type">Promise</span>&lt;<span class="hl-type">MetonaModelInfo</span>[]&gt;;
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaModelInfo</span> {
<span class="hl-prop">id</span>: <span class="hl-type">string</span>;
<span class="hl-prop">providerId</span>: <span class="hl-type">string</span>;
<span class="hl-prop">displayName</span>: <span class="hl-type">string</span>;
<span class="hl-prop">contextWindow</span>: <span class="hl-type">number</span>;
<span class="hl-prop">maxOutput</span>: <span class="hl-type">number</span>;
<span class="hl-prop">supportsStreaming</span>: <span class="hl-type">boolean</span>;
<span class="hl-prop">supportsThinking</span>: <span class="hl-type">boolean</span>;
<span class="hl-prop">supportsImages</span>: <span class="hl-type">boolean</span>;
<span class="hl-prop">supportsToolCalling</span>: <span class="hl-type">boolean</span>;
<span class="hl-prop">supportsStructuredOutput</span>: <span class="hl-type">boolean</span>;
<span class="hl-prop">pricing</span>?: <span class="hl-type">MetonaPricing</span>;
}
<span class="hl-kw">export interface</span> <span class="hl-type">MetonaPricing</span> {
<span class="hl-prop">inputPerMillion</span>: <span class="hl-type">number</span>;
<span class="hl-prop">outputPerMillion</span>: <span class="hl-type">number</span>;
<span class="hl-prop">currency</span>: <span class="hl-type">string</span>;
}</pre>
</div>
<h3>Adapter 实现清单</h3>
<table class="spec">
<tr><th>适配器</th><th>providerId</th><th>目标 API</th><th>传输格式</th></tr>
<tr>
<td class="field-name">DeepSeekAdapter</td>
<td class="field-type">deepseek</td>
<td>https://api.deepseek.com/chat/completions</td>
<td>OpenAI 兼容 JSON</td>
</tr>
<tr>
<td class="field-name">AgnesAdapter</td>
<td class="field-type">agnes-ai</td>
<td>https://apihub.agnes-ai.com/v1/chat/completions</td>
<td>OpenAI 兼容 JSON</td>
</tr>
<tr>
<td class="field-name">OllamaAdapter</td>
<td class="field-type">ollama</td>
<td>http://localhost:11434/api/chat</td>
<td>Ollama 原生 JSON / NDJSON</td>
</tr>
<tr>
<td class="field-name">AnthropicAdapter</td>
<td class="field-type">anthropic</td>
<td>https://api.anthropic.com/v1/messages</td>
<td>Anthropic 原生 JSON</td>
</tr>
<tr>
<td class="field-name">OpenAIAdapter</td>
<td class="field-type">openai</td>
<td>https://api.openai.com/v1/chat/completions</td>
<td>OpenAI 原生 JSON</td>
</tr>
</table>
<h3>Thinking 模式 Provider 映射表</h3>
<p class="desc">各 Provider 的思考模式控制方式不同,Adapter 负责将 <code>MetonaGenerationParams.thinkingEffort</code> 映射为目标 Provider 的原生参数:</p>
<table class="spec">
<tr><th>Provider</th><th>开启方式</th><th>thinkingEffort 映射</th><th>回传规则</th></tr>
<tr>
<td class="field-name">DeepSeek</td>
<td class="field-type"><code>thinking: {type: "enabled"}</code> + <code>reasoning_effort</code></td>
<td><code>low/medium → "high"</code>, <code>high → "high"</code>, <code>max → "max"</code></td>
<td>工具调用轮次的 <code>reasoning_content</code> <strong>必须</strong>回传上下文</td>
</tr>
<tr>
<td class="field-name">Agnes (OpenAI 兼容)</td>
<td class="field-type"><code>chat_template_kwargs: {enable_thinking: true}</code></td>
<td><code>low → false</code>, <code>medium/high/max → true</code></td>
<td>不强制回传</td>
</tr>
<tr>
<td class="field-name">Agnes (Anthropic 兼容)</td>
<td class="field-type"><code>thinking: {type: "enabled", budget_tokens: N}</code></td>
<td><code>low → 1024</code>, <code>medium → 2048</code>, <code>high → 4096</code>, <code>max → 8192</code></td>
<td>不强制回传</td>
</tr>
<tr>
<td class="field-name">Ollama</td>
<td class="field-type"><code>think: true/false</code><code>"high"/"medium"/"low"</code></td>
<td><code>low → "low"</code>, <code>medium → "medium"</code>, <code>high → "high"</code>, <code>max → true</code></td>
<td><code>message.thinking</code> 字段,不强制回传</td>
</tr>
</table>
<div class="note-box">
<strong>&#x1F4A1; 思考内容字段映射:</strong>
<br>• DeepSeek/Agnes(OpenAI): <code>reasoning_content</code><code>MetonaResponse.reasoningContent</code>
<br>• Agnes(Anthropic): <code>thinking</code> block → <code>MetonaResponse.reasoningContent</code>
<br>• Ollama: <code>message.thinking</code><code>MetonaResponse.reasoningContent</code>
<br>流式模式下,思考内容增量统一映射为 <code>reasoning_delta</code> 事件。
</div>
<h3>MVP 优先级</h3>
<p class="desc">当前优先实现以下 3 个 AdapterAnthropic 和 OpenAI 为未来计划:</p>
<table class="spec">
<tr><th>优先级</th><th>Adapter</th><th>状态</th></tr>
<tr><td class="field-name">P0</td><td>DeepSeekAdapter</td><td class="field-type">MVP 必须实现</td></tr>
<tr><td class="field-name">P0</td><td>AgnesAdapter</td><td class="field-type">MVP 必须实现</td></tr>
<tr><td class="field-name">P1</td><td>OllamaAdapter</td><td class="field-type">MVP 必须实现(本地推理)</td></tr>
<tr><td class="field-name">P2</td><td>AnthropicAdapter</td><td class="field-type">未来计划</td></tr>
<tr><td class="field-name">P2</td><td>OpenAIAdapter</td><td class="field-type">未来计划</td></tr>
</table>
<h3>Provider 故障转移策略</h3>
<p class="desc">当主 Provider 不可用时,Agent Loop 按以下策略处理:</p>
<table class="spec">
<tr><th>步骤</th><th>条件</th><th>动作</th></tr>
<tr><td class="field-name">1. 重试</td><td class="field-type"><code>MetonaError.retryable === true</code></td><td><code>retryAfterMs</code> 等待后重试(最多 3 次)</td></tr>
<tr><td class="field-name">2. 故障转移</td><td class="field-type">重试仍失败 + 已配置 fallback Provider</td><td>切换到备选 Provider 重新发送请求</td></tr>
<tr><td class="field-name">3. 通知用户</td><td class="field-type">故障转移触发时</td><td>通过 IPC 推送 <code>agent:providerSwitched</code> 事件,UI 显示 Toast 提示</td></tr>
<tr><td class="field-name">4. 报错</td><td class="field-type">无 fallback 或 fallback 也失败</td><td>返回 <code>MetonaError</code>UI 显示错误 + 重试按钮</td></tr>
</table>
<div class="note-box">
<strong>&#x1F4A1; 配置项:</strong><code>app_config</code> 表中增加以下配置:
<br><code>llm.fallbackProvider</code>string,备选 Provider ID
<br><code>llm.fallbackModel</code>string,备选模型名)
<br><code>llm.fallbackApiKey</code>string,备选 API Key,加密存储)
<br>用户可在设置界面配置备选 Provider。未配置时跳过故障转移步骤。
</div>
</section>
<hr class="section-divider">
<!-- ===== 完整示例 ===== -->
<section class="api-section" id="examples">
<h2>&#x1F4D6; 完整示例</h2>
<h3>示例 1:普通文本对话(非流式)</h3>
<p class="desc">用户发送问题,Agent Loop 构造请求,获得非流式回答。</p>
<div class="code-block">
<pre><span class="hl-cm">// ===== Agent Loop 构造 =====</span>
<span class="hl-kw">const</span> request: <span class="hl-type">MetonaRequest</span> = {
<span class="hl-prop">meta</span>: { <span class="hl-prop">sessionId</span>: <span class="hl-str">'s_abc'</span>, <span class="hl-prop">iteration</span>: <span class="hl-num">1</span>, <span class="hl-prop">requestId</span>: <span class="hl-str">'r_001'</span>, <span class="hl-prop">timestamp</span>: Date.<span class="hl-fn">now</span>(), <span class="hl-prop">agentVersion</span>: <span class="hl-str">'1.0.0'</span> },
<span class="hl-prop">systemPrompt</span>: {
<span class="hl-prop">roleDefinition</span>: <span class="hl-str">'你是一个专业的编程助手。'</span>,
<span class="hl-prop">outputConstraints</span>: <span class="hl-str">'用中文回答,代码块使用 ``` 包裹。'</span>,
<span class="hl-prop">safetyGuidelines</span>: <span class="hl-str">'不编造事实,不确定时如实说明。'</span>,
},
<span class="hl-prop">messages</span>: [
{ <span class="hl-prop">role</span>: <span class="hl-str">'user'</span>, <span class="hl-prop">content</span>: <span class="hl-str">'解释 JavaScript 的事件循环机制。'</span>, <span class="hl-prop">timestamp</span>: Date.<span class="hl-fn">now</span>() },
],
<span class="hl-prop">params</span>: { <span class="hl-prop">maxTokens</span>: <span class="hl-num">4096</span>, <span class="hl-prop">temperature</span>: <span class="hl-num">0.0</span>, <span class="hl-prop">stream</span>: <span class="hl-kw">false</span>, <span class="hl-prop">thinkingEnabled</span>: <span class="hl-kw">false</span> },
};
<span class="hl-cm">// ===== Adapter 返回 =====</span>
<span class="hl-kw">const</span> response: <span class="hl-type">MetonaResponse</span> = {
<span class="hl-prop">meta</span>: { <span class="hl-prop">requestId</span>: <span class="hl-str">'r_001'</span>, <span class="hl-prop">provider</span>: <span class="hl-str">'deepseek'</span>, <span class="hl-prop">model</span>: <span class="hl-str">'deepseek-v4-pro'</span>, <span class="hl-prop">latencyMs</span>: <span class="hl-num">850</span>, <span class="hl-prop">timestamp</span>: Date.<span class="hl-fn">now</span>() },
<span class="hl-prop">content</span>: <span class="hl-str">'JavaScript 的事件循环(Event Loop)是...\\n\\n```js\\nconsole.log(1)...\\n```'</span>,
<span class="hl-prop">usage</span>: { <span class="hl-prop">inputTokens</span>: <span class="hl-num">120</span>, <span class="hl-prop">outputTokens</span>: <span class="hl-num">350</span>, <span class="hl-prop">totalTokens</span>: <span class="hl-num">470</span> },
<span class="hl-prop">finishReason</span>: <span class="hl-type">MetonaFinishReason</span>.STOP,
};</pre>
</div>
<h3>示例 2ReAct 工具调用(流式)</h3>
<p class="desc">用户请求需要工具调用,Agent Loop 迭代两次。</p>
<div class="code-block">
<pre><span class="hl-cm">// ===== 第 1 轮:Agent 发送请求,LLM 返回工具调用(流式) =====</span>
<span class="hl-kw">const</span> request1: <span class="hl-type">MetonaRequest</span> = {
<span class="hl-prop">meta</span>: { <span class="hl-prop">sessionId</span>: <span class="hl-str">'s_xyz'</span>, <span class="hl-prop">iteration</span>: <span class="hl-num">1</span>, <span class="hl-prop">requestId</span>: <span class="hl-str">'r_002'</span>, <span class="hl-prop">timestamp</span>: Date.<span class="hl-fn">now</span>(), <span class="hl-prop">agentVersion</span>: <span class="hl-str">'1.0.0'</span> },
<span class="hl-prop">systemPrompt</span>: { <span class="hl-cm">/* ... */</span> },
<span class="hl-prop">messages</span>: [
{ <span class="hl-prop">role</span>: <span class="hl-str">'user'</span>, <span class="hl-prop">content</span>: <span class="hl-str">'读取 /home/user/data.csv 并分析内容。'</span>, <span class="hl-prop">timestamp</span>: Date.<span class="hl-fn">now</span>() },
],
<span class="hl-prop">tools</span>: [
{ <span class="hl-prop">name</span>: <span class="hl-str">'read_file'</span>, <span class="hl-prop">description</span>: <span class="hl-str">'读取文件内容'</span>, <span class="hl-cm">/* ... */</span> },
{ <span class="hl-prop">name</span>: <span class="hl-str">'analyze_csv'</span>, <span class="hl-prop">description</span>: <span class="hl-str">'分析 CSV 数据'</span>, <span class="hl-cm">/* ... */</span> },
],
<span class="hl-prop">params</span>: { <span class="hl-prop">stream</span>: <span class="hl-kw">true</span>, <span class="hl-prop">thinkingEnabled</span>: <span class="hl-kw">true</span> },
};
<span class="hl-cm">// 流式事件:</span>
<span class="hl-cm">// → thinking_start → reasoning_delta ... → thinking_end</span>
<span class="hl-cm">// → tool_call_complete { id: 'tc_1', name: 'read_file', args: { file_path: '/home/user/data.csv' } }</span>
<span class="hl-cm">// → done</span>
<span class="hl-cm">// ===== Agent Loop 执行工具后,构造第 2 轮请求 =====</span>
<span class="hl-kw">const</span> request2: <span class="hl-type">MetonaRequest</span> = {
<span class="hl-prop">meta</span>: { <span class="hl-cm">/* ... */</span>, <span class="hl-prop">iteration</span>: <span class="hl-num">2</span>, <span class="hl-prop">requestId</span>: <span class="hl-str">'r_003'</span> },
<span class="hl-cm">// 消息包含历史 + 工具结果</span>
<span class="hl-prop">messages</span>: [
{ <span class="hl-prop">role</span>: <span class="hl-str">'user'</span>, <span class="hl-prop">content</span>: <span class="hl-str">'读取 /home/user/data.csv 并分析内容。'</span>, <span class="hl-prop">timestamp</span>: t },
{ <span class="hl-prop">role</span>: <span class="hl-str">'assistant'</span>, <span class="hl-prop">content</span>: <span class="hl-str">''</span>, <span class="hl-prop">toolCalls</span>: [{ <span class="hl-prop">id</span>: <span class="hl-str">'tc_1'</span>, <span class="hl-prop">name</span>: <span class="hl-str">'read_file'</span>, <span class="hl-prop">args</span>: { <span class="hl-prop">file_path</span>: <span class="hl-str">'/home/user/data.csv'</span> }, <span class="hl-prop">iteration</span>: <span class="hl-num">1</span> }], <span class="hl-prop">timestamp</span>: t },
{
<span class="hl-prop">role</span>: <span class="hl-str">'tool'</span>,
<span class="hl-prop">content</span>: <span class="hl-str">''</span>,
<span class="hl-prop">toolResult</span>: { <span class="hl-prop">toolCallId</span>: <span class="hl-str">'tc_1'</span>, <span class="hl-prop">toolName</span>: <span class="hl-str">'read_file'</span>, <span class="hl-prop">result</span>: <span class="hl-str">'...'</span>, <span class="hl-prop">success</span>: <span class="hl-kw">true</span>, <span class="hl-prop">durationMs</span>: <span class="hl-num">5</span>, <span class="hl-prop">timestamp</span>: t,
<span class="hl-prop">summary</span>: <span class="hl-str">'文件 data.csv 包含 1000 行数据,字段为 name,age,email,city'</span> },
<span class="hl-prop">timestamp</span>: t
},
],
<span class="hl-prop">tools</span>: [<span class="hl-cm">/* 同上 */</span>],
<span class="hl-prop">params</span>: { <span class="hl-prop">stream</span>: <span class="hl-kw">true</span>, <span class="hl-prop">thinkingEnabled</span>: <span class="hl-kw">false</span> },
};</pre>
</div>
</section>
<hr class="section-divider">
<!-- ===== 迁移指南 ===== -->
<section class="api-section" id="migration">
<h2>&#x1F504; 迁移指南</h2>
<p class="desc">将现有代码迁移到 Metona IR 标准的步骤。</p>
<h3>文件结构</h3>
<div class="code-block">
<pre>electron/harness/types/
├── metona-request.ts <span class="hl-cm">// MetonaRequest, MetonaMessage, MetonaToolDef 等</span>
├── metona-response.ts <span class="hl-cm">// MetonaResponse, MetonaStreamEvent, MetonaError 等</span>
├── metona-context.ts <span class="hl-cm">// MetonaContext, MetonaSystemPrompt</span>
├── metona-memory.ts <span class="hl-cm">// MetonaMemoryItem</span>
├── metona-tool.ts <span class="hl-cm">// MetonaToolCall, MetonaToolResult</span>
├── metona-adapter.ts <span class="hl-cm">// IMetonaProviderAdapter 接口</span>
└── index.ts <span class="hl-cm">// 统一导出</span>
electron/harness/adapters/
├── base-adapter.ts <span class="hl-cm">// Adapter 基类(共享逻辑)</span>
├── deepseek.adapter.ts
├── agnes-ai.adapter.ts
├── ollama.adapter.ts
├── anthropic.adapter.ts
└── openai.adapter.ts</pre>
</div>
<h3>迁移检查清单</h3>
<table class="spec">
<tr><th>#</th><th>检查项</th><th>涉及文件</th></tr>
<tr><td class="field-num">1</td><td>Agent Loop Engine 只引用 MetonaRequest / MetonaResponse</td><td class="field-type">agent-loop/engine.ts</td></tr>
<tr><td class="field-num">2</td><td>Context Builder 输出 MetonaContext</td><td class="field-type">harness/prompts/</td></tr>
<tr><td class="field-num">3</td><td>Tool Registry 使用 MetonaToolDef / MetonaToolCall / MetonaToolResult</td><td class="field-type">harness/tools/</td></tr>
<tr><td class="field-num">4</td><td>Memory Manager 使用 MetonaMemoryItem</td><td class="field-type">harness/memory/</td></tr>
<tr><td class="field-num">5</td><td>IPC Preload 桥接层只传递 Metona IR 类型</td><td class="field-type">electron/preload.ts</td></tr>
<tr><td class="field-num">6</td><td>IPC Handlers 的输入输出为 Metona IR 类型</td><td class="field-type">electron/ipc/*.handlers.ts</td></tr>
<tr><td class="field-num">7</td><td>React 组件/Zustand Store 只读写 Metona IR 类型</td><td class="field-type">src/stores/, src/components/</td></tr>
<tr><td class="field-num">8</td><td>每个 Provider Adapter 实现 IMetonaProviderAdapter</td><td class="field-type">harness/adapters/</td></tr>
<tr><td class="field-num">9</td><td>流式事件通过 MetonaStreamEvent 推送</td><td class="field-type">hooks/useAgentStream.ts</td></tr>
<tr><td class="field-num">10</td><td>所有外部 API 原生类型不出现在 harness/ 和 src/ 中</td><td class="field-type">全局</td></tr>
</table>
<div class="note-box">
<strong>&#x26A0;&#xFE0F; 强制规则:</strong>违反第 10 条的代码不得合入主分支。Code Review 时以此标准为基准。
</div>
</section>
<!-- FOOTER -->
<div style="text-align:center; padding: 40px 0 20px; color: var(--text-dim); font-size: 13px; border-top: 1px solid var(--border);">
<p>&#x1F4DC; Metona 内部 API 请求与响应标准 —— 项目端到端类型安全的基础</p>
<p>版本: <strong style="color:var(--accent)">v1.0.0</strong> · 生效日期: <strong style="color:var(--accent)">2026-06-25</strong></p>
<p style="margin-top:8px;">所属项目: Metona (AI Agent Desktop) · 技术栈: TypeScript + React + SQLite + Electron</p>
</div>
<!-- Back to Top Button -->
<button id="backToTop" onclick="window.scrollTo({top:0,behavior:'smooth'})" style="
position: fixed;
bottom: 30px;
right: 30px;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--accent);
color: #000;
border: none;
cursor: pointer;
font-size: 20px;
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(245,158,11,0.3);
transition: transform 0.2s, opacity 0.2s;
z-index: 1000;
">&#x2191;</button>
</div>
</div>
<script>
document.querySelectorAll('.sidebar a[href^="#"]').forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const target = document.querySelector(a.getAttribute('href'));
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
const sections = document.querySelectorAll('.api-section');
const sidebarLinks = document.querySelectorAll('.sidebar a[href^="#"]');
const backToTopBtn = document.getElementById('backToTop');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(s => {
if (window.scrollY >= s.offsetTop - 100) current = s.id;
});
sidebarLinks.forEach(a => {
a.classList.toggle('active', a.getAttribute('href') === '#' + current);
});
// Back to top button visibility
if (backToTopBtn) {
backToTopBtn.style.display = window.scrollY > 300 ? 'flex' : 'none';
}
});
</script>
</body>
</html>