硬性契约:删除代码中一切写死的上下文窗口与最大输出上限(含六家模型元信息
钳制与全部兜底值)——唯一合法来源是设置面板「上下文长度」(llm.contextWindow)
与「最大输出上限」(llm.maxTokens),跨 Provider/模型原样透传。
P0 正确性收口:
- 迁移 11/12(SCHEMA_VERSION 5):记忆表 embedding 列 + 分 Provider 窗口键清理
- 记忆生命周期接线:会话终态清理 working memory / episodic 90 天 TTL / access_count 回写
- 回放缓冲模块化 + 会话终态清理(杜绝 4MB/会话内存滞留)
- i18n 收口:主进程 main-locale(zh/en,ui.locale 热切换)+ 渲染层 17 处出层
P1 能力演进:
- 本地向量混合检索:0.6×向量余弦 + 0.4×TF-IDF,Ollama embeddings 首次投产,
存量记忆惰性回填,嵌入不可用自动回退 TF-IDF
- MEMORY.md 维护闭环:固化去重消除截断盲区;两阶段维护(AI 建议 → 用户确认 →
原子改写 + 语义记忆双轨同步 + 审计);>50KB 告警
- 可观测闭环:cacheTokens 引擎→前端透传(Token 面板命中率/成本行)+ 输入框
上下文占用指示条
- MCP Prompts/Resources 对话可用:/mcp:{server}:{prompt} 与 @mcp:{server}:{uri}
P2 体验补全:
- 工具自定义策略(正则白/黑名单 + 频率 + 强制确认,热生效)
- 连续 ≥3 同类工具确认聚合为单弹框
- 会话消息游标分页(首屏 200 条向上翻页)
- 开机自启;Playwright + Electron E2E 冒烟(本地 mock LLM 零外联)
Review 回归修复:MCP 大小写失配 / 分页状态复位 / 清空=未配置语义(Number(null)=0
隐患)/ MEMORY.md 告警位置 / working_memories FK(迁移 13)/ 全局配置层废键清理;
附带根治权限加固启动时序、代理回环放行、safeStorage 降级、悬空 symlink 逃逸。
验证:typecheck/lint 0 问题;test:electron 2478/2478(0 跳过);E2E 2/2;
docs/v0.8.1-迭代实施清单.md 全项留档。
126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
/**
|
||
* Agnes AI Provider Adapter
|
||
*
|
||
* OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、多模态(图片 — URL + Base64)。
|
||
*
|
||
* v0.6.4 P3-1: 继承 OpenAICompatibleAdapter —— 传输/组装/回退链收敛到共享基类,
|
||
* 本文件只保留 Agnes 差异点:chat_template_kwargs 思考开关(v0.6.4 对称性修复)、
|
||
* 无条件 includeImages、非流式默认超时 300s。
|
||
* 注:Agnes API 未提供 /models 端点,listModels 使用基类默认实现。
|
||
*
|
||
* @see apis/agnes-ai-api-docs-20260625.html
|
||
*/
|
||
|
||
import log from 'electron-log';
|
||
import type { MetonaRequest } from '../types';
|
||
import type { MetonaModelInfo } from '../types/metona-adapter';
|
||
import { buildOpenAICompatibleMessages, buildOpenAICompatibleTools } from './shared/openai-format';
|
||
import { OpenAICompatibleAdapter } from './shared/openai-compatible-base';
|
||
|
||
export class AgnesAdapter extends OpenAICompatibleAdapter {
|
||
override readonly providerId: string = 'agnes';
|
||
readonly supportedModels = ['agnes-2.0-flash'];
|
||
readonly supportsToolCalling = true;
|
||
readonly supportsThinking = true;
|
||
|
||
// H-2 修复: Agnes 模型元信息(v0.8.1: 仅承载展示与能力声明 —— 窗口/输出上限
|
||
// 数值已按硬性契约删除,唯一合法来源是设置面板 llm.contextWindow / llm.maxTokens)
|
||
private static readonly MODEL_INFO: Record<string, MetonaModelInfo> = {
|
||
'agnes-2.0-flash': {
|
||
id: 'agnes-2.0-flash',
|
||
name: 'Agnes 2.0 Flash',
|
||
supportsToolCalling: true,
|
||
supportsThinking: true,
|
||
description: 'Agnes AI 快速版,支持多模态图片(URL + Base64)与思考模式',
|
||
},
|
||
};
|
||
|
||
// ===== 共享基类差异声明 =====
|
||
|
||
protected override chatCompletionsUrl(): string {
|
||
return `${this.config.baseURL}/chat/completions`;
|
||
}
|
||
|
||
protected override sendTimeoutMs(): number {
|
||
return 300_000;
|
||
}
|
||
|
||
protected override modelInfoTable(): Record<string, MetonaModelInfo> {
|
||
return AgnesAdapter.MODEL_INFO;
|
||
}
|
||
|
||
protected override providerLabel(): string {
|
||
return 'Agnes AI';
|
||
}
|
||
|
||
// ========== 协议参数映射(Agnes 差异点) ==========
|
||
|
||
protected override toNativeRequest(
|
||
request: MetonaRequest,
|
||
stream: boolean,
|
||
): Record<string, unknown> {
|
||
// v0.6.2: images 处理收敛至共享层(原索引对齐循环在孤立 tool 过滤后会错位)
|
||
const messages = buildOpenAICompatibleMessages(request, true);
|
||
const tools = buildOpenAICompatibleTools(request.tools);
|
||
|
||
const imageCount = request.messages.reduce((n, m) => n + (m.images?.length ?? 0), 0);
|
||
if (imageCount > 0) {
|
||
const firstUrl = request.messages.find((m) => m.images?.length)?.images?.[0]?.url ?? '';
|
||
log.info(
|
||
`[Agnes] Processing ${imageCount} image(s), first URL prefix: ${firstUrl.slice(0, 50)}`,
|
||
);
|
||
}
|
||
|
||
// v0.8.1 硬性契约: max_tokens 原样透传设置面板「最大输出上限」(llm.maxTokens),
|
||
// 删除了旧的按模型元信息钳制逻辑
|
||
const body: Record<string, unknown> = {
|
||
model: this.config.defaultModel,
|
||
messages,
|
||
temperature: request.params.temperature,
|
||
max_tokens: request.params.maxTokens,
|
||
stream,
|
||
};
|
||
|
||
if (stream) {
|
||
body.stream_options = { include_usage: true };
|
||
}
|
||
|
||
if (tools) {
|
||
body.tools = tools;
|
||
}
|
||
|
||
// C-3 修复 + v0.6.4 对称性修复: Thinking 模式 — Agnes 使用 chat_template_kwargs
|
||
// Agnes API 仅支持 enable_thinking: true/false,不支持 effort 级别
|
||
// thinkingEffort === 'low' 时映射为 false;thinkingEnabled 为 false 或未配置时
|
||
// 显式发送 enable_thinking:false —— 原实现只在 thinkingEnabled===true 时写该字段,
|
||
// 若服务端默认开启思考,客户端没有任何路径把它关掉(DeepSeek/MiMo 均显式发送
|
||
// disabled 保持对称,唯独此处漏了)。
|
||
// v0.8.0 修订(用户意图优先): enable_thinking 完全遵循用户配置,不再按
|
||
// supportsThinking 元信息硬门控 —— 预算耗尽风险由引擎空响应守卫的降级
|
||
// 重试链路兜底;元信息与配置不符时仅告警不拦截。
|
||
{
|
||
const effort = request.params.thinkingEffort ?? 'high';
|
||
// 未配置 thinkingEnabled 一律显式关闭 —— 与 DeepSeek/MiMo 的"服务端默认开启,
|
||
// 必须显式发送 disabled"口径对齐,让行为确定性不依赖服务端隐式默认。
|
||
const wantThinking = request.params.thinkingEnabled === true && effort !== 'low';
|
||
body.chat_template_kwargs = { enable_thinking: wantThinking };
|
||
if (
|
||
request.params.thinkingEnabled === true &&
|
||
wantThinking &&
|
||
AgnesAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking === false
|
||
) {
|
||
log.warn(
|
||
`[Agnes] model "${this.config.defaultModel}" metadata says thinking unsupported — sending enable_thinking per user config (degraded retry handles budget exhaustion)`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 停止序列
|
||
if (request.params.stopSequences?.length) {
|
||
body.stop = request.params.stopSequences;
|
||
}
|
||
|
||
return body;
|
||
}
|
||
}
|