/** * OpenAI 兼容 API 格式构建工具 * * 将 MetonaRequest 转换为 OpenAI /chat/completions 兼容的原生请求格式。 * DeepSeek、Agnes AI 和 MiMo 共享此工具,各自 Adapter 只需处理 Provider 特有的差异参数。 * * @see electron/harness/types/metona-request.ts — MetonaRequest 定义 * @see apis/deepseek-api-docs-20260518.html * @see apis/agnes-ai-api-docs-20260625.html * @see apis/mimo-api-docs-20260715.html */ import type { MetonaRequest, MetonaToolDef } from '../../types'; /** * 构建 OpenAI 兼容的 messages 数组 * * 处理: * - System Prompt 拼接(静态区 + 动态区 + 安全准则) * - 工具调用历史保留(reasoning_content + tool_calls) * - 工具结果注入(tool_call_id + content) * * 注意:图片(多模态)处理不属于此共享函数。 * 各 Provider 对多模态的支持不同(DeepSeek 不支持,Agnes/Ollama 支持但格式各异), * 应在各自 Adapter 的 toNativeRequest 中处理。 */ export function buildOpenAICompatibleMessages( request: MetonaRequest, ): Array> { const systemContent = [ request.systemPrompt.roleDefinition, request.systemPrompt.outputConstraints, request.systemPrompt.safetyGuidelines, request.systemPrompt.dynamicReminders, ] .filter(Boolean) .join('\n\n'); const nonSystemMessages = request.messages .filter((m) => m.role !== 'system') .map((m) => { // v0.3.0 修复: assistant 消息有 tool_calls 但 content 为空时,content 设为 null // DeepSeek/OpenAI API 要求有 tool_calls 的 assistant 消息 content 必须为 null 而非空字符串 const msg: Record = { role: m.role, content: m.content, }; // 注意:图片(多模态)处理不在此共享函数中。 // DeepSeek 不支持多模态,images 被静默丢弃是正确行为。 // Agnes/MiMo 各自的 toNativeRequest 中有独立的 images 处理。 // 审查修复: #27 曾在此添加 images 处理,但 DeepSeek 不支持多模态会导致 API 400,已撤销。 // === Assistant 消息 === if (m.role === 'assistant') { // 工具调用历史 if (m.toolCalls?.length) { msg.tool_calls = m.toolCalls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.args) }, })); // 有 tool_calls 时 content 必须为 null(API 规范) if (!m.content) msg.content = null; } // 推理内容(无论是否有工具调用,都保留 reasoning_content) if (m.reasoningContent) { msg.reasoning_content = m.reasoningContent; } } // === 工具执行结果 === if (m.role === 'tool' && m.toolResult) { msg.tool_call_id = m.toolResult.toolCallId; // CE-2 修复: 工具失败时 result 为 null,优先用 error 字段作为 content // 否则 LLM 看到 "null" 不知道失败原因,可能重复调用导致死循环 msg.content = m.toolResult.error ? m.toolResult.error : (typeof m.toolResult.result === 'string' ? m.toolResult.result : JSON.stringify(m.toolResult.result)); // #26 修复: 确保 tool 消息 content 不为 undefined // JSON.stringify(undefined) 返回 undefined(非字符串),会导致 content 字段在序列化后消失 // OpenAI/DeepSeek/Agnes API 严格要求 tool 消息必须有 content 字段,缺失会返回 400 if (msg.content === undefined) msg.content = ''; } return msg; }); return [{ role: 'system', content: systemContent }, ...nonSystemMessages]; } /** * 构建 OpenAI 兼容的 tools 数组 */ export function buildOpenAICompatibleTools( tools?: MetonaToolDef[], ): Array> | undefined { if (!tools?.length) return undefined; return tools.map((t) => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters, }, })); }