- {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')}
-
-
-
- | {t('aiSidebar.statsRateMetric')} |
- {t('aiSidebar.statsRateDesc')} |
- {t('aiSidebar.statsRateValue')} |
-
-
-
- {[
- ['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]) => (
-
- | {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')}
-
-
-
- | {t('aiSidebar.statsPeriod')} |
- {t('aiSidebar.statsTokens')} |
- {t('aiSidebar.statsRequests')} |
-
-
-
- {[
- ['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.${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')}
+
+
+
+ | {t('aiSidebar.statsRateMetric')} |
+ {t('aiSidebar.statsRateDesc')} |
+ {t('aiSidebar.statsRateValue')} |
+
+
+
+ {[
+ ['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]) => (
+
+ | {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')}
+
+
+
+ | {t('aiSidebar.statsPeriod')} |
+ {t('aiSidebar.statsTokens')} |
+ {t('aiSidebar.statsRequests')} |
+
+
+
+ {[
+ ['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.${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 ffe16f1..eb10dd5 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"electron:start": "npx electron ."
},
"dependencies": {
- "@tiptap/core": "^3.19.0",
+ "@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",
@@ -108,4 +108,4 @@
"createStartMenuShortcut": true
}
}
-}
\ No newline at end of file
+}