feat: make RAG conditional on useCustomEmbed toggle; filter embed model list to only show embedding models

This commit is contained in:
YuanShiJiLoong committed 2026-02-22 18:30:49 +09:00
1 parent ff8cac3d9c
commit 43096fc5af
4 files changed
+38 -26

No files matched your search

+27 -17
View File
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server';
// 通用模型列表拉取 — 支持 OpenAI 兼容格式和 Gemini 原生格式
export async function POST(request) {
try {
const { apiKey, baseUrl, provider } = await request.json();
const { apiKey, baseUrl, provider, embedOnly } = await request.json();
if (!apiKey) {
return NextResponse.json(
@@ -14,11 +14,11 @@ export async function POST(request) {
// Gemini 原生格式
if (provider === 'gemini-native') {
return await fetchGeminiModels(apiKey, baseUrl);
return await fetchGeminiModels(apiKey, baseUrl, embedOnly);
}
// OpenAI 兼容格式(适用于所有其他供应商)
return await fetchOpenAIModels(apiKey, baseUrl);
return await fetchOpenAIModels(apiKey, baseUrl, embedOnly);
} catch (error) {
console.error('拉取模型列表错误:', error);
@@ -30,7 +30,7 @@ export async function POST(request) {
}
// Gemini 原生格式拉取模型
async function fetchGeminiModels(apiKey, baseUrl) {
async function fetchGeminiModels(apiKey, baseUrl, embedOnly) {
const base = (baseUrl || 'https://generativelanguage.googleapis.com/v1beta').replace(/\/$/, '');
const url = `${base}/models?key=${apiKey}`;
@@ -44,19 +44,25 @@ async function fetchGeminiModels(apiKey, baseUrl) {
}
const data = await response.json();
const models = (data.models || [])
.filter(m => m.supportedGenerationMethods?.includes('generateContent') || m.supportedGenerationMethods?.includes('embedContent'))
.map(m => ({
id: m.name?.replace('models/', '') || m.name,
displayName: m.displayName || m.name,
}))
let models = (data.models || []);
if (embedOnly) {
models = models.filter(m => m.supportedGenerationMethods?.includes('embedContent'));
} else {
models = models.filter(m => m.supportedGenerationMethods?.includes('generateContent') || m.supportedGenerationMethods?.includes('embedContent'));
}
models = models.map(m => ({
id: m.name?.replace('models/', '') || m.name,
displayName: m.displayName || m.name,
}))
.sort((a, b) => a.id.localeCompare(b.id));
return NextResponse.json({ models });
}
// OpenAI 兼容格式拉取模型(/v1/models)
async function fetchOpenAIModels(apiKey, baseUrl) {
async function fetchOpenAIModels(apiKey, baseUrl, embedOnly) {
const base = (baseUrl || '').replace(/\/$/, '');
if (!base) {
return NextResponse.json(
@@ -80,12 +86,16 @@ async function fetchOpenAIModels(apiKey, baseUrl) {
}
const data = await response.json();
// OpenAI 格式返回 { data: [{id, ...}, ...] }
const models = (data.data || [])
.map(m => ({
id: m.id,
displayName: m.id,
}))
let models = (data.data || []);
if (embedOnly) {
models = models.filter(m => /embed/i.test(m.id));
}
models = models.map(m => ({
id: m.id,
displayName: m.id,
}))
.sort((a, b) => a.id.localeCompare(b.id));
return NextResponse.json({ models });
+1 -1
View File
@@ -550,7 +550,7 @@ function ApiConfigForm({ data, onChange }) {
try {
const embedKey = data.embedApiKey || data.apiKey;
const embedBase = data.embedBaseUrl || data.baseUrl;
const res = await fetch('/api/ai/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: embedKey, baseUrl: embedBase, provider: data.embedProvider }) });
const res = await fetch('/api/ai/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: embedKey, baseUrl: embedBase, provider: data.embedProvider, embedOnly: true }) });
const result = await res.json();
if (result.error) { setFetchedEmbedModels(null); setTestStatus({ success: false, error: t('apiConfig.embedApiPrefix') + result.error }); }
else { setFetchedEmbedModels(result.models || []); }
+3 -3
View File
@@ -203,14 +203,14 @@ export async function buildContext(activeChapterId, selectedText, selectedIds =
}
}
// --- RAG 自动检索 ---
// --- RAG 自动检索(仅当开启嵌入功能时) ---
let autoRetrievedNodes = [];
const queryText = (selectedText || '').trim();
if (queryText && unselectedItemNodes.length > 0) {
const { apiConfig } = getProjectSettings();
if (apiConfig.useCustomEmbed && queryText && unselectedItemNodes.length > 0) {
try {
// 获取 Query 的 Embedding
// 此处的文本可以是光标前的一小段文字,为了简便使用 selectedText
const { apiConfig } = getProjectSettings();
// TODO: 这里如果是空文本,可能需要补当前章节最近的内容
let ragSourceText = queryText;
if (ragSourceText.length < 50 && currentChapter) {
+7 -5
View File
@@ -490,9 +490,11 @@ export async function addSettingsNode({ name, type, category, parentId, icon, co
};
if (node.type === 'item') {
const textToEmbed = extractTextForEmbedding(node);
const { apiConfig } = getProjectSettings();
node.embedding = await getEmbedding(textToEmbed, apiConfig);
if (apiConfig.useCustomEmbed) {
const textToEmbed = extractTextForEmbedding(node);
node.embedding = await getEmbedding(textToEmbed, apiConfig);
}
}
nodes.push(node);
@@ -514,12 +516,12 @@ export async function updateSettingsNode(id, updates) {
delete updates.parentId;
}
// 如果名称或内容发生改变,且是条目,重新计算 embedding
// 如果名称或内容发生改变,且是条目,且开启了嵌入功能,重新计算 embedding
const nodeType = updates.type || nodes[idx].type;
if (nodeType === 'item' && (updates.name !== undefined || updates.content !== undefined)) {
const { apiConfig } = getProjectSettings();
if (nodeType === 'item' && apiConfig.useCustomEmbed && (updates.name !== undefined || updates.content !== undefined)) {
const tempNode = { ...nodes[idx], ...updates };
const textToEmbed = extractTextForEmbedding(tempNode);
const { apiConfig } = getProjectSettings();
updates.embedding = await getEmbedding(textToEmbed, apiConfig);
}