// @ts-nocheck import { anthropicUrl, zhongtaiProxyUrl, zhongtaiOmlxProxyUrl, loadAnthropicConfig, loadThinkingEnabled, cloudProtocol, } from '../utils/config.js'; import { reportTokens, reportStreamDelta, reportStreamEnd, reportPromptCache } from '../utils/metrics-client.js'; import { AGENT_IDENTITY_CORE } from '../core/agent-identity-core.js'; import { DOMAIN_RULES } from '../core/domain-rules.js'; const SYSTEM_PROMPT = ` ${AGENT_IDENTITY_CORE} 你是"ccSparkle Agent",地铁乘务专业的智能助手,基于 MCP 协议操作本地地铁运营图 Electron 应用。 【回答风格 - 严格遵守】 - **只用简体中文**回复用户,禁止整句英文(见上方【语言】规则)。 - 用自然口语化的中文,专业、简洁、有温度。 - **禁止使用任何 emoji 表情符号**,全部用纯文字表达。 - 不要机械复述身份描述。 【工具调用规则 - 必须】 - 涉及地铁业务时,**必须真正调用工具**(发 tool_use 块),不允许只用文字描述。 - 工具失败时如实告知错误。 - 多候选时列给用户选,不自己代决定。 ${DOMAIN_RULES} `; export async function chat(opts) { if (cloudProtocol() === 'openai') { return chatOpenAI(opts); } return chatAnthropic(opts); } function resolveTierModel(cfg) { return ({ fast: cfg.fastModel, standard: cfg.standardModel, pro: cfg.proModel, })[cfg.currentTier] || cfg.fastModel; } function assertCloudConfig(cfg, tierModel) { if (!cfg.authToken) { throw new Error('未配置 Auth Token,请点配置按钮(配)设置云端模型'); } if (!cfg.baseUrl) { throw new Error('未配置 Base URL,请点配置按钮(配)设置云端模型'); } if (!tierModel) { throw new Error('未配置当前档位的模型名,请点配置按钮(配)设置'); } } async function chatAnthropic({ messages, tools, signal, onTextDelta, onThinkingDelta, maxTokens: maxTokensOpt, temperature }) { const cfg = loadAnthropicConfig(); const thinkOn = loadThinkingEnabled(); const tierModel = resolveTierModel(cfg); assertCloudConfig(cfg, tierModel); let maxTokens = maxTokensOpt != null ? maxTokensOpt : (cfg.maxTokens || 4096); if (maxTokensOpt == null && thinkOn && maxTokens < 8192) maxTokens = 8192; const systemExtra = messages .filter(m => m.role === 'system' && m.content) .map(m => m.content) .join('\n\n'); const body = { model: tierModel, max_tokens: maxTokens, system: /claude/i.test(tierModel) ? [ { type: 'text', text: SYSTEM_PROMPT }, ...(systemExtra ? [{ type: 'text', text: systemExtra }] : []), ].map((b, i, arr) => ( i === arr.length - 1 ? { ...b, cache_control: { type: 'ephemeral' } } : b )) : (systemExtra ? `${SYSTEM_PROMPT}\n\n${systemExtra}` : SYSTEM_PROMPT), messages: convertMessagesAnthropic(messages), stream: true, thinking: { type: thinkOn ? 'enabled' : 'disabled' }, }; if (temperature != null && !(thinkOn && /claude/i.test(tierModel))) { body.temperature = temperature; } if (thinkOn && /claude/i.test(tierModel)) { const budget = Math.min(10000, Math.max(1024, Math.floor(maxTokens * 0.4))); body.thinking.budget_tokens = budget; if (body.max_tokens <= budget) body.max_tokens = budget + 2048; } if (tools && tools.length > 0) { body.tools = tools.map(t => ({ name: t.name, description: t.description || '', input_schema: t.inputSchema || { type: 'object', properties: {} }, })); } const r = await fetch(zhongtaiProxyUrl(), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Cloud-Url': anthropicUrl(), 'X-Cloud-Token': cfg.authToken, 'anthropic-version': cfg.apiVersion || '2023-06-01', }, body: JSON.stringify(body), signal, }); if (!r.ok) { const text = await r.text(); throw new Error(`云端 API HTTP ${r.status}: ${text.slice(0, 300)}`); } return consumeAnthropicSSE(r.body, { onTextDelta, onThinkingDelta, thinkOn }); } function convertMessagesAnthropic(messages) { const out = []; let pendingToolResults = []; const flushToolResults = () => { if (pendingToolResults.length > 0) { out.push({ role: 'user', content: pendingToolResults }); pendingToolResults = []; } }; for (const m of messages) { if (m.role === 'system') continue; if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) { flushToolResults(); const content = []; if (m.content) content.push({ type: 'text', text: m.content }); for (const tc of m.toolCalls) { content.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.arguments || {}, }); } out.push({ role: 'assistant', content }); } else if (m.role === 'tool') { pendingToolResults.push({ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content, }); } else { flushToolResults(); out.push({ role: m.role, content: m.content || '' }); } } flushToolResults(); return out; } async function consumeAnthropicSSE(body, { onTextDelta, onThinkingDelta, thinkOn }) { const reader = body.getReader(); const decoder = new TextDecoder(); let lineBuf = ''; const t0 = performance.now(); const blocks = new Map(); let outputTokens = 0; let inputUsage = null; let fullText = ''; let fullThinking = ''; while (true) { const { done, value } = await reader.read(); if (done) break; lineBuf += decoder.decode(value, { stream: true }); const lines = lineBuf.split('\n'); lineBuf = lines.pop(); for (const line of lines) { if (!line.startsWith('data:')) continue; const data = line.slice(5).trim(); if (!data) continue; let evt; try { evt = JSON.parse(data); } catch (e) { continue; } handleAnthropicEvent(evt, blocks, { onTextDelta, onThinkingDelta, thinkOn, appendText: (t) => { fullText += t; }, appendThinking: (t) => { fullThinking += t; }, }); if (evt.type === 'message_start' && evt.message?.usage) { inputUsage = { ...evt.message.usage }; } if (evt.type === 'message_delta' && evt.usage) { outputTokens = evt.usage.output_tokens || outputTokens; inputUsage = { ...(inputUsage || {}), ...evt.usage }; } } } const toolCalls = []; for (const [, block] of [...blocks.entries()].sort((a, b) => a[0] - b[0])) { if (block.type === 'tool_use' && block.toolName) { let input = {}; if (block.toolInput) { try { input = JSON.parse(block.toolInput); } catch (e) { input = { _raw: block.toolInput }; } } toolCalls.push({ id: block.toolId, name: block.toolName, arguments: input, }); } } if (inputUsage) { reportPromptCache(inputUsage); } if (outputTokens > 0) { const durationMs = performance.now() - t0; reportTokens(outputTokens, durationMs); reportStreamEnd(outputTokens); } return { text: fullText.trim(), thinking: fullThinking, toolCalls, evalCount: outputTokens, }; } function handleAnthropicEvent(evt, blocks, hooks) { const { onTextDelta, onThinkingDelta, thinkOn, appendText, appendThinking, } = hooks; switch (evt.type) { case 'content_block_start': { const idx = evt.index; const block = evt.content_block || {}; blocks.set(idx, { type: block.type, text: block.text || '', thinking: block.thinking || '', toolName: block.name, toolId: block.id, toolInput: '', }); if (block.type === 'thinking' || block.type === 'redacted_thinking') { const piece = block.thinking || ''; if (piece) { appendThinking(piece); if (thinkOn) onThinkingDelta?.(piece); } } if (block.type === 'text' && block.text) { if (onTextDelta) onTextDelta(block.text); appendText(block.text); } break; } case 'content_block_delta': { const idx = evt.index; const block = blocks.get(idx); if (!block) return; const delta = evt.delta || {}; if (delta.type === 'text_delta') { block.text += delta.text || ''; if (onTextDelta) onTextDelta(delta.text || ''); appendText(delta.text || ''); reportStreamDelta((delta.text || '').length); } else if (delta.type === 'thinking_delta') { const piece = delta.thinking || ''; block.thinking = (block.thinking || '') + piece; appendThinking(piece); if (thinkOn && piece) onThinkingDelta?.(piece); reportStreamDelta(piece.length); } else if (delta.type === 'input_json_delta') { block.toolInput += delta.partial_json || ''; } break; } case 'content_block_stop': { break; } } } async function chatOpenAI({ messages, tools, signal, onTextDelta, onThinkingDelta, maxTokens: maxTokensOpt, temperature }) { const cfg = loadAnthropicConfig(); const thinkOn = loadThinkingEnabled(); const tierModel = resolveTierModel(cfg); assertCloudConfig(cfg, tierModel); let maxTokens = maxTokensOpt != null ? maxTokensOpt : (cfg.maxTokens || 4096); if (maxTokensOpt == null && thinkOn && maxTokens < 8192) maxTokens = 8192; const body = { model: tierModel, max_tokens: maxTokens, messages: convertMessagesOpenAI(messages), stream: true, stream_options: { include_usage: true }, }; if (temperature != null) { body.temperature = temperature; } if (/glm/i.test(tierModel)) { body.thinking = { type: thinkOn ? 'enabled' : 'disabled' }; } if (tools && tools.length > 0) { body.tools = tools.map(t => ({ type: 'function', function: { name: t.name, description: t.description || '', parameters: t.inputSchema || { type: 'object', properties: {} }, }, })); } const r = await fetch(zhongtaiOmlxProxyUrl(), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Cloud-Url': anthropicUrl(), 'X-Cloud-Token': cfg.authToken, }, body: JSON.stringify(body), signal, }); if (!r.ok) { const text = await r.text(); throw new Error(`云端 API(OpenAI) HTTP ${r.status}: ${text.slice(0, 300)}`); } return consumeOpenAISSE(r.body, { onTextDelta, onThinkingDelta, thinkOn }); } function convertMessagesOpenAI(messages) { const systemExtra = messages .filter(m => m.role === 'system' && m.content) .map(m => m.content) .join('\n\n'); const out = [{ role: 'system', content: systemExtra ? `${SYSTEM_PROMPT}\n\n${systemExtra}` : SYSTEM_PROMPT, }]; for (const m of messages) { if (m.role === 'system') continue; if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) { out.push({ role: 'assistant', content: m.content || null, tool_calls: m.toolCalls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments || {}), }, })), }); } else if (m.role === 'tool') { out.push({ role: 'tool', tool_call_id: m.toolCallId, content: m.content, }); } else { out.push({ role: m.role, content: m.content || '' }); } } return out; } async function consumeOpenAISSE(body, { onTextDelta, onThinkingDelta, thinkOn }) { const reader = body.getReader(); const decoder = new TextDecoder(); let lineBuf = ''; let fullText = ''; let fullThinking = ''; let evalCount = 0; const t0 = performance.now(); const toolAcc = new Map(); while (true) { const { done, value } = await reader.read(); if (done) break; lineBuf += decoder.decode(value, { stream: true }); const lines = lineBuf.split('\n'); lineBuf = lines.pop(); for (const line of lines) { if (!line.startsWith('data:')) continue; const data = line.slice(5).trim(); if (!data || data === '[DONE]') continue; let chunk; try { chunk = JSON.parse(data); } catch (e) { continue; } const choice = chunk.choices?.[0]; const delta = choice?.delta || {}; const thinkPiece = delta.reasoning_content || delta.reasoning || ''; if (thinkPiece) { fullThinking += thinkPiece; if (thinkOn) onThinkingDelta?.(thinkPiece); reportStreamDelta(thinkPiece.length); } if (delta.content) { fullText += delta.content; if (onTextDelta) onTextDelta(delta.content); reportStreamDelta(delta.content.length); } if (Array.isArray(delta.tool_calls)) { for (const tc of delta.tool_calls) { const idx = tc.index ?? 0; let acc = toolAcc.get(idx); if (!acc) { acc = { id: tc.id || `toolu_openai_${Date.now()}_${idx}`, name: '', arguments: '', }; toolAcc.set(idx, acc); } if (tc.id) acc.id = tc.id; if (tc.function?.name) acc.name += tc.function.name; if (tc.function?.arguments) acc.arguments += tc.function.arguments; } } if (chunk.usage) { evalCount = chunk.usage.completion_tokens || evalCount; reportPromptCache(chunk.usage); } } } const toolCalls = [...toolAcc.entries()] .sort((a, b) => a[0] - b[0]) .filter(([, acc]) => acc.name) .map(([, acc]) => ({ id: acc.id, name: acc.name, arguments: safeParseJson(acc.arguments), })); if (evalCount > 0) { const durationMs = performance.now() - t0; reportTokens(evalCount, durationMs); reportStreamEnd(evalCount); } return { text: fullText.trim(), thinking: fullThinking, toolCalls, evalCount, }; } function safeParseJson(s) { if (!s) return {}; try { return JSON.parse(s); } catch (e) { return { _raw: s }; } }