/** * 统一调用后端 /api/aimodelapp/chat,优先流式 /api/aimodelapp/chat/stream 实时反馈进度 */ (function (global) { const DEFAULT_BAR_ID = '__ai_stream_status_bar'; function getToken() { return ( (global.AUTH_CONFIG && typeof global.AUTH_CONFIG.getToken === 'function' ? global.AUTH_CONFIG.getToken() : null) || global.localStorage.getItem('token') ); } function getBaseUrl() { return (global.API_CONFIG && global.API_CONFIG.baseUrl) || ''; } function ensureDefaultStreamBar() { if (typeof document === 'undefined') return null; let el = document.getElementById(DEFAULT_BAR_ID); if (!el) { el = document.createElement('div'); el.id = DEFAULT_BAR_ID; el.setAttribute('role', 'status'); el.style.cssText = [ 'position:fixed', 'bottom:0', 'left:0', 'right:0', 'z-index:2147483646', 'padding:10px 14px', 'font-size:13px', 'line-height:1.4', 'background:rgba(17,24,39,.94)', 'color:#e5e7eb', 'border-top:1px solid #374151', 'box-shadow:0 -4px 20px rgba(0,0,0,.15)', ].join(';'); document.body.appendChild(el); } return el; } function hideDefaultStreamBar() { if (typeof document === 'undefined') return; const el = document.getElementById(DEFAULT_BAR_ID); if (el) { el.style.display = 'none'; el.textContent = ''; } } function showDefaultProgress(text) { const el = ensureDefaultStreamBar(); if (!el) return; el.style.display = 'block'; el.textContent = text; } async function nonStreamComplete(messages, opts) { opts = opts || {}; const token = getToken(); if (!token) throw new Error('未登录,请先登录后使用AI功能'); const base = getBaseUrl(); const res = await fetch(base + '/api/aimodelapp/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token, }, body: JSON.stringify({ messages, provider: opts.provider || 'deepseek', model: opts.model || 'deepseek-chat', }), }); if (!res.ok) { let errMsg = res.statusText; try { const errData = await res.json(); errMsg = errData.error || errMsg; } catch (e) { /* ignore */ } throw new Error(errMsg || 'API 请求失败'); } const data = await res.json(); if (data.success && data.content != null) { return typeof data.content === 'string' ? data.content : String(data.content); } throw new Error(data.error || 'API 响应异常'); } /** * @param {{ role: string, content: string }[]} messages * @param {{ provider?: string, model?: string }} [opts] * @param {{ onStatus?: (s: string) => void, onDelta?: (chunk: string, full: string) => void } | null} [progress] */ async function tryStreamThenFallback(messages, opts, progress) { opts = opts || {}; const token = getToken(); if (!token) throw new Error('未登录,请先登录后使用AI功能'); const base = getBaseUrl(); const useExternal = progress && typeof progress === 'object'; const onStatus = useExternal && typeof progress.onStatus === 'function' ? progress.onStatus : showDefaultProgress; const onDelta = useExternal && typeof progress.onDelta === 'function' ? progress.onDelta : null; const fireStatus = (s) => { try { onStatus(s); } catch (e) { /* ignore */ } }; let res; try { fireStatus('正在连接模型…'); res = await fetch(base + '/api/aimodelapp/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', Authorization: 'Bearer ' + token, }, body: JSON.stringify({ messages, provider: opts.provider || 'deepseek', model: opts.model || 'deepseek-chat', }), }); } catch (e) { fireStatus('网络异常,改用普通请求…'); return nonStreamComplete(messages, opts); } if (!res.ok || !res.body) { fireStatus('流式通道不可用,改用普通请求…'); return nonStreamComplete(messages, opts); } fireStatus('正在生成…'); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let full = ''; let gotContent = false; let tick = 0; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line.startsWith('data:')) continue; const payload = line.slice(5).trim(); if (payload === '[DONE]') continue; let json; try { json = JSON.parse(payload); } catch (err) { continue; } const choice = json.choices && json.choices[0]; const delta = choice && choice.delta && choice.delta.content; if (typeof delta === 'string' && delta.length) { gotContent = true; full += delta; if (onDelta) { try { onDelta(delta, full); } catch (e) { /* ignore */ } } } tick += 1; if (tick % 8 === 0) { fireStatus('正在生成… 已输出 ' + full.length + ' 字'); } } } if (!full.trim() && !gotContent) { fireStatus('未收到流式内容,改用普通请求…'); return nonStreamComplete(messages, opts); } fireStatus('生成完成'); return full; } global.AiChat = { /** * @param {{ role: string, content: string }[]} messages * @param {{ provider?: string, model?: string }} [opts] * @param {{ onStatus?: (s: string) => void, onDelta?: (chunk: string, full: string) => void } | null} [progress] 可选;不传则使用底部默认状态条 * @returns {Promise} assistant 文本 */ async complete(messages, opts, progress) { try { return await tryStreamThenFallback(messages, opts, progress); } finally { hideDefaultStreamBar(); } }, /** 仅非流式(兼容旧逻辑) */ completeNonStream: nonStreamComplete, }; })(typeof window !== 'undefined' ? window : this);