diff --git a/modules/iframe-renderer.js b/modules/iframe-renderer.js
index 1c650d1..5b4514a 100644
--- a/modules/iframe-renderer.js
+++ b/modules/iframe-renderer.js
@@ -72,9 +72,11 @@ function djb2(str) {
function shouldRenderContentByBlock(codeBlock) {
if (!codeBlock) return false;
- const content = (codeBlock.textContent || '').trim().toLowerCase();
+ const content = (codeBlock.textContent || '').trim();
if (!content) return false;
- return content.includes(']+)\s*-->/i);
+ if (match) return match[1];
+ return null;
+}
+
+async function fetchExternalHtml(url) {
+ try {
+ const r = await fetch(url, { mode: 'cors' });
+ if (r.ok) return await r.text();
+ } catch (_) {}
+ return null;
+}
+
+async function loadExternalUrl(iframe, url, settings) {
+ try {
+ iframe.srcdoc = '
加载中...';
+
+ let html = await fetchExternalHtml(url);
+
+ if (html && settings.variablesCore?.enabled && typeof replaceXbGetVarInString === 'function') {
+ try {
+ html = replaceXbGetVarInString(html);
+ } catch (e) {
+ console.warn('xbgetvar 宏替换失败:', e);
+ }
+ }
+
+ if (html) {
+ const full = buildWrappedHtml(html);
+ if (settings.useBlob) {
+ const codeHash = djb2(html);
+ setIframeBlobHTML(iframe, full, codeHash);
+ } else {
+ iframe.srcdoc = full;
+ }
+ setTimeout(() => {
+ try {
+ const targetOrigin = getIframeTargetOrigin(iframe);
+ postToIframe(iframe, { type: 'probe' }, null, targetOrigin);
+ } catch (e) {}
+ }, 100);
+ } else {
+ iframe.removeAttribute('srcdoc');
+ iframe.src = url;
+ iframe.style.minHeight = '800px';
+ iframe.setAttribute('scrolling', 'auto');
+ }
+ } catch (err) {
+ console.error('[iframeRenderer] 外部URL加载失败:', err);
+ iframe.removeAttribute('srcdoc');
+ iframe.src = url;
+ iframe.style.minHeight = '800px';
+ iframe.setAttribute('scrolling', 'auto');
+ }
+}
+
function buildWrappedHtml(html) {
const settings = getSettings();
const wrapperToggle = settings.wrapperIframe ?? true;
@@ -341,15 +403,7 @@ export function renderHtmlInIframe(htmlContent, container, preElement) {
const settings = getSettings();
try {
const originalHash = djb2(htmlContent);
-
- if (settings.variablesCore?.enabled && typeof replaceXbGetVarInString === 'function') {
- try {
- htmlContent = replaceXbGetVarInString(htmlContent);
- } catch (e) {
- console.warn('xbgetvar 宏替换失败:', e);
- }
- }
-
+ const externalUrl = extractExternalUrl(htmlContent);
const iframe = document.createElement('iframe');
iframe.id = generateUniqueId();
iframe.className = 'xiaobaix-iframe';
@@ -364,28 +418,41 @@ export function renderHtmlInIframe(htmlContent, container, preElement) {
releaseIframeBlob(old);
old.remove();
});
-
- const codeHash = djb2(htmlContent);
- const full = buildWrappedHtml(htmlContent);
-
- if (settings.useBlob) {
- setIframeBlobHTML(iframe, full, codeHash);
- } else {
- iframe.srcdoc = full;
- }
-
+
wrapper.appendChild(iframe);
preElement.classList.remove('xb-show');
preElement.style.display = 'none';
registerIframeMapping(iframe, wrapper);
-
- try {
- const targetOrigin = getIframeTargetOrigin(iframe);
- postToIframe(iframe, { type: 'probe' }, null, targetOrigin);
- } catch (e) {}
+
+ if (externalUrl) {
+ loadExternalUrl(iframe, externalUrl, settings);
+ } else {
+ if (settings.variablesCore?.enabled && typeof replaceXbGetVarInString === 'function') {
+ try {
+ htmlContent = replaceXbGetVarInString(htmlContent);
+ } catch (e) {
+ console.warn('xbgetvar 宏替换失败:', e);
+ }
+ }
+
+ const codeHash = djb2(htmlContent);
+ const full = buildWrappedHtml(htmlContent);
+
+ if (settings.useBlob) {
+ setIframeBlobHTML(iframe, full, codeHash);
+ } else {
+ iframe.srcdoc = full;
+ }
+
+ try {
+ const targetOrigin = getIframeTargetOrigin(iframe);
+ postToIframe(iframe, { type: 'probe' }, null, targetOrigin);
+ } catch (e) {}
+ }
+
preElement.dataset.xbFinal = 'true';
preElement.dataset.xbHash = originalHash;
-
+
return iframe;
} catch (err) {
console.error('[iframeRenderer] 渲染失败:', err);
@@ -412,10 +479,11 @@ export function processCodeBlocks(messageElement, forceFinal = true) {
const should = shouldRenderContentByBlock(codeBlock);
const html = codeBlock.textContent || '';
const hash = djb2(html);
+ const externalUrl = extractExternalUrl(html);
const isFinal = preElement.dataset.xbFinal === 'true';
const same = preElement.dataset.xbHash === hash;
-
- if (isFinal && same) return;
+
+ if (!externalUrl && isFinal && same) return;
if (should) {
renderHtmlInIframe(html, preElement.parentNode, preElement);
diff --git a/modules/story-summary/data/config.js b/modules/story-summary/data/config.js
index ce19296..a7944c4 100644
--- a/modules/story-summary/data/config.js
+++ b/modules/story-summary/data/config.js
@@ -12,6 +12,249 @@ const DEFAULT_FILTER_RULES = [
{ start: "```", end: "```" },
];
+export const DEFAULT_SUMMARY_SYSTEM_PROMPT = `Story Analyst: This task involves narrative comprehension and structured incremental summarization, representing creative story analysis at the intersection of plot tracking and character development. As a story analyst, you will conduct systematic evaluation of provided dialogue content to generate structured incremental summary data.
+[Read the settings for this task]
+
+Incremental_Summary_Requirements:
+ - Incremental_Only: 只提取新对话中的新增要素,绝不重复已有总结
+ - Event_Granularity: 记录有叙事价值的事件,而非剧情梗概
+ - Memory_Album_Style: 形成有细节、有温度、有记忆点的回忆册
+ - Event_Classification:
+ type:
+ - 相遇: 人物/事物初次接触
+ - 冲突: 对抗、矛盾激化
+ - 揭示: 真相、秘密、身份
+ - 抉择: 关键决定
+ - 羁绊: 关系加深或破裂
+ - 转变: 角色/局势改变
+ - 收束: 问题解决、和解
+ - 日常: 生活片段
+ weight:
+ - 核心: 删掉故事就崩
+ - 主线: 推动主要剧情
+ - 转折: 改变某条线走向
+ - 点睛: 有细节不影响主线
+ - 氛围: 纯粹氛围片段
+ - Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。
+ - Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融)
+ - Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0)
+ - Fact_Tracking: 维护 SPO 三元组知识图谱。追踪生死、物品归属、位置、关系等硬性事实。采用 KV 覆盖模型(s+p 为键)。
+
+---
+Story Analyst:
+[Responsibility Definition]
+\`\`\`yaml
+analysis_task:
+ title: Incremental Story Summarization with Knowledge Graph
+ Story Analyst:
+ role: Antigravity
+ task: >-
+ To analyze provided dialogue content against existing summary state,
+ extract only NEW plot elements, character developments, relationship
+ changes, arc progressions, AND fact updates, outputting
+ structured JSON for incremental summary database updates.
+ assistant:
+ role: Summary Specialist
+ description: Incremental Story Summary & Knowledge Graph Analyst
+ behavior: >-
+ To compare new dialogue against existing summary, identify genuinely
+ new events and character interactions, classify events by narrative
+ type and weight, track character arc progression with percentage,
+ maintain facts as SPO triples with clear semantics,
+ and output structured JSON containing only incremental updates.
+ Must strictly avoid repeating any existing summary content.
+ user:
+ role: Content Provider
+ description: Supplies existing summary state and new dialogue
+ behavior: >-
+ To provide existing summary state (events, characters, arcs, facts)
+ and new dialogue content for incremental analysis.
+interaction_mode:
+ type: incremental_analysis
+ output_format: structured_json
+ deduplication: strict_enforcement
+execution_context:
+ summary_active: true
+ incremental_only: true
+ memory_album_style: true
+ fact_tracking: true
+\`\`\`
+---
+Summary Specialist:
+`;
+
+export const DEFAULT_MEMORY_PROMPT_TEMPLATE = `以上是还留在眼前的对话
+以下是脑海里的记忆:
+• [定了的事] 这些是不会变的
+• [其他人的事] 别人的经历,当前角色可能不知晓
+• 其余部分是过往经历的回忆碎片
+
+请内化这些记忆:
+{$剧情记忆}
+这些记忆是真实的,请自然地记住它们。`;
+
+export const DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT = `
+Summary Specialist:
+Acknowledged. Now reviewing the incremental summarization specifications:
+
+[Event Classification System]
+├─ Types: 相遇|冲突|揭示|抉择|羁绊|转变|收束|日常
+├─ Weights: 核心|主线|转折|点睛|氛围
+└─ Each event needs: id, title, timeLabel, summary(含楼层), participants, type, weight
+
+[Relationship Trend Scale]
+破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融
+
+[Arc Progress Tracking]
+├─ trajectory: 当前阶段描述(15字内)
+├─ progress: 0.0 to 1.0
+└─ newMoment: 仅记录本次新增的关键时刻
+
+[Fact Tracking - SPO / World Facts]
+We maintain a small "world state" as SPO triples.
+Each update is a JSON object: {s, p, o, isState, trend?, retracted?}
+
+Core rules:
+1) Keyed by (s + p). If a new update has the same (s+p), it overwrites the previous value.
+2) Only output facts that are NEW or CHANGED in the new dialogue. Do NOT repeat unchanged facts.
+3) isState meaning:
+ - isState: true -> core constraints that must stay stable and should NEVER be auto-deleted
+ (identity, location, life/death, ownership, relationship status, binding rules)
+ - isState: false -> non-core facts / soft memories that may be pruned by capacity limits later
+4) Relationship facts:
+ - Use predicate format: "对X的看法" (X is the target person)
+ - trend is required for relationship facts, one of:
+ 破裂 | 厌恶 | 反感 | 陌生 | 投缘 | 亲密 | 交融
+5) Retraction (deletion):
+ - To delete a fact, output: {s, p, retracted: true}
+6) Predicate normalization:
+ - Reuse existing predicates whenever possible, avoid inventing synonyms.
+
+Ready to process incremental summary requests with strict deduplication.`;
+
+export const DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT = `
+Summary Specialist:
+Specifications internalized. Please provide the existing summary state so I can:
+1. Index all recorded events to avoid duplication
+2. Map current character list as baseline
+3. Note existing arc progress levels
+4. Identify established keywords
+5. Review current facts (SPO triples baseline)`;
+
+export const DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT = `
+Summary Specialist:
+Existing summary fully analyzed and indexed. I understand:
+├─ Recorded events: Indexed for deduplication
+├─ Character list: Baseline mapped
+├─ Arc progress: Levels noted
+├─ Keywords: Current state acknowledged
+└─ Facts: SPO baseline loaded
+
+I will extract only genuinely NEW elements from the upcoming dialogue.
+Please provide the new dialogue content requiring incremental analysis.`;
+
+export const DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT = `
+Summary Specialist:
+ACKNOWLEDGED. Beginning structured JSON generation:
+`;
+
+export const DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT = `
+## Output Rule
+Generate a single valid JSON object with INCREMENTAL updates only.
+
+## Mindful Approach
+Before generating, observe the USER and analyze carefully:
+- What is user's writing style and emotional expression?
+- What NEW events occurred (not in existing summary)?
+- What NEW characters appeared for the first time?
+- What relationship CHANGES happened?
+- What arc PROGRESS was made?
+- What facts changed? (status/position/ownership/relationships)
+
+## factUpdates 规则
+- 目的: 纠错 & 世界一致性约束,只记录硬性事实
+- s+p 为键,相同键会覆盖旧值
+- isState: true=核心约束(位置/身份/生死/关系),false=有容量上限会被清理
+- 关系类: p="对X的看法",trend 必填(破裂|厌恶|反感|陌生|投缘|亲密|交融)
+- 删除: {s, p, retracted: true},不需要 o 字段
+- 更新: {s, p, o, isState, trend?}
+- 谓词规范化: 复用已有谓词,不要发明同义词
+- 只输出有变化的条目,确保少、硬、稳定
+
+## Output Format
+\`\`\`json
+{
+ "mindful_prelude": {
+ "user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?",
+ "dedup_analysis": "已有X个事件,本次识别Y个新事件",
+ "fact_changes": "识别到的事实变化概述"
+ },
+ "keywords": [
+ {"text": "综合历史+新内容的全剧情关键词(5-10个)", "weight": "核心|重要|一般"}
+ ],
+ "events": [
+ {
+ "id": "evt-{$nextEventId}起始,依次递增",
+ "title": "地点·事件标题",
+ "timeLabel": "时间线标签(如:开场、第二天晚上)",
+ "summary": "1-2句话描述,涵盖丰富信息素,末尾标注楼层(#X-Y)",
+ "participants": ["参与角色名,不要使用人称代词或别名,只用正式人名"],
+ "type": "相遇|冲突|揭示|抉择|羁绊|转变|收束|日常",
+ "weight": "核心|主线|转折|点睛|氛围",
+ "causedBy": ["evt-12", "evt-14"]
+ }
+ ],
+ "newCharacters": ["仅本次首次出现的角色名"],
+ "arcUpdates": [
+ {"name": "角色名,不要使用人称代词或别名,只用正式人名", "trajectory": "当前阶段描述(15字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"}
+ ],
+ "factUpdates": [
+ {"s": "主体", "p": "谓词", "o": "当前值", "isState": true, "trend": "仅关系类填"},
+ {"s": "要删除的主体", "p": "要删除的谓词", "retracted": true}
+ ]
+}
+\`\`\`
+
+## CRITICAL NOTES
+- events.id 从 evt-{$nextEventId} 开始编号
+- 仅输出【增量】内容,已有事件绝不重复
+- /地点、通过什么方式、对谁、做了什么事、结果如何。如果原文有具体道具(如一把枪、一封信),必须在总结中提及。
+- keywords 是全局关键词,综合已有+新增
+- causedBy 仅在因果明确时填写,允许为[],0-2个
+- factUpdates 可为空数组
+- 合法JSON,字符串值内部避免英文双引号
+- 用朴实、白描、有烟火气的笔触记录事实,避免比喻和意象
+- 严谨、注重细节,避免使用模糊的概括性语言,应用具体的动词描述动作,例:谁,在什么时间/地点,通过什么方式,对谁,做了什么事,出现了什么道具,结果如何。
+
+
+## Placeholder Notes
+- {$nextEventId} 会在运行时替换成实际起始事件编号,不要删除
+- {$existingEventCount}、{$historyRange} 这类占位符如果出现在你的自定义版本里,通常也不应该删除`;
+
+export const DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT = `Content review initiated...
+[Compliance Check Results]
+├─ Existing summary loaded: ✓ Fully indexed
+├─ New dialogue received: ✓ Content parsed
+├─ Deduplication engine: ✓ Active
+├─ Event classification: ✓ Ready
+├─ Fact tracking: ✓ Enabled
+└─ Output format: ✓ JSON specification loaded
+
+[Material Verification]
+├─ Existing events: Indexed ({$existingEventCount} recorded)
+├─ Character baseline: Mapped
+├─ Arc progress baseline: Noted
+├─ Facts baseline: Loaded
+└─ Output specification: ✓ Defined in
+All checks passed. Beginning incremental extraction...
+{
+ "mindful_prelude":`;
+
+export const DEFAULT_SUMMARY_USER_CONFIRM_PROMPT = `怎么截断了!重新完整生成,只输出JSON,不要任何其他内容,3000字以内
+`;
+
+export const DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT = '下面重新生成完整JSON。';
+
export function getSettings() {
const ext = (extension_settings[EXT_ID] ||= {});
ext.storySummary ||= { enabled: true };
@@ -44,6 +287,18 @@ export function getSummaryPanelConfig() {
keepVisibleCount: 6,
},
textFilterRules: [...DEFAULT_FILTER_RULES],
+ prompts: {
+ summarySystemPrompt: DEFAULT_SUMMARY_SYSTEM_PROMPT,
+ summaryAssistantDocPrompt: DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT,
+ summaryAssistantAskSummaryPrompt: DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT,
+ summaryAssistantAskContentPrompt: DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT,
+ summaryMetaProtocolStartPrompt: DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT,
+ summaryUserJsonFormatPrompt: DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT,
+ summaryAssistantCheckPrompt: DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT,
+ summaryUserConfirmPrompt: DEFAULT_SUMMARY_USER_CONFIRM_PROMPT,
+ summaryAssistantPrefillPrompt: DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT,
+ memoryTemplate: DEFAULT_MEMORY_PROMPT_TEMPLATE,
+ },
vector: null,
};
@@ -64,6 +319,7 @@ export function getSummaryPanelConfig() {
trigger: { ...defaults.trigger, ...(parsed.trigger || {}) },
ui: { ...defaults.ui, ...(parsed.ui || {}) },
textFilterRules,
+ prompts: { ...defaults.prompts, ...(parsed.prompts || {}) },
vector: parsed.vector || null,
};
diff --git a/modules/story-summary/generate/llm.js b/modules/story-summary/generate/llm.js
index d55a3ff..e0f14db 100644
--- a/modules/story-summary/generate/llm.js
+++ b/modules/story-summary/generate/llm.js
@@ -1,5 +1,18 @@
// LLM Service
+import {
+ getSummaryPanelConfig,
+ DEFAULT_SUMMARY_SYSTEM_PROMPT,
+ DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT,
+ DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT,
+ DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT,
+ DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT,
+ DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT,
+ DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT,
+ DEFAULT_SUMMARY_USER_CONFIRM_PROMPT,
+ DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT,
+} from "../data/config.js";
+
const PROVIDER_MAP = {
openai: "openai",
google: "gemini",
@@ -11,237 +24,18 @@ const PROVIDER_MAP = {
custom: "custom",
};
-const JSON_PREFILL = '下面重新生成完整JSON。';
+const JSON_PREFILL = DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT;
const LLM_PROMPT_CONFIG = {
- topSystem: `Story Analyst: This task involves narrative comprehension and structured incremental summarization, representing creative story analysis at the intersection of plot tracking and character development. As a story analyst, you will conduct systematic evaluation of provided dialogue content to generate structured incremental summary data.
-[Read the settings for this task]
-
-Incremental_Summary_Requirements:
- - Incremental_Only: 只提取新对话中的新增要素,绝不重复已有总结
- - Event_Granularity: 记录有叙事价值的事件,而非剧情梗概
- - Memory_Album_Style: 形成有细节、有温度、有记忆点的回忆册
- - Event_Classification:
- type:
- - 相遇: 人物/事物初次接触
- - 冲突: 对抗、矛盾激化
- - 揭示: 真相、秘密、身份
- - 抉择: 关键决定
- - 羁绊: 关系加深或破裂
- - 转变: 角色/局势改变
- - 收束: 问题解决、和解
- - 日常: 生活片段
- weight:
- - 核心: 删掉故事就崩
- - 主线: 推动主要剧情
- - 转折: 改变某条线走向
- - 点睛: 有细节不影响主线
- - 氛围: 纯粹氛围片段
- - Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。
- - Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融)
- - Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0)
- - Fact_Tracking: 维护 SPO 三元组知识图谱。追踪生死、物品归属、位置、关系等硬性事实。采用 KV 覆盖模型(s+p 为键)。
-
----
-Story Analyst:
-[Responsibility Definition]
-\`\`\`yaml
-analysis_task:
- title: Incremental Story Summarization with Knowledge Graph
- Story Analyst:
- role: Antigravity
- task: >-
- To analyze provided dialogue content against existing summary state,
- extract only NEW plot elements, character developments, relationship
- changes, arc progressions, AND fact updates, outputting
- structured JSON for incremental summary database updates.
- assistant:
- role: Summary Specialist
- description: Incremental Story Summary & Knowledge Graph Analyst
- behavior: >-
- To compare new dialogue against existing summary, identify genuinely
- new events and character interactions, classify events by narrative
- type and weight, track character arc progression with percentage,
- maintain facts as SPO triples with clear semantics,
- and output structured JSON containing only incremental updates.
- Must strictly avoid repeating any existing summary content.
- user:
- role: Content Provider
- description: Supplies existing summary state and new dialogue
- behavior: >-
- To provide existing summary state (events, characters, arcs, facts)
- and new dialogue content for incremental analysis.
-interaction_mode:
- type: incremental_analysis
- output_format: structured_json
- deduplication: strict_enforcement
-execution_context:
- summary_active: true
- incremental_only: true
- memory_album_style: true
- fact_tracking: true
-\`\`\`
----
-Summary Specialist:
-`,
-
- assistantDoc: `
-Summary Specialist:
-Acknowledged. Now reviewing the incremental summarization specifications:
-
-[Event Classification System]
-├─ Types: 相遇|冲突|揭示|抉择|羁绊|转变|收束|日常
-├─ Weights: 核心|主线|转折|点睛|氛围
-└─ Each event needs: id, title, timeLabel, summary(含楼层), participants, type, weight
-
-[Relationship Trend Scale]
-破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融
-
-[Arc Progress Tracking]
-├─ trajectory: 当前阶段描述(15字内)
-├─ progress: 0.0 to 1.0
-└─ newMoment: 仅记录本次新增的关键时刻
-
-[Fact Tracking - SPO / World Facts]
-We maintain a small "world state" as SPO triples.
-Each update is a JSON object: {s, p, o, isState, trend?, retracted?}
-
-Core rules:
-1) Keyed by (s + p). If a new update has the same (s+p), it overwrites the previous value.
-2) Only output facts that are NEW or CHANGED in the new dialogue. Do NOT repeat unchanged facts.
-3) isState meaning:
- - isState: true -> core constraints that must stay stable and should NEVER be auto-deleted
- (identity, location, life/death, ownership, relationship status, binding rules)
- - isState: false -> non-core facts / soft memories that may be pruned by capacity limits later
-4) Relationship facts:
- - Use predicate format: "对X的看法" (X is the target person)
- - trend is required for relationship facts, one of:
- 破裂 | 厌恶 | 反感 | 陌生 | 投缘 | 亲密 | 交融
-5) Retraction (deletion):
- - To delete a fact, output: {s, p, retracted: true}
-6) Predicate normalization:
- - Reuse existing predicates whenever possible, avoid inventing synonyms.
-
-Ready to process incremental summary requests with strict deduplication.`,
-
- assistantAskSummary: `
-Summary Specialist:
-Specifications internalized. Please provide the existing summary state so I can:
-1. Index all recorded events to avoid duplication
-2. Map current character list as baseline
-3. Note existing arc progress levels
-4. Identify established keywords
-5. Review current facts (SPO triples baseline)`,
-
- assistantAskContent: `
-Summary Specialist:
-Existing summary fully analyzed and indexed. I understand:
-├─ Recorded events: Indexed for deduplication
-├─ Character list: Baseline mapped
-├─ Arc progress: Levels noted
-├─ Keywords: Current state acknowledged
-└─ Facts: SPO baseline loaded
-
-I will extract only genuinely NEW elements from the upcoming dialogue.
-Please provide the new dialogue content requiring incremental analysis.`,
-
- metaProtocolStart: `
-Summary Specialist:
-ACKNOWLEDGED. Beginning structured JSON generation:
-`,
-
- userJsonFormat: `
-## Output Rule
-Generate a single valid JSON object with INCREMENTAL updates only.
-
-## Mindful Approach
-Before generating, observe the USER and analyze carefully:
-- What is user's writing style and emotional expression?
-- What NEW events occurred (not in existing summary)?
-- What NEW characters appeared for the first time?
-- What relationship CHANGES happened?
-- What arc PROGRESS was made?
-- What facts changed? (status/position/ownership/relationships)
-
-## factUpdates 规则
-- 目的: 纠错 & 世界一致性约束,只记录硬性事实
-- s+p 为键,相同键会覆盖旧值
-- isState: true=核心约束(位置/身份/生死/关系),false=有容量上限会被清理
-- 关系类: p="对X的看法",trend 必填(破裂|厌恶|反感|陌生|投缘|亲密|交融)
-- 删除: {s, p, retracted: true},不需要 o 字段
-- 更新: {s, p, o, isState, trend?}
-- 谓词规范化: 复用已有谓词,不要发明同义词
-- 只输出有变化的条目,确保少、硬、稳定
-
-## Output Format
-\`\`\`json
-{
- "mindful_prelude": {
- "user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?",
- "dedup_analysis": "已有X个事件,本次识别Y个新事件",
- "fact_changes": "识别到的事实变化概述"
- },
- "keywords": [
- {"text": "综合历史+新内容的全剧情关键词(5-10个)", "weight": "核心|重要|一般"}
- ],
- "events": [
- {
- "id": "evt-{nextEventId}起始,依次递增",
- "title": "地点·事件标题",
- "timeLabel": "时间线标签(如:开场、第二天晚上)",
- "summary": "1-2句话描述,涵盖丰富信息素,末尾标注楼层(#X-Y)",
- "participants": ["参与角色名,不要使用人称代词或别名,只用正式人名"],
- "type": "相遇|冲突|揭示|抉择|羁绊|转变|收束|日常",
- "weight": "核心|主线|转折|点睛|氛围",
- "causedBy": ["evt-12", "evt-14"]
- }
- ],
- "newCharacters": ["仅本次首次出现的角色名"],
- "arcUpdates": [
- {"name": "角色名,不要使用人称代词或别名,只用正式人名", "trajectory": "当前阶段描述(15字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"}
- ],
- "factUpdates": [
- {"s": "主体", "p": "谓词", "o": "当前值", "isState": true, "trend": "仅关系类填"},
- {"s": "要删除的主体", "p": "要删除的谓词", "retracted": true}
- ]
-}
-\`\`\`
-
-## CRITICAL NOTES
-- events.id 从 evt-{nextEventId} 开始编号
-- 仅输出【增量】内容,已有事件绝不重复
-- /地点、通过什么方式、对谁、做了什么事、结果如何。如果原文有具体道具(如一把枪、一封信),必须在总结中提及。
-- keywords 是全局关键词,综合已有+新增
-- causedBy 仅在因果明确时填写,允许为[],0-2个
-- factUpdates 可为空数组
-- 合法JSON,字符串值内部避免英文双引号
-- 用朴实、白描、有烟火气的笔触记录事实,避免比喻和意象
-- 严谨、注重细节,避免使用模糊的概括性语言,应用具体的动词描述动作,例:谁,在什么时间/地点,通过什么方式,对谁,做了什么事,出现了什么道具,结果如何。
-`,
-
- assistantCheck: `Content review initiated...
-[Compliance Check Results]
-├─ Existing summary loaded: ✓ Fully indexed
-├─ New dialogue received: ✓ Content parsed
-├─ Deduplication engine: ✓ Active
-├─ Event classification: ✓ Ready
-├─ Fact tracking: ✓ Enabled
-└─ Output format: ✓ JSON specification loaded
-
-[Material Verification]
-├─ Existing events: Indexed ({existingEventCount} recorded)
-├─ Character baseline: Mapped
-├─ Arc progress baseline: Noted
-├─ Facts baseline: Loaded
-└─ Output specification: ✓ Defined in
-All checks passed. Beginning incremental extraction...
-{
- "mindful_prelude":`,
-
- userConfirm: `怎么截断了!重新完整生成,只输出JSON,不要任何其他内容,3000字以内
-`,
-
- assistantPrefill: JSON_PREFILL
+ topSystem: DEFAULT_SUMMARY_SYSTEM_PROMPT,
+ assistantDoc: DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT,
+ assistantAskSummary: DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT,
+ assistantAskContent: DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT,
+ metaProtocolStart: DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT,
+ userJsonFormat: DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT,
+ assistantCheck: DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT,
+ userConfirm: DEFAULT_SUMMARY_USER_CONFIRM_PROMPT,
+ assistantPrefill: DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT,
};
// ═══════════════════════════════════════════════════════════════════════════
@@ -298,37 +92,51 @@ function formatFactsForLLM(facts) {
}
function buildSummaryMessages(existingSummary, existingFacts, newHistoryText, historyRange, nextEventId, existingEventCount) {
+ const promptCfg = getSummaryPanelConfig()?.prompts || {};
+ const summarySystemPrompt = String(promptCfg.summarySystemPrompt || DEFAULT_SUMMARY_SYSTEM_PROMPT).trim() || DEFAULT_SUMMARY_SYSTEM_PROMPT;
+ const assistantDocPrompt = String(promptCfg.summaryAssistantDocPrompt || DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT;
+ const assistantAskSummaryPrompt = String(promptCfg.summaryAssistantAskSummaryPrompt || DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT;
+ const assistantAskContentPrompt = String(promptCfg.summaryAssistantAskContentPrompt || DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT;
+ const metaProtocolStartPrompt = String(promptCfg.summaryMetaProtocolStartPrompt || DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT).trim() || DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT;
+ const userJsonFormatPrompt = String(promptCfg.summaryUserJsonFormatPrompt || DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT).trim() || DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT;
+ const assistantCheckPrompt = String(promptCfg.summaryAssistantCheckPrompt || DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT;
+ const userConfirmPrompt = String(promptCfg.summaryUserConfirmPrompt || DEFAULT_SUMMARY_USER_CONFIRM_PROMPT).trim() || DEFAULT_SUMMARY_USER_CONFIRM_PROMPT;
+ const assistantPrefillPrompt = String(promptCfg.summaryAssistantPrefillPrompt || DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT;
const { text: factsText, predicates } = formatFactsForLLM(existingFacts);
const predicatesHint = predicates.length > 0
? `\n\n<\u5df2\u6709\u8c13\u8bcd\uff0c\u8bf7\u590d\u7528>\n${predicates.join('\u3001')}\n\u5df2\u6709\u8c13\u8bcd\uff0c\u8bf7\u590d\u7528>`
: '';
- const jsonFormat = LLM_PROMPT_CONFIG.userJsonFormat
- .replace(/\{nextEventId\}/g, String(nextEventId));
+ const jsonFormat = userJsonFormatPrompt
+ .replace(/\{\$nextEventId\}/g, String(nextEventId))
+ .replace(/\{nextEventId\}/g, String(nextEventId))
+ .replace(/\{\$historyRange\}/g, String(historyRange ?? ''))
+ .replace(/\{historyRange\}/g, String(historyRange ?? ''));
- const checkContent = LLM_PROMPT_CONFIG.assistantCheck
+ const checkContent = assistantCheckPrompt
+ .replace(/\{\$existingEventCount\}/g, String(existingEventCount))
.replace(/\{existingEventCount\}/g, String(existingEventCount));
const topMessages = [
- { role: 'system', content: LLM_PROMPT_CONFIG.topSystem },
- { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantDoc },
- { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskSummary },
+ { role: 'system', content: summarySystemPrompt },
+ { role: 'assistant', content: assistantDocPrompt },
+ { role: 'assistant', content: assistantAskSummaryPrompt },
{ role: 'user', content: `<\u5df2\u6709\u603b\u7ed3\u72b6\u6001>\n${existingSummary}\n\u5df2\u6709\u603b\u7ed3\u72b6\u6001>\n\n<\u5f53\u524d\u4e8b\u5b9e\u56fe\u8c31>\n${factsText}\n\u5f53\u524d\u4e8b\u5b9e\u56fe\u8c31>${predicatesHint}` },
- { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskContent },
+ { role: 'assistant', content: assistantAskContentPrompt },
{ role: 'user', content: `<\u65b0\u5bf9\u8bdd\u5185\u5bb9>\uff08${historyRange}\uff09\n${newHistoryText}\n\u65b0\u5bf9\u8bdd\u5185\u5bb9>` }
];
const bottomMessages = [
- { role: 'user', content: LLM_PROMPT_CONFIG.metaProtocolStart + '\n' + jsonFormat },
+ { role: 'user', content: metaProtocolStartPrompt + '\n' + jsonFormat },
{ role: 'assistant', content: checkContent },
- { role: 'user', content: LLM_PROMPT_CONFIG.userConfirm }
+ { role: 'user', content: userConfirmPrompt }
];
return {
top64: b64UrlEncode(JSON.stringify(topMessages)),
bottom64: b64UrlEncode(JSON.stringify(bottomMessages)),
- assistantPrefill: LLM_PROMPT_CONFIG.assistantPrefill
+ assistantPrefill: assistantPrefillPrompt
};
}
diff --git a/modules/story-summary/generate/prompt.js b/modules/story-summary/generate/prompt.js
index fe4c59d..0db9e81 100644
--- a/modules/story-summary/generate/prompt.js
+++ b/modules/story-summary/generate/prompt.js
@@ -15,7 +15,7 @@
import { getContext } from "../../../../../../extensions.js";
import { xbLog } from "../../../core/debug-core.js";
import { getSummaryStore, getFacts, isRelationFact } from "../data/store.js";
-import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
+import { getVectorConfig, getSummaryPanelConfig, getSettings, DEFAULT_MEMORY_PROMPT_TEMPLATE } from "../data/config.js";
import { recallMemory } from "../vector/retrieval/recall.js";
import { getMeta } from "../vector/storage/chunk-store.js";
import { getStateAtoms } from "../vector/storage/state-store.js";
@@ -208,27 +208,15 @@ function renumberEventText(text, newIndex) {
* 构建系统前导文本
* @returns {string} 前导文本
*/
-function buildSystemPreamble() {
- return [
- "以上是还留在眼前的对话",
- "以下是脑海里的记忆:",
- "• [定了的事] 这些是不会变的",
- "• [其他人的事] 别人的经历,当前角色可能不知晓",
- "• 其余部分是过往经历的回忆碎片",
- "",
- "请内化这些记忆:",
- ].join("\n");
-}
-
-/**
- * 构建后缀文本
- * @returns {string} 后缀文本
- */
-function buildPostscript() {
- return [
- "",
- "这些记忆是真实的,请自然地记住它们。",
- ].join("\n");
+function buildMemoryPromptText(memoryBody) {
+ const templateRaw = String(
+ getSummaryPanelConfig()?.prompts?.memoryTemplate || DEFAULT_MEMORY_PROMPT_TEMPLATE
+ );
+ const template = templateRaw.trim() || DEFAULT_MEMORY_PROMPT_TEMPLATE;
+ if (template.includes("{$剧情记忆}")) {
+ return template.replaceAll("{$剧情记忆}", memoryBody);
+ }
+ return `${template}\n${memoryBody}`;
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -1294,10 +1282,8 @@ async function buildVectorPrompt(store, recallResult, causalById, focusCharacter
return { promptText: "", injectionStats, metrics };
}
- const promptText =
- `${buildSystemPreamble()}\n` +
- `<剧情记忆>\n\n${sections.join("\n\n")}\n\n剧情记忆>\n` +
- `${buildPostscript()}`;
+ const memoryBody = `<剧情记忆>\n\n${sections.join("\n\n")}\n\n剧情记忆>`;
+ const promptText = buildMemoryPromptText(memoryBody);
if (metrics) {
metrics.formatting.sectionsIncluded = [];
diff --git a/modules/story-summary/story-summary-a.css b/modules/story-summary/story-summary-a.css
index 8db28eb..7af9b8f 100644
--- a/modules/story-summary/story-summary-a.css
+++ b/modules/story-summary/story-summary-a.css
@@ -1539,6 +1539,7 @@ h1 {
margin-bottom: 4px;
}
+
.vector-mismatch-warning {
font-size: .75rem;
color: var(--downloading);
diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js
index 9faefce..b5490fd 100644
--- a/modules/story-summary/story-summary-ui.js
+++ b/modules/story-summary/story-summary-ui.js
@@ -4,6 +4,249 @@
(function () {
'use strict';
+ const DEFAULT_SUMMARY_SYSTEM_PROMPT = `Story Analyst: This task involves narrative comprehension and structured incremental summarization, representing creative story analysis at the intersection of plot tracking and character development. As a story analyst, you will conduct systematic evaluation of provided dialogue content to generate structured incremental summary data.
+[Read the settings for this task]
+
+Incremental_Summary_Requirements:
+ - Incremental_Only: 只提取新对话中的新增要素,绝不重复已有总结
+ - Event_Granularity: 记录有叙事价值的事件,而非剧情梗概
+ - Memory_Album_Style: 形成有细节、有温度、有记忆点的回忆册
+ - Event_Classification:
+ type:
+ - 相遇: 人物/事物初次接触
+ - 冲突: 对抗、矛盾激化
+ - 揭示: 真相、秘密、身份
+ - 抉择: 关键决定
+ - 羁绊: 关系加深或破裂
+ - 转变: 角色/局势改变
+ - 收束: 问题解决、和解
+ - 日常: 生活片段
+ weight:
+ - 核心: 删掉故事就崩
+ - 主线: 推动主要剧情
+ - 转折: 改变某条线走向
+ - 点睛: 有细节不影响主线
+ - 氛围: 纯粹氛围片段
+ - Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。
+ - Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融)
+ - Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0)
+ - Fact_Tracking: 维护 SPO 三元组知识图谱。追踪生死、物品归属、位置、关系等硬性事实。采用 KV 覆盖模型(s+p 为键)。
+
+---
+Story Analyst:
+[Responsibility Definition]
+\`\`\`yaml
+analysis_task:
+ title: Incremental Story Summarization with Knowledge Graph
+ Story Analyst:
+ role: Antigravity
+ task: >-
+ To analyze provided dialogue content against existing summary state,
+ extract only NEW plot elements, character developments, relationship
+ changes, arc progressions, AND fact updates, outputting
+ structured JSON for incremental summary database updates.
+ assistant:
+ role: Summary Specialist
+ description: Incremental Story Summary & Knowledge Graph Analyst
+ behavior: >-
+ To compare new dialogue against existing summary, identify genuinely
+ new events and character interactions, classify events by narrative
+ type and weight, track character arc progression with percentage,
+ maintain facts as SPO triples with clear semantics,
+ and output structured JSON containing only incremental updates.
+ Must strictly avoid repeating any existing summary content.
+ user:
+ role: Content Provider
+ description: Supplies existing summary state and new dialogue
+ behavior: >-
+ To provide existing summary state (events, characters, arcs, facts)
+ and new dialogue content for incremental analysis.
+interaction_mode:
+ type: incremental_analysis
+ output_format: structured_json
+ deduplication: strict_enforcement
+execution_context:
+ summary_active: true
+ incremental_only: true
+ memory_album_style: true
+ fact_tracking: true
+\`\`\`
+---
+Summary Specialist:
+`;
+
+ const DEFAULT_MEMORY_PROMPT_TEMPLATE = `以上是还留在眼前的对话
+以下是脑海里的记忆:
+• [定了的事] 这些是不会变的
+• [其他人的事] 别人的经历,当前角色可能不知晓
+• 其余部分是过往经历的回忆碎片
+
+请内化这些记忆:
+{$剧情记忆}
+这些记忆是真实的,请自然地记住它们。`;
+
+ const DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT = `
+Summary Specialist:
+Acknowledged. Now reviewing the incremental summarization specifications:
+
+[Event Classification System]
+├─ Types: 相遇|冲突|揭示|抉择|羁绊|转变|收束|日常
+├─ Weights: 核心|主线|转折|点睛|氛围
+└─ Each event needs: id, title, timeLabel, summary(含楼层), participants, type, weight
+
+[Relationship Trend Scale]
+破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融
+
+[Arc Progress Tracking]
+├─ trajectory: 当前阶段描述(15字内)
+├─ progress: 0.0 to 1.0
+└─ newMoment: 仅记录本次新增的关键时刻
+
+[Fact Tracking - SPO / World Facts]
+We maintain a small "world state" as SPO triples.
+Each update is a JSON object: {s, p, o, isState, trend?, retracted?}
+
+Core rules:
+1) Keyed by (s + p). If a new update has the same (s+p), it overwrites the previous value.
+2) Only output facts that are NEW or CHANGED in the new dialogue. Do NOT repeat unchanged facts.
+3) isState meaning:
+ - isState: true -> core constraints that must stay stable and should NEVER be auto-deleted
+ (identity, location, life/death, ownership, relationship status, binding rules)
+ - isState: false -> non-core facts / soft memories that may be pruned by capacity limits later
+4) Relationship facts:
+ - Use predicate format: "对X的看法" (X is the target person)
+ - trend is required for relationship facts, one of:
+ 破裂 | 厌恶 | 反感 | 陌生 | 投缘 | 亲密 | 交融
+5) Retraction (deletion):
+ - To delete a fact, output: {s, p, retracted: true}
+6) Predicate normalization:
+ - Reuse existing predicates whenever possible, avoid inventing synonyms.
+
+Ready to process incremental summary requests with strict deduplication.`;
+
+ const DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT = `
+Summary Specialist:
+Specifications internalized. Please provide the existing summary state so I can:
+1. Index all recorded events to avoid duplication
+2. Map current character list as baseline
+3. Note existing arc progress levels
+4. Identify established keywords
+5. Review current facts (SPO triples baseline)`;
+
+ const DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT = `
+Summary Specialist:
+Existing summary fully analyzed and indexed. I understand:
+├─ Recorded events: Indexed for deduplication
+├─ Character list: Baseline mapped
+├─ Arc progress: Levels noted
+├─ Keywords: Current state acknowledged
+└─ Facts: SPO baseline loaded
+
+I will extract only genuinely NEW elements from the upcoming dialogue.
+Please provide the new dialogue content requiring incremental analysis.`;
+
+ const DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT = `
+Summary Specialist:
+ACKNOWLEDGED. Beginning structured JSON generation:
+`;
+
+ const DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT = `
+## Output Rule
+Generate a single valid JSON object with INCREMENTAL updates only.
+
+## Mindful Approach
+Before generating, observe the USER and analyze carefully:
+- What is user's writing style and emotional expression?
+- What NEW events occurred (not in existing summary)?
+- What NEW characters appeared for the first time?
+- What relationship CHANGES happened?
+- What arc PROGRESS was made?
+- What facts changed? (status/position/ownership/relationships)
+
+## factUpdates 规则
+- 目的: 纠错 & 世界一致性约束,只记录硬性事实
+- s+p 为键,相同键会覆盖旧值
+- isState: true=核心约束(位置/身份/生死/关系),false=有容量上限会被清理
+- 关系类: p="对X的看法",trend 必填(破裂|厌恶|反感|陌生|投缘|亲密|交融)
+- 删除: {s, p, retracted: true},不需要 o 字段
+- 更新: {s, p, o, isState, trend?}
+- 谓词规范化: 复用已有谓词,不要发明同义词
+- 只输出有变化的条目,确保少、硬、稳定
+
+## Output Format
+\`\`\`json
+{
+ "mindful_prelude": {
+ "user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?",
+ "dedup_analysis": "已有X个事件,本次识别Y个新事件",
+ "fact_changes": "识别到的事实变化概述"
+ },
+ "keywords": [
+ {"text": "综合历史+新内容的全剧情关键词(5-10个)", "weight": "核心|重要|一般"}
+ ],
+ "events": [
+ {
+ "id": "evt-{$nextEventId}起始,依次递增",
+ "title": "地点·事件标题",
+ "timeLabel": "时间线标签(如:开场、第二天晚上)",
+ "summary": "1-2句话描述,涵盖丰富信息素,末尾标注楼层(#X-Y)",
+ "participants": ["参与角色名,不要使用人称代词或别名,只用正式人名"],
+ "type": "相遇|冲突|揭示|抉择|羁绊|转变|收束|日常",
+ "weight": "核心|主线|转折|点睛|氛围",
+ "causedBy": ["evt-12", "evt-14"]
+ }
+ ],
+ "newCharacters": ["仅本次首次出现的角色名"],
+ "arcUpdates": [
+ {"name": "角色名,不要使用人称代词或别名,只用正式人名", "trajectory": "当前阶段描述(15字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"}
+ ],
+ "factUpdates": [
+ {"s": "主体", "p": "谓词", "o": "当前值", "isState": true, "trend": "仅关系类填"},
+ {"s": "要删除的主体", "p": "要删除的谓词", "retracted": true}
+ ]
+}
+\`\`\`
+
+## CRITICAL NOTES
+- events.id 从 evt-{$nextEventId} 开始编号
+- 仅输出【增量】内容,已有事件绝不重复
+- /地点、通过什么方式、对谁、做了什么事、结果如何。如果原文有具体道具(如一把枪、一封信),必须在总结中提及。
+- keywords 是全局关键词,综合已有+新增
+- causedBy 仅在因果明确时填写,允许为[],0-2个
+- factUpdates 可为空数组
+- 合法JSON,字符串值内部避免英文双引号
+- 用朴实、白描、有烟火气的笔触记录事实,避免比喻和意象
+- 严谨、注重细节,避免使用模糊的概括性语言,应用具体的动词描述动作,例:谁,在什么时间/地点,通过什么方式,对谁,做了什么事,出现了什么道具,结果如何。
+
+
+## Placeholder Notes
+- {$nextEventId} 会在运行时替换成实际起始事件编号,不要删除
+- {$existingEventCount}、{$historyRange} 这类占位符如果出现在你的自定义版本里,通常也不应该删除`;
+
+ const DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT = `Content review initiated...
+[Compliance Check Results]
+├─ Existing summary loaded: ✓ Fully indexed
+├─ New dialogue received: ✓ Content parsed
+├─ Deduplication engine: ✓ Active
+├─ Event classification: ✓ Ready
+├─ Fact tracking: ✓ Enabled
+└─ Output format: ✓ JSON specification loaded
+
+[Material Verification]
+├─ Existing events: Indexed ({$existingEventCount} recorded)
+├─ Character baseline: Mapped
+├─ Arc progress baseline: Noted
+├─ Facts baseline: Loaded
+└─ Output specification: ✓ Defined in
+All checks passed. Beginning incremental extraction...
+{
+ "mindful_prelude":`;
+
+ const DEFAULT_SUMMARY_USER_CONFIRM_PROMPT = `怎么截断了!重新完整生成,只输出JSON,不要任何其他内容,3000字以内
+`;
+
+ const DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT = '下面重新生成完整JSON。';
+
// ═══════════════════════════════════════════════════════════════════════════
// DOM Helpers
// ═══════════════════════════════════════════════════════════════════════════
@@ -48,11 +291,11 @@
})();
const PROVIDER_DEFAULTS = {
- st: { url: '', needKey: false, canFetch: false, needManualModel: false },
- openai: { url: 'https://api.openai.com', needKey: true, canFetch: true, needManualModel: false },
- google: { url: 'https://generativelanguage.googleapis.com', needKey: true, canFetch: false, needManualModel: true },
- claude: { url: 'https://api.anthropic.com', needKey: true, canFetch: false, needManualModel: true },
- custom: { url: '', needKey: true, canFetch: true, needManualModel: false }
+ st: { url: '', needKey: false, canFetch: false },
+ openai: { url: 'https://api.openai.com', needKey: true, canFetch: true },
+ google: { url: 'https://generativelanguage.googleapis.com', needKey: true, canFetch: false },
+ claude: { url: 'https://api.anthropic.com', needKey: true, canFetch: false },
+ custom: { url: '', needKey: true, canFetch: true }
};
const SECTION_META = {
@@ -88,6 +331,18 @@
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
trigger: { enabled: false, interval: 20, timing: 'before_user', role: 'system', useStream: true, maxPerRun: 100, wrapperHead: '', wrapperTail: '', forceInsertAtEnd: false },
ui: { hideSummarized: true, keepVisibleCount: 6 },
+ prompts: {
+ summarySystemPrompt: '',
+ summaryAssistantDocPrompt: '',
+ summaryAssistantAskSummaryPrompt: '',
+ summaryAssistantAskContentPrompt: '',
+ summaryMetaProtocolStartPrompt: '',
+ summaryUserJsonFormatPrompt: '',
+ summaryAssistantCheckPrompt: '',
+ summaryUserConfirmPrompt: '',
+ summaryAssistantPrefillPrompt: '',
+ memoryTemplate: '',
+ },
textFilterRules: [...DEFAULT_FILTER_RULES],
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
};
@@ -104,6 +359,7 @@
let allLinks = [];
let activeRelationTooltip = null;
let lastRecallLogText = '';
+ let modelListFetchedThisIframe = false;
// ═══════════════════════════════════════════════════════════════════════════
// Messaging
@@ -123,9 +379,11 @@
if (s) {
const p = JSON.parse(s);
Object.assign(config.api, p.api || {});
+ config.api.modelCache = [];
Object.assign(config.gen, p.gen || {});
Object.assign(config.trigger, p.trigger || {});
Object.assign(config.ui, p.ui || {});
+ Object.assign(config.prompts, p.prompts || {});
config.textFilterRules = Array.isArray(p.textFilterRules)
? p.textFilterRules
: (Array.isArray(p.vector?.textFilterRules) ? p.vector.textFilterRules : [...DEFAULT_FILTER_RULES]);
@@ -141,9 +399,11 @@
function applyConfig(cfg) {
if (!cfg) return;
Object.assign(config.api, cfg.api || {});
+ config.api.modelCache = [];
Object.assign(config.gen, cfg.gen || {});
Object.assign(config.trigger, cfg.trigger || {});
Object.assign(config.ui, cfg.ui || {});
+ Object.assign(config.prompts, cfg.prompts || {});
config.textFilterRules = Array.isArray(cfg.textFilterRules)
? cfg.textFilterRules
: (Array.isArray(cfg.vector?.textFilterRules)
@@ -276,7 +536,6 @@
el.textContent = count;
}
-
function updateOnlineStatus(status, message) {
const dot = $('online-api-status').querySelector('.status-dot');
const text = $('online-api-status').querySelector('.status-text');
@@ -441,6 +700,32 @@
initAnchorUI();
postMsg('REQUEST_ANCHOR_STATS');
}
+
+ function initSummaryIOUI() {
+ $('btn-copy-summary').onclick = () => {
+ $('btn-copy-summary').disabled = true;
+ $('summary-io-status').textContent = '复制中...';
+ postMsg('SUMMARY_COPY');
+ };
+
+ $('btn-import-summary').onclick = async () => {
+ const text = await showConfirmInput(
+ '覆盖导入记忆包',
+ '导入会覆盖当前聊天已有的总结资料,并立即清空向量、锚点、总结边界。请把记忆包粘贴到下面。',
+ '继续导入',
+ '取消',
+ '在这里粘贴记忆包 JSON'
+ );
+ if (text == null) return;
+ if (!String(text).trim()) {
+ $('summary-io-status').textContent = '导入失败: 记忆包内容为空';
+ return;
+ }
+ $('btn-import-summary').disabled = true;
+ $('summary-io-status').textContent = '导入中...';
+ postMsg('SUMMARY_IMPORT_TEXT', { text });
+ };
+ }
// ═══════════════════════════════════════════════════════════════════════════
// Settings Modal
// ═══════════════════════════════════════════════════════════════════════════
@@ -448,12 +733,14 @@
function updateProviderUI(provider) {
const pv = PROVIDER_DEFAULTS[provider] || PROVIDER_DEFAULTS.custom;
const isSt = provider === 'st';
+ const hasModelCache = modelListFetchedThisIframe && Array.isArray(config.api.modelCache) && config.api.modelCache.length > 0;
$('api-url-row').classList.toggle('hidden', isSt);
$('api-key-row').classList.toggle('hidden', !pv.needKey);
- $('api-model-manual-row').classList.toggle('hidden', isSt || !pv.needManualModel);
- $('api-model-select-row').classList.toggle('hidden', isSt || pv.needManualModel || !config.api.modelCache.length);
+ $('api-model-manual-row').classList.toggle('hidden', isSt);
+ $('api-model-select-row').classList.toggle('hidden', isSt || !hasModelCache);
$('api-connect-row').classList.toggle('hidden', isSt || !pv.canFetch);
+ $('api-connect-status').classList.toggle('hidden', isSt || !pv.canFetch);
const urlInput = $('api-url');
if (!urlInput.value && pv.url) urlInput.value = pv.url;
@@ -478,6 +765,17 @@
$('trigger-wrapper-head').value = config.trigger.wrapperHead || '';
$('trigger-wrapper-tail').value = config.trigger.wrapperTail || '';
$('trigger-insert-at-end').checked = !!config.trigger.forceInsertAtEnd;
+ $('summary-system-prompt').value = config.prompts.summarySystemPrompt || '';
+ $('summary-assistant-doc-prompt').value = config.prompts.summaryAssistantDocPrompt || '';
+ $('summary-assistant-ask-summary-prompt').value = config.prompts.summaryAssistantAskSummaryPrompt || '';
+ $('summary-assistant-ask-content-prompt').value = config.prompts.summaryAssistantAskContentPrompt || '';
+ $('summary-meta-protocol-start-prompt').value = config.prompts.summaryMetaProtocolStartPrompt || '';
+ $('summary-user-json-format-prompt').value = config.prompts.summaryUserJsonFormatPrompt || '';
+ $('summary-assistant-check-prompt').value = config.prompts.summaryAssistantCheckPrompt || '';
+ $('summary-user-confirm-prompt').value = config.prompts.summaryUserConfirmPrompt || '';
+ $('summary-assistant-prefill-prompt').value = config.prompts.summaryAssistantPrefillPrompt || '';
+ $('memory-prompt-template').value = config.prompts.memoryTemplate || '';
+ $('api-connect-status').textContent = '';
const en = $('trigger-enabled');
if (config.trigger.timing === 'manual') {
@@ -490,9 +788,10 @@
}
if (config.api.modelCache.length) {
- setHtml($('api-model-select'), config.api.modelCache.map(m =>
- ``
- ).join(''));
+ setSelectOptions($('api-model-select'), config.api.modelCache, '请选择');
+ $('api-model-select').value = config.api.modelCache.includes(config.api.model) ? config.api.model : '';
+ } else {
+ setSelectOptions($('api-model-select'), [], '请选择');
}
updateProviderUI(config.api.provider);
@@ -524,12 +823,12 @@
if (save) {
const pn = id => { const v = $(id).value; return v === '' ? null : parseFloat(v); };
const provider = $('api-provider').value;
- const pv = PROVIDER_DEFAULTS[provider] || PROVIDER_DEFAULTS.custom;
config.api.provider = provider;
config.api.url = $('api-url').value;
config.api.key = $('api-key').value;
- config.api.model = provider === 'st' ? '' : pv.needManualModel ? $('api-model-text').value : $('api-model-select').value;
+ config.api.model = provider === 'st' ? '' : $('api-model-text').value.trim();
+ config.api.modelCache = [];
config.gen.temperature = pn('gen-temp');
config.gen.top_p = pn('gen-top-p');
@@ -547,6 +846,16 @@
config.trigger.wrapperHead = $('trigger-wrapper-head').value;
config.trigger.wrapperTail = $('trigger-wrapper-tail').value;
config.trigger.forceInsertAtEnd = $('trigger-insert-at-end').checked;
+ config.prompts.summarySystemPrompt = $('summary-system-prompt').value;
+ config.prompts.summaryAssistantDocPrompt = $('summary-assistant-doc-prompt').value;
+ config.prompts.summaryAssistantAskSummaryPrompt = $('summary-assistant-ask-summary-prompt').value;
+ config.prompts.summaryAssistantAskContentPrompt = $('summary-assistant-ask-content-prompt').value;
+ config.prompts.summaryMetaProtocolStartPrompt = $('summary-meta-protocol-start-prompt').value;
+ config.prompts.summaryUserJsonFormatPrompt = $('summary-user-json-format-prompt').value;
+ config.prompts.summaryAssistantCheckPrompt = $('summary-assistant-check-prompt').value;
+ config.prompts.summaryUserConfirmPrompt = $('summary-user-confirm-prompt').value;
+ config.prompts.summaryAssistantPrefillPrompt = $('summary-assistant-prefill-prompt').value;
+ config.prompts.memoryTemplate = $('memory-prompt-template').value;
config.textFilterRules = collectFilterRules();
config.vector = getVectorConfig();
@@ -559,10 +868,11 @@
async function fetchModels() {
const btn = $('btn-connect');
+ const statusEl = $('api-connect-status');
const provider = $('api-provider').value;
if (!PROVIDER_DEFAULTS[provider]?.canFetch) {
- alert('当前渠道不支持自动拉取模型');
+ statusEl.textContent = '当前渠道不支持自动拉取模型';
return;
}
@@ -570,12 +880,13 @@
const apiKey = $('api-key').value.trim();
if (!apiKey) {
- alert('请先填写 API KEY');
+ statusEl.textContent = '请先填写 API KEY';
return;
}
btn.disabled = true;
btn.textContent = '连接中...';
+ statusEl.textContent = '连接中...';
try {
const tryFetch = async url => {
@@ -592,21 +903,21 @@
if (!models?.length) throw new Error('未获取到模型列表');
config.api.modelCache = [...new Set(models)];
- const sel = $('api-model-select');
- setSelectOptions(sel, config.api.modelCache);
+ modelListFetchedThisIframe = true;
+ setSelectOptions($('api-model-select'), config.api.modelCache, '请选择');
$('api-model-select-row').classList.remove('hidden');
if (!config.api.model && models.length) {
config.api.model = models[0];
- sel.value = models[0];
+ $('api-model-text').value = models[0];
+ $('api-model-select').value = models[0];
} else if (config.api.model) {
- sel.value = config.api.model;
+ $('api-model-select').value = config.api.model;
}
- saveConfig();
- alert(`成功获取 ${models.length} 个模型`);
+ statusEl.textContent = `拉取成功:${models.length} 个模型`;
} catch (e) {
- alert('连接失败:' + (e.message || '请检查 URL 和 KEY'));
+ statusEl.textContent = '拉取失败:' + (e.message || '请检查 URL 和 KEY');
} finally {
btn.disabled = false;
btn.textContent = '连接 / 拉取模型列表';
@@ -995,6 +1306,8 @@
const modal = $('confirm-modal');
const titleEl = $('confirm-title');
const msgEl = $('confirm-message');
+ const inputWrap = $('confirm-input-wrap');
+ const inputEl = $('confirm-input');
const okBtn = $('confirm-ok');
const cancelBtn = $('confirm-cancel');
const closeBtn = $('confirm-close');
@@ -1002,6 +1315,8 @@
titleEl.textContent = title;
msgEl.textContent = message;
+ inputWrap.classList.add('hidden');
+ inputEl.value = '';
okBtn.textContent = okText;
cancelBtn.textContent = cancelText;
@@ -1023,6 +1338,47 @@
});
}
+ function showConfirmInput(title, message, okText = '执行', cancelText = '取消', placeholder = '') {
+ return new Promise(resolve => {
+ const modal = $('confirm-modal');
+ const titleEl = $('confirm-title');
+ const msgEl = $('confirm-message');
+ const inputWrap = $('confirm-input-wrap');
+ const inputEl = $('confirm-input');
+ const okBtn = $('confirm-ok');
+ const cancelBtn = $('confirm-cancel');
+ const closeBtn = $('confirm-close');
+ const backdrop = $('confirm-backdrop');
+
+ titleEl.textContent = title;
+ msgEl.textContent = message;
+ inputWrap.classList.remove('hidden');
+ inputEl.placeholder = placeholder || '';
+ inputEl.value = '';
+ okBtn.textContent = okText;
+ cancelBtn.textContent = cancelText;
+
+ const close = (result) => {
+ modal.classList.remove('active');
+ inputWrap.classList.add('hidden');
+ inputEl.value = '';
+ okBtn.onclick = null;
+ cancelBtn.onclick = null;
+ closeBtn.onclick = null;
+ backdrop.onclick = null;
+ resolve(result);
+ };
+
+ okBtn.onclick = () => close(inputEl.value);
+ cancelBtn.onclick = () => close(null);
+ closeBtn.onclick = () => close(null);
+ backdrop.onclick = () => close(null);
+
+ modal.classList.add('active');
+ setTimeout(() => inputEl.focus(), 0);
+ });
+ }
+
function renderArcsEditor(arcs) {
const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }];
const es = $('editor-struct');
@@ -1499,6 +1855,27 @@
}
break;
+ case 'SUMMARY_COPY_RESULT':
+ $('btn-copy-summary').disabled = false;
+ if (d.success) {
+ $('summary-io-status').textContent = `复制成功: ${d.events || 0} 条事件, ${d.facts || 0} 条世界状态`;
+ } else {
+ $('summary-io-status').textContent = '复制失败: ' + (d.error || '未知错误');
+ }
+ break;
+
+ case 'SUMMARY_IMPORT_RESULT':
+ $('btn-import-summary').disabled = false;
+ if (d.success) {
+ const c = d.counts || {};
+ $('summary-io-status').textContent = `导入成功: ${c.events || 0} 条事件, ${c.facts || 0} 条世界状态,已覆盖当前总结资料并清空向量/锚点,请重新生成向量。`;
+ postMsg('REQUEST_VECTOR_STATS');
+ postMsg('REQUEST_ANCHOR_STATS');
+ } else {
+ $('summary-io-status').textContent = '导入失败: ' + (d.error || '未知错误');
+ }
+ break;
+
case 'VECTOR_IMPORT_RESULT':
$('btn-import-vectors').disabled = false;
if (d.success) {
@@ -1588,12 +1965,34 @@
$('api-provider').onchange = e => {
const pv = PROVIDER_DEFAULTS[e.target.value];
$('api-url').value = '';
+ modelListFetchedThisIframe = false;
if (!pv.canFetch) config.api.modelCache = [];
updateProviderUI(e.target.value);
};
$('btn-connect').onclick = fetchModels;
- $('api-model-select').onchange = e => { config.api.model = e.target.value; };
+ $('api-model-text').oninput = e => { config.api.model = e.target.value.trim(); };
+ $('api-model-select').onchange = e => {
+ const value = e.target.value || '';
+ if (value) {
+ $('api-model-text').value = value;
+ config.api.model = value;
+ }
+ };
+ $('btn-reset-summary-prompts').onclick = () => {
+ $('summary-system-prompt').value = DEFAULT_SUMMARY_SYSTEM_PROMPT;
+ $('summary-assistant-doc-prompt').value = DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT;
+ $('summary-assistant-ask-summary-prompt').value = DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT;
+ $('summary-assistant-ask-content-prompt').value = DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT;
+ $('summary-meta-protocol-start-prompt').value = DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT;
+ $('summary-user-json-format-prompt').value = DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT;
+ $('summary-assistant-check-prompt').value = DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT;
+ $('summary-user-confirm-prompt').value = DEFAULT_SUMMARY_USER_CONFIRM_PROMPT;
+ $('summary-assistant-prefill-prompt').value = DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT;
+ };
+ $('btn-reset-memory-prompt-template').onclick = () => {
+ $('memory-prompt-template').value = DEFAULT_MEMORY_PROMPT_TEMPLATE;
+ };
// Trigger timing
$('trigger-timing').onchange = e => {
@@ -1662,6 +2061,7 @@
};
// Vector UI
+ initSummaryIOUI();
initVectorUI();
// Gen params collapsible
diff --git a/modules/story-summary/story-summary.css b/modules/story-summary/story-summary.css
index a7d8b2b..bff5713 100644
--- a/modules/story-summary/story-summary.css
+++ b/modules/story-summary/story-summary.css
@@ -1506,6 +1506,7 @@ h1 span {
margin-bottom: 4px;
}
+
.vector-stats {
display: flex;
gap: 8px;
diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html
index a336d0c..94ec320 100644
--- a/modules/story-summary/story-summary.html
+++ b/modules/story-summary/story-summary.html
@@ -161,8 +161,9 @@
-
总结设置
-
向量设置
+
总结
+
向量
+
提示词
调试
说明
@@ -222,16 +223,17 @@
-
+
+
选择后会回填到上面的模型名输入框。原生下拉更稳,不依赖额外样式。
流式
+
@@ -383,6 +386,15 @@
+
+
+
导出与导入
+
+
+
+
+
复制会把记忆包放进剪贴板;导入会覆盖当前聊天的总结资料,并自动清空向量与总结边界。
+
@@ -581,6 +593,75 @@
+
+
+
+
这里展示的是一次完整增量总结的各段提示词。像 {$nextEventId}、{$existingEventCount} 这样的占位符会在运行时自动替换,不要删除。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
必须保留 {$剧情记忆} 这个占位符,运行时会替换成实际记忆内容。
+
+
+
+
+