- 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 导入导致黑屏
1262 lines
75 KiB
HTML
1262 lines
75 KiB
HTML
<!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>⚙️ Metona IR <span class="badge">v1.0</span></h2>
|
||
</div>
|
||
|
||
<div class="sidebar-section">设计理念</div>
|
||
<a href="#philosophy">🎯 核心思想</a>
|
||
<a href="#architecture">🏗️ 架构概览</a>
|
||
|
||
<div class="sidebar-section">请求标准</div>
|
||
<a href="#request">📤 MetonaRequest</a>
|
||
<a href="#request-messages">💬 消息格式</a>
|
||
<a href="#request-tools">🔧 工具定义</a>
|
||
|
||
<div class="sidebar-section">响应标准</div>
|
||
<a href="#response">📥 MetonaResponse</a>
|
||
<a href="#response-stream">⚡ 流式响应</a>
|
||
<a href="#response-thinking">🧠 思考内容</a>
|
||
<a href="#response-toolcall">🔌 工具调用</a>
|
||
<a href="#response-error">❌ 错误格式</a>
|
||
|
||
<div class="sidebar-section">上下文 & 记忆</div>
|
||
<a href="#context">📋 上下文标准</a>
|
||
<a href="#memory">🧩 记忆格式</a>
|
||
|
||
<div class="sidebar-section">工程规范</div>
|
||
<a href="#adapter">🔗 Provider Adapter</a>
|
||
<a href="#examples">📖 完整示例</a>
|
||
<a href="#migration">🔄 迁移指南</a>
|
||
</nav>
|
||
|
||
<div class="main">
|
||
|
||
<div class="hero">
|
||
<h1>Metona 内部 API 请求与响应标准</h1>
|
||
<p>Metona Internal Representation (IR) —— 项目内所有 AI 交互的统一数据格式。无论底层对接 DeepSeek、Ollama、Agnes 还是其他 LLM,Agent 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>📋 文档层级:</strong>本文档是 <strong>类型系统与数据格式的权威定义</strong>,与《构建指南》第四章(ReAct)、第五章(Harness)对应。冲突时以本文档为准。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<!-- ===== 核心思想 ===== -->
|
||
<section class="api-section" id="philosophy">
|
||
<h2>🎯 核心思想</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>🏗️ 架构概览</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>📤 请求标准: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 Prompt,Adaper 负责拼接为 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>💬 消息格式: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>💡 设计要点:</strong>部分 LLM 的 <code>reasoning_content</code> 需要伴随 <code>toolCalls</code> 回传上下文。
|
||
MetonaMessage 统一携带 <code>reasoningContent</code>,由 Adapter 决定是否需要回传。
|
||
</div>
|
||
|
||
<div class="note-box">
|
||
<strong>🖼 多模态消息转换:</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 => ({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>🔧 工具定义: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><<span class="hl-type">string</span>, <span class="hl-type">MetonaParamField</span>>;
|
||
<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>MetonaToolDef(IR 标准)</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>🔗 转换规则:</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>⚠️ 强制规则:</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>📥 响应标准: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>⚡ 流式响应: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>💡 拼接规则:</strong>Adapter 维护一个 <code>Map<number, {name: string, argsBuffer: string}></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>🧠 思考内容: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>💡 流式思考:</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>🔌 工具调用: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><<span class="hl-type">string</span>, <span class="hl-type">unknown</span>>; <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>🔧 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>:同 DeepSeek(OpenAI 兼容)
|
||
<br>• <strong>Ollama</strong>:请求参数 <code>tools</code>,响应 <code>message.tool_calls</code>
|
||
<br>三个 Provider 均原生支持 Tool Calling,正则解析仅作为 Provider 不支持时的降级方案。
|
||
</div>
|
||
|
||
<div class="note-box">
|
||
<strong>🧠 解析策略优先级:</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>❌ 错误格式: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>📋 上下文标准: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>💡 上下文压缩策略(Context Compression):</strong>
|
||
<br>当 <code>usageRatio > 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 < 0.5</code>
|
||
<br><strong>注意区分</strong>:上下文压缩(压缩 LLM 对话窗口)≠ 记忆压缩(清理 SQLite 记忆库),两者由不同组件负责。
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 记忆格式 ===== -->
|
||
<section class="api-section" id="memory">
|
||
<h2>🧩 记忆格式: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>🔗 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><<span class="hl-type">boolean</span>>;
|
||
|
||
<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><<span class="hl-type">MetonaResponse</span>>;
|
||
|
||
<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><<span class="hl-type">MetonaResponse</span>>;
|
||
|
||
<span class="hl-cm">/** 获取可用模型列表 */</span>
|
||
<span class="hl-fn">listModels</span>(): <span class="hl-type">Promise</span><<span class="hl-type">MetonaModelInfo</span>[]>;
|
||
}
|
||
|
||
<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>💡 思考内容字段映射:</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 个 Adapter,Anthropic 和 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>💡 配置项:</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>📖 完整示例</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>示例 2:ReAct 工具调用(流式)</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>🔄 迁移指南</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>⚠️ 强制规则:</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>📜 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;
|
||
">↑</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> |