@@ -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<string>} 角色名集合(规范化后)
*/
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<object>} {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 ;
@@ -671,23 +567,14 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities
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 = recentBud get. 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<object>} {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 } ) ;
}