From 8fdce7b9a1f72d887fa5aa11108931cd8eddf904 Mon Sep 17 00:00:00 2001 From: bielie Date: Sun, 8 Feb 2026 18:12:55 +0800 Subject: [PATCH] fix: qwen thinking toggle and recall log styles --- modules/story-summary/generate/prompt.js | 410 +++++------------- modules/story-summary/story-summary.css | 45 +- .../story-summary/vector/llm/llm-service.js | 10 +- modules/streaming-generation.js | 6 + 4 files changed, 147 insertions(+), 324 deletions(-) diff --git a/modules/story-summary/generate/prompt.js b/modules/story-summary/generate/prompt.js index a30465b..f12cade 100644 --- a/modules/story-summary/generate/prompt.js +++ b/modules/story-summary/generate/prompt.js @@ -1,5 +1,5 @@ // ═══════════════════════════════════════════════════════════════════════════ -// Story Summary - Prompt Injection (v2 - DSL 版) +// Story Summary - Prompt Injection (v3 - DSL 版 + Orphan 分组修复) // - 仅负责"构建注入文本",不负责写入 extension_prompts // - 注入发生在 story-summary.js:GENERATION_STARTED 时写入 extension_prompts // ═══════════════════════════════════════════════════════════════════════════ @@ -23,10 +23,6 @@ const MODULE_ID = "summaryPrompt"; let lastRecallFailAt = 0; const RECALL_FAIL_COOLDOWN_MS = 10_000; -/** - * 检查是否可以通知召回失败 - * @returns {boolean} - */ function canNotifyRecallFail() { const now = Date.now(); if (now - lastRecallFailAt < RECALL_FAIL_COOLDOWN_MS) return false; @@ -50,11 +46,6 @@ const TOP_N_STAR = 5; // 工具函数 // ───────────────────────────────────────────────────────────────────────────── -/** - * 估算 token 数量 - * @param {string} text - 文本 - * @returns {number} token 数 - */ function estimateTokens(text) { if (!text) return 0; const s = String(text); @@ -62,13 +53,6 @@ function estimateTokens(text) { return Math.ceil(zh + (s.length - zh) / 4); } -/** - * 带预算控制的行推入 - * @param {Array} lines - 行数组 - * @param {string} text - 文本 - * @param {object} state - 预算状态 {used, max} - * @returns {boolean} 是否成功 - */ function pushWithBudget(lines, text, state) { const t = estimateTokens(text); if (state.used + t > state.max) return false; @@ -77,12 +61,6 @@ function pushWithBudget(lines, text, state) { return true; } -/** - * 计算余弦相似度 - * @param {Array} a - 向量 a - * @param {Array} b - 向量 b - * @returns {number} 相似度 - */ function cosineSimilarity(a, b) { if (!a?.length || !b?.length || a.length !== b.length) return 0; let dot = 0, nA = 0, nB = 0; @@ -94,11 +72,6 @@ function cosineSimilarity(a, b) { return nA && nB ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0; } -/** - * 解析楼层范围 - * @param {string} summary - 摘要文本 - * @returns {object|null} {start, end} - */ function parseFloorRange(summary) { if (!summary) return null; const match = String(summary).match(/\(#(\d+)(?:-(\d+))?\)/); @@ -108,22 +81,12 @@ function parseFloorRange(summary) { return { start, end }; } -/** - * 清理摘要中的楼层标记 - * @param {string} summary - 摘要文本 - * @returns {string} 清理后的文本 - */ function cleanSummary(summary) { return String(summary || "") .replace(/\s*\(#\d+(?:-\d+)?\)\s*$/, "") .trim(); } -/** - * 规范化字符串(用于比较) - * @param {string} s - 字符串 - * @returns {string} 规范化后的字符串 - */ function normalize(s) { return String(s || '') .normalize('NFKC') @@ -136,22 +99,11 @@ function normalize(s) { // 上下文配对工具函数 // ───────────────────────────────────────────────────────────────────────────── -/** - * 获取上下文楼层 - * @param {object} chunk - chunk 对象 - * @returns {number} 配对楼层,-1 表示无效 - */ function getContextFloor(chunk) { if (chunk.isL0) return -1; return chunk.isUser ? chunk.floor + 1 : chunk.floor - 1; } -/** - * 选择配对 chunk - * @param {Array} candidates - 候选 chunks - * @param {object} mainChunk - 主 chunk - * @returns {object|null} 配对 chunk - */ function pickContextChunk(candidates, mainChunk) { if (!candidates?.length) return null; const targetIsUser = !mainChunk.isUser; @@ -160,12 +112,6 @@ function pickContextChunk(candidates, mainChunk) { return candidates[0]; } -/** - * 格式化上下文 chunk 行 - * @param {object} chunk - chunk 对象 - * @param {boolean} isAbove - 是否在主 chunk 上方 - * @returns {string} 格式化的行 - */ function formatContextChunkLine(chunk, isAbove) { const { name1, name2 } = getContext(); const speaker = chunk.isUser ? (name1 || "用户") : (chunk.speaker || name2 || "角色"); @@ -178,10 +124,6 @@ function formatContextChunkLine(chunk, isAbove) { // 系统前导与后缀 // ───────────────────────────────────────────────────────────────────────────── -/** - * 构建系统前导 - * @returns {string} - */ function buildSystemPreamble() { return [ "以上是还留在眼前的对话", @@ -193,10 +135,6 @@ function buildSystemPreamble() { ].join("\n"); } -/** - * 构建后缀 - * @returns {string} - */ function buildPostscript() { return [ "", @@ -208,28 +146,20 @@ function buildPostscript() { // L1 Facts 分层过滤 // ───────────────────────────────────────────────────────────────────────────── -/** - * 从 store 获取所有已知角色名 - * @param {object} store - summary store - * @returns {Set} 角色名集合(规范化后) - */ function getKnownCharacters(store) { const names = new Set(); - // 从 arcs 获取 const arcs = store?.json?.arcs || []; for (const a of arcs) { if (a.name) names.add(normalize(a.name)); } - // 从 characters.main 获取 const main = store?.json?.characters?.main || []; for (const m of main) { const name = typeof m === 'string' ? m : m.name; if (name) names.add(normalize(name)); } - // 从当前角色获取 const { name1, name2 } = getContext(); if (name1) names.add(normalize(name1)); if (name2) names.add(normalize(name2)); @@ -237,77 +167,42 @@ function getKnownCharacters(store) { return names; } -/** - * 解析关系类 fact 的目标人物 - * @param {string} predicate - 谓词,如 "对蓝袖的看法" - * @returns {string|null} 目标人物名 - */ function parseRelationTarget(predicate) { const match = String(predicate || '').match(/^对(.+)的/); return match ? match[1] : null; } -/** - * 过滤 facts(分层策略) - * - * 规则: - * - isState=true:全量保留 - * - 关系类(谓词匹配 /^对.+的/):from 或 to 在 focus 中 - * - 人物状态类(主体是已知角色名):主体在 focus 中 - * - 其他(物品/地点/规则):全量保留 - * - * @param {Array} facts - 所有 facts - * @param {Array} focusEntities - 焦点实体 - * @param {Set} knownCharacters - 已知角色名集合 - * @returns {Array} 过滤后的 facts - */ function filterFactsByRelevance(facts, focusEntities, knownCharacters) { if (!facts?.length) return []; const focusSet = new Set((focusEntities || []).map(normalize)); return facts.filter(f => { - // 1. isState=true:全量保留 if (f._isState === true) return true; - // 2. 关系类:from 或 to 在 focus 中 if (isRelationFact(f)) { const from = normalize(f.s); const target = parseRelationTarget(f.p); const to = target ? normalize(target) : ''; - // 任一方在 focus 中即保留 if (focusSet.has(from) || focusSet.has(to)) return true; - - // 都不在 focus 中则过滤 return false; } - // 3. 主体是已知角色名:检查是否在 focus 中 const subjectNorm = normalize(f.s); if (knownCharacters.has(subjectNorm)) { return focusSet.has(subjectNorm); } - // 4. 主体不是人名(物品/地点/规则等):保留 return true; }); } -/** - * 格式化 facts 用于注入 - * @param {Array} facts - facts 数组 - * @param {Array} focusEntities - 焦点实体 - * @param {Set} knownCharacters - 已知角色名集合 - * @returns {Array} 格式化后的行 - */ function formatFactsForInjection(facts, focusEntities, knownCharacters) { - // 先过滤 const filtered = filterFactsByRelevance(facts, focusEntities, knownCharacters); if (!filtered.length) return []; - // 按 since 降序排序(最新的优先) return filtered .sort((a, b) => (b.since || 0) - (a.since || 0)) .map(f => { @@ -323,11 +218,6 @@ function formatFactsForInjection(facts, focusEntities, knownCharacters) { // 格式化函数 // ───────────────────────────────────────────────────────────────────────────── -/** - * 格式化角色弧光行 - * @param {object} a - 弧光对象 - * @returns {string} - */ function formatArcLine(a) { const moments = (a.moments || []) .map(m => (typeof m === "string" ? m : m.text)) @@ -339,11 +229,6 @@ function formatArcLine(a) { return `- ${a.name}:${a.trajectory}`; } -/** - * 格式化 chunk 完整行 - * @param {object} c - chunk 对象 - * @returns {string} - */ function formatChunkFullLine(c) { const { name1, name2 } = getContext(); @@ -355,38 +240,6 @@ function formatChunkFullLine(c) { return `› #${c.floor + 1} [${speaker}] ${String(c.text || "").trim()}`; } -/** - * 格式化带上下文的 chunk - * @param {object} mainChunk - 主 chunk - * @param {object|null} contextChunk - 上下文 chunk - * @returns {Array} 格式化的行数组 - */ -function formatChunkWithContext(mainChunk, contextChunk) { - const lines = []; - const mainLine = formatChunkFullLine(mainChunk); - - if (!contextChunk) { - lines.push(mainLine); - return lines; - } - - if (contextChunk.floor < mainChunk.floor) { - lines.push(formatContextChunkLine(contextChunk, true)); - lines.push(mainLine); - } else { - lines.push(mainLine); - lines.push(formatContextChunkLine(contextChunk, false)); - } - - return lines; -} - -/** - * 格式化因果事件行 - * @param {object} causalItem - 因果项 - * @param {Map} causalById - 因果映射 - * @returns {string} - */ function formatCausalEventLine(causalItem, causalById) { const ev = causalItem?.event || {}; const depth = Math.max(1, Math.min(9, causalItem?._causalDepth || 1)); @@ -415,22 +268,11 @@ function formatCausalEventLine(causalItem, causalById) { return lines.join("\n"); } -/** - * 重新编号事件文本 - * @param {string} text - 事件文本 - * @param {number} newIndex - 新编号 - * @returns {string} - */ function renumberEventText(text, newIndex) { const s = String(text || ""); return s.replace(/^(\s*)\d+(\.\s*(?:【)?)/, `$1${newIndex}$2`); } -/** - * 获取事件排序键 - * @param {object} ev - 事件对象 - * @returns {number} - */ function getEventSortKey(ev) { const r = parseFloorRange(ev?.summary); if (r) return r.start; @@ -438,20 +280,98 @@ function getEventSortKey(ev) { return m ? parseInt(m[1], 10) : Number.MAX_SAFE_INTEGER; } +// ───────────────────────────────────────────────────────────────────────────── +// 按楼层分组装配 orphan chunks(修复上下文重复) +// ───────────────────────────────────────────────────────────────────────────── + +function assembleOrphansByFloor(orphanCandidates, contextChunksByFloor, budget) { + if (!orphanCandidates?.length) { + return { lines: [], l0Count: 0, contextPairsCount: 0 }; + } + + // 1. 按楼层分组 + const byFloor = new Map(); + for (const c of orphanCandidates) { + const arr = byFloor.get(c.floor) || []; + arr.push(c); + byFloor.set(c.floor, arr); + } + + // 2. 楼层内按 chunkIdx 排序 + for (const [, chunks] of byFloor) { + chunks.sort((a, b) => (a.chunkIdx ?? 0) - (b.chunkIdx ?? 0)); + } + + // 3. 按楼层顺序装配 + const floorsSorted = Array.from(byFloor.keys()).sort((a, b) => a - b); + + const lines = []; + let l0Count = 0; + let contextPairsCount = 0; + + for (const floor of floorsSorted) { + const chunks = byFloor.get(floor); + if (!chunks?.length) continue; + + // 分离 L0 和 L1 + const l0Chunks = chunks.filter(c => c.isL0); + const l1Chunks = chunks.filter(c => !c.isL0); + + // L0 直接输出(不需要上下文) + for (const c of l0Chunks) { + const line = formatChunkFullLine(c); + if (!pushWithBudget(lines, line, budget)) { + return { lines, l0Count, contextPairsCount }; + } + l0Count++; + } + + // L1 按楼层统一处理 + if (l1Chunks.length > 0) { + const firstChunk = l1Chunks[0]; + const pairFloor = getContextFloor(firstChunk); + const pairCandidates = contextChunksByFloor.get(pairFloor) || []; + const contextChunk = pickContextChunk(pairCandidates, firstChunk); + + // 上下文在前 + if (contextChunk && contextChunk.floor < floor) { + const contextLine = formatContextChunkLine(contextChunk, true); + if (!pushWithBudget(lines, contextLine, budget)) { + return { lines, l0Count, contextPairsCount }; + } + contextPairsCount++; + } + + // 输出该楼层所有 L1 chunks + for (const c of l1Chunks) { + const line = formatChunkFullLine(c); + if (!pushWithBudget(lines, line, budget)) { + return { lines, l0Count, contextPairsCount }; + } + } + + // 上下文在后 + if (contextChunk && contextChunk.floor > floor) { + const contextLine = formatContextChunkLine(contextChunk, false); + if (!pushWithBudget(lines, contextLine, budget)) { + return { lines, l0Count, contextPairsCount }; + } + contextPairsCount++; + } + } + } + + return { lines, l0Count, contextPairsCount }; +} + // ───────────────────────────────────────────────────────────────────────────── // 非向量模式 // ───────────────────────────────────────────────────────────────────────────── -/** - * 构建非向量模式的 prompt - * @param {object} store - summary store - * @returns {string} - */ function buildNonVectorPrompt(store) { const data = store.json || {}; const sections = []; - // L1 facts(非向量模式不做分层过滤,全量注入) const allFacts = getFacts(); const factLines = allFacts .filter(f => !f.retracted) @@ -494,10 +414,6 @@ function buildNonVectorPrompt(store) { ); } -/** - * 构建非向量模式的注入文本 - * @returns {string} - */ export function buildNonVectorPromptText() { if (!getSettings().storySummary?.enabled) { return ""; @@ -524,16 +440,6 @@ export function buildNonVectorPromptText() { // 向量模式:预算装配 // ───────────────────────────────────────────────────────────────────────────── -/** - * 构建向量模式的 prompt - * @param {object} store - summary store - * @param {object} recallResult - 召回结果 - * @param {Map} causalById - 因果映射 - * @param {Array} focusEntities - 焦点实体 - * @param {object} meta - 元数据 - * @param {object} metrics - 指标对象 - * @returns {Promise} {promptText, injectionLogText, injectionStats, metrics} - */ async function buildVectorPrompt(store, recallResult, causalById, focusEntities = [], meta = null, metrics = null) { const T_Start = performance.now(); @@ -541,7 +447,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities const data = store.json || {}; const total = { used: 0, max: MAIN_BUDGET_MAX }; - // 预装配容器 const assembled = { facts: { lines: [], tokens: 0 }, arcs: { lines: [], tokens: 0 }, @@ -573,7 +478,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities }; // ═══════════════════════════════════════════════════════════════════════ - // [优先级 1] 世界约束 - 最高优先级(带分层过滤) + // [优先级 1] 世界约束 // ═══════════════════════════════════════════════════════════════════════ const T_L1_Start = performance.now(); @@ -582,7 +487,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities const knownCharacters = getKnownCharacters(store); const factLines = formatFactsForInjection(allFacts, focusEntities, knownCharacters); - // METRICS: L1 指标 if (metrics) { metrics.l1.factsTotal = allFacts.length; metrics.l1.factsFiltered = allFacts.length - factLines.length; @@ -599,7 +503,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities injectionStats.facts.tokens = l1Budget.used; injectionStats.facts.filtered = allFacts.length - factLines.length; - // METRICS if (metrics) { metrics.l1.factsInjected = assembled.facts.lines.length; metrics.l1.tokens = l1Budget.used; @@ -613,7 +516,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities } // ═══════════════════════════════════════════════════════════════════════ - // [优先级 2] 人物弧光 - 预留预算 + // [优先级 2] 人物弧光 // ═══════════════════════════════════════════════════════════════════════ if (data.arcs?.length && total.used < total.max) { @@ -652,13 +555,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities const chunks = recallResult?.chunks || []; const usedChunkIds = new Set(); - /** - * 为事件选择最佳证据 chunk - * @param {object} eventObj - 事件对象 - * @returns {object|null} 最佳 chunk - */ - - // 优先 L0 虚拟 chunk,否则按 chunkIdx 选第一个 function pickBestChunkForEvent(eventObj) { const range = parseFloorRange(eventObj?.summary); if (!range) return null; @@ -667,27 +563,18 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities for (const c of chunks) { if (usedChunkIds.has(c.chunkId)) continue; if (c.floor < range.start || c.floor > range.end) continue; - + if (!best) { best = c; } else if (c.isL0 && !best.isL0) { - // L0 优先 best = c; } else if (c.isL0 === best.isL0 && (c.chunkIdx ?? 0) < (best.chunkIdx ?? 0)) { - // 同类型按 chunkIdx 选靠前的 best = c; } } return best; - } + } - /** - * 格式化带证据的事件 - * @param {object} e - 事件召回项 - * @param {number} idx - 索引 - * @param {object|null} chunk - 证据 chunk - * @returns {string} - */ function formatEventWithEvidence(e, idx, chunk) { const ev = e.event || {}; const time = ev.timeLabel || ""; @@ -775,7 +662,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities }); } - // 重排 selectedDirect.sort((a, b) => getEventSortKey(a.event) - getEventSortKey(b.event)); selectedSimilar.sort((a, b) => getEventSortKey(a.event) - getEventSortKey(b.event)); @@ -829,47 +715,22 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities } if (orphanCandidates.length && total.used < total.max) { - const orphans = orphanCandidates - .sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0))); - const l1Budget = { used: 0, max: Math.min(ORPHAN_MAX, total.max - total.used) }; - let l0Count = 0; - let contextPairsCount = 0; - for (const c of orphans) { - if (c.isL0) { - const line = formatChunkFullLine(c); - if (!pushWithBudget(assembled.orphans.lines, line, l1Budget)) break; - injectionStats.orphans.injected++; - l0Count++; - continue; - } - - const pairFloor = getContextFloor(c); - const pairCandidates = contextChunksByFloor.get(pairFloor) || []; - const contextChunk = pickContextChunk(pairCandidates, c); - - const formattedLines = formatChunkWithContext(c, contextChunk); - - let allAdded = true; - for (const line of formattedLines) { - if (!pushWithBudget(assembled.orphans.lines, line, l1Budget)) { - allAdded = false; - break; - } - } - - if (!allAdded) break; - - injectionStats.orphans.injected++; - if (contextChunk) contextPairsCount++; - } + const result = assembleOrphansByFloor( + orphanCandidates.sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0))), + contextChunksByFloor, + l1Budget + ); + assembled.orphans.lines = result.lines; assembled.orphans.tokens = l1Budget.used; total.used += l1Budget.used; + + injectionStats.orphans.injected = result.lines.length; injectionStats.orphans.tokens = l1Budget.used; - injectionStats.orphans.l0Count = l0Count; - injectionStats.orphans.contextPairs = contextPairsCount; + injectionStats.orphans.l0Count = result.l0Count; + injectionStats.orphans.contextPairs = result.contextPairsCount; } // ═══════════════════════════════════════════════════════════════════════ @@ -891,7 +752,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities if (pairFloor >= 0) recentContextFloors.add(pairFloor); } - let recentContextChunksByFloor = new Map(); if (chatId && recentContextFloors.size > 0) { const newFloors = Array.from(recentContextFloors).filter(f => !contextChunksByFloor.has(f)); if (newFloors.length > 0) { @@ -907,47 +767,25 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities xbLog.warn(MODULE_ID, "获取近期配对chunks失败", e); } } - recentContextChunksByFloor = contextChunksByFloor; } - const recentOrphans = recentOrphanCandidates - .sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0))); + if (recentOrphanCandidates.length) { + const recentBudget = { used: 0, max: RECENT_ORPHAN_MAX }; - const recentBudget = { used: 0, max: RECENT_ORPHAN_MAX }; - let recentContextPairsCount = 0; + const result = assembleOrphansByFloor( + recentOrphanCandidates.sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0))), + contextChunksByFloor, + recentBudget + ); - for (const c of recentOrphans) { - if (c.isL0) { - const line = formatChunkFullLine(c); - if (!pushWithBudget(assembled.recentOrphans.lines, line, recentBudget)) break; - recentOrphanStats.injected++; - continue; - } + assembled.recentOrphans.lines = result.lines; + assembled.recentOrphans.tokens = recentBudget.used; - const pairFloor = getContextFloor(c); - const pairCandidates = recentContextChunksByFloor.get(pairFloor) || []; - const contextChunk = pickContextChunk(pairCandidates, c); - - const formattedLines = formatChunkWithContext(c, contextChunk); - - let allAdded = true; - for (const line of formattedLines) { - if (!pushWithBudget(assembled.recentOrphans.lines, line, recentBudget)) { - allAdded = false; - break; - } - } - - if (!allAdded) break; - - recentOrphanStats.injected++; - if (contextChunk) recentContextPairsCount++; + recentOrphanStats.injected = result.lines.length; + recentOrphanStats.tokens = recentBudget.used; + recentOrphanStats.floorRange = `${recentStart + 1}~${recentEnd + 1}楼`; + recentOrphanStats.contextPairs = result.contextPairsCount; } - - assembled.recentOrphans.tokens = recentBudget.used; - recentOrphanStats.tokens = recentBudget.used; - recentOrphanStats.floorRange = `${recentStart + 1}~${recentEnd + 1}楼`; - recentOrphanStats.contextPairs = recentContextPairsCount; } // ═══════════════════════════════════════════════════════════════════════ @@ -990,9 +828,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities `<剧情记忆>\n\n${sections.join("\n\n")}\n\n\n` + `${buildPostscript()}`; - // METRICS: 更新 L4 和 Budget 指标 if (metrics) { - // L4 指标 metrics.l4.sectionsIncluded = []; if (assembled.facts.lines.length) metrics.l4.sectionsIncluded.push('constraints'); if (assembled.events.direct.length) metrics.l4.sectionsIncluded.push('direct_events'); @@ -1004,7 +840,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities metrics.l4.formattingTime = Math.round(performance.now() - T_L4_Start); metrics.timing.l4Formatting = metrics.l4.formattingTime; - // Budget 指标 metrics.budget.total = total.used + (assembled.recentOrphans.tokens || 0); metrics.budget.limit = TOTAL_BUDGET_MAX; metrics.budget.utilization = Math.round(metrics.budget.total / TOTAL_BUDGET_MAX * 100); @@ -1016,13 +851,11 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities arcs: assembled.arcs.tokens, }; - // L3 额外指标 metrics.l3.tokens = injectionStats.orphans.tokens + (recentOrphanStats.tokens || 0); metrics.l3.contextPairsAdded = injectionStats.orphans.contextPairs + recentOrphanStats.contextPairs; metrics.l3.assemblyTime = Math.round(performance.now() - T_Start - (metrics.timing.l1Constraints || 0) - metrics.l4.formattingTime); metrics.timing.l3Assembly = metrics.l3.assemblyTime; - // 质量指标 const totalFacts = allFacts.length; metrics.quality.constraintCoverage = totalFacts > 0 ? Math.round(assembled.facts.lines.length / totalFacts * 100) @@ -1035,7 +868,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities ? Math.round(chunksWithEvents / totalChunks * 100) : 0; - // 检测问题 metrics.quality.potentialIssues = detectIssues(metrics); } @@ -1046,13 +878,6 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities // 因果证据补充 // ───────────────────────────────────────────────────────────────────────────── -/** - * 为因果事件附加证据 chunk - * @param {Array} causalEvents - 因果事件列表 - * @param {Map} eventVectorMap - 事件向量映射 - * @param {Map} chunkVectorMap - chunk 向量映射 - * @param {Map} chunksMap - chunk 映射 - */ async function attachEvidenceToCausalEvents(causalEvents, eventVectorMap, chunkVectorMap, chunksMap) { for (const c of causalEvents) { c._evidenceChunk = null; @@ -1100,12 +925,6 @@ async function attachEvidenceToCausalEvents(causalEvents, eventVectorMap, chunkV // 向量模式:召回 + 注入 // ───────────────────────────────────────────────────────────────────────────── -/** - * 构建向量模式的注入文本 - * @param {boolean} excludeLastAi - 是否排除最后一条 AI 消息 - * @param {object} hooks - 钩子 {postToFrame, echo, pendingUserMessage} - * @returns {Promise} {text, logText} - */ export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) { const { postToFrame = null, echo = null, pendingUserMessage = null } = hooks; @@ -1156,7 +975,6 @@ export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) { metrics: recallResult?.metrics || null, }; - // 给因果事件挂证据 const causalEvents = recallResult.causalEvents || []; if (causalEvents.length > 0) { if (chatId) { @@ -1228,7 +1046,6 @@ export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) { return { text: "", logText: "\n[Vector Recall Empty]\nNo recall candidates / vectors not ready.\n" }; } - // 拼装向量 prompt,传入 focusEntities 和 metrics const { promptText, metrics: promptMetrics } = await buildVectorPrompt( store, recallResult, @@ -1238,16 +1055,13 @@ export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) { recallResult?.metrics || null ); - // wrapper const cfg = getSummaryPanelConfig(); let finalText = String(promptText || ""); if (cfg.trigger?.wrapperHead) finalText = cfg.trigger.wrapperHead + "\n" + finalText; if (cfg.trigger?.wrapperTail) finalText = finalText + "\n" + cfg.trigger.wrapperTail; - // METRICS: 生成完整的指标日志 const metricsLogText = promptMetrics ? formatMetricsLog(promptMetrics) : ''; - // 发给 iframe if (postToFrame) { postToFrame({ type: "RECALL_LOG", text: metricsLogText }); } diff --git a/modules/story-summary/story-summary.css b/modules/story-summary/story-summary.css index cf30ed2..ec9b19a 100644 --- a/modules/story-summary/story-summary.css +++ b/modules/story-summary/story-summary.css @@ -1455,23 +1455,25 @@ h1 span { } #recall-log-content { - flex: 1; - min-height: 0; - white-space: pre-wrap; - font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; + font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; font-size: 12px; line-height: 1.6; - background: var(--bg3); - padding: 16px; - border-radius: 4px; - overflow-y: auto; + color: #e8e8e8; + white-space: pre-wrap !important; + overflow-x: hidden !important; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } .recall-empty { - color: var(--txt3); + color: #999; text-align: center; padding: 40px; font-style: italic; + font-size: .8125rem; + line-height: 1.8; } /* 移动端适配 */ @@ -1483,9 +1485,11 @@ h1 span { border-radius: 0; } + .debug-log-viewer, #recall-log-content { font-size: 11px; padding: 12px; + line-height: 1.5; } } @@ -2732,14 +2736,18 @@ h1 span { margin-bottom: 4px; } +/* ═══════════════════════════════════════════════════════════════════════════ + Recall Log / Debug Log + ═══════════════════════════════════════════════════════════════════════════ */ + .debug-log-viewer { - background: #1e1e1e; - color: #d4d4d4; + background: #1a1a1a; + color: #e0e0e0; padding: 16px; border-radius: 8px; - font-family: 'Consolas', 'Monaco', monospace; + font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; font-size: 12px; - line-height: 1.5; + line-height: 1.6; max-height: 60vh; overflow-y: auto; overflow-x: hidden; @@ -2749,7 +2757,7 @@ h1 span { } .recall-empty { - color: var(--txt3); + color: #999; text-align: center; padding: 40px; font-style: italic; @@ -2884,15 +2892,6 @@ h1 span { Metrics Log Styling ═══════════════════════════════════════════════════════════════════════════ */ -#recall-log-content { - font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; - font-size: 11px; - line-height: 1.5; - white-space: pre; - overflow-x: auto; - tab-size: 4; -} - #recall-log-content .metric-warn { color: #f59e0b; } diff --git a/modules/story-summary/vector/llm/llm-service.js b/modules/story-summary/vector/llm/llm-service.js index 37f2357..537f9eb 100644 --- a/modules/story-summary/vector/llm/llm-service.js +++ b/modules/story-summary/vector/llm/llm-service.js @@ -29,7 +29,7 @@ function b64UrlEncode(str) { /** * 统一LLM调用 - 走酒馆后端(非流式) - * 修复:assistant prefill 用 bottomassistant 参数传递 + * assistant prefill 用 bottomassistant 参数传递 */ export async function callLLM(messages, options = {}) { const { @@ -46,10 +46,10 @@ export async function callLLM(messages, options = {}) { throw new Error('L0 requires siliconflow API key'); } - // ★ 关键修复:分离 assistant prefill + // 分离 assistant prefill let topMessages = [...messages]; let assistantPrefill = ''; - + if (topMessages.length > 0 && topMessages[topMessages.length - 1]?.role === 'assistant') { const lastMsg = topMessages.pop(); assistantPrefill = lastMsg.content || ''; @@ -70,6 +70,10 @@ export async function callLLM(messages, options = {}) { apipassword: apiKey, model: DEFAULT_L0_MODEL, }; + const isQwen3 = String(DEFAULT_L0_MODEL || '').includes('Qwen3'); + if (isQwen3) { + args.enable_thinking = 'false'; + } // ★ 用 bottomassistant 参数传递 prefill if (assistantPrefill) { diff --git a/modules/streaming-generation.js b/modules/streaming-generation.js index a134f61..b795050 100644 --- a/modules/streaming-generation.js +++ b/modules/streaming-generation.js @@ -240,6 +240,9 @@ class StreamingGeneration { include_reasoning: oai_settings?.show_thoughts ?? true, reasoning_effort: oai_settings?.reasoning_effort || 'medium', }; + if (baseOptions?.enable_thinking !== undefined) body.enable_thinking = baseOptions.enable_thinking; + if (baseOptions?.thinking_budget !== undefined) body.thinking_budget = baseOptions.thinking_budget; + if (baseOptions?.min_p !== undefined) body.min_p = baseOptions.min_p; // Claude 专用:top_k if (source === chat_completion_sources.CLAUDE) { @@ -949,6 +952,9 @@ class StreamingGeneration { temperature: this.parseOpt(args, 'temperature'), presence_penalty: this.parseOpt(args, 'presence_penalty'), frequency_penalty: this.parseOpt(args, 'frequency_penalty'), + enable_thinking: this.parseOpt(args, 'enable_thinking'), + thinking_budget: this.parseOpt(args, 'thinking_budget'), + min_p: this.parseOpt(args, 'min_p'), }; let parsedStop; try {