Compare commits
7 Commits
6601471aac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5daff85f3f | |||
| 3d1f6dc274 | |||
| 37b2f15d05 | |||
| 485016abdd | |||
| 7b921b80e0 | |||
| af7e0f689d | |||
| 5424dae2d6 |
@@ -254,6 +254,50 @@ export const DEFAULT_SUMMARY_USER_CONFIRM_PROMPT = `怎么截断了!重新完
|
|||||||
</Chat_History>`;
|
</Chat_History>`;
|
||||||
|
|
||||||
export const DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT = '下面重新生成完整JSON。';
|
export const DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT = '下面重新生成完整JSON。';
|
||||||
|
const DEFAULT_VECTOR_PROVIDER = "siliconflow";
|
||||||
|
const DEFAULT_L0_URL = "https://api.siliconflow.cn/v1";
|
||||||
|
const DEFAULT_OPENROUTER_URL = "https://openrouter.ai/api/v1";
|
||||||
|
const DEFAULT_L0_MODEL = "Qwen/Qwen3-8B";
|
||||||
|
const DEFAULT_EMBEDDING_MODEL = "BAAI/bge-m3";
|
||||||
|
const DEFAULT_RERANK_MODEL = "BAAI/bge-reranker-v2-m3";
|
||||||
|
|
||||||
|
function getVectorProviderDefaultUrl(provider) {
|
||||||
|
return provider === "openrouter" ? DEFAULT_OPENROUTER_URL : DEFAULT_L0_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultProviderProfile(provider, model = "") {
|
||||||
|
return {
|
||||||
|
url: provider === "custom" ? "" : getVectorProviderDefaultUrl(provider),
|
||||||
|
key: "",
|
||||||
|
model: model || "",
|
||||||
|
modelCache: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProviderProfiles(supportedProviders, srcProfiles, currentProvider, currentValues, defaultModel) {
|
||||||
|
const out = {};
|
||||||
|
supportedProviders.forEach((provider) => {
|
||||||
|
const raw = srcProfiles?.[provider] || {};
|
||||||
|
const defaults = createDefaultProviderProfile(provider, defaultModel);
|
||||||
|
out[provider] = {
|
||||||
|
url: String(raw.url || defaults.url || "").trim(),
|
||||||
|
key: String(raw.key || "").trim(),
|
||||||
|
model: String(raw.model || defaults.model || "").trim(),
|
||||||
|
modelCache: Array.isArray(raw.modelCache) ? raw.modelCache.filter(Boolean) : [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentProvider && out[currentProvider]) {
|
||||||
|
if (currentValues?.url && !out[currentProvider].url) out[currentProvider].url = String(currentValues.url).trim();
|
||||||
|
if (currentValues?.key && !out[currentProvider].key) out[currentProvider].key = String(currentValues.key).trim();
|
||||||
|
if (currentValues?.model && !out[currentProvider].model) out[currentProvider].model = String(currentValues.model).trim();
|
||||||
|
if (Array.isArray(currentValues?.modelCache) && !out[currentProvider].modelCache.length) {
|
||||||
|
out[currentProvider].modelCache = currentValues.modelCache.filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export function getSettings() {
|
export function getSettings() {
|
||||||
const ext = (extension_settings[EXT_ID] ||= {});
|
const ext = (extension_settings[EXT_ID] ||= {});
|
||||||
@@ -261,6 +305,63 @@ export function getSettings() {
|
|||||||
return ext;
|
return ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOpenAiCompatApiConfig(src, defaults = {}) {
|
||||||
|
const provider = String(src?.provider || defaults.provider || DEFAULT_VECTOR_PROVIDER).toLowerCase();
|
||||||
|
const supportedProviders = Array.isArray(defaults.supportedProviders) && defaults.supportedProviders.length
|
||||||
|
? defaults.supportedProviders
|
||||||
|
: [provider, "custom"];
|
||||||
|
const providers = normalizeProviderProfiles(
|
||||||
|
supportedProviders,
|
||||||
|
src?.providers,
|
||||||
|
provider,
|
||||||
|
src,
|
||||||
|
defaults.model || ""
|
||||||
|
);
|
||||||
|
const current = providers[provider] || createDefaultProviderProfile(provider, defaults.model || "");
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
url: String(current.url || "").trim(),
|
||||||
|
key: String(current.key || defaults.key || "").trim(),
|
||||||
|
model: String(current.model || defaults.model || "").trim(),
|
||||||
|
modelCache: Array.isArray(current.modelCache) ? current.modelCache.filter(Boolean) : [],
|
||||||
|
providers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVectorConfig(rawVector = null) {
|
||||||
|
const legacyOnline = rawVector?.online || {};
|
||||||
|
const sharedProvider = String(legacyOnline.provider || DEFAULT_VECTOR_PROVIDER).toLowerCase();
|
||||||
|
const sharedUrl = String(legacyOnline.url || (sharedProvider === "openrouter" ? DEFAULT_OPENROUTER_URL : DEFAULT_L0_URL)).trim();
|
||||||
|
const sharedKey = String(legacyOnline.key || "").trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: !!rawVector?.enabled,
|
||||||
|
engine: "online",
|
||||||
|
l0Concurrency: Math.max(1, Math.min(50, Number(rawVector?.l0Concurrency) || 10)),
|
||||||
|
l0Api: normalizeOpenAiCompatApiConfig(rawVector?.l0Api, {
|
||||||
|
provider: sharedProvider,
|
||||||
|
url: sharedUrl,
|
||||||
|
key: sharedKey,
|
||||||
|
model: DEFAULT_L0_MODEL,
|
||||||
|
supportedProviders: ["siliconflow", "openrouter", "custom"],
|
||||||
|
}),
|
||||||
|
embeddingApi: normalizeOpenAiCompatApiConfig(rawVector?.embeddingApi, {
|
||||||
|
provider: DEFAULT_VECTOR_PROVIDER,
|
||||||
|
url: DEFAULT_L0_URL,
|
||||||
|
key: sharedKey,
|
||||||
|
model: DEFAULT_EMBEDDING_MODEL,
|
||||||
|
supportedProviders: ["siliconflow", "custom"],
|
||||||
|
}),
|
||||||
|
rerankApi: normalizeOpenAiCompatApiConfig(rawVector?.rerankApi, {
|
||||||
|
provider: DEFAULT_VECTOR_PROVIDER,
|
||||||
|
url: DEFAULT_L0_URL,
|
||||||
|
key: sharedKey,
|
||||||
|
model: DEFAULT_RERANK_MODEL,
|
||||||
|
supportedProviders: ["siliconflow", "custom"],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getSummaryPanelConfig() {
|
export function getSummaryPanelConfig() {
|
||||||
const clampKeepVisibleCount = (value) => {
|
const clampKeepVisibleCount = (value) => {
|
||||||
const n = Number.parseInt(value, 10);
|
const n = Number.parseInt(value, 10);
|
||||||
@@ -299,7 +400,7 @@ export function getSummaryPanelConfig() {
|
|||||||
summaryAssistantPrefillPrompt: DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT,
|
summaryAssistantPrefillPrompt: DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT,
|
||||||
memoryTemplate: DEFAULT_MEMORY_PROMPT_TEMPLATE,
|
memoryTemplate: DEFAULT_MEMORY_PROMPT_TEMPLATE,
|
||||||
},
|
},
|
||||||
vector: null,
|
vector: normalizeVectorConfig(),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -320,7 +421,7 @@ export function getSummaryPanelConfig() {
|
|||||||
ui: { ...defaults.ui, ...(parsed.ui || {}) },
|
ui: { ...defaults.ui, ...(parsed.ui || {}) },
|
||||||
textFilterRules,
|
textFilterRules,
|
||||||
prompts: { ...defaults.prompts, ...(parsed.prompts || {}) },
|
prompts: { ...defaults.prompts, ...(parsed.prompts || {}) },
|
||||||
vector: parsed.vector || null,
|
vector: normalizeVectorConfig(parsed.vector || null),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result.trigger.timing === "manual") result.trigger.enabled = false;
|
if (result.trigger.timing === "manual") result.trigger.enabled = false;
|
||||||
@@ -349,16 +450,7 @@ export function getVectorConfig() {
|
|||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
|
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
const cfg = parsed.vector || null;
|
return parsed.vector ? normalizeVectorConfig(parsed.vector) : normalizeVectorConfig();
|
||||||
if (!cfg) return null;
|
|
||||||
|
|
||||||
// Keep vector side normalized to online + siliconflow.
|
|
||||||
cfg.engine = "online";
|
|
||||||
cfg.online = cfg.online || {};
|
|
||||||
cfg.online.provider = "siliconflow";
|
|
||||||
cfg.online.model = "BAAI/bge-m3";
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -376,15 +468,7 @@ export function saveVectorConfig(vectorCfg) {
|
|||||||
const raw = localStorage.getItem("summary_panel_config") || "{}";
|
const raw = localStorage.getItem("summary_panel_config") || "{}";
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
|
|
||||||
parsed.vector = {
|
parsed.vector = normalizeVectorConfig(vectorCfg || null);
|
||||||
enabled: !!vectorCfg?.enabled,
|
|
||||||
engine: "online",
|
|
||||||
online: {
|
|
||||||
provider: "siliconflow",
|
|
||||||
key: vectorCfg?.online?.key || "",
|
|
||||||
model: "BAAI/bge-m3",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
localStorage.setItem("summary_panel_config", JSON.stringify(parsed));
|
localStorage.setItem("summary_panel_config", JSON.stringify(parsed));
|
||||||
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, parsed);
|
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, parsed);
|
||||||
|
|||||||
@@ -26,18 +26,6 @@ const PROVIDER_MAP = {
|
|||||||
|
|
||||||
const JSON_PREFILL = DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT;
|
const JSON_PREFILL = DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT;
|
||||||
|
|
||||||
const LLM_PROMPT_CONFIG = {
|
|
||||||
topSystem: DEFAULT_SUMMARY_SYSTEM_PROMPT,
|
|
||||||
assistantDoc: DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT,
|
|
||||||
assistantAskSummary: DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT,
|
|
||||||
assistantAskContent: DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT,
|
|
||||||
metaProtocolStart: DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT,
|
|
||||||
userJsonFormat: DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT,
|
|
||||||
assistantCheck: DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT,
|
|
||||||
userConfirm: DEFAULT_SUMMARY_USER_CONFIRM_PROMPT,
|
|
||||||
assistantPrefill: DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 工具函数
|
// 工具函数
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -806,11 +806,7 @@ function buildNonVectorPrompt(store) {
|
|||||||
|
|
||||||
if (!sections.length) return "";
|
if (!sections.length) return "";
|
||||||
|
|
||||||
return (
|
return buildMemoryPromptText(sections.join("\n\n"));
|
||||||
`${buildSystemPreamble()}\n` +
|
|
||||||
`<剧情记忆>\n\n${sections.join("\n\n")}\n\n</剧情记忆>\n` +
|
|
||||||
`${buildPostscript()}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -297,6 +297,73 @@ All checks passed. Beginning incremental extraction...
|
|||||||
claude: { url: 'https://api.anthropic.com', needKey: true, canFetch: false },
|
claude: { url: 'https://api.anthropic.com', needKey: true, canFetch: false },
|
||||||
custom: { url: '', needKey: true, canFetch: true }
|
custom: { url: '', needKey: true, canFetch: true }
|
||||||
};
|
};
|
||||||
|
const VECTOR_PROVIDER_DEFAULTS = {
|
||||||
|
siliconflow: { url: 'https://api.siliconflow.cn/v1', needKey: true, canFetch: true },
|
||||||
|
openrouter: { url: 'https://openrouter.ai/api/v1', needKey: true, canFetch: true },
|
||||||
|
custom: { url: '', needKey: true, canFetch: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
const VECTOR_API_SUPPORTED_PROVIDERS = {
|
||||||
|
l0: ['siliconflow', 'openrouter', 'custom'],
|
||||||
|
embedding: ['siliconflow', 'custom'],
|
||||||
|
rerank: ['siliconflow', 'custom'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const VECTOR_API_DEFAULT_MODELS = {
|
||||||
|
l0: 'Qwen/Qwen3-8B',
|
||||||
|
embedding: 'BAAI/bge-m3',
|
||||||
|
rerank: 'BAAI/bge-reranker-v2-m3',
|
||||||
|
};
|
||||||
|
|
||||||
|
function setStatusText(el, message, kind = '') {
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = message || '';
|
||||||
|
el.style.color = kind === 'error'
|
||||||
|
? '#ef4444'
|
||||||
|
: kind === 'success'
|
||||||
|
? '#22c55e'
|
||||||
|
: kind === 'loading'
|
||||||
|
? '#f59e0b'
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultProviderProfile(provider, model = '') {
|
||||||
|
const pv = VECTOR_PROVIDER_DEFAULTS[provider] || VECTOR_PROVIDER_DEFAULTS.custom;
|
||||||
|
return {
|
||||||
|
url: pv.url || '',
|
||||||
|
key: '',
|
||||||
|
model: model || '',
|
||||||
|
modelCache: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProviderProfiles(prefix, apiCfg = {}) {
|
||||||
|
const supported = VECTOR_API_SUPPORTED_PROVIDERS[prefix] || ['custom'];
|
||||||
|
const model = apiCfg.model || VECTOR_API_DEFAULT_MODELS[prefix] || '';
|
||||||
|
const out = {};
|
||||||
|
supported.forEach(provider => {
|
||||||
|
const raw = apiCfg.providers?.[provider] || {};
|
||||||
|
const defaults = createDefaultProviderProfile(provider, model);
|
||||||
|
out[provider] = {
|
||||||
|
url: String(raw.url || defaults.url || '').trim(),
|
||||||
|
key: String(raw.key || '').trim(),
|
||||||
|
model: String(raw.model || defaults.model || '').trim(),
|
||||||
|
modelCache: Array.isArray(raw.modelCache) ? raw.modelCache.filter(Boolean) : [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentProvider = String(apiCfg.provider || supported[0] || 'custom').toLowerCase();
|
||||||
|
if (out[currentProvider]) {
|
||||||
|
if (apiCfg.url && !out[currentProvider].url) out[currentProvider].url = String(apiCfg.url).trim();
|
||||||
|
if (apiCfg.key && !out[currentProvider].key) out[currentProvider].key = String(apiCfg.key).trim();
|
||||||
|
if (apiCfg.model && !out[currentProvider].model) out[currentProvider].model = String(apiCfg.model).trim();
|
||||||
|
if (Array.isArray(apiCfg.modelCache) && !out[currentProvider].modelCache.length) {
|
||||||
|
out[currentProvider].modelCache = apiCfg.modelCache.filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
const SECTION_META = {
|
const SECTION_META = {
|
||||||
keywords: { title: '编辑关键词', hint: '每行一个关键词,格式:关键词|权重(核心/重要/一般)' },
|
keywords: { title: '编辑关键词', hint: '每行一个关键词,格式:关键词|权重(核心/重要/一般)' },
|
||||||
@@ -344,7 +411,33 @@ All checks passed. Beginning incremental extraction...
|
|||||||
memoryTemplate: '',
|
memoryTemplate: '',
|
||||||
},
|
},
|
||||||
textFilterRules: [...DEFAULT_FILTER_RULES],
|
textFilterRules: [...DEFAULT_FILTER_RULES],
|
||||||
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
|
vector: {
|
||||||
|
enabled: false,
|
||||||
|
engine: 'online',
|
||||||
|
l0Concurrency: 10,
|
||||||
|
l0Api: {
|
||||||
|
provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'Qwen/Qwen3-8B', modelCache: [],
|
||||||
|
providers: {
|
||||||
|
siliconflow: createDefaultProviderProfile('siliconflow', 'Qwen/Qwen3-8B'),
|
||||||
|
openrouter: createDefaultProviderProfile('openrouter', 'Qwen/Qwen3-8B'),
|
||||||
|
custom: createDefaultProviderProfile('custom', 'Qwen/Qwen3-8B'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
embeddingApi: {
|
||||||
|
provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'BAAI/bge-m3', modelCache: [],
|
||||||
|
providers: {
|
||||||
|
siliconflow: createDefaultProviderProfile('siliconflow', 'BAAI/bge-m3'),
|
||||||
|
custom: createDefaultProviderProfile('custom', 'BAAI/bge-m3'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rerankApi: {
|
||||||
|
provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'BAAI/bge-reranker-v2-m3', modelCache: [],
|
||||||
|
providers: {
|
||||||
|
siliconflow: createDefaultProviderProfile('siliconflow', 'BAAI/bge-reranker-v2-m3'),
|
||||||
|
custom: createDefaultProviderProfile('custom', 'BAAI/bge-reranker-v2-m3'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] };
|
let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] };
|
||||||
@@ -369,6 +462,45 @@ All checks passed. Beginning incremental extraction...
|
|||||||
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type, ...data }, PARENT_ORIGIN);
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type, ...data }, PARENT_ORIGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeVectorConfigUI(raw = null) {
|
||||||
|
const base = JSON.parse(JSON.stringify(config.vector));
|
||||||
|
const legacyOnline = raw?.online || {};
|
||||||
|
const sharedKey = String(legacyOnline.key || '').trim();
|
||||||
|
const sharedUrl = String(legacyOnline.url || '').trim();
|
||||||
|
|
||||||
|
if (raw) {
|
||||||
|
base.enabled = !!raw.enabled;
|
||||||
|
base.engine = 'online';
|
||||||
|
base.l0Concurrency = Math.max(1, Math.min(50, Number(raw.l0Concurrency) || 10));
|
||||||
|
Object.assign(base.l0Api, {
|
||||||
|
provider: raw.l0Api?.provider || legacyOnline.provider || base.l0Api.provider,
|
||||||
|
url: raw.l0Api?.url || sharedUrl || base.l0Api.url,
|
||||||
|
key: raw.l0Api?.key || sharedKey || base.l0Api.key,
|
||||||
|
model: raw.l0Api?.model || base.l0Api.model,
|
||||||
|
modelCache: Array.isArray(raw.l0Api?.modelCache) ? raw.l0Api.modelCache : [],
|
||||||
|
providers: normalizeProviderProfiles('l0', raw.l0Api || {}),
|
||||||
|
});
|
||||||
|
Object.assign(base.embeddingApi, {
|
||||||
|
provider: raw.embeddingApi?.provider || base.embeddingApi.provider,
|
||||||
|
url: raw.embeddingApi?.url || sharedUrl || base.embeddingApi.url,
|
||||||
|
key: raw.embeddingApi?.key || sharedKey || base.embeddingApi.key,
|
||||||
|
model: raw.embeddingApi?.model || legacyOnline.model || base.embeddingApi.model,
|
||||||
|
modelCache: Array.isArray(raw.embeddingApi?.modelCache) ? raw.embeddingApi.modelCache : [],
|
||||||
|
providers: normalizeProviderProfiles('embedding', raw.embeddingApi || {}),
|
||||||
|
});
|
||||||
|
Object.assign(base.rerankApi, {
|
||||||
|
provider: raw.rerankApi?.provider || base.rerankApi.provider,
|
||||||
|
url: raw.rerankApi?.url || sharedUrl || base.rerankApi.url,
|
||||||
|
key: raw.rerankApi?.key || sharedKey || base.rerankApi.key,
|
||||||
|
model: raw.rerankApi?.model || base.rerankApi.model,
|
||||||
|
modelCache: Array.isArray(raw.rerankApi?.modelCache) ? raw.rerankApi.modelCache : [],
|
||||||
|
providers: normalizeProviderProfiles('rerank', raw.rerankApi || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// Config Management
|
// Config Management
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -387,7 +519,7 @@ All checks passed. Beginning incremental extraction...
|
|||||||
config.textFilterRules = Array.isArray(p.textFilterRules)
|
config.textFilterRules = Array.isArray(p.textFilterRules)
|
||||||
? p.textFilterRules
|
? p.textFilterRules
|
||||||
: (Array.isArray(p.vector?.textFilterRules) ? p.vector.textFilterRules : [...DEFAULT_FILTER_RULES]);
|
: (Array.isArray(p.vector?.textFilterRules) ? p.vector.textFilterRules : [...DEFAULT_FILTER_RULES]);
|
||||||
if (p.vector) config.vector = p.vector;
|
if (p.vector) config.vector = normalizeVectorConfigUI(p.vector);
|
||||||
if (config.trigger.timing === 'manual' && config.trigger.enabled) {
|
if (config.trigger.timing === 'manual' && config.trigger.enabled) {
|
||||||
config.trigger.enabled = false;
|
config.trigger.enabled = false;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
@@ -409,7 +541,7 @@ All checks passed. Beginning incremental extraction...
|
|||||||
: (Array.isArray(cfg.vector?.textFilterRules)
|
: (Array.isArray(cfg.vector?.textFilterRules)
|
||||||
? cfg.vector.textFilterRules
|
? cfg.vector.textFilterRules
|
||||||
: (Array.isArray(config.textFilterRules) ? config.textFilterRules : [...DEFAULT_FILTER_RULES]));
|
: (Array.isArray(config.textFilterRules) ? config.textFilterRules : [...DEFAULT_FILTER_RULES]));
|
||||||
if (cfg.vector) config.vector = cfg.vector;
|
if (cfg.vector) config.vector = normalizeVectorConfigUI(cfg.vector);
|
||||||
if (config.trigger.timing === 'manual') config.trigger.enabled = false;
|
if (config.trigger.timing === 'manual') config.trigger.enabled = false;
|
||||||
localStorage.setItem('summary_panel_config', JSON.stringify(config));
|
localStorage.setItem('summary_panel_config', JSON.stringify(config));
|
||||||
}
|
}
|
||||||
@@ -422,7 +554,14 @@ All checks passed. Beginning incremental extraction...
|
|||||||
config.textFilterRules = collectFilterRules();
|
config.textFilterRules = collectFilterRules();
|
||||||
}
|
}
|
||||||
if (!config.vector) {
|
if (!config.vector) {
|
||||||
config.vector = { enabled: false, engine: 'online', online: { provider: 'siliconflow', key: '', model: 'BAAI/bge-m3' } };
|
config.vector = {
|
||||||
|
enabled: false,
|
||||||
|
engine: 'online',
|
||||||
|
l0Concurrency: 10,
|
||||||
|
l0Api: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'Qwen/Qwen3-8B', modelCache: [] },
|
||||||
|
embeddingApi: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'BAAI/bge-m3', modelCache: [] },
|
||||||
|
rerankApi: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'BAAI/bge-reranker-v2-m3', modelCache: [] }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
localStorage.setItem('summary_panel_config', JSON.stringify(config));
|
localStorage.setItem('summary_panel_config', JSON.stringify(config));
|
||||||
postMsg('SAVE_PANEL_CONFIG', { config });
|
postMsg('SAVE_PANEL_CONFIG', { config });
|
||||||
@@ -435,15 +574,160 @@ All checks passed. Beginning incremental extraction...
|
|||||||
// Vector Config UI
|
// Vector Config UI
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function getVectorApiConfig(prefix) {
|
||||||
|
const provider = $(`${prefix}-api-provider`)?.value || 'siliconflow';
|
||||||
|
const providers = normalizeProviderProfiles(prefix, config.vector?.[`${prefix}Api`] || {});
|
||||||
|
providers[provider] = {
|
||||||
|
url: $(`${prefix}-api-url`)?.value?.trim() || '',
|
||||||
|
key: $(`${prefix}-api-key`)?.value?.trim() || '',
|
||||||
|
model: $(`${prefix}-api-model-text`)?.value?.trim() || '',
|
||||||
|
modelCache: Array.isArray(config.vector?.[`${prefix}Api`]?.providers?.[provider]?.modelCache)
|
||||||
|
? [...config.vector[`${prefix}Api`].providers[provider].modelCache]
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
url: providers[provider]?.url || '',
|
||||||
|
key: providers[provider]?.key || '',
|
||||||
|
model: providers[provider]?.model || '',
|
||||||
|
modelCache: Array.isArray(providers[provider]?.modelCache) ? [...providers[provider].modelCache] : [],
|
||||||
|
providers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVectorApiConfig(prefix, cfg) {
|
||||||
|
const next = cfg || {};
|
||||||
|
const provider = next.provider || 'siliconflow';
|
||||||
|
const profiles = normalizeProviderProfiles(prefix, next);
|
||||||
|
const profile = profiles[provider] || createDefaultProviderProfile(provider, VECTOR_API_DEFAULT_MODELS[prefix]);
|
||||||
|
$(`${prefix}-api-provider`).value = provider;
|
||||||
|
$(`${prefix}-api-url`).value = profile.url || '';
|
||||||
|
$(`${prefix}-api-key`).value = profile.key || '';
|
||||||
|
$(`${prefix}-api-model-text`).value = profile.model || '';
|
||||||
|
|
||||||
|
const cache = Array.isArray(profile.modelCache) ? profile.modelCache : [];
|
||||||
|
setSelectOptions($(`${prefix}-api-model-select`), cache, '请选择');
|
||||||
|
$(`${prefix}-api-model-select`).value = cache.includes(profile.model) ? profile.model : '';
|
||||||
|
updateVectorProviderUI(prefix, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCurrentVectorApiProfile(prefix, providerOverride = null) {
|
||||||
|
const apiCfg = config.vector[`${prefix}Api`] ||= {};
|
||||||
|
const provider = providerOverride || $(`${prefix}-api-provider`)?.value || apiCfg.provider || 'siliconflow';
|
||||||
|
apiCfg.providers = normalizeProviderProfiles(prefix, apiCfg);
|
||||||
|
apiCfg.providers[provider] = {
|
||||||
|
url: $(`${prefix}-api-url`)?.value?.trim() || '',
|
||||||
|
key: $(`${prefix}-api-key`)?.value?.trim() || '',
|
||||||
|
model: $(`${prefix}-api-model-text`)?.value?.trim() || '',
|
||||||
|
modelCache: Array.isArray(apiCfg.providers?.[provider]?.modelCache) ? [...apiCfg.providers[provider].modelCache] : [],
|
||||||
|
};
|
||||||
|
apiCfg.provider = provider;
|
||||||
|
apiCfg.url = apiCfg.providers[provider].url;
|
||||||
|
apiCfg.key = apiCfg.providers[provider].key;
|
||||||
|
apiCfg.model = apiCfg.providers[provider].model;
|
||||||
|
apiCfg.modelCache = [...apiCfg.providers[provider].modelCache];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVectorProviderUI(prefix, provider) {
|
||||||
|
const pv = VECTOR_PROVIDER_DEFAULTS[provider] || VECTOR_PROVIDER_DEFAULTS.custom;
|
||||||
|
const apiCfg = config.vector?.[`${prefix}Api`] || {};
|
||||||
|
apiCfg.providers = normalizeProviderProfiles(prefix, apiCfg);
|
||||||
|
const profile = apiCfg.providers[provider] || createDefaultProviderProfile(provider, VECTOR_API_DEFAULT_MODELS[prefix]);
|
||||||
|
const cache = Array.isArray(profile.modelCache) ? profile.modelCache : [];
|
||||||
|
const hasModelCache = cache.length > 0;
|
||||||
|
|
||||||
|
$(`${prefix}-api-url-row`).classList.toggle('hidden', false);
|
||||||
|
$(`${prefix}-api-key-row`).classList.toggle('hidden', !pv.needKey);
|
||||||
|
$(`${prefix}-api-model-manual-row`).classList.toggle('hidden', false);
|
||||||
|
$(`${prefix}-api-model-select-row`).classList.toggle('hidden', !hasModelCache);
|
||||||
|
$(`${prefix}-api-connect-row`).classList.toggle('hidden', !pv.canFetch);
|
||||||
|
$(`${prefix}-api-connect-status`).classList.toggle('hidden', !pv.canFetch);
|
||||||
|
|
||||||
|
const urlInput = $(`${prefix}-api-url`);
|
||||||
|
if (urlInput) {
|
||||||
|
if (provider === 'custom') {
|
||||||
|
urlInput.readOnly = false;
|
||||||
|
urlInput.placeholder = 'https://your-openai-compatible-api/v1';
|
||||||
|
urlInput.value = profile.url || '';
|
||||||
|
} else {
|
||||||
|
urlInput.value = pv.url || '';
|
||||||
|
urlInput.readOnly = true;
|
||||||
|
urlInput.placeholder = pv.url || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$(`${prefix}-api-key`).value = profile.key || '';
|
||||||
|
$(`${prefix}-api-model-text`).value = profile.model || '';
|
||||||
|
setSelectOptions($(`${prefix}-api-model-select`), cache, '请选择');
|
||||||
|
$(`${prefix}-api-model-select`).value = cache.includes(profile.model) ? profile.model : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveVectorApiSection(prefix) {
|
||||||
|
saveCurrentVectorApiProfile(prefix);
|
||||||
|
saveConfig();
|
||||||
|
setStatusText($(`${prefix}-api-connect-status`), '此组配置已保存', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchVectorModels(prefix) {
|
||||||
|
const provider = $(`${prefix}-api-provider`).value;
|
||||||
|
const pv = VECTOR_PROVIDER_DEFAULTS[provider] || VECTOR_PROVIDER_DEFAULTS.custom;
|
||||||
|
const statusEl = $(`${prefix}-api-connect-status`);
|
||||||
|
const btn = $(`${prefix}-btn-connect`);
|
||||||
|
if (!pv.canFetch) {
|
||||||
|
statusEl.textContent = '当前渠道不支持自动拉取模型';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseUrl = $(`${prefix}-api-url`).value.trim().replace(/\/+$/, '');
|
||||||
|
const apiKey = $(`${prefix}-api-key`).value.trim();
|
||||||
|
if (!apiKey) {
|
||||||
|
statusEl.textContent = '请先填写 API KEY';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '连接中...';
|
||||||
|
statusEl.textContent = '连接中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tryFetch = async url => {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' }
|
||||||
|
});
|
||||||
|
return res.ok ? (await res.json())?.data?.map(m => m?.id).filter(Boolean) || null : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (baseUrl.endsWith('/v1')) baseUrl = baseUrl.slice(0, -3);
|
||||||
|
let models = await tryFetch(`${baseUrl}/v1/models`);
|
||||||
|
if (!models) models = await tryFetch(`${baseUrl}/models`);
|
||||||
|
if (!models?.length) throw new Error('未获取到模型列表');
|
||||||
|
|
||||||
|
const apiCfg = config.vector[`${prefix}Api`] ||= {};
|
||||||
|
apiCfg.providers = normalizeProviderProfiles(prefix, apiCfg);
|
||||||
|
apiCfg.providers[provider] ||= createDefaultProviderProfile(provider, VECTOR_API_DEFAULT_MODELS[prefix]);
|
||||||
|
apiCfg.providers[provider].modelCache = [...new Set(models)];
|
||||||
|
setSelectOptions($(`${prefix}-api-model-select`), apiCfg.providers[provider].modelCache, '请选择');
|
||||||
|
$(`${prefix}-api-model-select-row`).classList.remove('hidden');
|
||||||
|
if (!$(`${prefix}-api-model-text`).value.trim()) {
|
||||||
|
$(`${prefix}-api-model-text`).value = models[0];
|
||||||
|
$(`${prefix}-api-model-select`).value = models[0];
|
||||||
|
}
|
||||||
|
statusEl.textContent = `拉取成功:${models.length} 个模型`;
|
||||||
|
} catch (e) {
|
||||||
|
statusEl.textContent = '拉取失败:' + (e.message || '请检查 URL 和 KEY');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '连接 / 拉取模型列表';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getVectorConfig() {
|
function getVectorConfig() {
|
||||||
return {
|
return {
|
||||||
enabled: $('vector-enabled')?.checked || false,
|
enabled: $('vector-enabled')?.checked || false,
|
||||||
engine: 'online',
|
engine: 'online',
|
||||||
online: {
|
l0Concurrency: Math.max(1, Math.min(50, Number($('vector-l0-concurrency')?.value) || 10)),
|
||||||
provider: 'siliconflow',
|
l0Api: getVectorApiConfig('l0'),
|
||||||
key: $('vector-api-key')?.value?.trim() || '',
|
embeddingApi: getVectorApiConfig('embedding'),
|
||||||
model: 'BAAI/bge-m3',
|
rerankApi: getVectorApiConfig('rerank'),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,11 +735,10 @@ All checks passed. Beginning incremental extraction...
|
|||||||
if (!cfg) return;
|
if (!cfg) return;
|
||||||
$('vector-enabled').checked = !!cfg.enabled;
|
$('vector-enabled').checked = !!cfg.enabled;
|
||||||
$('vector-config-area').classList.toggle('hidden', !cfg.enabled);
|
$('vector-config-area').classList.toggle('hidden', !cfg.enabled);
|
||||||
|
$('vector-l0-concurrency').value = String(Math.max(1, Math.min(50, Number(cfg.l0Concurrency) || 10)));
|
||||||
if (cfg.online?.key) {
|
loadVectorApiConfig('l0', cfg.l0Api || {});
|
||||||
$('vector-api-key').value = cfg.online.key;
|
loadVectorApiConfig('embedding', cfg.embeddingApi || {});
|
||||||
}
|
loadVectorApiConfig('rerank', cfg.rerankApi || {});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -536,13 +819,6 @@ All checks passed. Beginning incremental extraction...
|
|||||||
el.textContent = count;
|
el.textContent = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOnlineStatus(status, message) {
|
|
||||||
const dot = $('online-api-status').querySelector('.status-dot');
|
|
||||||
const text = $('online-api-status').querySelector('.status-text');
|
|
||||||
dot.className = 'status-dot ' + status;
|
|
||||||
text.textContent = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateVectorStats(stats) {
|
function updateVectorStats(stats) {
|
||||||
$('vector-atom-count').textContent = stats.stateVectors || 0;
|
$('vector-atom-count').textContent = stats.stateVectors || 0;
|
||||||
$('vector-chunk-count').textContent = stats.chunkCount || 0;
|
$('vector-chunk-count').textContent = stats.chunkCount || 0;
|
||||||
@@ -646,16 +922,45 @@ All checks passed. Beginning incremental extraction...
|
|||||||
$('vector-config-area').classList.toggle('hidden', !e.target.checked);
|
$('vector-config-area').classList.toggle('hidden', !e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
$('btn-test-vector-api').onclick = () => {
|
['l0', 'embedding', 'rerank'].forEach(prefix => {
|
||||||
saveConfig(); // 先保存新 Key 到 localStorage
|
$(`${prefix}-api-key-toggle`).onclick = () => {
|
||||||
|
const input = $(`${prefix}-api-key`);
|
||||||
|
const btn = $(`${prefix}-api-key-toggle`);
|
||||||
|
if (!input || !btn) return;
|
||||||
|
const show = input.type === 'password';
|
||||||
|
input.type = show ? 'text' : 'password';
|
||||||
|
btn.textContent = show ? '隐藏' : '显示';
|
||||||
|
};
|
||||||
|
|
||||||
|
$(`${prefix}-api-provider`).onchange = e => {
|
||||||
|
const target = config.vector[`${prefix}Api`] ||= {};
|
||||||
|
const previousProvider = target.provider || 'siliconflow';
|
||||||
|
saveCurrentVectorApiProfile(prefix, previousProvider);
|
||||||
|
target.providers = normalizeProviderProfiles(prefix, target);
|
||||||
|
target.provider = e.target.value;
|
||||||
|
target.providers[e.target.value] ||= createDefaultProviderProfile(e.target.value, VECTOR_API_DEFAULT_MODELS[prefix]);
|
||||||
|
updateVectorProviderUI(prefix, e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
$(`${prefix}-api-model-select`).onchange = e => {
|
||||||
|
if (e.target.value) $(`${prefix}-api-model-text`).value = e.target.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
$(`${prefix}-btn-connect`).onclick = () => fetchVectorModels(prefix);
|
||||||
|
$(`${prefix}-btn-save`).onclick = () => saveVectorApiSection(prefix);
|
||||||
|
$(`${prefix}-btn-test`).onclick = () => {
|
||||||
|
const btn = $(`${prefix}-btn-test`);
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
setStatusText($(`${prefix}-api-connect-status`), '测试中...', 'loading');
|
||||||
|
saveConfig();
|
||||||
|
const cfg = getVectorConfig();
|
||||||
postMsg('VECTOR_TEST_ONLINE', {
|
postMsg('VECTOR_TEST_ONLINE', {
|
||||||
provider: 'siliconflow',
|
target: prefix,
|
||||||
config: {
|
provider: cfg[`${prefix}Api`].provider,
|
||||||
key: $('vector-api-key').value.trim(),
|
config: cfg[`${prefix}Api`],
|
||||||
model: 'BAAI/bge-m3',
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
$('btn-add-filter-rule').onclick = addFilterRule;
|
$('btn-add-filter-rule').onclick = addFilterRule;
|
||||||
|
|
||||||
@@ -701,6 +1006,17 @@ All checks passed. Beginning incremental extraction...
|
|||||||
postMsg('REQUEST_ANCHOR_STATS');
|
postMsg('REQUEST_ANCHOR_STATS');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateVectorOnlineStatus(target, status, message) {
|
||||||
|
const prefix = target || 'embedding';
|
||||||
|
const btn = $(`${prefix}-btn-test`);
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
setStatusText(
|
||||||
|
$(`${prefix}-api-connect-status`),
|
||||||
|
message || '',
|
||||||
|
status === 'error' ? 'error' : status === 'success' ? 'success' : 'loading'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function initSummaryIOUI() {
|
function initSummaryIOUI() {
|
||||||
$('btn-copy-summary').onclick = () => {
|
$('btn-copy-summary').onclick = () => {
|
||||||
$('btn-copy-summary').disabled = true;
|
$('btn-copy-summary').disabled = true;
|
||||||
@@ -1803,7 +2119,7 @@ All checks passed. Beginning incremental extraction...
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'VECTOR_ONLINE_STATUS':
|
case 'VECTOR_ONLINE_STATUS':
|
||||||
updateOnlineStatus(d.status, d.message);
|
updateVectorOnlineStatus(d.target, d.status, d.message);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'VECTOR_STATS':
|
case 'VECTOR_STATS':
|
||||||
|
|||||||
@@ -414,31 +414,180 @@
|
|||||||
|
|
||||||
<div id="vector-config-area" class="hidden">
|
<div id="vector-config-area" class="hidden">
|
||||||
|
|
||||||
<!-- Step 0: API Key -->
|
<!-- Step 0: API Config -->
|
||||||
<div class="neo-card" style="margin-top: 24px;">
|
<div class="neo-card" style="margin-top: 24px;">
|
||||||
<div class="neo-card-title">
|
<div class="neo-card-title">
|
||||||
<span class="neo-badge">Step.0</span>
|
<span class="neo-badge">Step.0</span>
|
||||||
<span>填写 API Key</span>
|
<span>配置模型 API</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-field full">
|
<div class="settings-field full">
|
||||||
<label>硅基流动 API Key</label>
|
<div class="settings-hint" style="margin-bottom:12px;">
|
||||||
<input type="password" id="vector-api-key" placeholder="sk-xxx">
|
推荐给 L0 使用便宜或免费的高并发渠道。<br>
|
||||||
<div class="settings-hint">
|
自定义渠道请填写 OpenAI 兼容基础地址(通常为 /v1)。Embedding 会自动补 <code>/embeddings</code>,Rerank 会自动补 <code>/rerank</code>。
|
||||||
💡 <a href="https://siliconflow.cn" target="_blank">硅基流动</a>
|
|
||||||
内置使用的模型完全免费。建议实名认证以获得更高并发。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<details class="settings-collapse" id="l0-api-collapse" style="margin-bottom:12px;">
|
||||||
|
<summary class="settings-collapse-header">
|
||||||
|
<span>L0 锚点提取模型</span>
|
||||||
|
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="settings-collapse-content">
|
||||||
|
<label>L0 并发数</label>
|
||||||
|
<input type="number" id="vector-l0-concurrency" min="1" max="50" step="1" value="10">
|
||||||
|
<div class="settings-hint" style="margin-bottom:16px;">请视 API 情况自行调整。</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
|
<div class="settings-field">
|
||||||
|
<label>渠道</label>
|
||||||
|
<select id="l0-api-provider">
|
||||||
|
<option value="siliconflow">硅基流动</option>
|
||||||
|
<option value="openrouter">OpenRouter</option>
|
||||||
|
<option value="custom">自定义</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="l0-api-url-row">
|
||||||
<div class="settings-field full">
|
<div class="settings-field full">
|
||||||
<div class="engine-status-row" style="margin-top: 0;">
|
<label>API URL</label>
|
||||||
<div class="engine-status" id="online-api-status">
|
<input type="text" id="l0-api-url" placeholder="https://api.siliconflow.cn/v1">
|
||||||
<span class="status-dot"></span>
|
|
||||||
<span class="status-text">未测试</span>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm" id="btn-test-vector-api">测试连接</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row" id="l0-api-key-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>API KEY</label>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<input type="password" id="l0-api-key" placeholder="sk-xxx" style="flex:1;">
|
||||||
|
<button type="button" class="btn btn-sm" id="l0-api-key-toggle" style="flex:0 0 auto; min-width:64px;">显示</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="l0-api-model-manual-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>模型名</label>
|
||||||
|
<input type="text" id="l0-api-model-text" placeholder="可手动填写,如 Qwen/Qwen3-8B">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row hidden" id="l0-api-model-select-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>已拉取模型</label>
|
||||||
|
<select id="l0-api-model-select"><option value="">请选择</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-btn-row" id="l0-api-connect-row" style="margin-bottom:8px;">
|
||||||
|
<button class="btn btn-sm" id="l0-btn-connect" style="flex:1">拉取</button>
|
||||||
|
<button class="btn btn-sm" id="l0-btn-test" style="flex:1">测试</button>
|
||||||
|
<button class="btn btn-sm btn-p" id="l0-btn-save" style="flex:1">保存</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-hint" id="l0-api-connect-status" style="margin-bottom:16px;"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="settings-collapse" id="embedding-api-collapse" style="margin-bottom:12px;">
|
||||||
|
<summary class="settings-collapse-header">
|
||||||
|
<span>Embedding 模型</span>
|
||||||
|
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="settings-collapse-content">
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-field">
|
||||||
|
<label>渠道</label>
|
||||||
|
<select id="embedding-api-provider">
|
||||||
|
<option value="siliconflow">硅基流动</option>
|
||||||
|
<option value="custom">自定义</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="embedding-api-url-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>API URL</label>
|
||||||
|
<input type="text" id="embedding-api-url" placeholder="https://api.siliconflow.cn/v1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="embedding-api-key-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>API KEY</label>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<input type="password" id="embedding-api-key" placeholder="sk-xxx" style="flex:1;">
|
||||||
|
<button type="button" class="btn btn-sm" id="embedding-api-key-toggle" style="flex:0 0 auto; min-width:64px;">显示</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="embedding-api-model-manual-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>模型名</label>
|
||||||
|
<input type="text" id="embedding-api-model-text" placeholder="如 BAAI/bge-m3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row hidden" id="embedding-api-model-select-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>已拉取模型</label>
|
||||||
|
<select id="embedding-api-model-select"><option value="">请选择</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-btn-row" id="embedding-api-connect-row" style="margin-bottom:8px;">
|
||||||
|
<button class="btn btn-sm" id="embedding-btn-connect" style="flex:1">拉取</button>
|
||||||
|
<button class="btn btn-sm" id="embedding-btn-test" style="flex:1">测试</button>
|
||||||
|
<button class="btn btn-sm btn-p" id="embedding-btn-save" style="flex:1">保存</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-hint" id="embedding-api-connect-status" style="margin-bottom:16px;"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="settings-collapse" id="rerank-api-collapse">
|
||||||
|
<summary class="settings-collapse-header">
|
||||||
|
<span>Rerank 模型</span>
|
||||||
|
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="settings-collapse-content">
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-field">
|
||||||
|
<label>渠道</label>
|
||||||
|
<select id="rerank-api-provider">
|
||||||
|
<option value="siliconflow">硅基流动</option>
|
||||||
|
<option value="custom">自定义</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="rerank-api-url-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>API URL</label>
|
||||||
|
<input type="text" id="rerank-api-url" placeholder="https://api.siliconflow.cn/v1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="rerank-api-key-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>API KEY</label>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<input type="password" id="rerank-api-key" placeholder="sk-xxx" style="flex:1;">
|
||||||
|
<button type="button" class="btn btn-sm" id="rerank-api-key-toggle" style="flex:0 0 auto; min-width:64px;">显示</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="rerank-api-model-manual-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>模型名</label>
|
||||||
|
<input type="text" id="rerank-api-model-text" placeholder="如 BAAI/bge-reranker-v2-m3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row hidden" id="rerank-api-model-select-row">
|
||||||
|
<div class="settings-field full">
|
||||||
|
<label>已拉取模型</label>
|
||||||
|
<select id="rerank-api-model-select"><option value="">请选择</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-btn-row" id="rerank-api-connect-row" style="margin-bottom:8px;">
|
||||||
|
<button class="btn btn-sm" id="rerank-btn-connect" style="flex:1">拉取</button>
|
||||||
|
<button class="btn btn-sm" id="rerank-btn-test" style="flex:1">测试</button>
|
||||||
|
<button class="btn btn-sm btn-p" id="rerank-btn-save" style="flex:1">保存</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-hint" id="rerank-api-connect-status"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ import { runSummaryGeneration } from "./generate/generator.js";
|
|||||||
|
|
||||||
// vector service
|
// vector service
|
||||||
import { embed, getEngineFingerprint, testOnlineService } from "./vector/utils/embedder.js";
|
import { embed, getEngineFingerprint, testOnlineService } from "./vector/utils/embedder.js";
|
||||||
|
import { testL0Service } from "./vector/llm/llm-service.js";
|
||||||
|
import { testRerankService } from "./vector/llm/reranker.js";
|
||||||
|
|
||||||
// tokenizer
|
// tokenizer
|
||||||
import { preload as preloadTokenizer, injectEntities, isReady as isTokenizerReady } from "./vector/utils/tokenizer.js";
|
import { preload as preloadTokenizer, injectEntities, isReady as isTokenizerReady } from "./vector/utils/tokenizer.js";
|
||||||
@@ -356,8 +358,8 @@ async function handleAnchorGenerate() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vectorCfg.online?.key) {
|
if (!vectorCfg.l0Api?.key) {
|
||||||
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置 API Key" });
|
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置 L0 API Key" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,17 +423,23 @@ function handleAnchorCancel() {
|
|||||||
postToFrame({ type: "ANCHOR_GEN_PROGRESS", current: -1, total: 0 });
|
postToFrame({ type: "ANCHOR_GEN_PROGRESS", current: -1, total: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTestOnlineService(provider, config) {
|
async function handleTestOnlineService(provider, config, target = "embedding") {
|
||||||
try {
|
try {
|
||||||
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "连接中..." });
|
postToFrame({ type: "VECTOR_ONLINE_STATUS", target, status: "downloading", message: "连接中..." });
|
||||||
const result = await testOnlineService(provider, config);
|
let result;
|
||||||
|
if (target === "l0") result = await testL0Service(config);
|
||||||
|
else if (target === "rerank") result = await testRerankService(config);
|
||||||
|
else result = await testOnlineService(provider, config);
|
||||||
postToFrame({
|
postToFrame({
|
||||||
type: "VECTOR_ONLINE_STATUS",
|
type: "VECTOR_ONLINE_STATUS",
|
||||||
|
target,
|
||||||
status: "success",
|
status: "success",
|
||||||
message: `连接成功 (${result.dims}维)`,
|
message: target === "embedding"
|
||||||
|
? `连接成功 (${result.dims}维)`
|
||||||
|
: (result.message || "连接成功"),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message });
|
postToFrame({ type: "VECTOR_ONLINE_STATUS", target, status: "error", message: e.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,8 +456,8 @@ async function handleGenerateVectors(vectorCfg) {
|
|||||||
const { chatId, chat } = getContext();
|
const { chatId, chat } = getContext();
|
||||||
if (!chatId || !chat?.length) return;
|
if (!chatId || !chat?.length) return;
|
||||||
|
|
||||||
if (!vectorCfg.online?.key) {
|
if (!vectorCfg.embeddingApi?.key) {
|
||||||
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置 API Key" });
|
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置 Embedding API Key" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1631,7 +1639,7 @@ async function handleFrameMessage(event) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "VECTOR_TEST_ONLINE":
|
case "VECTOR_TEST_ONLINE":
|
||||||
handleTestOnlineService(data.provider, data.config);
|
handleTestOnlineService(data.provider, data.config, data.target || "embedding");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "VECTOR_GENERATE":
|
case "VECTOR_GENERATE":
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
// 每楼层 1-2 个场景锚点(非碎片原子),60-100 字场景摘要
|
// 每楼层 1-2 个场景锚点(非碎片原子),60-100 字场景摘要
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import { callLLM, parseJson } from './llm-service.js';
|
import { callLLM, cancelAllL0Requests, parseJson } from './llm-service.js';
|
||||||
import { xbLog } from '../../../../core/debug-core.js';
|
import { xbLog } from '../../../../core/debug-core.js';
|
||||||
import { filterText } from '../utils/text-filter.js';
|
import { filterText } from '../utils/text-filter.js';
|
||||||
|
|
||||||
const MODULE_ID = 'atom-extraction';
|
const MODULE_ID = 'atom-extraction';
|
||||||
|
|
||||||
const CONCURRENCY = 10;
|
const CONCURRENCY = 10;
|
||||||
const RETRY_COUNT = 2;
|
const RETRY_COUNT = 1;
|
||||||
const RETRY_DELAY = 500;
|
const RETRY_DELAY = 500;
|
||||||
const DEFAULT_TIMEOUT = 40000;
|
const DEFAULT_TIMEOUT = 40000;
|
||||||
const STAGGER_DELAY = 80;
|
const STAGGER_DELAY = 80;
|
||||||
@@ -25,6 +25,7 @@ let batchCancelled = false;
|
|||||||
|
|
||||||
export function cancelBatchExtraction() {
|
export function cancelBatchExtraction() {
|
||||||
batchCancelled = true;
|
batchCancelled = true;
|
||||||
|
cancelAllL0Requests();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBatchCancelled() {
|
export function isBatchCancelled() {
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
// vector/llm/llm-service.js - 修复 prefill 传递方式
|
// vector/llm/llm-service.js - 修复 prefill 传递方式
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
import { xbLog } from '../../../../core/debug-core.js';
|
import { xbLog } from '../../../../core/debug-core.js';
|
||||||
import { getApiKey } from './siliconflow.js';
|
import { getVectorConfig } from '../../data/config.js';
|
||||||
|
|
||||||
const MODULE_ID = 'vector-llm-service';
|
const MODULE_ID = 'vector-llm-service';
|
||||||
const SILICONFLOW_API_URL = 'https://api.siliconflow.cn/v1';
|
|
||||||
const DEFAULT_L0_MODEL = 'Qwen/Qwen3-8B';
|
const DEFAULT_L0_MODEL = 'Qwen/Qwen3-8B';
|
||||||
|
const DEFAULT_L0_API_URL = 'https://api.siliconflow.cn/v1';
|
||||||
|
|
||||||
let callCounter = 0;
|
let callCounter = 0;
|
||||||
|
const activeL0SessionIds = new Set();
|
||||||
|
let l0KeyIndex = 0;
|
||||||
|
|
||||||
function getStreamingModule() {
|
function getStreamingModule() {
|
||||||
const mod = window.xiaobaixStreamingGeneration;
|
const mod = window.xiaobaixStreamingGeneration;
|
||||||
@@ -27,6 +29,39 @@ function b64UrlEncode(str) {
|
|||||||
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getL0ApiConfig() {
|
||||||
|
const cfg = getVectorConfig() || {};
|
||||||
|
return cfg.l0Api || {
|
||||||
|
provider: 'siliconflow',
|
||||||
|
url: DEFAULT_L0_API_URL,
|
||||||
|
key: '',
|
||||||
|
model: DEFAULT_L0_MODEL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeL0ApiConfig(apiConfig = null) {
|
||||||
|
const fallback = getL0ApiConfig();
|
||||||
|
const next = apiConfig || {};
|
||||||
|
return {
|
||||||
|
provider: String(next.provider || fallback.provider || 'siliconflow').trim(),
|
||||||
|
url: String(next.url || fallback.url || DEFAULT_L0_API_URL).trim(),
|
||||||
|
key: String(next.key || fallback.key || '').trim(),
|
||||||
|
model: String(next.model || fallback.model || DEFAULT_L0_MODEL).trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextKey(rawKey) {
|
||||||
|
const keys = String(rawKey || '')
|
||||||
|
.split(/[,;|\n]+/)
|
||||||
|
.map(k => k.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!keys.length) return '';
|
||||||
|
if (keys.length === 1) return keys[0];
|
||||||
|
const idx = l0KeyIndex % keys.length;
|
||||||
|
l0KeyIndex = (l0KeyIndex + 1) % keys.length;
|
||||||
|
return keys[idx];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一LLM调用 - 走酒馆后端(非流式)
|
* 统一LLM调用 - 走酒馆后端(非流式)
|
||||||
* 临时改为标准 messages 调用,避免 bottomassistant prefill 兼容性问题。
|
* 临时改为标准 messages 调用,避免 bottomassistant prefill 兼容性问题。
|
||||||
@@ -36,12 +71,14 @@ export async function callLLM(messages, options = {}) {
|
|||||||
temperature = 0.2,
|
temperature = 0.2,
|
||||||
max_tokens = 500,
|
max_tokens = 500,
|
||||||
timeout = 40000,
|
timeout = 40000,
|
||||||
|
apiConfig = null,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const mod = getStreamingModule();
|
const mod = getStreamingModule();
|
||||||
if (!mod) throw new Error('Streaming module not ready');
|
if (!mod) throw new Error('Streaming module not ready');
|
||||||
|
|
||||||
const apiKey = getApiKey() || '';
|
const apiCfg = normalizeL0ApiConfig(apiConfig);
|
||||||
|
const apiKey = getNextKey(apiCfg.key);
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error('L0 requires siliconflow API key');
|
throw new Error('L0 requires siliconflow API key');
|
||||||
}
|
}
|
||||||
@@ -59,16 +96,17 @@ export async function callLLM(messages, options = {}) {
|
|||||||
temperature: String(temperature),
|
temperature: String(temperature),
|
||||||
max_tokens: String(max_tokens),
|
max_tokens: String(max_tokens),
|
||||||
api: 'openai',
|
api: 'openai',
|
||||||
apiurl: SILICONFLOW_API_URL,
|
apiurl: String(apiCfg.url || DEFAULT_L0_API_URL).trim(),
|
||||||
apipassword: apiKey,
|
apipassword: apiKey,
|
||||||
model: DEFAULT_L0_MODEL,
|
model: String(apiCfg.model || DEFAULT_L0_MODEL).trim(),
|
||||||
};
|
};
|
||||||
const isQwen3 = String(DEFAULT_L0_MODEL || '').includes('Qwen3');
|
const isQwen3 = String(args.model || '').includes('Qwen3');
|
||||||
if (isQwen3) {
|
if (isQwen3) {
|
||||||
args.enable_thinking = 'false';
|
args.enable_thinking = 'false';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
activeL0SessionIds.add(uniqueId);
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
setTimeout(() => reject(new Error(`L0 request timeout after ${timeout}ms`)), timeout);
|
setTimeout(() => reject(new Error(`L0 request timeout after ${timeout}ms`)), timeout);
|
||||||
});
|
});
|
||||||
@@ -80,9 +118,38 @@ export async function callLLM(messages, options = {}) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
xbLog.error(MODULE_ID, 'LLM调用失败', e);
|
xbLog.error(MODULE_ID, 'LLM调用失败', e);
|
||||||
throw e;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
activeL0SessionIds.delete(uniqueId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function testL0Service(apiConfig = {}) {
|
||||||
|
if (!apiConfig?.key) {
|
||||||
|
throw new Error('请配置 L0 API Key');
|
||||||
|
}
|
||||||
|
const result = await callLLM([
|
||||||
|
{ role: 'system', content: '你是一个测试助手。请只输出 OK。' },
|
||||||
|
{ role: 'user', content: '只输出 OK' },
|
||||||
|
], {
|
||||||
|
apiConfig,
|
||||||
|
temperature: 0,
|
||||||
|
max_tokens: 16,
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
const text = String(result || '').trim();
|
||||||
|
if (!text) throw new Error('返回为空');
|
||||||
|
return { success: true, message: `连接成功:${text.slice(0, 60)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelAllL0Requests() {
|
||||||
|
const mod = getStreamingModule();
|
||||||
|
if (!mod?.cancel) return;
|
||||||
|
for (const sessionId of activeL0SessionIds) {
|
||||||
|
try { mod.cancel(sessionId); } catch {}
|
||||||
|
}
|
||||||
|
activeL0SessionIds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
export function parseJson(text) {
|
export function parseJson(text) {
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
let s = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
let s = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
||||||
|
|||||||
@@ -4,15 +4,38 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
import { xbLog } from '../../../../core/debug-core.js';
|
import { xbLog } from '../../../../core/debug-core.js';
|
||||||
import { getApiKey } from './siliconflow.js';
|
import { getVectorConfig } from '../../data/config.js';
|
||||||
|
|
||||||
const MODULE_ID = 'reranker';
|
const MODULE_ID = 'reranker';
|
||||||
const RERANK_URL = 'https://api.siliconflow.cn/v1/rerank';
|
const DEFAULT_RERANK_URL = 'https://api.siliconflow.cn/v1';
|
||||||
const RERANK_MODEL = 'BAAI/bge-reranker-v2-m3';
|
const RERANK_MODEL = 'BAAI/bge-reranker-v2-m3';
|
||||||
const DEFAULT_TIMEOUT = 15000;
|
const DEFAULT_TIMEOUT = 15000;
|
||||||
const MAX_DOCUMENTS = 100; // API 限制
|
const MAX_DOCUMENTS = 100; // API 限制
|
||||||
const RERANK_BATCH_SIZE = 20;
|
const RERANK_BATCH_SIZE = 20;
|
||||||
const RERANK_MAX_CONCURRENCY = 5;
|
const RERANK_MAX_CONCURRENCY = 5;
|
||||||
|
let rerankKeyIndex = 0;
|
||||||
|
|
||||||
|
function getRerankApiConfig() {
|
||||||
|
const cfg = getVectorConfig() || {};
|
||||||
|
return cfg.rerankApi || {
|
||||||
|
provider: 'siliconflow',
|
||||||
|
url: DEFAULT_RERANK_URL,
|
||||||
|
key: '',
|
||||||
|
model: RERANK_MODEL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextRerankKey(rawKey) {
|
||||||
|
const keys = String(rawKey || '')
|
||||||
|
.split(/[,;|\n]+/)
|
||||||
|
.map(k => k.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!keys.length) return '';
|
||||||
|
if (keys.length === 1) return keys[0];
|
||||||
|
const idx = rerankKeyIndex % keys.length;
|
||||||
|
rerankKeyIndex = (rerankKeyIndex + 1) % keys.length;
|
||||||
|
return keys[idx];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对文档列表进行 Rerank 精排
|
* 对文档列表进行 Rerank 精排
|
||||||
@@ -37,7 +60,8 @@ export async function rerank(query, documents, options = {}) {
|
|||||||
return { results: [], failed: false };
|
return { results: [], failed: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = getApiKey();
|
const apiCfg = getRerankApiConfig();
|
||||||
|
const key = getNextRerankKey(apiCfg.key);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
xbLog.warn(MODULE_ID, '未配置 API Key,跳过 rerank');
|
xbLog.warn(MODULE_ID, '未配置 API Key,跳过 rerank');
|
||||||
return { results: documents.map((_, i) => ({ index: i, relevance_score: 0 })), failed: true };
|
return { results: documents.map((_, i) => ({ index: i, relevance_score: 0 })), failed: true };
|
||||||
@@ -72,14 +96,15 @@ export async function rerank(query, documents, options = {}) {
|
|||||||
try {
|
try {
|
||||||
const T0 = performance.now();
|
const T0 = performance.now();
|
||||||
|
|
||||||
const response = await fetch(RERANK_URL, {
|
const baseUrl = String(apiCfg.url || DEFAULT_RERANK_URL).replace(/\/+$/, '');
|
||||||
|
const response = await fetch(`${baseUrl}/rerank`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${key}`,
|
'Authorization': `Bearer ${key}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: RERANK_MODEL,
|
model: String(apiCfg.model || RERANK_MODEL),
|
||||||
// Zero-darkbox: do not silently truncate query.
|
// Zero-darkbox: do not silently truncate query.
|
||||||
query,
|
query,
|
||||||
documents: validDocs,
|
documents: validDocs,
|
||||||
@@ -248,19 +273,51 @@ export async function rerankChunks(query, chunks, options = {}) {
|
|||||||
/**
|
/**
|
||||||
* 测试 Rerank 服务连接
|
* 测试 Rerank 服务连接
|
||||||
*/
|
*/
|
||||||
export async function testRerankService() {
|
export async function testRerankService(apiConfig = {}) {
|
||||||
const key = getApiKey();
|
const next = {
|
||||||
if (!key) {
|
provider: String(apiConfig.provider || 'siliconflow').trim(),
|
||||||
throw new Error('请配置硅基 API Key');
|
url: String(apiConfig.url || DEFAULT_RERANK_URL).trim(),
|
||||||
|
key: String(apiConfig.key || '').trim(),
|
||||||
|
model: String(apiConfig.model || RERANK_MODEL).trim(),
|
||||||
|
};
|
||||||
|
if (!next.key) {
|
||||||
|
throw new Error('请配置 Rerank API Key');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const key = getNextRerankKey(next.key);
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
||||||
try {
|
try {
|
||||||
const { results } = await rerank('测试查询', ['测试文档1', '测试文档2'], { topN: 2 });
|
const baseUrl = String(next.url || DEFAULT_RERANK_URL).replace(/\/+$/, '');
|
||||||
|
const response = await fetch(`${baseUrl}/rerank`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${key}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: next.model,
|
||||||
|
query: '测试查询',
|
||||||
|
documents: ['测试文档1', '测试文档2'],
|
||||||
|
top_n: 2,
|
||||||
|
return_documents: false,
|
||||||
|
}),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text().catch(() => '');
|
||||||
|
throw new Error(`Rerank API ${response.status}: ${errorText.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const results = Array.isArray(data.results) ? data.results : [];
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `连接成功,返回 ${results.length} 个结果`,
|
message: `连接成功:返回 ${results.length} 个结果`,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`连接失败: ${e.message}`);
|
throw new Error(`连接失败: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// siliconflow.js - Embedding + 多 Key 轮询
|
// siliconflow.js - OpenAI-compatible Embedding + 多 Key 轮询
|
||||||
//
|
//
|
||||||
// 在 API Key 输入框中用逗号、分号、竖线或换行分隔多个 Key,例如:
|
// 在 API Key 输入框中用逗号、分号、竖线或换行分隔多个 Key,例如:
|
||||||
// sk-aaa,sk-bbb,sk-ccc
|
// sk-aaa,sk-bbb,sk-ccc
|
||||||
// 每次调用自动轮询到下一个 Key,并发请求会均匀分布到所有 Key 上。
|
// 每次调用自动轮询到下一个 Key,并发请求会均匀分布到所有 Key 上。
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
import { getVectorConfig } from '../../data/config.js';
|
||||||
|
|
||||||
const BASE_URL = 'https://api.siliconflow.cn';
|
const BASE_URL = 'https://api.siliconflow.cn';
|
||||||
const EMBEDDING_MODEL = 'BAAI/bge-m3';
|
const EMBEDDING_MODEL = 'BAAI/bge-m3';
|
||||||
|
|
||||||
// ★ 多 Key 轮询状态
|
// ★ 多 Key 轮询状态
|
||||||
let _keyIndex = 0;
|
let _keyIndex = 0;
|
||||||
|
|
||||||
|
function getEmbeddingApiConfig() {
|
||||||
|
const cfg = getVectorConfig() || {};
|
||||||
|
return cfg.embeddingApi || {
|
||||||
|
provider: 'siliconflow',
|
||||||
|
url: `${BASE_URL}/v1`,
|
||||||
|
key: '',
|
||||||
|
model: EMBEDDING_MODEL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 localStorage 解析所有 Key(支持逗号、分号、竖线、换行分隔)
|
* 从 localStorage 解析所有 Key(支持逗号、分号、竖线、换行分隔)
|
||||||
*/
|
*/
|
||||||
function parseKeys() {
|
function parseKeys(rawKey) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem('summary_panel_config');
|
const keyStr = String(rawKey || '');
|
||||||
if (raw) {
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
const keyStr = parsed.vector?.online?.key || '';
|
|
||||||
return keyStr
|
return keyStr
|
||||||
.split(/[,;|\n]+/)
|
.split(/[,;|\n]+/)
|
||||||
.map(k => k.trim())
|
.map(k => k.trim())
|
||||||
.filter(k => k.length > 0);
|
.filter(k => k.length > 0);
|
||||||
}
|
|
||||||
} catch { }
|
} catch { }
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -34,8 +42,8 @@ function parseKeys() {
|
|||||||
* 获取下一个可用的 API Key(轮询)
|
* 获取下一个可用的 API Key(轮询)
|
||||||
* 每次调用返回不同的 Key,自动循环
|
* 每次调用返回不同的 Key,自动循环
|
||||||
*/
|
*/
|
||||||
export function getApiKey() {
|
export function getApiKey(rawKey = null) {
|
||||||
const keys = parseKeys();
|
const keys = parseKeys(rawKey ?? getEmbeddingApiConfig().key);
|
||||||
if (!keys.length) return null;
|
if (!keys.length) return null;
|
||||||
if (keys.length === 1) return keys[0];
|
if (keys.length === 1) return keys[0];
|
||||||
|
|
||||||
@@ -51,7 +59,7 @@ export function getApiKey() {
|
|||||||
* 获取当前配置的 Key 数量(供外部模块动态调整并发用)
|
* 获取当前配置的 Key 数量(供外部模块动态调整并发用)
|
||||||
*/
|
*/
|
||||||
export function getKeyCount() {
|
export function getKeyCount() {
|
||||||
return Math.max(1, parseKeys().length);
|
return Math.max(1, parseKeys(getEmbeddingApiConfig().key).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -61,22 +69,24 @@ export function getKeyCount() {
|
|||||||
export async function embed(texts, options = {}) {
|
export async function embed(texts, options = {}) {
|
||||||
if (!texts?.length) return [];
|
if (!texts?.length) return [];
|
||||||
|
|
||||||
const key = getApiKey();
|
const apiCfg = options.apiConfig || getEmbeddingApiConfig();
|
||||||
if (!key) throw new Error('未配置硅基 API Key');
|
const key = getApiKey(apiCfg.key);
|
||||||
|
if (!key) throw new Error('未配置 Embedding API Key');
|
||||||
|
|
||||||
const { timeout = 30000, signal } = options;
|
const { timeout = 30000, signal } = options;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${BASE_URL}/v1/embeddings`, {
|
const baseUrl = String(apiCfg.url || `${BASE_URL}/v1`).replace(/\/+$/, '');
|
||||||
|
const response = await fetch(`${baseUrl}/embeddings`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${key}`,
|
'Authorization': `Bearer ${key}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: EMBEDDING_MODEL,
|
model: String(apiCfg.model || EMBEDDING_MODEL),
|
||||||
input: texts,
|
input: texts,
|
||||||
}),
|
}),
|
||||||
signal: signal || controller.signal,
|
signal: signal || controller.signal,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { filterText } from '../utils/text-filter.js';
|
|||||||
const MODULE_ID = 'state-integration';
|
const MODULE_ID = 'state-integration';
|
||||||
|
|
||||||
// ★ 并发配置
|
// ★ 并发配置
|
||||||
const CONCURRENCY = 50;
|
const DEFAULT_CONCURRENCY = 10;
|
||||||
const STAGGER_DELAY = 15;
|
const STAGGER_DELAY = 15;
|
||||||
const DEBUG_CONCURRENCY = true;
|
const DEBUG_CONCURRENCY = true;
|
||||||
const R_AGG_MAX_CHARS = 256;
|
const R_AGG_MAX_CHARS = 256;
|
||||||
@@ -168,7 +168,9 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
return { built: 0 };
|
return { built: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
xbLog.info(MODULE_ID, `增量 L0 提取:pending=${pendingPairs.length}, concurrency=${CONCURRENCY}`);
|
const concurrency = Math.max(1, Math.min(50, Number(vectorCfg?.l0Concurrency) || DEFAULT_CONCURRENCY));
|
||||||
|
|
||||||
|
xbLog.info(MODULE_ID, `增量 L0 提取:pending=${pendingPairs.length}, concurrency=${concurrency}`);
|
||||||
|
|
||||||
let completed = 0;
|
let completed = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@@ -181,14 +183,6 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
// ★ Phase 1: 收集所有新提取的 atoms(不向量化)
|
// ★ Phase 1: 收集所有新提取的 atoms(不向量化)
|
||||||
const allNewAtoms = [];
|
const allNewAtoms = [];
|
||||||
|
|
||||||
// ★ 限流检测:连续失败 N 次后暂停并降速
|
|
||||||
let consecutiveFailures = 0;
|
|
||||||
let rateLimited = false;
|
|
||||||
const RATE_LIMIT_THRESHOLD = 6; // 连续失败多少次触发限流保护
|
|
||||||
const RATE_LIMIT_WAIT_MS = 60000; // 限流后等待时间(60 秒)
|
|
||||||
const RETRY_INTERVAL_MS = 1000; // 降速模式下每次请求间隔(1 秒)
|
|
||||||
const RETRY_CONCURRENCY = 1; // ★ 降速模式下的并发数(默认1,建议不要超过5)
|
|
||||||
|
|
||||||
// ★ 通用处理单个 pair 的逻辑(复用于正常模式和降速模式)
|
// ★ 通用处理单个 pair 的逻辑(复用于正常模式和降速模式)
|
||||||
const processPair = async (pair, idx, workerId) => {
|
const processPair = async (pair, idx, workerId) => {
|
||||||
const floor = pair.aiFloor;
|
const floor = pair.aiFloor;
|
||||||
@@ -209,9 +203,6 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
throw new Error('llm_failed');
|
throw new Error('llm_failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ 成功:重置连续失败计数
|
|
||||||
consecutiveFailures = 0;
|
|
||||||
|
|
||||||
if (!atoms.length) {
|
if (!atoms.length) {
|
||||||
setL0FloorStatus(floor, { status: 'empty', reason: 'llm_empty', atoms: 0 });
|
setL0FloorStatus(floor, { status: 'empty', reason: 'llm_empty', atoms: 0 });
|
||||||
} else {
|
} else {
|
||||||
@@ -231,13 +222,6 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
reason: String(e?.message || e).replace(/\s+/g, ' ').slice(0, 120),
|
reason: String(e?.message || e).replace(/\s+/g, ' ').slice(0, 120),
|
||||||
});
|
});
|
||||||
failed++;
|
failed++;
|
||||||
|
|
||||||
// ★ 限流检测:连续失败累加
|
|
||||||
consecutiveFailures++;
|
|
||||||
if (consecutiveFailures >= RATE_LIMIT_THRESHOLD && !rateLimited) {
|
|
||||||
rateLimited = true;
|
|
||||||
xbLog.warn(MODULE_ID, `连续失败 ${consecutiveFailures} 次,疑似触发 API 限流,将暂停所有并发`);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
active--;
|
active--;
|
||||||
if (!extractionCancelled) {
|
if (!extractionCancelled) {
|
||||||
@@ -252,12 +236,12 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ★ 并发池处理(保持固定并发度)
|
// ★ 并发池处理(保持固定并发度)
|
||||||
const poolSize = Math.min(CONCURRENCY, pendingPairs.length);
|
const poolSize = Math.min(concurrency, pendingPairs.length);
|
||||||
let nextIndex = 0;
|
let nextIndex = 0;
|
||||||
let started = 0;
|
let started = 0;
|
||||||
const runWorker = async (workerId) => {
|
const runWorker = async (workerId) => {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (extractionCancelled || rateLimited) return;
|
if (extractionCancelled) return;
|
||||||
const idx = nextIndex++;
|
const idx = nextIndex++;
|
||||||
if (idx >= pendingPairs.length) return;
|
if (idx >= pendingPairs.length) return;
|
||||||
|
|
||||||
@@ -267,7 +251,7 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
await new Promise(r => setTimeout(r, stagger * STAGGER_DELAY));
|
await new Promise(r => setTimeout(r, stagger * STAGGER_DELAY));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extractionCancelled || rateLimited) return;
|
if (extractionCancelled) return;
|
||||||
|
|
||||||
await processPair(pair, idx, workerId);
|
await processPair(pair, idx, workerId);
|
||||||
}
|
}
|
||||||
@@ -279,61 +263,6 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
|
|||||||
xbLog.info(MODULE_ID, `L0 pool done completed=${completed}/${total} failed=${failed} peakActive=${peakActive} elapsedMs=${elapsed}`);
|
xbLog.info(MODULE_ID, `L0 pool done completed=${completed}/${total} failed=${failed} peakActive=${peakActive} elapsedMs=${elapsed}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════
|
|
||||||
// ★ 限流恢复:重置进度,从头开始以限速模式慢慢跑
|
|
||||||
// ═════════════════════════════════════════════════════════════════════
|
|
||||||
if (rateLimited && !extractionCancelled) {
|
|
||||||
const waitSec = RATE_LIMIT_WAIT_MS / 1000;
|
|
||||||
xbLog.info(MODULE_ID, `限流保护:将重置进度并从头开始降速重来(并发=${RETRY_CONCURRENCY}, 间隔=${RETRY_INTERVAL_MS}ms)`);
|
|
||||||
onProgress?.(`疑似限流,${waitSec}s 后降速重头开始...`, completed, total);
|
|
||||||
|
|
||||||
await new Promise(r => setTimeout(r, RATE_LIMIT_WAIT_MS));
|
|
||||||
|
|
||||||
if (!extractionCancelled) {
|
|
||||||
// ★ 核心逻辑:重置计数器,让 UI 从 0 开始跑,给用户“重头开始”的反馈
|
|
||||||
rateLimited = false;
|
|
||||||
consecutiveFailures = 0;
|
|
||||||
completed = 0;
|
|
||||||
failed = 0;
|
|
||||||
|
|
||||||
let retryNextIdx = 0;
|
|
||||||
|
|
||||||
xbLog.info(MODULE_ID, `限流恢复:开始降速模式扫描 ${pendingPairs.length} 个楼层`);
|
|
||||||
|
|
||||||
const retryWorkers = Math.min(RETRY_CONCURRENCY, pendingPairs.length);
|
|
||||||
const runRetryWorker = async (wid) => {
|
|
||||||
while (true) {
|
|
||||||
if (extractionCancelled) return;
|
|
||||||
const idx = retryNextIdx++;
|
|
||||||
if (idx >= pendingPairs.length) return;
|
|
||||||
|
|
||||||
const pair = pendingPairs[idx];
|
|
||||||
const floor = pair.aiFloor;
|
|
||||||
|
|
||||||
// ★ 检查该楼层状态
|
|
||||||
const st = getL0FloorStatus(floor);
|
|
||||||
if (st?.status === 'ok' || st?.status === 'empty') {
|
|
||||||
// 刚才已经成功了,直接跳过(仅增加进度计数)
|
|
||||||
completed++;
|
|
||||||
onProgress?.(`提取: ${completed}/${total} (跳过已完成)`, completed, total);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ 没做过的,用 slow 模式处理
|
|
||||||
await processPair(pair, idx, `retry-${wid}`);
|
|
||||||
|
|
||||||
// 每个请求后休息,避免再次触发限流
|
|
||||||
if (idx < pendingPairs.length - 1 && RETRY_INTERVAL_MS > 0) {
|
|
||||||
await new Promise(r => setTimeout(r, RETRY_INTERVAL_MS));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all(Array.from({ length: retryWorkers }, (_, i) => runRetryWorker(i)));
|
|
||||||
xbLog.info(MODULE_ID, `降速重头开始阶段结束`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saveMetadataDebounced?.();
|
saveMetadataDebounced?.();
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|||||||
@@ -83,16 +83,6 @@ function uint8ToBase64(uint8) {
|
|||||||
return btoa(result);
|
return btoa(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// base64 → Uint8Array
|
|
||||||
function base64ToUint8(base64) {
|
|
||||||
const binary = atob(base64);
|
|
||||||
const bytes = new Uint8Array(binary.length);
|
|
||||||
for (let i = 0; i < binary.length; i++) {
|
|
||||||
bytes[i] = binary.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务器备份文件名
|
// 服务器备份文件名
|
||||||
function getBackupFilename(chatId) {
|
function getBackupFilename(chatId) {
|
||||||
// chatId 可能含中文/特殊字符,ST 只接受 [a-zA-Z0-9_-]
|
// chatId 可能含中文/特殊字符,ST 只接受 [a-zA-Z0-9_-]
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// Story Summary - Embedder (v2 - 统一硅基)
|
// Story Summary - Embedder
|
||||||
// 所有 embedding 请求转发到 siliconflow.js
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
import { embed as sfEmbed, getApiKey } from '../llm/siliconflow.js';
|
import { embed as sfEmbed } from '../llm/siliconflow.js';
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 统一 embed 接口
|
// 统一 embed 接口
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export async function embed(texts, config, options = {}) {
|
export async function embed(texts, config, options = {}) {
|
||||||
// 忽略旧的 config 参数,统一走硅基
|
|
||||||
return await sfEmbed(texts, options);
|
return await sfEmbed(texts, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,8 +16,10 @@ export async function embed(texts, config, options = {}) {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export function getEngineFingerprint(config) {
|
export function getEngineFingerprint(config) {
|
||||||
// 统一使用硅基 bge-m3
|
const api = config?.embeddingApi || {};
|
||||||
return 'siliconflow:bge-m3:1024';
|
const provider = String(api.provider || 'siliconflow').toLowerCase();
|
||||||
|
const model = String(api.model || 'BAAI/bge-m3').trim() || 'BAAI/bge-m3';
|
||||||
|
return `${provider}:${model}:1024`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -47,14 +47,13 @@ export async function deleteLocalModelCache() { }
|
|||||||
// 在线服务测试
|
// 在线服务测试
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export async function testOnlineService() {
|
export async function testOnlineService(_provider, config = {}) {
|
||||||
const key = getApiKey();
|
if (!config?.key) {
|
||||||
if (!key) {
|
throw new Error('请配置 Embedding API Key');
|
||||||
throw new Error('请配置硅基 API Key');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [vec] = await sfEmbed(['测试连接']);
|
const [vec] = await sfEmbed(['测试连接'], { apiConfig: config });
|
||||||
return { success: true, dims: vec?.length || 0 };
|
return { success: true, dims: vec?.length || 0 };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`连接失败: ${e.message}`);
|
throw new Error(`连接失败: ${e.message}`);
|
||||||
@@ -62,7 +61,6 @@ export async function testOnlineService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchOnlineModels() {
|
export async function fetchOnlineModels() {
|
||||||
// 硅基模型固定
|
|
||||||
return ['BAAI/bge-m3'];
|
return ['BAAI/bge-m3'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +76,6 @@ export const ONLINE_PROVIDERS = {
|
|||||||
siliconflow: {
|
siliconflow: {
|
||||||
id: 'siliconflow',
|
id: 'siliconflow',
|
||||||
name: '硅基流动',
|
name: '硅基流动',
|
||||||
baseUrl: 'https://api.siliconflow.cn',
|
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user