Files
LittleWhiteBox/modules/story-summary/generate/prompt.js

796 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ═══════════════════════════════════════════════════════════════════════════
// Story Summary - Prompt Injection (Final Clean Version)
// - 注入只在 GENERATION_STARTED 发生(由 story-summary.js 调用)
// - 向量关闭:注入全量总结(世界/事件/弧光)
// - 向量开启:召回 + 预算装配注入
// - 没有“快速注入”写入 extension_prompts避免覆盖/残留/竞态
// ═══════════════════════════════════════════════════════════════════════════
import { getContext } from "../../../../../../extensions.js";
import {
extension_prompts,
extension_prompt_types,
extension_prompt_roles,
} from "../../../../../../../script.js";
import { xbLog } from "../../../core/debug-core.js";
import { getSummaryStore } from "../data/store.js";
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
import { recallMemory, buildQueryText } from "../vector/recall.js";
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors } from "../vector/chunk-store.js";
const MODULE_ID = "summaryPrompt";
const SUMMARY_PROMPT_KEY = "LittleWhiteBox_StorySummary";
// ─────────────────────────────────────────────────────────────────────────────
// 召回失败提示节流(避免连续生成刷屏)
// ─────────────────────────────────────────────────────────────────────────────
let lastRecallFailAt = 0;
const RECALL_FAIL_COOLDOWN_MS = 10_000;
function canNotifyRecallFail() {
const now = Date.now();
if (now - lastRecallFailAt < RECALL_FAIL_COOLDOWN_MS) return false;
lastRecallFailAt = now;
return true;
}
// ─────────────────────────────────────────────────────────────────────────────
// 预算常量(向量模式使用)
// ─────────────────────────────────────────────────────────────────────────────
const BUDGET = { total: 10000 };
const L3_MAX = Math.floor(BUDGET.total * 0.20); // 2000
const ARCS_MAX = Math.floor(BUDGET.total * 0.15); // 1500
// ─────────────────────────────────────────────────────────────────────────────
// 工具函数
// ─────────────────────────────────────────────────────────────────────────────
function estimateTokens(text) {
if (!text) return 0;
const s = String(text);
const zh = (s.match(/[\u4e00-\u9fff]/g) || []).length;
return Math.ceil(zh + (s.length - zh) / 4);
}
function pushWithBudget(lines, text, state) {
const t = estimateTokens(text);
if (state.used + t > state.max) return false;
lines.push(text);
state.used += t;
return true;
}
function cosineSimilarity(a, b) {
if (!a?.length || !b?.length || a.length !== b.length) return 0;
let dot = 0, nA = 0, nB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
nA += a[i] * a[i];
nB += b[i] * b[i];
}
return nA && nB ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
}
// 从 summary 解析楼层范围:(#321-322) 或 (#321)
function parseFloorRange(summary) {
if (!summary) return null;
const match = String(summary).match(/\(#(\d+)(?:-(\d+))?\)/);
if (!match) return null;
const start = Math.max(0, parseInt(match[1], 10) - 1);
const end = Math.max(0, (match[2] ? parseInt(match[2], 10) : parseInt(match[1], 10)) - 1);
return { start, end };
}
// 去掉 summary 末尾楼层标记(按你要求:事件本体不显示楼层范围)
function cleanSummary(summary) {
return String(summary || "")
.replace(/\s*\(#\d+(?:-\d+)?\)\s*$/, "")
.trim();
}
// ─────────────────────────────────────────────────────────────────────────────
// 系统前导与后缀
// ─────────────────────────────────────────────────────────────────────────────
function buildSystemPreamble() {
return [
"以上内容为因上下文窗口限制保留的可见历史",
"【剧情记忆】为对以上可见、不可见历史的总结",
"1) 【世界状态】属于硬约束",
"2) 【事件/证据/碎片/人物弧光】可用于补全上下文与动机。",
"",
"请阅读并内化以下剧情记忆:",
].join("\n");
}
function buildPostscript() {
return [
"",
"——",
].join("\n");
}
// ─────────────────────────────────────────────────────────────────────────────
// 格式化函数
// ─────────────────────────────────────────────────────────────────────────────
function formatWorldLines(world) {
return [...(world || [])]
.sort((a, b) => (b.floor || 0) - (a.floor || 0))
.map(w => `- ${w.topic}${w.content}`);
}
function formatArcLine(a) {
const moments = (a.moments || [])
.map(m => (typeof m === "string" ? m : m.text))
.filter(Boolean);
if (moments.length) {
return `- ${a.name}${moments.join(" → ")}(当前:${a.trajectory}`;
}
return `- ${a.name}${a.trajectory}`;
}
// 完整 chunk 输出(不截断)
function formatChunkFullLine(c) {
const speaker = c.isUser ? "{{user}}" : "{{char}}";
return ` #${c.floor + 1} [${speaker}] ${String(c.text || "").trim()}`;
}
// 因果事件格式(仅作为“前因线索”展示,仍保留楼层提示)
function formatCausalEventLine(causalItem, causalById) {
const ev = causalItem?.event || {};
const depth = Math.max(1, Math.min(9, causalItem?._causalDepth || 1));
const indent = " │" + " ".repeat(depth - 1);
const prefix = `${indent}├─ 前因`;
const time = ev.timeLabel ? `${ev.timeLabel}` : "";
const people = (ev.participants || []).join(" / ");
const summary = cleanSummary(ev.summary);
const r = parseFloorRange(ev.summary);
const floorHint = r ? `(#${r.start + 1}${r.end !== r.start ? `-${r.end + 1}` : ""})` : "";
const lines = [];
lines.push(`${prefix}${time}${people ? ` ${people}` : ""}`);
const body = `${summary}${floorHint ? ` ${floorHint}` : ""}`.trim();
lines.push(`${indent} ${body}`);
const evidence = causalItem._evidenceChunk;
if (evidence) {
const speaker = evidence.speaker || "角色";
const preview = String(evidence.text || "");
const clip = preview.length > 60 ? preview.slice(0, 60) + "..." : preview;
lines.push(`${indent} #${evidence.floor + 1} [${speaker}] ${clip}`);
}
return lines.join("\n");
}
// ─────────────────────────────────────────────────────────────────────────────
// 装配日志(开发调试用)
// ─────────────────────────────────────────────────────────────────────────────
function formatInjectionLog(stats, details) {
const pct = (n, d) => (d > 0 ? Math.round((n / d) * 100) : 0);
const lines = [
"",
"╔══════════════════════════════════════════════════════════════╗",
"║ Prompt 装配报告 ║",
"╠══════════════════════════════════════════════════════════════╣",
`║ 总预算: ${stats.budget.max} tokens`,
`║ 已使用: ${stats.budget.used} tokens (${pct(stats.budget.used, stats.budget.max)}%)`,
`║ 剩余: ${stats.budget.max - stats.budget.used} tokens`,
"╚══════════════════════════════════════════════════════════════╝",
"",
];
// 世界状态
lines.push("┌─────────────────────────────────────────────────────────────┐");
lines.push("│ [1] 世界状态 (上限 20% = 2000) │");
lines.push("└─────────────────────────────────────────────────────────────┘");
lines.push(` 注入: ${stats.world.count} 条 | ${stats.world.tokens} tokens`);
lines.push("");
// 事件
lines.push("┌─────────────────────────────────────────────────────────────┐");
lines.push("│ [2] 事件(含证据) │");
lines.push("└─────────────────────────────────────────────────────────────┘");
lines.push(` 选入: ${stats.events.selected} 条 | 事件本体: ${stats.events.tokens} tokens`);
lines.push(` 挂载证据: ${stats.evidence.attached} 条 | 证据: ${stats.evidence.tokens} tokens`);
lines.push(` DIRECT: ${details.directCount || 0} | SIMILAR: ${details.similarCount || 0}`);
if (details.eventList?.length) {
lines.push(" ────────────────────────────────────────");
details.eventList.slice(0, 20).forEach((ev, i) => {
const type = ev.isDirect ? "D" : "S";
const hasE = ev.hasEvidence ? " +E" : "";
const title = (ev.title || "(无标题)").slice(0, 32);
lines.push(` ${String(i + 1).padStart(2)}. [${type}${hasE}] ${title} (${ev.tokens} tok, sim=${ev.similarity.toFixed(3)})`);
});
if (details.eventList.length > 20) lines.push(` ... 还有 ${details.eventList.length - 20}`);
}
lines.push("");
// 碎片
lines.push("┌─────────────────────────────────────────────────────────────┐");
lines.push("│ [3] 记忆碎片(按楼层从早到晚) │");
lines.push("└─────────────────────────────────────────────────────────────┘");
lines.push(` 注入: ${stats.orphans.injected} 条 | ${stats.orphans.tokens} tokens`);
lines.push("");
// 弧光
lines.push("┌─────────────────────────────────────────────────────────────┐");
lines.push("│ [4] 人物弧光(上限 15% = 1500放在最底 │");
lines.push("└─────────────────────────────────────────────────────────────┘");
lines.push(` 注入: ${stats.arcs.count} 条 | ${stats.arcs.tokens} tokens`);
lines.push("");
// 预算条形
lines.push("┌─────────────────────────────────────────────────────────────┐");
lines.push("│ 【预算分布】 │");
lines.push("└─────────────────────────────────────────────────────────────┘");
const total = stats.budget.max;
const bar = (tokens, label) => {
const width = Math.round((tokens / total) * 40);
const pctStr = pct(tokens, total) + "%";
return ` ${label.padEnd(6)} ${"█".repeat(width).padEnd(40)} ${String(tokens).padStart(5)} (${pctStr})`;
};
lines.push(bar(stats.world.tokens, "世界"));
lines.push(bar(stats.events.tokens, "事件"));
lines.push(bar(stats.evidence.tokens, "证据"));
lines.push(bar(stats.orphans.tokens, "碎片"));
lines.push(bar(stats.arcs.tokens, "弧光"));
lines.push(bar(stats.budget.max - stats.budget.used, "剩余"));
lines.push("");
return lines.join("\n");
}
// ─────────────────────────────────────────────────────────────────────────────
// 非向量模式:全量总结注入(世界 + 事件 + 弧光)
// 仅在 GENERATION_STARTED 调用
// ─────────────────────────────────────────────────────────────────────────────
function buildNonVectorPrompt(store) {
const data = store.json || {};
const sections = [];
if (data.world?.length) {
const lines = formatWorldLines(data.world);
sections.push(`[世界状态] 请严格遵守\n${lines.join("\n")}`);
}
if (data.events?.length) {
const lines = data.events.map((ev, i) => {
const time = ev.timeLabel || "";
const title = ev.title || "";
const people = (ev.participants || []).join(" / ");
const summary = cleanSummary(ev.summary);
const header = time ? `${i + 1}.【${time}${title || people}` : `${i + 1}. ${title || people}`;
return `${header}\n ${summary}`;
});
sections.push(`[剧情记忆]\n\n${lines.join("\n\n")}`);
}
if (data.arcs?.length) {
const lines = data.arcs.map(formatArcLine);
sections.push(`[人物弧光]\n${lines.join("\n")}`);
}
if (!sections.length) return "";
return (
`${buildSystemPreamble()}\n` +
`<剧情记忆>\n\n${sections.join("\n\n")}\n\n</剧情记忆>\n` +
`${buildPostscript()}`
);
}
export async function injectNonVectorPrompt(postToFrame = null) {
if (!getSettings().storySummary?.enabled) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const { chat } = getContext();
const store = getSummaryStore();
if (!store?.json) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
let text = buildNonVectorPrompt(store);
if (!text.trim()) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
// wrapper沿用面板设置
const cfg = getSummaryPanelConfig();
if (cfg.trigger?.wrapperHead) text = cfg.trigger.wrapperHead + "\n" + text;
if (cfg.trigger?.wrapperTail) text = text + "\n" + cfg.trigger.wrapperTail;
const lastIdx = store.lastSummarizedMesId ?? 0;
let depth = (chat?.length || 0) - lastIdx - 1;
if (depth < 0) depth = 0;
if (cfg.trigger?.forceInsertAtEnd) depth = 10000;
extension_prompts[SUMMARY_PROMPT_KEY] = {
value: text,
position: extension_prompt_types.IN_CHAT,
depth,
role: extension_prompt_roles.SYSTEM,
};
if (postToFrame) {
postToFrame({ type: "RECALL_LOG", text: "\n[Non-vector] Injected full summary prompt.\n" });
}
}
// ─────────────────────────────────────────────────────────────
// 向量模式:预算装配(世界 → 事件(带证据) → 碎片 → 弧光)
// ─────────────────────────────────────────────────────────────
async function buildVectorPrompt(store, recallResult, causalById, queryEntities = []) {
const data = store.json || {};
const total = { used: 0, max: BUDGET.total };
const sections = [];
const injectionStats = {
budget: { max: BUDGET.total, used: 0 },
world: { count: 0, tokens: 0 },
arcs: { count: 0, tokens: 0 },
events: { selected: 0, tokens: 0 },
evidence: { attached: 0, tokens: 0 },
orphans: { injected: 0, tokens: 0 },
};
const details = {
eventList: [],
directCount: 0,
similarCount: 0,
};
// [世界状态]20%
const worldLines = formatWorldLines(data.world);
if (worldLines.length) {
const l3 = { used: 0, max: Math.min(L3_MAX, total.max - total.used) };
const lines = [];
for (const line of worldLines) {
if (!pushWithBudget(lines, line, l3)) break;
}
if (lines.length) {
sections.push(`[世界状态] 请严格遵守\n${lines.join("\n")}`);
total.used += l3.used;
injectionStats.world.count = lines.length;
injectionStats.world.tokens = l3.used;
}
}
// 事件(含证据)
const recalledEvents = (recallResult?.events || []).filter(e => e?.event?.summary);
const chunks = recallResult?.chunks || [];
const usedChunkIds = new Set();
function pickBestChunkForEvent(eventObj) {
const range = parseFloorRange(eventObj?.summary);
if (!range) return null;
let best = null;
for (const c of chunks) {
if (usedChunkIds.has(c.chunkId)) continue;
if (c.floor < range.start || c.floor > range.end) continue;
if (!best || (c.similarity || 0) > (best.similarity || 0)) best = c;
}
return best;
}
function formatEventWithEvidence(e, idx, chunk) {
const ev = e.event || {};
const time = ev.timeLabel || "";
const title = String(ev.title || "").trim();
const people = (ev.participants || []).join(" / ").trim();
const summary = cleanSummary(ev.summary);
const displayTitle = title || people || ev.id || "事件";
const header = time ? `${idx}.【${time}${displayTitle}` : `${idx}. ${displayTitle}`;
const lines = [header];
if (people && displayTitle !== people) lines.push(` ${people}`);
lines.push(` ${summary}`);
for (const cid of ev.causedBy || []) {
const c = causalById?.get(cid);
if (c) lines.push(formatCausalEventLine(c, causalById));
}
if (chunk) {
lines.push(` ${formatChunkFullLine(chunk)}`);
}
return lines.join("\n");
}
// 候选按相似度从高到低(保证高分优先拥有证据)
const candidates = [...recalledEvents].sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
let idxDirect = 1;
let idxSimilar = 1;
const selectedDirectTexts = [];
const selectedSimilarTexts = [];
for (const e of candidates) {
if (total.used >= total.max) break;
const isDirect = e._recallType === "DIRECT";
const idx = isDirect ? idxDirect : idxSimilar;
const bestChunk = pickBestChunkForEvent(e.event);
// 先尝试“带证据”
let text = formatEventWithEvidence(e, idx, bestChunk);
let cost = estimateTokens(text);
let hasEvidence = !!bestChunk;
// 塞不下就退化成“不带证据”
if (total.used + cost > total.max) {
text = formatEventWithEvidence(e, idx, null);
cost = estimateTokens(text);
hasEvidence = false;
if (total.used + cost > total.max) {
continue;
}
}
// 写入
if (isDirect) {
selectedDirectTexts.push(text);
idxDirect++;
} else {
selectedSimilarTexts.push(text);
idxSimilar++;
}
injectionStats.events.selected++;
total.used += cost;
// tokens 拆分记账(事件本体 vs 证据)
if (hasEvidence && bestChunk) {
const chunkLine = formatChunkFullLine(bestChunk);
const ct = estimateTokens(chunkLine);
injectionStats.evidence.attached++;
injectionStats.evidence.tokens += ct;
usedChunkIds.add(bestChunk.chunkId);
// 事件本体 tokens = cost - ct粗略但够调试
injectionStats.events.tokens += Math.max(0, cost - ct);
} else {
injectionStats.events.tokens += cost;
}
details.eventList.push({
title: e.event?.title || e.event?.id,
isDirect,
hasEvidence,
tokens: cost,
similarity: e.similarity || 0,
});
}
details.directCount = selectedDirectTexts.length;
details.similarCount = selectedSimilarTexts.length;
if (selectedDirectTexts.length) {
sections.push(`[亲身经历]\n\n${selectedDirectTexts.join("\n\n")}`);
}
if (selectedSimilarTexts.length) {
sections.push(`[相关背景]\n\n${selectedSimilarTexts.join("\n\n")}`);
}
// [记忆碎片]orphans 按楼层从早到晚,完整 chunk
if (chunks.length && total.used < total.max) {
const orphans = chunks
.filter(c => !usedChunkIds.has(c.chunkId))
.sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0)));
const l1 = { used: 0, max: total.max - total.used };
const lines = [];
for (const c of orphans) {
const line = formatChunkFullLine(c);
if (!pushWithBudget(lines, line, l1)) break;
injectionStats.orphans.injected++;
}
if (lines.length) {
sections.push(`[记忆碎片]\n${lines.join("\n")}`);
total.used += l1.used;
injectionStats.orphans.tokens = l1.used;
}
}
// [人物弧光]:放最底,且上限 15%(只保留 user + queryEntities
if (data.arcs?.length && total.used < total.max) {
const { name1 } = getContext();
const userName = String(name1 || "").trim();
const relevant = new Set(
[userName, ...(queryEntities || [])]
.map(s => String(s || "").trim())
.filter(Boolean)
);
const filtered = (data.arcs || []).filter(a => {
const n = String(a?.name || "").trim();
return n && relevant.has(n);
});
if (filtered.length) {
const arcBudget = { used: 0, max: Math.min(ARCS_MAX, total.max - total.used) };
const arcLines = [];
for (const a of filtered) {
const line = formatArcLine(a);
if (!pushWithBudget(arcLines, line, arcBudget)) break;
}
if (arcLines.length) {
sections.push(`[人物弧光]\n${arcLines.join("\n")}`);
total.used += arcBudget.used;
injectionStats.arcs.count = arcLines.length;
injectionStats.arcs.tokens = arcBudget.used;
}
}
}
injectionStats.budget.used = total.used;
if (!sections.length) {
return { promptText: "", injectionLogText: "", injectionStats };
}
const promptText =
`${buildSystemPreamble()}\n` +
`<剧情记忆>\n\n${sections.join("\n\n")}\n\n</剧情记忆>\n` +
`${buildPostscript()}`;
const injectionLogText = formatInjectionLog(injectionStats, details);
return { promptText, injectionLogText, injectionStats };
}
// ─────────────────────────────────────────────────────────────────────────────
// 因果证据补充(给 causalEvents 挂 evidence chunk
// ─────────────────────────────────────────────────────────────────────────────
async function attachEvidenceToCausalEvents(causalEvents, eventVectorMap, chunkVectorMap, chunksMap) {
for (const c of causalEvents) {
c._evidenceChunk = null;
const ev = c.event;
if (!ev?.id) continue;
const evVec = eventVectorMap.get(ev.id);
if (!evVec?.length) continue;
const range = parseFloorRange(ev.summary);
if (!range) continue;
const candidateChunks = [];
for (const [chunkId, chunk] of chunksMap) {
if (chunk.floor >= range.start && chunk.floor <= range.end) {
const vec = chunkVectorMap.get(chunkId);
if (vec?.length) candidateChunks.push({ chunk, vec });
}
}
if (!candidateChunks.length) continue;
let best = null;
let bestSim = -1;
for (const { chunk, vec } of candidateChunks) {
const sim = cosineSimilarity(evVec, vec);
if (sim > bestSim) {
bestSim = sim;
best = chunk;
}
}
if (best && bestSim > 0.3) {
c._evidenceChunk = {
floor: best.floor,
speaker: best.speaker,
text: best.text,
similarity: bestSim,
};
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// ✅ 向量模式:召回 + 注入(供 story-summary.js 在 GENERATION_STARTED 调用)
// ─────────────────────────────────────────────────────────────────────────────
export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
const { postToFrame = null, echo = null } = hooks;
if (!getSettings().storySummary?.enabled) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const { chat } = getContext();
const store = getSummaryStore();
if (!store?.json) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const allEvents = store.json.events || [];
const lastIdx = store.lastSummarizedMesId ?? 0;
const length = chat?.length || 0;
if (lastIdx >= length) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) {
// 向量没开,不该走这条
return;
}
let recallResult = null;
let causalById = new Map();
try {
const queryText = buildQueryText(chat, 2, excludeLastAi);
recallResult = await recallMemory(queryText, allEvents, vectorCfg, { excludeLastAi });
recallResult = {
...recallResult,
events: recallResult?.events || [],
chunks: recallResult?.chunks || [],
causalEvents: recallResult?.causalEvents || [],
queryEntities: recallResult?.queryEntities || [],
logText: recallResult?.logText || "",
};
// 给因果事件挂证据(用于因果行展示)
const causalEvents = recallResult.causalEvents || [];
if (causalEvents.length > 0) {
const { chatId } = getContext();
if (chatId) {
try {
const floors = new Set();
for (const c of causalEvents) {
const r = parseFloorRange(c.event?.summary);
if (!r) continue;
for (let f = r.start; f <= r.end; f++) floors.add(f);
}
const [chunks, chunkVecs, eventVecs] = await Promise.all([
getChunksByFloors(chatId, Array.from(floors)),
getAllChunkVectors(chatId),
getAllEventVectors(chatId),
]);
const chunksMap = new Map(chunks.map(c => [c.chunkId, c]));
const chunkVectorMap = new Map(chunkVecs.map(v => [v.chunkId, v.vector]));
const eventVectorMap = new Map(eventVecs.map(v => [v.eventId, v.vector]));
await attachEvidenceToCausalEvents(causalEvents, eventVectorMap, chunkVectorMap, chunksMap);
} catch (e) {
xbLog.warn(MODULE_ID, "Causal evidence attachment failed", e);
}
}
}
causalById = new Map(
recallResult.causalEvents
.map(c => [c?.event?.id, c])
.filter(x => x[0])
);
} catch (e) {
xbLog.error(MODULE_ID, "向量召回失败", e);
// 显式提示(节流)
if (echo && canNotifyRecallFail()) {
const msg = String(e?.message || "未知错误").replace(/\s+/g, " ").slice(0, 200);
await echo(`/echo severity=warning 向量召回失败:${msg}`);
}
// iframe 日志也写一份
if (postToFrame) {
postToFrame({
type: "RECALL_LOG",
text: `\n[Vector Recall Failed]\n${String(e?.stack || e?.message || e)}\n`,
});
}
// 清空本次注入,避免残留误导
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
// 成功但结果为空:也提示,并清空注入(不降级)
const hasUseful =
(recallResult?.events?.length || 0) > 0 ||
(recallResult?.chunks?.length || 0) > 0 ||
(recallResult?.causalEvents?.length || 0) > 0;
if (!hasUseful) {
if (echo && canNotifyRecallFail()) {
await echo(
"/echo severity=warning 向量召回失败:没有可用召回结果(请先在面板中生成向量,或检查指纹不匹配)"
);
}
if (postToFrame) {
postToFrame({
type: "RECALL_LOG",
text: "\n[Vector Recall Empty]\nNo recall candidates / vectors not ready.\n",
});
}
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
// 拼装向量 prompt
const { promptText, injectionLogText } = await buildVectorPrompt(
store,
recallResult,
causalById,
recallResult?.queryEntities || []
);
// 写入 extension_prompts真正注入
await writePromptToExtensionPrompts(promptText, store, chat);
// 发给涌现窗口:召回报告 + 装配报告
if (postToFrame) {
const recallLog = recallResult.logText || "";
postToFrame({ type: "RECALL_LOG", text: recallLog + (injectionLogText || "") });
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 写入 extension_prompts统一入口
// ─────────────────────────────────────────────────────────────────────────────
async function writePromptToExtensionPrompts(text, store, chat) {
const cfg = getSummaryPanelConfig();
let finalText = String(text || "");
if (cfg.trigger?.wrapperHead) finalText = cfg.trigger.wrapperHead + "\n" + finalText;
if (cfg.trigger?.wrapperTail) finalText = finalText + "\n" + cfg.trigger.wrapperTail;
if (!finalText.trim()) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const lastIdx = store.lastSummarizedMesId ?? 0;
let depth = (chat?.length || 0) - lastIdx - 1;
if (depth < 0) depth = 0;
if (cfg.trigger?.forceInsertAtEnd) depth = 10000;
extension_prompts[SUMMARY_PROMPT_KEY] = {
value: finalText,
position: extension_prompt_types.IN_CHAT,
depth,
role: extension_prompt_roles.SYSTEM,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// 清理 prompt供 story-summary.js 调用)
// ─────────────────────────────────────────────────────────────────────────────
export function clearSummaryExtensionPrompt() {
delete extension_prompts[SUMMARY_PROMPT_KEY];
}