Files
InfoGenie/InfoGenie-frontend/public/aimodelapp/shared/ai-chat.js
2026-04-01 22:03:57 +08:00

216 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 统一调用后端 /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<string>} assistant 文本
*/
async complete(messages, opts, progress) {
try {
return await tryStreamThenFallback(messages, opts, progress);
} finally {
hideDefaultStreamBar();
}
},
/** 仅非流式(兼容旧逻辑) */
completeNonStream: nonStreamComplete,
};
})(typeof window !== 'undefined' ? window : this);