feat: 更新项目代码

This commit is contained in:
2026-04-01 22:03:57 +08:00
parent 1c81d4e6ea
commit 284b5a5260
53 changed files with 6240 additions and 22665 deletions

View File

@@ -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);