内置 Provider(翻译 / Embedding / 正文 OCR)¶
agentero 是一个内置 provider id:凭证在构建期编入 Host,用户选中它之后不需要填任何 baseUrl / API Key / model,设置面板也不为它渲染凭证卡片。它是新装用户的默认选项(仅当本次构建注入了 key)。
三个消费方:
| 能力 | 设置项 | 模型兜底 |
|---|---|---|
| 翻译 | translate.provider |
tencent/Hunyuan-MT-7B |
| arXiv Daily 推荐 embedding | embedding.source |
BAAI/bge-m3 |
PAPER.md 正文解析 OCR |
layout.parserBackend |
PaddlePaddle/PaddleOCR-VL-1.5 |
代码:src-tauri/src/features/system/builtin/mod.rs(凭证模块)、crates/agentero-core/src/features/translate/sources/hunyuan_mt.rs(翻译 source)、src-tauri/src/features/system/settings/mod.rs(embedding / layout 凭证解析)、src-tauri/src/features/paper/analyze/body_engines/(引擎注册)。
构建期环境变量¶
| 变量 | 兜底 | 秘密 |
|---|---|---|
AGENTERO_BUILTIN_BASE_URL |
https://api.qiyuanchen.top/v1 |
否 |
AGENTERO_BUILTIN_API_KEY |
无——必须注入 | 是 |
AGENTERO_BUILTIN_TRANSLATE_MODEL |
tencent/Hunyuan-MT-7B |
否 |
AGENTERO_BUILTIN_EMBEDDING_MODEL |
BAAI/bge-m3 |
否 |
AGENTERO_BUILTIN_OCR_MODEL |
PaddlePaddle/PaddleOCR-VL-1.5 |
否 |
- 五个变量全部经
option_env!在 Rust 侧编译期解析(形态为option_env!(...).map(str::trim).filter(|v| !v.is_empty()),空白串等同未设置)。TS 侧不读任何AGENTERO_BUILTIN_*:import.meta.env.VITE_*会把 key 内联进 JS bundle,等于把秘密发到 webview,因此明确不走那条路。 - 用
option_env!而非env!:env!在变量缺失时编译失败,会打断本地 dev / contributor / 无 secret 的 CI。仓库必须能在没有任何 secret 的情况下构建。 - 没有注入 key 的构建里
available()为false,builtin_provider_status返回available: false,UI 隐藏或禁用内置选项,default_translate_provider()/default_parser_backend()回落到tencenttransmart/local,embedding_config()穿透到已存值(仍为空则返回None,照常触发recommend.no_embedding)。 - 既有先例:
posthog_key()(src-tauri/src/core/telemetry/mod.rs)用的就是同一套option_env!+ trim + filter-empty,见 telemetry.md §开关语义。
构建脚本转发:src-tauri/build.rs 的 forward_build_env() 为全部六个变量(AGENTERO_POSTHOG_KEY 加五个 AGENTERO_BUILTIN_*)发 cargo:rerun-if-env-changed,并在环境变量缺失时回退读仓库根 .env(gitignored),经 cargo:rustc-env= 重新导出。因此
- 本地既可以直接导出环境变量,也可以把
AGENTERO_BUILTIN_API_KEY写进.env;显式环境变量优先。 - 改变量值会触发
src-tauri重编。这一条是必需的而非便利:cargo 默认不追踪option_env!读到的变量,缺了rerun-if-env-changed时换 key 不会重编,发布包会静默带着旧值或空值出去且毫无征兆。 - 运行时
std::env::set_var仍然改变不了已解析的值——builtin/mod.rs的测试块里有注释标注这一点。
密钥边界¶
- 唯一读取口是
builtin::api_key();baseUrl 与三个 model 访问器保持私有,Host 消费方统一走status()。 builtin_provider_status只返回非秘密字段{ available, baseUrl, translateModel, embeddingModel, ocrModel }。测试断言序列化后既没有key/apiKey/secret/token/auth/credential/mask这类字段名,也不含 key 本身(无前缀、无长度、无 mask、无 hash 派生物)。- 内置 key 不写入
AppSettings:normalize_layout_provider_configs的PROVIDERS白名单是["paddle", "mineru", "openaiCompatible"],任何agentero卡片在每次保存时被retain丢弃并由persist写盘,所以它进不了settings.json,也就到不了 webview。redact_secrets/merge_secrets只处理固定字段清单,AppSettings上的新字段一律会被回显给 webview——这是不给内置 key 加设置字段的原因。 - 前端只消费
available一个字段;baseUrl 与 model id 不显示到 UI。
网关与真实的安全边界¶
AGENTERO_BUILTIN_BASE_URL 的兜底 https://api.qiyuanchen.top/v1 是产品方自己的网关,不是直连上游 provider。这是「客户端内嵌 key」风险可控的前提:model 白名单、per-IP 限流、花费上限与上游 key 轮换都能在服务端强制。
但不要过度声称安全:
- 内嵌在已发布客户端二进制里的 key,对拿到安装包的人仍然是可提取的——
strings扫二进制,或者用户在自己机器上跑 mitmproxy 看Authorization头即可。网关限制的是损失面(能调哪些 model、调多少),不能阻止提取。 - 因此对外文案不应把它描述为「安全」或「用户拿不到 key」。
- 彻底解法(未实现):把内嵌 key 换成网关签发的 per-install activation token,届时可获得吊销与 per-user 配额,而客户端 provider 形状不变。
编译期语义的运维后果¶
- 因为是编译期变量:换 model id、或轮换客户端侧 key,都必须发新版才能生效。
- 网关侧轮换上游 key 不需要发版——客户端只认网关。
- 发布构建必须注入
AGENTERO_BUILTIN_API_KEY(其余四个可选)。步骤与 secret 约定见 ../test/release.md §内置 Provider 构建期注入。
翻译:Hunyuan-MT¶
tencent/Hunyuan-MT-7B 是专用 MT 模型,不是 instruct 模型:它只遵循自己被微调过的那套模板。因此内置路径刻意不复用 openai_compatible.rs 的长规则提示词,也不给模型看 [[n]] 批量标记(那套对齐协议依赖指令遵循)。
| 项 | 值 |
|---|---|
| 模板(逐字) | Translate the following segment into <target_language>, without additional explanation.<source_text> |
| 消息形状 | 单条 user message,无 system message(对比 openai_compatible.rs 是 system + user) |
| 源语言 | 模板里没有它的位置;模型自动检测,sourceLang 不参与 |
temperature |
0.2(沿用 openai_compatible 惯例;技术报告未规定解码参数) |
| 端点 | {base_url}/chat/completions,Authorization: Bearer <key> |
[[n]] 批量:Host 侧拆分与重组¶
前端仍按 buildNumberedPayload(src/lib/pdf/layout/layout-translate.ts,批内 payload ≤ LAYOUT_TRANSLATE_BATCH_CHARS = 4500 字符)产出 [[n]] 编号批次,见 ../frontend/translate.md。Host 收到后:
- 按字节扫描行首的
[[n]],要求从 1 开始递增;遇到行中标记或乱序即停止扫描(避免把含字面[[1]]的散文切碎),余下文本留在当前段内。 - 标记 1 之前的文本作为无标记段;每段 trim,空段丢弃。
- 每段一个独立请求,
StreamExt::buffered(3)——既有并发上限又保证结果顺序,省掉索引管线。并发数 3 对齐openai_vlm.rs的PAGE_CONCURRENCY。 - 重组为
"{marker} {text}"以"\n\n"连接,与前端buildNumberedPayload的输出字节一致。
段数少于前端预期时,parseNumberedTranslation 返回 null 并回退为逐段翻译,段落不会错位。
⟦n⟧ 行内占位符(前端 mask.ts 用于保护引用 / URL / 行内公式)原样透传,不剥离。这是一个未经真实 key 验证的假设,见下方「限制与后续」。
语言映射¶
代码只映射 Rust 实际可能收到的值:en → English,zh-CN(含 zh / Chinese)→ Chinese,另有防御性的 ui → English,未知值 / auto / 空 → English + debug 日志。查表大小写不敏感,也接受英文名本身。
真正的约束在 UI:目标语言白名单是 settings/mod.rs 的 TR_TARGETS = ["ui", "en", "zh-CN"],前端 union 是 TranslateTargetLang = "ui" | "en" | "zh-CN",而 "ui" 在前端解析阶段就已经换成 en / zh-CN。所以 Hunyuan-MT 更宽的语言支持目前不可达。完整 37 语言表见下方,作为将来扩目标语言的依据。
错误¶
- 构建里没有 key 时,
src-tauri/src/features/translate/commands.rs在任何.await之前返回AppError::domain(ERR_NO_BUILTIN_KEY),标记为translate.no_builtin_key,而不是放一个无法认证的请求出去。 finish_reason为length/max_tokens/content_filter时报「translation incomplete…retry with a smaller chunk」。"agentero"刻意不在 Rust 的FREE_PROVIDERS里(CLIcli/src/commands/translate.rs用它门控--provider,随后以api_key: None调用,会让 CLI 接受一个无法认证的 provider),也不在COMMERCIAL_PROVIDERS里(那个列表驱动 WebView 凭证卡片,内置 provider 不该渲染任何卡片)。- 两份「免费 provider」清单刻意不一致:前端
FreeTranslateProviderId/FREE_MT_PROVIDER_IDS(src/lib/translate/types.ts)含agentero——这样它复用无 key 引擎的管线、且因为不是CommercialTranslateProviderId,translate-pane.tsx不会为它渲染凭证卡片,COMMERCIAL_MT_DEFAULT_BASE_URLS/COMMERCIAL_MT_DOCS_URLS这两个 total Record 也不需要新条目。Rust 的FREE_PROVIDERS不含它(理由见上条)。改任何一份清单时都要意识到另一份是反的。 - 探测:
probeFreeMtProviders显式把agentero过滤掉(探测它会真的发一次翻译请求),可用性只来自builtin_provider_status。
支持语言(Hunyuan-MT,37)¶
代码只实现上节所述的可达值;下表是模型侧的完整支持面,扩 TR_TARGETS 时按 Abbr 对照。
| English name | Abbr | 中文名 |
|---|---|---|
| Chinese | zh | 中文 |
| English | en | 英语 |
| French | fr | 法语 |
| Portuguese | pt | 葡萄牙语 |
| Spanish | es | 西班牙语 |
| Japanese | ja | 日语 |
| Turkish | tr | 土耳其语 |
| Russian | ru | 俄语 |
| Arabic | ar | 阿拉伯语 |
| Korean | ko | 韩语 |
| Thai | th | 泰语 |
| Italian | it | 意大利语 |
| German | de | 德语 |
| Vietnamese | vi | 越南语 |
| Malay | ms | 马来语 |
| Indonesian | id | 印尼语 |
| Filipino | tl | 菲律宾语 |
| Hindi | hi | 印地语 |
| Traditional Chinese | zh-Hant | 繁体中文 |
| Polish | pl | 波兰语 |
| Czech | cs | 捷克语 |
| Dutch | nl | 荷兰语 |
| Khmer | km | 高棉语 |
| Burmese | my | 缅甸语 |
| Persian | fa | 波斯语 |
| Gujarati | gu | 古吉拉特语 |
| Urdu | ur | 乌尔都语 |
| Telugu | te | 泰卢固语 |
| Marathi | mr | 马拉地语 |
| Hebrew | he | 希伯来语 |
| Bengali | bn | 孟加拉语 |
| Tamil | ta | 泰米尔语 |
| Ukrainian | uk | 乌克兰语 |
| Tibetan | bo | 藏语 |
| Kazakh | kk | 哈萨克语 |
| Mongolian | mn | 蒙古语 |
| Uyghur | ug | 维吾尔语 |
| Cantonese | yue | 粤语 |
Embedding:arXiv Daily 推荐¶
EmbeddingSettings 新增 source: "builtin" | "custom"。它是普通的 #[serde(default)](空串即「未设置」),不是 default = "..." 返回 "builtin":否则一个填了 BYOK 字段但没有 source 键的旧 settings.json,与「用户显式选了内置」无法区分。
迁移规则(Rust resolve_embedding_source() 与前端 normalizeEmbeddingSettings 必须逐条一致):
- 显式的
"builtin"/"custom"优先; - 否则
baseUrl/apiKey/model任一非空(全*掩码也算非空)⇒"custom"; - 三项全空 ⇒
"builtin"; - 未知值按 2–3 重新推断。
也就是说:已经填过自定义端点的老用户不会被静默切走,只有真正全新 / 全空的配置才变成内置。
embedding_config() 在解析出的 source 非 "custom" 且 builtin::available() 时返回网关三元组;否则穿透到已存值。这条链路只服务 arXiv 每日推荐(../development/plaza.md §3.4),Host 命令 recommend_arxiv 透明继承,无需改动。
向量缓存安全:embed_cache 的主键是 (text_hash, model)(catalog/schema.rs v6),所有读写都按 model 过滤,所以换 embedding 模型不会读到旧向量。
正文解析 OCR:PAPER.md¶
"agentero" 加进 PARSER_BACKENDS,复用既有的 OpenAiVlmBodyEngine,没有新引擎。该引擎本来就同时处理 PaddleOCR-VL(提示词 "OCR:",剥掉 <|LOC_123|> 坐标 token)与 deepseek-ai/DeepSeek-OCR(剥掉 grounding 标注),按 model id 自动选提示词。
| 项 | 值 |
|---|---|
| 注册 | body_engines/mod.rs 里以 engines::register_engine(BUILTIN_PROVIDER_ID, …) 注册一个 OpenAiVlmBodyEngine::new(BUILTIN_PROVIDER_ID) |
| 引擎 id | 引擎携带自己注册时的 id,因此回退提示读作 agentero failed: … 而非 openaiCompatible failed: … |
| 凭证 | layout_api_key / layout_base_url / layout_model 在 getter 层特判内置 id,所有调用方自动正确 |
| prompt / language / force-OCR | layout_prompt / layout_language / layout_is_ocr 不特判 → None / None / false。提示词由 model id 推导;后两项是 MinerU 专用(mineru.rs),VLM 引擎不读。前端 PARSER_PROVIDERS 里内置描述符的 requiresApiKey / supports* 全 false,isProviderCardConfigurable 因此把整张卡过滤掉——不是隐藏两个控件,而是什么都不渲染 |
providerConfigs 卡片 |
无。layout_provider_settings_key("agentero") 返回 "agentero" 只是为了给凭证 HashMap 一个稳定的键,白名单会把任何落盘的卡片丢掉 |
| hosted layout engine | 不注册。layout/hosted/engine.rs 的 engine_for("agentero") 返回 None → 明确的 unknown provider 错误。这是正确的:前端不会为它渲染凭证卡片,也没有探测按钮 |
两个容易漏的点:
PARSER_BACKENDS白名单必须含"agentero"。normalize()在每次保存时都会跑,未列入的 backend 会被重置为default_parser_backend()并由persist写盘——不加就等于永远选不上。- 未注册的 backend 会静默回退到
LocalBodyEngine(crates/agentero-core/src/features/paper/analyze/parse/engines/mod.rs的engine_for)而不报错,漏注册只会悄悄退化成本地解析。因此有测试显式断言engine_for("agentero")解析到 VLM 引擎。
大小写:register_engine 与 engine_for 都小写化,所以引擎查表大小写不敏感;而凭证 HashMap 取值经 provider_for_backend → layout_provider_settings_key(返回规范 camelCase)是大小写敏感的。"agentero" 全小写,三处字符串天然重合。引擎实现在 src-tauri/src/features/paper/analyze/body_engines/,trait 与注册表在 crates/agentero-core/.../parse/engines/。
版面分析不走内置(layout / parser 不对称)¶
"agentero" 是 parser(正文)backend,不是 layout(版面分析)backend。 LAYOUT_BACKENDS 仍是 ["local", "paddle", "mineru"],default_layout_backend() 无条件返回 "local",而 default_parser_backend() 在有编译期 key 时优先返回内置。
原因:版面分析跑在随包的 PP-DocLayoutV3 ONNX 模型上,首次使用下载到 XDG cache 目录,再经 agentero-model:// scheme 喂给 webview 的 onnxruntime-web——已经免费且离线(见 ../frontend/pdf-layout-analysis.md §模型落盘)。把版面分析默认切到云端网关会让每个 PDF 都产生费用,且没有任何质量收益。有测试 layout_backend_never_becomes_builtin 守住这一点。
默认值¶
| 函数 | 有编译期 key | 无 key |
|---|---|---|
default_translate_provider() |
agentero |
tencenttransmart |
default_parser_backend() |
agentero |
local |
default_layout_backend() |
local |
local |
EmbeddingSettings::source(推断) |
builtin(仅当 BYOK 三项全空) |
builtin(同左;但 embedding_config() 穿透到空值 → 功能禁用) |
新装用户没有 settings.json,read_file 返回 AppSettings::default(),因此由 Rust 的 default_*() 决定首次安装的取值。前端 TS 的 defaults 只在浏览器 dev(无 Tauri、也就不可能有 key)里生效,所以刻意保持在非内置值上,两边不需要一致。
限制与后续¶
已登记的未决项(多数需要真实 key 才能验证,不阻塞发布):
⟦n⟧占位符透传未验证:Host 假设 Hunyuan-MT 原样返回mask.ts插入的⟦n⟧。需要一次真实 key 的整篇 PDF 翻译,确认占位符没被吞掉或改写(吞掉会触发 chain 用原文重译一次)。- 模板空格待 A/B:arXiv Hunyuan-MT 技术报告渲染为
…explanation. <source_text>(有空格),实现用的是模型文档模板的无空格形式。值得一次真实对比再定。 temperature = 0.2沿用仓库惯例;技术报告没有规定解码参数。- 多段翻译没有全局 deadline:最坏墙钟时间是
ceil(n/3) × timeout(timeout默认 30s,钳制 1–30s)。整篇 PDF 的一批最多十几段,实测前不设总闸。 arxiv_rec_state不按 model 建键:当日已排序结果在切换 embedding 来源后的首次运行会被复用,除非force。既存行为,本次不改(详见 ../development/plaza.md §3.4)。- 目标语言只有 en / zh-CN:扩到 Hunyuan-MT 的 37 语言需要同时改
TR_TARGETS、前端TranslateTargetLangunion 与目标语言选择器,并在hunyuan_target_name补映射。 - 内嵌 key → per-install activation token:由网关签发、可吊销、可做 per-user 配额;客户端 provider 形状不变。这是内嵌 key 可提取问题的真正解法。
- 新手引导没有「Agentero 内置」这一档:
translate/layout两个引导步只有「填自己的 Key」与「用免费引擎 / 本地模型」二选一。translate-step.tsx的「用系统默认」原先写入静态前端默认值、会覆盖 Host 解析出的内置默认,现已改为按可用性解析(与default_translate_provider()一致),因此一路点过引导不再丢失内置默认;但引导界面本身仍未把内置作为显式选项呈现。见 ../frontend/onboarding.md。
前端 provider / 面板 / i18n 落点见 ../frontend/settings.md、../frontend/translate.md。