2026-02-08 12:22:45 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// vector/llm/llm-service.js - 修复 prefill 传递方式
|
2026-02-06 11:22:02 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
import { xbLog } from '../../../../core/debug-core.js';
|
2026-02-16 17:25:34 +08:00
|
|
|
|
import { getApiKey } from './siliconflow.js';
|
2026-02-06 11:22:02 +08:00
|
|
|
|
|
|
|
|
|
|
const MODULE_ID = 'vector-llm-service';
|
2026-02-08 12:22:45 +08:00
|
|
|
|
const SILICONFLOW_API_URL = 'https://api.siliconflow.cn/v1';
|
2026-02-06 15:08:20 +08:00
|
|
|
|
const DEFAULT_L0_MODEL = 'Qwen/Qwen3-8B';
|
2026-02-06 11:22:02 +08:00
|
|
|
|
|
|
|
|
|
|
let callCounter = 0;
|
|
|
|
|
|
|
|
|
|
|
|
function getStreamingModule() {
|
|
|
|
|
|
const mod = window.xiaobaixStreamingGeneration;
|
|
|
|
|
|
return mod?.xbgenrawCommand ? mod : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function generateUniqueId(prefix = 'llm') {
|
|
|
|
|
|
callCounter = (callCounter + 1) % 100000;
|
|
|
|
|
|
return `${prefix}-${callCounter}-${Date.now().toString(36)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function b64UrlEncode(str) {
|
|
|
|
|
|
const utf8 = new TextEncoder().encode(String(str));
|
|
|
|
|
|
let bin = '';
|
|
|
|
|
|
utf8.forEach(b => bin += String.fromCharCode(b));
|
|
|
|
|
|
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 统一LLM调用 - 走酒馆后端(非流式)
|
2026-02-08 18:12:55 +08:00
|
|
|
|
* assistant prefill 用 bottomassistant 参数传递
|
2026-02-06 11:22:02 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export async function callLLM(messages, options = {}) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
temperature = 0.2,
|
|
|
|
|
|
max_tokens = 500,
|
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
|
|
const mod = getStreamingModule();
|
2026-02-06 15:08:20 +08:00
|
|
|
|
if (!mod) throw new Error('Streaming module not ready');
|
|
|
|
|
|
|
2026-02-16 17:25:34 +08:00
|
|
|
|
const apiKey = getApiKey() || '';
|
2026-02-06 15:08:20 +08:00
|
|
|
|
if (!apiKey) {
|
|
|
|
|
|
throw new Error('L0 requires siliconflow API key');
|
|
|
|
|
|
}
|
2026-02-06 11:22:02 +08:00
|
|
|
|
|
2026-02-08 18:12:55 +08:00
|
|
|
|
// 分离 assistant prefill
|
2026-02-08 12:22:45 +08:00
|
|
|
|
let topMessages = [...messages];
|
|
|
|
|
|
let assistantPrefill = '';
|
2026-02-08 18:12:55 +08:00
|
|
|
|
|
2026-02-08 12:22:45 +08:00
|
|
|
|
if (topMessages.length > 0 && topMessages[topMessages.length - 1]?.role === 'assistant') {
|
|
|
|
|
|
const lastMsg = topMessages.pop();
|
|
|
|
|
|
assistantPrefill = lastMsg.content || '';
|
|
|
|
|
|
}
|
2026-02-06 11:22:02 +08:00
|
|
|
|
|
2026-02-08 12:22:45 +08:00
|
|
|
|
const top64 = b64UrlEncode(JSON.stringify(topMessages));
|
2026-02-06 11:22:02 +08:00
|
|
|
|
const uniqueId = generateUniqueId('l0');
|
|
|
|
|
|
|
|
|
|
|
|
const args = {
|
|
|
|
|
|
as: 'user',
|
|
|
|
|
|
nonstream: 'true',
|
|
|
|
|
|
top64,
|
|
|
|
|
|
id: uniqueId,
|
|
|
|
|
|
temperature: String(temperature),
|
|
|
|
|
|
max_tokens: String(max_tokens),
|
2026-02-06 15:08:20 +08:00
|
|
|
|
api: 'openai',
|
|
|
|
|
|
apiurl: SILICONFLOW_API_URL,
|
|
|
|
|
|
apipassword: apiKey,
|
|
|
|
|
|
model: DEFAULT_L0_MODEL,
|
2026-02-06 11:22:02 +08:00
|
|
|
|
};
|
2026-02-08 18:12:55 +08:00
|
|
|
|
const isQwen3 = String(DEFAULT_L0_MODEL || '').includes('Qwen3');
|
|
|
|
|
|
if (isQwen3) {
|
|
|
|
|
|
args.enable_thinking = 'false';
|
|
|
|
|
|
}
|
2026-02-06 11:22:02 +08:00
|
|
|
|
|
2026-02-08 12:22:45 +08:00
|
|
|
|
// ★ 用 bottomassistant 参数传递 prefill
|
|
|
|
|
|
if (assistantPrefill) {
|
|
|
|
|
|
args.bottomassistant = assistantPrefill;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 11:22:02 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const result = await mod.xbgenrawCommand(args, '');
|
|
|
|
|
|
return String(result ?? '');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
xbLog.error(MODULE_ID, 'LLM调用失败', e);
|
|
|
|
|
|
throw e;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function parseJson(text) {
|
|
|
|
|
|
if (!text) return null;
|
|
|
|
|
|
let s = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
|
|
|
|
|
try { return JSON.parse(s); } catch { }
|
|
|
|
|
|
const i = s.indexOf('{'), j = s.lastIndexOf('}');
|
|
|
|
|
|
if (i !== -1 && j > i) try { return JSON.parse(s.slice(i, j + 1)); } catch { }
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|