diff --git a/app/components/AiSidebar.js b/app/components/AiSidebar.js index 7b58994..52fe8f2 100644 --- a/app/components/AiSidebar.js +++ b/app/components/AiSidebar.js @@ -13,10 +13,11 @@ import { useAppStore } from '../store/useAppStore'; import ChatMarkdown from './ChatMarkdown'; import { useI18n } from '../lib/useI18n'; -// 解析消息中的 [SETTINGS_ACTION] 块 +// 解析消息中的 [SETTINGS_ACTION] 块(支持可选的代码围栏包裹) function parseSettingsActions(content) { if (!content) return { parts: [content || ''], actions: [] }; - const regex = /\[SETTINGS_ACTION\]\s*([\s\S]*?)\s*\[\/SETTINGS_ACTION\]/g; + // 匹配带或不带 ``` 包裹的 [SETTINGS_ACTION] 块 + const regex = /(?:```[^\n]*\n)?\[SETTINGS_ACTION\]\s*([\s\S]*?)\s*\[\/SETTINGS_ACTION\](?:\n```)?/g; const parts = []; const actions = []; let lastIndex = 0; @@ -1006,7 +1007,7 @@ export default function AiSidebar({ onInsertText }) { e.stopPropagation(); // 提取正文:去掉系统块、markdown标记、编辑点评 let text = (msg.content || '') - .replace(/\[SETTINGS_ACTION\][\s\S]*?\[\\?\/SETTINGS_ACTION\]/g, '') + .replace(/(?:```[^\n]*\n)?\[SETTINGS_ACTION\][\s\S]*?\[\\?\/SETTINGS_ACTION\](?:\n```)?/g, '') .replace(/^#{1,6}\s+/gm, '') // 去掉标题 # .replace(/\*\*(.+?)\*\*/g, '$1') // **粗体** → 粗体 .replace(/\*(.+?)\*/g, '$1') // *斜体* → 斜体 @@ -1025,7 +1026,8 @@ export default function AiSidebar({ onInsertText }) { }} >{t('aiSidebar.copy')} - )} + ) + } ); })} @@ -1058,368 +1060,375 @@ export default function AiSidebar({ onInsertText }) { - )} + ) + } {/* ==================== 📋 存档 Tab ==================== */} - {activeTab === 'archive' && ( -
-
- setArchiveSearch(e.target.value)} - /> -
-
- {filteredArchive.length === 0 && ( -
-
{t('aiSidebar.emptyArchiveIcon')}
-
{t('aiSidebar.emptyArchiveTitle')}
-
{t('aiSidebar.emptyArchiveHint')}
-
- )} - {[...filteredArchive].reverse().map(item => ( -
setExpandedArchive(expandedArchive === item.id ? null : item.id)} - > -
- - {t(`aiSidebar.statuses.${item.status}`) || item.status} - - {t(`aiSidebar.modes.${item.mode}`) || item.mode} - - {new Date(item.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })} - -
-
- {item.text?.slice(0, 60)}… -
- {expandedArchive === item.id && ( -
-
{item.text}
-
- - -
-
- )} -
- ))} -
-
- )} - - {/* ==================== 📚 参考 Tab ==================== */} - {activeTab === 'reference' && ( -
- {/* Token 预算进度条 */} -
-
- {t('aiSidebar.tokenUsage')} - - {totalSelectedTokens.toLocaleString()} / {(INPUT_TOKEN_BUDGET / 1000).toFixed(0)}k - -
-
-
+
+ setArchiveSearch(e.target.value)} />
-
- - {/* 搜索框 */} -
- setContextSearch(e.target.value)} - /> -
- - {/* 分组列表 */} -
- {Object.entries(groupedItems).length === 0 && ( -
-
{t('aiSidebar.emptyContextIcon')}
-
{t('aiSidebar.emptyContextTitle')}
-
- {contextSearch ? t('aiSidebar.emptyContextHint1') : t('aiSidebar.emptyContextHint2')} +
+ {filteredArchive.length === 0 && ( +
+
{t('aiSidebar.emptyArchiveIcon')}
+
{t('aiSidebar.emptyArchiveTitle')}
+
{t('aiSidebar.emptyArchiveHint')}
-
- )} - {Object.entries(groupedItems).map(([groupName, items]) => { - const isCollapsed = collapsedGroups.has(groupName); - const checkedCount = items.filter(it => contextSelection?.has(it.id)).length; - const groupTokens = items - .filter(it => contextSelection?.has(it.id)) - .reduce((sum, it) => sum + it.tokens, 0); - const allGroupChecked = checkedCount === items.length; - - return ( -
-
toggleCollapse(groupName)} - > - - {isCollapsed ? '▶' : '▼'} + )} + {[...filteredArchive].reverse().map(item => ( +
setExpandedArchive(expandedArchive === item.id ? null : item.id)} + > +
+ + {t(`aiSidebar.statuses.${item.status}`) || item.status} - 0} - ref={el => { - if (el) el.indeterminate = checkedCount > 0 && checkedCount < items.length; - }} - onChange={(e) => { - e.stopPropagation(); - toggleGroup(groupName); - }} - onClick={e => e.stopPropagation()} - className="context-group-check" - /> - - {groupName} ({checkedCount}/{items.length}) - - - {groupTokens > 0 ? `${groupTokens.toLocaleString()}t` : '—'} + {t(`aiSidebar.modes.${item.mode}`) || item.mode} + + {new Date(item.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
- {!isCollapsed && ( -
- {items.map(item => ( - - ))} +
+ {item.text?.slice(0, 60)}… +
+ {expandedArchive === item.id && ( +
+
{item.text}
+
+ + +
)}
- ); - })} + ))} +
+ ) + } - {/* 批量操作 */} -
- - - - + {/* ==================== 📚 参考 Tab ==================== */} + { + activeTab === 'reference' && ( +
+ {/* Token 预算进度条 */} +
+
+ {t('aiSidebar.tokenUsage')} + + {totalSelectedTokens.toLocaleString()} / {(INPUT_TOKEN_BUDGET / 1000).toFixed(0)}k + +
+
+
+
+
+ + {/* 搜索框 */} +
+ setContextSearch(e.target.value)} + /> +
+ + {/* 分组列表 */} +
+ {Object.entries(groupedItems).length === 0 && ( +
+
{t('aiSidebar.emptyContextIcon')}
+
{t('aiSidebar.emptyContextTitle')}
+
+ {contextSearch ? t('aiSidebar.emptyContextHint1') : t('aiSidebar.emptyContextHint2')} +
+
+ )} + {Object.entries(groupedItems).map(([groupName, items]) => { + const isCollapsed = collapsedGroups.has(groupName); + const checkedCount = items.filter(it => contextSelection?.has(it.id)).length; + const groupTokens = items + .filter(it => contextSelection?.has(it.id)) + .reduce((sum, it) => sum + it.tokens, 0); + const allGroupChecked = checkedCount === items.length; + + return ( +
+
toggleCollapse(groupName)} + > + + {isCollapsed ? '▶' : '▼'} + + 0} + ref={el => { + if (el) el.indeterminate = checkedCount > 0 && checkedCount < items.length; + }} + onChange={(e) => { + e.stopPropagation(); + toggleGroup(groupName); + }} + onClick={e => e.stopPropagation()} + className="context-group-check" + /> + + {groupName} ({checkedCount}/{items.length}) + + + {groupTokens > 0 ? `${groupTokens.toLocaleString()}t` : '—'} + +
+ {!isCollapsed && ( +
+ {items.map(item => ( + + ))} +
+ )} +
+ ); + })} +
+ + {/* 批量操作 */} +
+ + + + +
-
- )} + ) + } {/* ==================== 📊 统计 Tab ==================== */} - {activeTab === 'stats' && ( -
-
- {tokenStats.totalRequests === 0 ? ( -
-
📊
-
{t('aiSidebar.statsNoData')}
-
{t('aiSidebar.statsNoDataHint')}
-
- ) : ( - <> - {/* 汇总卡片 */} -
-
-
{tokenStats.totalTokens.toLocaleString()}
-
{t('aiSidebar.statsTotalTokens')}
-
-
-
{tokenStats.totalPromptTokens.toLocaleString()}
-
{t('aiSidebar.statsTotalInput')}
-
-
-
{tokenStats.totalCompletionTokens.toLocaleString()}
-
{t('aiSidebar.statsTotalOutput')}
-
-
-
{tokenStats.totalRequests}
-
{t('aiSidebar.statsTotalRequests')}
-
-
-
{tokenStats.trackedDays}
-
{t('aiSidebar.statsTrackedDays')}
-
+ { + activeTab === 'stats' && ( +
+
+ {tokenStats.totalRequests === 0 ? ( +
+
📊
+
{t('aiSidebar.statsNoData')}
+
{t('aiSidebar.statsNoDataHint')}
- - {/* 消耗速率 */} -
-
{t('aiSidebar.statsRates')}
-
{t('aiSidebar.statsRatesHint')}
- - - - - - - - - - {[ - ['TPS', tokenStats.rates.tps, t('aiSidebar.statsRateTPS')], - ['TPM', tokenStats.rates.tpm, t('aiSidebar.statsRateTPM')], - ['TPH', tokenStats.rates.tph, t('aiSidebar.statsRateTPH')], - ['TPD', tokenStats.rates.tpd, t('aiSidebar.statsRateTPD')], - ['RPM', tokenStats.rates.rpm, t('aiSidebar.statsRateRPM')], - ['RPH', tokenStats.rates.rph, t('aiSidebar.statsRateRPH')], - ['RPD', tokenStats.rates.rpd, t('aiSidebar.statsRateRPD')], - ].map(([key, value, label]) => ( - - - - - - ))} - -
{t('aiSidebar.statsRateMetric')}{t('aiSidebar.statsRateDesc')}{t('aiSidebar.statsRateValue')}
{key}{label}{value < 1 ? value.toFixed(2) : value < 10 ? value.toFixed(1) : Math.round(value).toLocaleString()}
-
- - {/* 近期请求速度 */} - {tokenStats.recentSpeeds.length > 0 && ( -
-
{t('aiSidebar.statsRecentSpeeds')}
-
- {(() => { - const maxSpeed = Math.max(...tokenStats.recentSpeeds.map(s => s.speed)); - return tokenStats.recentSpeeds.map((s, i) => ( -
-
- {s.speed.toFixed(0)} -
- )); - })()} + ) : ( + <> + {/* 汇总卡片 */} +
+
+
{tokenStats.totalTokens.toLocaleString()}
+
{t('aiSidebar.statsTotalTokens')}
+
+
+
{tokenStats.totalPromptTokens.toLocaleString()}
+
{t('aiSidebar.statsTotalInput')}
+
+
+
{tokenStats.totalCompletionTokens.toLocaleString()}
+
{t('aiSidebar.statsTotalOutput')}
+
+
+
{tokenStats.totalRequests}
+
{t('aiSidebar.statsTotalRequests')}
+
+
+
{tokenStats.trackedDays}
+
{t('aiSidebar.statsTrackedDays')}
- )} - {/* 消耗预估 */} -
-
{t('aiSidebar.statsProjections')}
-
{t('aiSidebar.statsProjectionsHint')}
- - - - - - - - - - {[ - ['statsPeriodDay', tokenStats.projections.perDay], - ['statsPeriodWeek', tokenStats.projections.perWeek], - ['statsPeriodMonth', tokenStats.projections.perMonth], - ['statsPeriodQuarter', tokenStats.projections.perQuarter], - ['statsPeriodYear', tokenStats.projections.perYear], - ].map(([key, data]) => ( - - - - - - ))} - -
{t('aiSidebar.statsPeriod')}{t('aiSidebar.statsTokens')}{t('aiSidebar.statsRequests')}
{t(`aiSidebar.${key}`)}{data.tokens.toLocaleString()}{data.requests}
-
- - {/* 渠道/模型分类统计 */} - {tokenStats.modelBreakdown.length > 0 && ( + {/* 消耗速率 */}
-
{t('aiSidebar.statsModelBreakdown')}
-
{t('aiSidebar.statsModelBreakdownHint')}
- {tokenStats.modelBreakdown.map((m, idx) => { - const bgGradient = getProviderColor(m.provider, m.model); - return ( -
-
-
- - - {m.provider} - - {m.model} -
-
- {Math.round(m.tokenPercent)}% -
-
- -
-
-
- -
-
- {m.tokens.toLocaleString()} - Tokens -
-
- {m.requests} - {t('aiSidebar.statsRequests')} -
-
- - {m.promptTokens > 1000 ? (m.promptTokens / 1000).toFixed(1) + 'k' : m.promptTokens} / {m.completionTokens > 1000 ? (m.completionTokens / 1000).toFixed(1) + 'k' : m.completionTokens} - - In / Out -
- {m.avgSpeed > 0 && ( -
- {m.avgSpeed.toFixed(1)} - t/s -
- )} -
-
- ); - })} +
{t('aiSidebar.statsRates')}
+
{t('aiSidebar.statsRatesHint')}
+ + + + + + + + + + {[ + ['TPS', tokenStats.rates.tps, t('aiSidebar.statsRateTPS')], + ['TPM', tokenStats.rates.tpm, t('aiSidebar.statsRateTPM')], + ['TPH', tokenStats.rates.tph, t('aiSidebar.statsRateTPH')], + ['TPD', tokenStats.rates.tpd, t('aiSidebar.statsRateTPD')], + ['RPM', tokenStats.rates.rpm, t('aiSidebar.statsRateRPM')], + ['RPH', tokenStats.rates.rph, t('aiSidebar.statsRateRPH')], + ['RPD', tokenStats.rates.rpd, t('aiSidebar.statsRateRPD')], + ].map(([key, value, label]) => ( + + + + + + ))} + +
{t('aiSidebar.statsRateMetric')}{t('aiSidebar.statsRateDesc')}{t('aiSidebar.statsRateValue')}
{key}{label}{value < 1 ? value.toFixed(2) : value < 10 ? value.toFixed(1) : Math.round(value).toLocaleString()}
- )} - {/* 清空按钮 */} -
- -
- - )} + {/* 近期请求速度 */} + {tokenStats.recentSpeeds.length > 0 && ( +
+
{t('aiSidebar.statsRecentSpeeds')}
+
+ {(() => { + const maxSpeed = Math.max(...tokenStats.recentSpeeds.map(s => s.speed)); + return tokenStats.recentSpeeds.map((s, i) => ( +
+
+ {s.speed.toFixed(0)} +
+ )); + })()} +
+
+ )} + + {/* 消耗预估 */} +
+
{t('aiSidebar.statsProjections')}
+
{t('aiSidebar.statsProjectionsHint')}
+ + + + + + + + + + {[ + ['statsPeriodDay', tokenStats.projections.perDay], + ['statsPeriodWeek', tokenStats.projections.perWeek], + ['statsPeriodMonth', tokenStats.projections.perMonth], + ['statsPeriodQuarter', tokenStats.projections.perQuarter], + ['statsPeriodYear', tokenStats.projections.perYear], + ].map(([key, data]) => ( + + + + + + ))} + +
{t('aiSidebar.statsPeriod')}{t('aiSidebar.statsTokens')}{t('aiSidebar.statsRequests')}
{t(`aiSidebar.${key}`)}{data.tokens.toLocaleString()}{data.requests}
+
+ + {/* 渠道/模型分类统计 */} + {tokenStats.modelBreakdown.length > 0 && ( +
+
{t('aiSidebar.statsModelBreakdown')}
+
{t('aiSidebar.statsModelBreakdownHint')}
+ {tokenStats.modelBreakdown.map((m, idx) => { + const bgGradient = getProviderColor(m.provider, m.model); + return ( +
+
+
+ + + {m.provider} + + {m.model} +
+
+ {Math.round(m.tokenPercent)}% +
+
+ +
+
+
+ +
+
+ {m.tokens.toLocaleString()} + Tokens +
+
+ {m.requests} + {t('aiSidebar.statsRequests')} +
+
+ + {m.promptTokens > 1000 ? (m.promptTokens / 1000).toFixed(1) + 'k' : m.promptTokens} / {m.completionTokens > 1000 ? (m.completionTokens / 1000).toFixed(1) + 'k' : m.completionTokens} + + In / Out +
+ {m.avgSpeed > 0 && ( +
+ {m.avgSpeed.toFixed(1)} + t/s +
+ )} +
+
+ ); + })} +
+ )} + + {/* 清空按钮 */} +
+ +
+ + )} +
-
- )} -
+ ) + } +
); } diff --git a/app/lib/context-engine.js b/app/lib/context-engine.js index 1f5aa22..058111e 100644 --- a/app/lib/context-engine.js +++ b/app/lib/context-engine.js @@ -134,7 +134,7 @@ export async function getContextItems(activeChapterId) { id: `chapter-${ch.id}`, group: '📑 章节', name: `第${i + 1}章 ${ch.title}${i > currentIndex ? '(后续)' : ''}`, - tokens: estimateTokens(text ? text.slice(0, 1500) : ''), + tokens: estimateTokens(text), category: 'chapter', enabled: i < currentIndex, // 前文章节默认启用,后续章节默认不启用 }); @@ -180,65 +180,60 @@ export async function buildContext(activeChapterId, selectedText, selectedIds = // 获取所有有效的设定条目 const allValidItemNodes = nodes.filter(n => n.type === 'item' && n.enabled !== false); - // 如果有 selectedIds,提取手动勾选的设定 - const manualItemNodes = []; - const unselectedItemNodes = []; + let finalItemNodes; - for (const n of allValidItemNodes) { - if (selectedIds && selectedIds.has(`setting-${n.id}`)) { - manualItemNodes.push(n); - } else if (!selectedIds) { - // selectedIds 为 null — 兼容旧逻辑,全部走 RAG - unselectedItemNodes.push(n); - } else { - unselectedItemNodes.push(n); - } - } + if (!selectedIds) { + // selectedIds 为 null — 用户没有手动勾选任何参考 + // 包含所有启用的设定条目,再用 RAG 补充排序(如果可用) + finalItemNodes = [...allValidItemNodes]; + } else { + // 用户手动勾选了参考条目 + const manualItemNodes = []; + const unselectedItemNodes = []; - // --- RAG 自动检索(仅当开启嵌入功能时) --- - let autoRetrievedNodes = []; - const queryText = (selectedText || '').trim(); - if (settings.apiConfig?.useCustomEmbed && queryText && unselectedItemNodes.length > 0) { - try { - // 获取 Query 的 Embedding - // 此处的文本可以是光标前的一小段文字,为了简便使用 selectedText - // TODO: 这里如果是空文本,可能需要补当前章节最近的内容 - let ragSourceText = queryText; - if (ragSourceText.length < 50 && currentChapter) { - // 文本太短,用当前章节末尾补充 - const stripChapText = stripHtml(currentChapter.content || '').slice(-200); - ragSourceText = ragSourceText + '\n' + stripChapText; + for (const n of allValidItemNodes) { + if (selectedIds.has(`setting-${n.id}`)) { + manualItemNodes.push(n); + } else { + unselectedItemNodes.push(n); } - - const queryVector = await getEmbedding(ragSourceText, settings.apiConfig); - if (queryVector) { - // 计算相似度 - const scoredNodes = unselectedItemNodes.map(n => { - if (!n.embedding) return { node: n, score: -1 }; - return { node: n, score: cosineSimilarity(queryVector, n.embedding) }; - }).filter(x => x.score > 0.3); // 设置一个基础阈值 0.3 - - // 按分数降序 - scoredNodes.sort((a, b) => b.score - a.score); - - // 取 Top-K (比如最多取 5 个) - autoRetrievedNodes = scoredNodes.slice(0, 5).map(x => x.node); - } - } catch (e) { - console.error('RAG Retrieval failed:', e); } - } - // 合并手动与自动检索的节点 - const itemNodes = [...manualItemNodes, ...autoRetrievedNodes]; - // 去重 - const finalItemNodes = Array.from(new Set(itemNodes)); + // --- RAG 自动检索(仅当有手动勾选时,对未勾选项做 RAG 补充) --- + let autoRetrievedNodes = []; + const queryText = (selectedText || '').trim(); + if (settings.apiConfig?.useCustomEmbed && queryText && unselectedItemNodes.length > 0) { + try { + let ragSourceText = queryText; + if (ragSourceText.length < 50 && currentChapter) { + const stripChapText = stripHtml(currentChapter.content || '').slice(-200); + ragSourceText = ragSourceText + '\n' + stripChapText; + } + + const queryVector = await getEmbedding(ragSourceText, settings.apiConfig); + if (queryVector) { + const scoredNodes = unselectedItemNodes.map(n => { + if (!n.embedding) return { node: n, score: -1 }; + return { node: n, score: cosineSimilarity(queryVector, n.embedding) }; + }).filter(x => x.score > 0.3); + + scoredNodes.sort((a, b) => b.score - a.score); + autoRetrievedNodes = scoredNodes.slice(0, 5).map(x => x.node); + } + } catch (e) { + console.error('RAG Retrieval failed:', e); + } + } + + // 合并手动与自动检索的节点,去重 + finalItemNodes = Array.from(new Set([...manualItemNodes, ...autoRetrievedNodes])); + } const writingMode = getWritingMode(); // 先构建各模块的原始文本 const rawModules = { - bookInfo: (selectedIds && selectedIds.has('bookinfo')) ? buildBookInfoContext(settings.bookInfo) : '', + bookInfo: (!selectedIds || selectedIds.has('bookinfo')) ? buildBookInfoContext(settings.bookInfo) : '', characters: buildCharactersContext(finalItemNodes.filter(n => n.category === 'character')), locations: buildLocationsContext(finalItemNodes.filter(n => n.category === 'location'), nodes), worldbuilding: buildWorldContext(finalItemNodes.filter(n => n.category === 'world'), nodes), @@ -596,17 +591,6 @@ function buildPreviousContext(chapters, currentIndex) { return prevChapters.map((ch, i) => { const text = stripHtml(ch.content || ''); if (!text) return `第${i + 1}章「${ch.title}」:(空)`; - - if (i === currentIndex - 1) { - // 前一章:保留更多内容 - const excerpt = text.length > 1500 ? '...' + text.slice(-1500) : text; - return `第${i + 1}章「${ch.title}」(前一章完整回顾):\n${excerpt}`; - } - - // 远距章节:只保留简要摘要 - if (text.length > 300) { - return `第${i + 1}章「${ch.title}」:\n${text.slice(0, 150)}…(中间省略)…${text.slice(-100)}`; - } return `第${i + 1}章「${ch.title}」:\n${text}`; }).join('\n\n'); } @@ -620,15 +604,6 @@ function buildPreviousContextFiltered(chapters, currentIndex, selectedIds) { const i = chapters.indexOf(ch); const text = stripHtml(ch.content || ''); if (!text) return `第${i + 1}章「${ch.title}」:(空)`; - - if (i === currentIndex - 1) { - const excerpt = text.length > 1500 ? '...' + text.slice(-1500) : text; - return `第${i + 1}章「${ch.title}」(前一章完整回顾):\n${excerpt}`; - } - - if (text.length > 300) { - return `第${i + 1}章「${ch.title}」:\n${text.slice(0, 150)}…(中间省略)…${text.slice(-100)}`; - } return `第${i + 1}章「${ch.title}」:\n${text}`; }).join('\n\n'); } @@ -699,11 +674,9 @@ function getModeInstruction(mode) { 【设定集管理能力】 当用户要求你创建、修改或删除设定集条目时,你可以在回复中嵌入操作指令块。格式如下: -\`\`\` [SETTINGS_ACTION] {"action":"add","category":"character","name":"角色姓名","content":{"role":"主角","personality":"...","background":"..."}} [/SETTINGS_ACTION] -\`\`\` 可用的 action: "add"(新增)、"update"(更新,需提供 nodeId)、"delete"(删除,需提供 nodeId) @@ -720,6 +693,7 @@ function getModeInstruction(mode) { - 每个操作块只包含一个 JSON 对象 - 如果需要多个操作,使用多个 [SETTINGS_ACTION] 块 - 操作块前后必须有正常的文字说明 +- 不要用代码围栏(\`\`\`)包裹操作块,直接使用 [SETTINGS_ACTION] 标签 - update 示例:{"action":"update","nodeId":"具体id","name":"新名称","content":{...}} - delete 示例:{"action":"delete","nodeId":"具体id"} - 在正文中已有角色/设定出现时,如果用户要求,可以从正文分析内容并自动创建设定`; diff --git a/app/lib/settings.js b/app/lib/settings.js index 00aff4b..6a4084b 100644 --- a/app/lib/settings.js +++ b/app/lib/settings.js @@ -499,8 +499,12 @@ export async function addSettingsNode({ name, type, category, parentId, icon, co if (node.type === 'item') { const { apiConfig } = getProjectSettings(); if (apiConfig.useCustomEmbed) { - const textToEmbed = extractTextForEmbedding(node); - node.embedding = await getEmbedding(textToEmbed, apiConfig); + try { + const textToEmbed = extractTextForEmbedding(node); + node.embedding = await getEmbedding(textToEmbed, apiConfig); + } catch (e) { + console.warn('[Settings] Embedding failed for new node, will retry later:', e.message); + } } } @@ -594,6 +598,7 @@ export async function moveSettingsNode(id, newParentId) { } // 重新计算所有条目的 embedding (Async) +// 每次请求间隔 500ms 以避免超出 TPM 限制 export async function rebuildAllEmbeddings(onProgress) { const nodes = await getSettingsNodes(); const { apiConfig } = getProjectSettings(); @@ -616,6 +621,10 @@ export async function rebuildAllEmbeddings(onProgress) { } done++; onProgress?.(done, items.length, failed); + // 请求间隔:避免超出 TPM / RPM 限制 + if (done < items.length) { + await new Promise(r => setTimeout(r, 700)); + } } await saveSettingsNodes(nodes); diff --git a/next.config.mjs b/next.config.mjs index 9f47928..d9d6625 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,8 +2,8 @@ const nextConfig = { output: 'standalone', devIndicators: false, - // pdf-parse 仅在服务端 API 路由中使用,将其标记为外部包避免打包解析 - serverExternalPackages: ['pdf-parse'], + // pdf-parse / word-extractor 仅在服务端 API 路由中使用,标记为外部包避免打包解析 + serverExternalPackages: ['pdf-parse', 'word-extractor'], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 0609ed2..612ff1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { "name": "author-app", - "version": "1.1.4", + "version": "1.1.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "author-app", - "version": "1.1.4", + "version": "1.1.10", "dependencies": { + "@tiptap/core": "^3.20.0", "@tiptap/extension-character-count": "^3.19.0", "@tiptap/extension-color": "^3.19.0", "@tiptap/extension-font-family": "^3.19.0", @@ -2300,16 +2301,16 @@ } }, "node_modules/@tiptap/core": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz", - "integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==", + "version": "3.20.0", + "resolved": "https://registry.npmmirror.com/@tiptap/core/-/core-3.20.0.tgz", + "integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.19.0" + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/extension-blockquote": { @@ -2786,9 +2787,9 @@ } }, "node_modules/@tiptap/pm": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz", - "integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==", + "version": "3.20.0", + "resolved": "https://registry.npmmirror.com/@tiptap/pm/-/pm-3.20.0.tgz", + "integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.3.0", diff --git a/package.json b/package.json index bebb8c7..eb10dd5 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "electron:start": "npx electron ." }, "dependencies": { + "@tiptap/core": "^3.20.0", "@tiptap/extension-character-count": "^3.19.0", "@tiptap/extension-color": "^3.19.0", "@tiptap/extension-font-family": "^3.19.0", @@ -107,4 +108,4 @@ "createStartMenuShortcut": true } } -} \ No newline at end of file +}