216 lines
6.7 KiB
JavaScript
216 lines
6.7 KiB
JavaScript
/**
|
||
* 统一调用后端 /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);
|