feat: 更新项目代码
This commit is contained in:
@@ -1,27 +1,127 @@
|
||||
/**
|
||||
* 统一调用后端 /api/aimodelapp/chat(提示词由前端 ai-prompts.js 组装)
|
||||
* 统一调用后端 /api/aimodelapp/chat,优先流式 /api/aimodelapp/chat/stream 实时反馈进度
|
||||
*/
|
||||
(function (global) {
|
||||
global.AiChat = {
|
||||
/**
|
||||
* @param {{ role: string, content: string }[]} messages
|
||||
* @param {{ provider?: string, model?: string }} [opts]
|
||||
* @returns {Promise<string>} assistant 文本
|
||||
*/
|
||||
async complete(messages, opts) {
|
||||
opts = opts || {};
|
||||
const token =
|
||||
(global.AUTH_CONFIG && typeof global.AUTH_CONFIG.getToken === 'function'
|
||||
? global.AUTH_CONFIG.getToken()
|
||||
: null) || global.localStorage.getItem('token');
|
||||
if (!token) {
|
||||
throw new Error('未登录,请先登录后使用AI功能');
|
||||
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 */
|
||||
}
|
||||
const base = (global.API_CONFIG && global.API_CONFIG.baseUrl) || '';
|
||||
const res = await fetch(base + '/api/aimodelapp/chat', {
|
||||
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({
|
||||
@@ -30,19 +130,86 @@
|
||||
model: opts.model || 'deepseek-chat',
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
let errMsg = res.statusText;
|
||||
} 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 {
|
||||
const errData = await res.json();
|
||||
errMsg = errData.error || errMsg;
|
||||
} catch (e) { /* ignore */ }
|
||||
throw new Error(errMsg || 'API 请求失败');
|
||||
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 + ' 字');
|
||||
}
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.success && data.content != null) {
|
||||
return typeof data.content === 'string' ? data.content : String(data.content);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
throw new Error(data.error || 'API 响应异常');
|
||||
},
|
||||
|
||||
/** 仅非流式(兼容旧逻辑) */
|
||||
completeNonStream: nonStreamComplete,
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
|
||||
Reference in New Issue
Block a user