Refine story summary prompts and vector sync

This commit is contained in:
2026-01-30 00:55:04 +08:00
parent 6aaed2af4a
commit d87c8a0207
7 changed files with 1174 additions and 432 deletions

View File

@@ -36,7 +36,7 @@ Incremental_Summary_Requirements:
- 转折: 改变某条线走向 - 转折: 改变某条线走向
- 点睛: 有细节不影响主线 - 点睛: 有细节不影响主线
- 氛围: 纯粹氛围片段 - 氛围: 纯粹氛围片段
- Causal_Chain: 为每个新事件标注直接前因事件IDcausedBy0-2个只填 evt-数字 形式,必须指向已存在事件”或“本次新输出事件”。不要写解释文字 - Causal_Chain: 为每个新事件标注直接前因事件IDcausedBy。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个只填 evt-数字指向已存在本次新输出事件。
- Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融) - Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融)
- Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0) - Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0)
- World_State_Tracking: 维护当前世界的硬性约束。解决"什么不能违反"。采用 KV 覆盖模型,追踪生死、物品归属、秘密知情、关系状态、环境规则等不可违背的事实。(覆盖式更新) - World_State_Tracking: 维护当前世界的硬性约束。解决"什么不能违反"。采用 KV 覆盖模型,追踪生死、物品归属、秘密知情、关系状态、环境规则等不可违背的事实。(覆盖式更新)
@@ -215,10 +215,7 @@ Before generating, observe the USER and analyze carefully:
- events.id 从 evt-{nextEventId} 开始编号 - events.id 从 evt-{nextEventId} 开始编号
- 仅输出【增量】内容,已有事件绝不重复 - 仅输出【增量】内容,已有事件绝不重复
- keywords 是全局关键词,综合已有+新增 - keywords 是全局关键词,综合已有+新增
- causedBy 规则 - causedBy 仅在因果明确时填写,允许为[]0-2个详见上方 Causal_Chain 规则
- 数组最多2个无前因则 []
- 只能填 evt-数字(例如 evt-12
- 必须引用“已存在事件”或“本次新输出事件”(允许引用本次 JSON 内较早出现的事件)
- worldUpdate 可为空数组 - worldUpdate 可为空数组
- 合法JSON字符串值内部避免英文双引号 - 合法JSON字符串值内部避免英文双引号
- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象 - 用朴实、白描、有烟火气的笔触记录,避免比喻和意象

View File

@@ -91,11 +91,11 @@ function cleanSummary(summary) {
function buildSystemPreamble() { function buildSystemPreamble() {
return [ return [
"以上内容为因上下文窗口限制保留的可见历史", "以上内容为因上下文窗口限制保留的可见历史",
"【剧情记忆】为对以上可见不可见历史的总结", "以下【剧情记忆】是对可见不可见历史的总结",
"1) 【世界状态】属于硬约束", " 【世界约束】记录着已确立的事实",
"2) 【事件/证据/碎片/人物弧光】可用于补全上下文与动机。", "• 其余部分是过往经历的回忆碎片",
"", "",
"请阅读并内化以下剧情记忆:", "请内化这些记忆:",
].join("\n"); ].join("\n");
} }
@@ -275,7 +275,7 @@ function buildNonVectorPrompt(store) {
if (data.world?.length) { if (data.world?.length) {
const lines = formatWorldLines(data.world); const lines = formatWorldLines(data.world);
sections.push(`[世界约束] 规则手册,请严格遵守\n${lines.join("\n")}`); sections.push(`[世界约束] 已确立的事实\n${lines.join("\n")}`);
} }
if (data.events?.length) { if (data.events?.length) {
@@ -602,7 +602,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
// 1. 世界约束 // 1. 世界约束
if (assembled.world.lines.length) { if (assembled.world.lines.length) {
sections.push(`[世界约束] 规则手册,请严格遵守\n${assembled.world.lines.join("\n")}`); sections.push(`[世界约束] 已确立的事实\n${assembled.world.lines.join("\n")}`);
} }
// 2. 核心经历 // 2. 核心经历

View File

@@ -299,16 +299,18 @@
const items = rules?.length ? rules : []; const items = rules?.length ? rules : [];
setHtml(list, items.map((r, i) => ` setHtml(list, items.map((r, i) => `
<div class="filter-rule-item" data-idx="${i}" style="display:flex;gap:6px;align-items:center"> <div class="filter-rule-item" data-idx="${i}">
<input type="text" class="filter-rule-start" placeholder="起始(可空)" value="${h(r.start || '')}" style="flex:1;padding:6px 8px;font-size:.8125rem"> <div class="filter-rule-inputs">
<span style="color:var(--txt3)">→</span> <input type="text" class="filter-rule-start" placeholder="起始(可空)" value="${h(r.start || '')}">
<input type="text" class="filter-rule-end" placeholder="结束(可空)" value="${h(r.end || '')}" style="flex:1;padding:6px 8px;font-size:.8125rem"> <span class="rule-arrow">⬇</span>
<button class="btn btn-sm btn-del filter-rule-del" style="padding:4px 8px">✕</button> <input type="text" class="filter-rule-end" placeholder="结束(可空)" value="${h(r.end || '')}">
</div>
<button class="btn-del-rule">✕</button>
</div> </div>
`).join('')); `).join(''));
// 绑定删除 // 绑定删除
list.querySelectorAll('.filter-rule-del').forEach(btn => { list.querySelectorAll('.btn-del-rule').forEach(btn => {
btn.onclick = () => { btn.onclick = () => {
btn.closest('.filter-rule-item')?.remove(); btn.closest('.filter-rule-item')?.remove();
}; };
@@ -338,14 +340,15 @@
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'filter-rule-item'; div.className = 'filter-rule-item';
div.dataset.idx = idx; div.dataset.idx = idx;
div.style.cssText = 'display:flex;gap:6px;align-items:center';
setHtml(div, ` setHtml(div, `
<input type="text" class="filter-rule-start" placeholder="起始(可空)" value="" style="flex:1;padding:6px 8px;font-size:.8125rem"> <div class="filter-rule-inputs">
<span style="color:var(--txt3)">→</span> <input type="text" class="filter-rule-start" placeholder="起始(可空)" value="">
<input type="text" class="filter-rule-end" placeholder="结束(可空)" value="" style="flex:1;padding:6px 8px;font-size:.8125rem"> <span class="rule-arrow">⬇</span>
<button class="btn btn-sm btn-del filter-rule-del" style="padding:4px 8px">✕</button> <input type="text" class="filter-rule-end" placeholder="结束(可空)" value="">
</div>
<button class="btn-del-rule">✕</button>
`); `);
div.querySelector('.filter-rule-del').onclick = () => div.remove(); div.querySelector('.btn-del-rule').onclick = () => div.remove();
list.appendChild(div); list.appendChild(div);
} }
@@ -550,7 +553,24 @@
updateProviderUI(config.api.provider); updateProviderUI(config.api.provider);
if (config.vector) loadVectorConfig(config.vector); if (config.vector) loadVectorConfig(config.vector);
// Initialize sub-options visibility
const autoSummaryOptions = $('auto-summary-options');
if (autoSummaryOptions) {
autoSummaryOptions.classList.toggle('hidden', !config.trigger.enabled);
}
const insertWrapperOptions = $('insert-wrapper-options');
if (insertWrapperOptions) {
insertWrapperOptions.classList.toggle('hidden', !config.trigger.forceInsertAtEnd);
}
$('settings-modal').classList.add('active'); $('settings-modal').classList.add('active');
// Default to first tab
$$('.settings-tab').forEach(t => t.classList.remove('active'));
$$('.settings-tab[data-tab="tab-summary"]').forEach(t => t.classList.add('active'));
$$('.tab-pane').forEach(p => p.classList.remove('active'));
$('tab-summary').classList.add('active');
postMsg('SETTINGS_OPENED'); postMsg('SETTINGS_OPENED');
} }
@@ -1202,17 +1222,6 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
} }
} }
function openRecallLog() {
updateRecallLogDisplay();
$('recall-log-modal').classList.add('active');
postMsg('FULLSCREEN_OPENED');
}
function closeRecallLog() {
$('recall-log-modal').classList.remove('active');
postMsg('FULLSCREEN_CLOSED');
}
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// Editor // Editor
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
@@ -1666,6 +1675,27 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
$('settings-cancel').onclick = () => closeSettings(false); $('settings-cancel').onclick = () => closeSettings(false);
$('settings-save').onclick = () => closeSettings(true); $('settings-save').onclick = () => closeSettings(true);
// Settings tabs
$$('.settings-tab').forEach(tab => {
tab.onclick = () => {
const targetId = tab.dataset.tab;
if (!targetId) return;
// Update tab active state
$$('.settings-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Update pane active state
$$('.tab-pane').forEach(p => p.classList.remove('active'));
$(targetId).classList.add('active');
// If switching to debug tab, refresh log
if (targetId === 'tab-debug') {
postMsg('REQUEST_RECALL_LOG');
}
};
});
// API provider change // API provider change
$('api-provider').onchange = e => { $('api-provider').onchange = e => {
const pv = PROVIDER_DEFAULTS[e.target.value]; const pv = PROVIDER_DEFAULTS[e.target.value];
@@ -1729,11 +1759,6 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
$('hf-guide-backdrop').onclick = closeHfGuide; $('hf-guide-backdrop').onclick = closeHfGuide;
$('hf-guide-close').onclick = closeHfGuide; $('hf-guide-close').onclick = closeHfGuide;
// Recall log
$('btn-recall').onclick = openRecallLog;
$('recall-log-backdrop').onclick = closeRecallLog;
$('recall-log-close').onclick = closeRecallLog;
// Character selector // Character selector
$('char-sel-trigger').onclick = e => { $('char-sel-trigger').onclick = e => {
e.stopPropagation(); e.stopPropagation();
@@ -1748,6 +1773,36 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
// Vector UI // Vector UI
initVectorUI(); initVectorUI();
// Gen params collapsible
const genParamsToggle = $('gen-params-toggle');
const genParamsContent = $('gen-params-content');
if (genParamsToggle && genParamsContent) {
genParamsToggle.onclick = () => {
const collapse = genParamsToggle.closest('.settings-collapse');
collapse.classList.toggle('open');
genParamsContent.classList.toggle('hidden');
};
}
// Auto summary sub-options toggle
const triggerEnabled = $('trigger-enabled');
const autoSummaryOptions = $('auto-summary-options');
if (triggerEnabled && autoSummaryOptions) {
triggerEnabled.onchange = () => {
autoSummaryOptions.classList.toggle('hidden', !triggerEnabled.checked);
};
}
// Force insert sub-options toggle
const triggerInsertAtEnd = $('trigger-insert-at-end');
const insertWrapperOptions = $('insert-wrapper-options');
if (triggerInsertAtEnd && insertWrapperOptions) {
triggerInsertAtEnd.onchange = () => {
insertWrapperOptions.classList.toggle('hidden', !triggerInsertAtEnd.checked);
};
}
// Resize // Resize
window.onresize = () => { window.onresize = () => {
relationChart?.resize(); relationChart?.resize();

View File

@@ -59,7 +59,8 @@ main {
min-height: 0; min-height: 0;
} }
.left, .right { .left,
.right {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
@@ -68,8 +69,10 @@ main {
/* 关键词卡片:固定高度 */ /* 关键词卡片:固定高度 */
.left>.card:first-child { .left>.card:first-child {
flex: 0 0 auto; /* 关键词:不伸缩 */ flex: 0 0 auto;
/* 关键词:不伸缩 */
} }
/* ═══════════════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════════════
Typography Typography
═══════════════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════════════ */
@@ -153,9 +156,9 @@ h1 span {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px 16px; padding: 4px 0;
background: var(--bg3); background: transparent;
border: 1px solid var(--bdr); border: none;
font-size: .8125rem; font-size: .8125rem;
color: var(--txt2); color: var(--txt2);
cursor: pointer; cursor: pointer;
@@ -163,7 +166,7 @@ h1 span {
} }
.chk-label:hover { .chk-label:hover {
border-color: var(--acc); color: var(--txt);
} }
.chk-label input { .chk-label input {
@@ -290,33 +293,25 @@ h1 span {
padding: 10px 14px; padding: 10px 14px;
} }
.btn-recall { .btn-debug {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: var(--bg2);
color: #fff; color: var(--txt2);
border-color: #667eea; border: 1px solid var(--bdr);
position: relative; display: flex;
overflow: hidden; align-items: center;
gap: 6px;
justify-content: center;
} }
.btn-recall:hover { .btn-debug svg {
background: linear-gradient(135deg, #5a67d8 0%, #6b46a1 100%); width: 14px;
border-color: #5a67d8; height: 14px;
} }
.btn-recall::after { .btn-debug:hover {
content: ''; background: var(--bg3);
position: absolute; border-color: var(--acc);
top: -50%; color: var(--txt);
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent 40%, rgba(255,255,255,.15) 50%, transparent 60%);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) rotate(45deg); }
100% { transform: translateX(100%) rotate(45deg); }
} }
/* ═══════════════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════════════
@@ -700,13 +695,40 @@ h1 span {
white-space: nowrap; white-space: nowrap;
} }
.trend-broken { background: rgba(68, 68, 68, .15); color: #444; } .trend-broken {
.trend-hate { background: rgba(139, 0, 0, .15); color: #8b0000; } background: rgba(68, 68, 68, .15);
.trend-dislike { background: rgba(205, 92, 92, .15); color: #cd5c5c; } color: #444;
.trend-stranger { background: rgba(136, 136, 136, .15); color: #888; } }
.trend-click { background: rgba(102, 205, 170, .15); color: #4a9a7e; }
.trend-close { background: rgba(235, 106, 106, .15); color: var(--hl); } .trend-hate {
.trend-merge { background: rgba(199, 21, 133, .2); color: #c71585; } background: rgba(139, 0, 0, .15);
color: #8b0000;
}
.trend-dislike {
background: rgba(205, 92, 92, .15);
color: #cd5c5c;
}
.trend-stranger {
background: rgba(136, 136, 136, .15);
color: #888;
}
.trend-click {
background: rgba(102, 205, 170, .15);
color: #4a9a7e;
}
.trend-close {
background: rgba(235, 106, 106, .15);
color: var(--hl);
}
.trend-merge {
background: rgba(199, 21, 133, .2);
color: #c71585;
}
/* ═══════════════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════════════
Custom Select Custom Select
@@ -787,8 +809,15 @@ h1 span {
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
/* ═══════════════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════════════
@@ -1041,8 +1070,10 @@ h1 span {
letter-spacing: .05em; letter-spacing: .05em;
} }
.settings-field input, .settings-field input:not([type="checkbox"]):not([type="radio"]),
.settings-field select { .settings-field select {
width: 100%;
max-width: 100%;
padding: 10px 14px; padding: 10px 14px;
background: var(--bg3); background: var(--bg3);
border: 1px solid var(--bdr); border: 1px solid var(--bdr);
@@ -1050,6 +1081,22 @@ h1 span {
color: var(--txt); color: var(--txt);
outline: none; outline: none;
transition: border-color .2s; transition: border-color .2s;
box-sizing: border-box;
}
.settings-field input[type="checkbox"],
.settings-field input[type="radio"] {
width: auto;
height: auto;
}
.settings-field select {
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 4.5 6 7.5 9 4.5'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
} }
.settings-field input:focus, .settings-field input:focus,
@@ -1113,6 +1160,10 @@ h1 span {
.engine-option input { .engine-option input {
accent-color: var(--hl); accent-color: var(--hl);
width: 18px;
height: 18px;
margin: 0;
cursor: pointer;
} }
.engine-area { .engine-area {
@@ -1132,19 +1183,35 @@ h1 span {
margin-bottom: 4px; margin-bottom: 4px;
} }
.engine-card-desc { .engine-status-row {
font-size: .75rem; display: flex;
color: var(--txt3); justify-content: space-between;
margin-bottom: 12px; align-items: center;
gap: 12px;
margin-top: 12px;
} }
.engine-status { .engine-status {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
gap: 6px; gap: 6px;
font-size: .8125rem; font-size: .8125rem;
margin-bottom: 12px; color: var(--txt3);
flex: 1;
/* 占 1/3 */
}
.engine-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
flex: 2;
/* 占 2/3 */
}
/* 针对在线测试连接按钮的特殊处理 */
#btn-test-vector-api {
flex: 2;
} }
.status-dot { .status-dot {
@@ -1154,15 +1221,37 @@ h1 span {
background: var(--txt3); background: var(--txt3);
} }
.status-dot.ready { background: #22c55e; } .status-dot.ready {
.status-dot.cached { background: #3b82f6; } background: #22c55e;
.status-dot.downloading { background: #f59e0b; animation: pulse 1s infinite; } }
.status-dot.error { background: #ef4444; }
.status-dot.success { background: #22c55e; } .status-dot.cached {
background: #3b82f6;
}
.status-dot.downloading {
background: #f59e0b;
animation: pulse 1s infinite;
}
.status-dot.error {
background: #ef4444;
}
.status-dot.success {
background: #22c55e;
}
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; } 0%,
100% {
opacity: 1;
}
50% {
opacity: .5;
}
} }
.engine-progress { .engine-progress {
@@ -1195,8 +1284,8 @@ h1 span {
.engine-actions { .engine-actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: center; justify-content: flex-end;
flex-wrap: wrap; flex: 2;
} }
.model-select-row { .model-select-row {
@@ -1218,8 +1307,8 @@ h1 span {
.model-desc { .model-desc {
font-size: .75rem; font-size: .75rem;
color: var(--txt3); color: var(--txt3);
text-align: center; text-align: left;
margin-bottom: 12px; margin-bottom: 4px;
} }
.vector-stats { .vector-stats {
@@ -1684,8 +1773,7 @@ h1 span {
.btn-group { .btn-group {
width: 100%; width: 100%;
display: grid; display: flex;
grid-template-columns: repeat(4, 1fr);
gap: 6px; gap: 6px;
} }
@@ -1714,7 +1802,8 @@ h1 span {
gap: 16px; gap: 16px;
} }
.left, .right { .left,
.right {
gap: 16px; gap: 16px;
} }
@@ -1882,7 +1971,9 @@ h1 span {
font-size: .6875rem; font-size: .6875rem;
} }
main, .left, .right { main,
.left,
.right {
gap: 12px; gap: 12px;
} }
@@ -2177,3 +2268,412 @@ h1 span {
padding: 6px 8px; padding: 6px 8px;
} }
} }
/* ═══════════════════════════════════════════════════════════════════════════
New Settings Styles
═══════════════════════════════════════════════════════════════════════════ */
.settings-modal-box {
max-width: 680px;
}
/* Collapsible Section */
.settings-collapse {
margin-top: 20px;
border-radius: 8px;
overflow: hidden;
}
.settings-collapse-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
cursor: pointer;
font-size: .8125rem;
font-weight: 500;
color: var(--txt2);
transition: all .2s;
}
.settings-collapse-header:hover {
background: var(--bdr);
}
.collapse-icon {
width: 16px;
height: 16px;
transition: transform .2s;
}
.settings-collapse.open .collapse-icon {
transform: rotate(180deg);
}
.settings-collapse-content {
padding: 16px;
border-top: 1px solid var(--bdr);
}
/* Checkbox Group */
.settings-checkbox-group {
margin-bottom: 20px;
padding: 0;
background: transparent;
border: none;
}
.settings-checkbox-group:last-child {
margin-bottom: 0;
}
.settings-checkbox {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.settings-checkbox input[type="checkbox"] {
display: none;
}
.checkbox-mark {
width: 20px;
height: 20px;
border: 2px solid var(--bdr);
border-radius: 4px;
background: var(--bg2);
position: relative;
transition: all .2s;
flex-shrink: 0;
}
.settings-checkbox input:checked+.checkbox-mark {
background: var(--acc);
border-color: var(--acc);
}
.settings-checkbox input:checked+.checkbox-mark::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-label {
font-size: .875rem;
color: var(--txt);
}
.settings-checkbox-group .settings-hint {
margin-left: 30px;
margin-top: 4px;
}
/* Sub Options */
.settings-sub-options {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--bdr);
}
/* Filter Rules */
.filter-rules-section {
margin-top: 20px;
padding: 16px;
background: var(--bg3);
border: 1px solid var(--bdr);
border-radius: 8px;
}
.filter-rules-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
gap: 12px;
}
.filter-rules-header label {
font-size: .75rem;
color: var(--txt3);
text-transform: uppercase;
letter-spacing: .05em;
font-weight: 600;
flex: 1;
/* 1/3 */
}
.btn-add {
flex: 2;
/* 2/3 */
justify-content: center;
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
}
.filter-rules-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.filter-rule-item {
display: flex;
gap: 8px;
align-items: flex-start;
padding: 10px 12px;
background: var(--bg2);
border: 1px solid var(--bdr2);
border-radius: 6px;
}
.filter-rule-inputs {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex: 1;
}
.filter-rule-item input {
width: 100%;
padding: 8px 10px;
background: var(--bg3);
border: 1px solid var(--bdr);
font-size: .8125rem;
color: var(--txt);
border-radius: 4px;
}
.filter-rule-item input:focus {
border-color: var(--acc);
outline: none;
}
.filter-rule-item .rule-arrow {
color: var(--txt3);
font-size: .875rem;
flex-shrink: 0;
padding: 2px 0;
}
.filter-rule-item .btn-del-rule {
padding: 6px 10px;
background: transparent;
border: 1px solid var(--hl);
color: var(--hl);
cursor: pointer;
border-radius: 4px;
font-size: .75rem;
transition: all .2s;
flex-shrink: 0;
align-self: center;
}
.filter-rule-item .btn-del-rule:hover {
background: var(--hl-soft);
}
/* Vector Stats - Original horizontal layout */
.vector-stats {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 16px;
font-size: .875rem;
color: var(--txt2);
margin-top: 8px;
}
.vector-stat-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.vector-stat-label {
font-size: .75rem;
color: var(--txt3);
}
.vector-stat-value {
color: var(--txt2);
}
.vector-stat-value strong {
color: var(--hl);
}
.vector-stat-sep {
color: var(--txt3);
align-self: center;
}
.vector-io-section {
border-top: 1px solid var(--bdr);
padding-top: 16px;
margin-top: 16px;
}
/* Mobile Settings Responsive */
@media (max-width: 768px) {
.settings-modal-box {
max-width: 100%;
}
.settings-collapse-header {
padding: 14px 16px;
}
.settings-checkbox-group {
padding: 14px;
}
.checkbox-label {
font-size: .8125rem;
}
.vector-stats {
gap: 8px;
}
.vector-stat-sep {
display: none;
}
.vector-stat-col {
flex-direction: row;
gap: 4px;
}
.settings-field {
min-width: 100px;
}
}
@media (max-width: 480px) {
.settings-checkbox-group {
padding: 12px;
}
.checkbox-mark {
width: 18px;
height: 18px;
}
.settings-checkbox input:checked+.checkbox-mark::after {
left: 5px;
top: 1px;
width: 4px;
height: 9px;
}
.filter-rules-section {
padding: 12px;
}
.filter-rule-item {
padding: 8px 10px;
}
.filter-rule-item .btn-del-rule {
padding: 4px 8px;
}
.settings-sub-options .settings-row {
flex-direction: column;
}
}
/* Settings Tabs */
.settings-tabs {
display: flex;
gap: 24px;
align-self: flex-end;
/* 使底部边框与 header 底部对齐 */
margin-bottom: -20px;
/* 抵消 modal-head 的 padding让边框贴合底部 */
}
.settings-tab {
font-size: .875rem;
color: var(--txt3);
cursor: pointer;
padding-bottom: 20px;
/* 增加内边距使点击区域更大且贴合底部 */
border-bottom: 2px solid transparent;
transition: all .2s;
user-select: none;
text-transform: uppercase;
letter-spacing: .1em;
font-weight: 500;
}
.settings-tab:hover {
color: var(--txt);
}
.settings-tab.active {
color: var(--hl);
border-bottom-color: var(--hl);
font-weight: 600;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
animation: fadeIn .3s ease;
}
.debug-log-header {
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px dashed var(--bdr2);
}
.debug-title {
font-size: .875rem;
font-weight: 600;
color: var(--txt);
margin-bottom: 4px;
}
.debug-log-viewer {
width: 100%;
height: 400px;
background: var(--bg3);
border: 1px solid var(--bdr);
border-radius: 6px;
padding: 12px;
font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
color: var(--txt2);
overflow-y: auto;
white-space: pre-wrap;
margin: 0;
}
.recall-empty {
color: var(--txt3);
text-align: center;
padding: 40px;
font-style: italic;
font-size: .8125rem;
line-height: 1.8;
}

View File

@@ -1,6 +1,7 @@
<!-- story-summary.html --> <!-- story-summary.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
@@ -8,6 +9,7 @@
<title>剧情总结 · Story Summary</title> <title>剧情总结 · Story Summary</title>
<link rel="stylesheet" href="story-summary.css"> <link rel="stylesheet" href="story-summary.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<!-- Header --> <!-- Header -->
@@ -37,18 +39,19 @@
<div class="controls"> <div class="controls">
<label class="chk-label"> <label class="chk-label">
<input type="checkbox" id="hide-summarized"> <input type="checkbox" id="hide-summarized">
<span>聊天时隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留<input type="number" id="keep-visible-count" min="0" max="50" value="3">楼)</span> <span>隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留<input type="number" id="keep-visible-count"
min="0" max="50" value="3">楼)</span>
</label> </label>
<span class="spacer"></span> <span class="spacer"></span>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-icon" id="btn-settings"> <button class="btn btn-icon" id="btn-settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3" /> <circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> <path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg> </svg>
<span>设置</span> <span>设置</span>
</button> </button>
<button class="btn btn-recall" id="btn-recall">涌现</button>
<button class="btn" id="btn-clear">清空</button> <button class="btn" id="btn-clear">清空</button>
<button class="btn btn-p" id="btn-generate">总结</button> <button class="btn btn-p" id="btn-generate">总结</button>
</div> </div>
@@ -92,8 +95,10 @@
<div class="sec-title">人物关系</div> <div class="sec-title">人物关系</div>
<div class="sec-actions"> <div class="sec-actions">
<button class="sec-btn sec-icon" id="btn-fullscreen-relations" title="全屏查看"> <button class="sec-btn sec-icon" id="btn-fullscreen-relations" title="全屏查看">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/> stroke-width="2">
<path
d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
</svg> </svg>
</button> </button>
<button class="sec-btn" data-section="characters">编辑</button> <button class="sec-btn" data-section="characters">编辑</button>
@@ -132,7 +137,8 @@
<h2 id="editor-title">编辑</h2> <h2 id="editor-title">编辑</h2>
<button class="modal-close" id="editor-close"> <button class="modal-close" id="editor-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -152,17 +158,24 @@
<!-- Settings Modal --> <!-- Settings Modal -->
<div class="modal" id="settings-modal"> <div class="modal" id="settings-modal">
<div class="modal-bg" id="settings-backdrop"></div> <div class="modal-bg" id="settings-backdrop"></div>
<div class="modal-box"> <div class="modal-box settings-modal-box">
<div class="modal-head"> <div class="modal-head">
<h2>设置</h2> <div class="settings-tabs">
<div class="settings-tab active" data-tab="tab-summary">总结设置</div>
<div class="settings-tab" data-tab="tab-vector">向量设置</div>
<div class="settings-tab" data-tab="tab-debug">调试</div>
</div>
<button class="modal-close" id="settings-close"> <button class="modal-close" id="settings-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<!-- API Config --> <!-- Tab 1: Summary Settings -->
<div class="tab-pane active" id="tab-summary">
<!-- API Config & Gen Params Combined -->
<div class="settings-section"> <div class="settings-section">
<div class="settings-section-title">API 配置</div> <div class="settings-section-title">API 配置</div>
<div class="settings-row"> <div class="settings-row">
@@ -181,7 +194,7 @@
<div class="settings-field full"> <div class="settings-field full">
<label>API URL</label> <label>API URL</label>
<input type="text" id="api-url" placeholder="https://api.openai.com 或代理地址"> <input type="text" id="api-url" placeholder="https://api.openai.com 或代理地址">
<div class="settings-hint">不同渠道默认端点OpenAI/v1Gemini/v1betaClaude/v1</div> <div class="settings-hint">默认端点OpenAI/v1Gemini/v1betaClaude/v1</div>
</div> </div>
</div> </div>
<div class="settings-row hidden" id="api-key-row"> <div class="settings-row hidden" id="api-key-row">
@@ -204,22 +217,36 @@
</select> </select>
</div> </div>
</div> </div>
<div class="settings-btn-row hidden" id="api-connect-row"> <div class="settings-btn-row hidden" id="api-connect-row"
<button class="btn btn-sm btn-p" id="btn-connect">连接 / 拉取模型列表</button> style="display: flex; gap: 12px; align-items: center; justify-content: space-between;">
</div> <button class="btn btn-sm btn-p" id="btn-connect" style="flex: 4;">连接 / 拉取模型列表</button>
<label class="chk-label compact"
style="margin: 0; flex: 1; display: flex; align-items: center; gap: 6px; white-space: nowrap; justify-content: center;">
<input type="checkbox" id="trigger-stream" checked>
<span>流式</span>
</label>
</div> </div>
<!-- Gen Params --> <!-- Collapsible Gen Params -->
<div class="settings-section"> <div class="settings-collapse">
<div class="settings-section-title">生成参数</div> <div class="settings-collapse-header" id="gen-params-toggle">
<span>生成参数</span>
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
<div class="settings-collapse-content hidden" id="gen-params-content">
<div class="settings-row"> <div class="settings-row">
<div class="settings-field"> <div class="settings-field">
<label>Temperature</label> <label>Temperature</label>
<input type="number" id="gen-temp" step="0.01" min="0" max="2" placeholder="未设置"> <input type="number" id="gen-temp" step="0.01" min="0" max="2"
placeholder="未设置">
</div> </div>
<div class="settings-field"> <div class="settings-field">
<label>Top P</label> <label>Top P</label>
<input type="number" id="gen-top-p" step="0.01" min="0" max="1" placeholder="未设置"> <input type="number" id="gen-top-p" step="0.01" min="0" max="1"
placeholder="未设置">
</div> </div>
<div class="settings-field"> <div class="settings-field">
<label>Top K</label> <label>Top K</label>
@@ -229,11 +256,15 @@
<div class="settings-row"> <div class="settings-row">
<div class="settings-field"> <div class="settings-field">
<label>存在惩罚</label> <label>存在惩罚</label>
<input type="number" id="gen-presence" step="0.01" min="-2" max="2" placeholder="未设置"> <input type="number" id="gen-presence" step="0.01" min="-2" max="2"
placeholder="未设置">
</div> </div>
<div class="settings-field"> <div class="settings-field">
<label>频率惩罚</label> <label>频率惩罚</label>
<input type="number" id="gen-frequency" step="0.01" min="-2" max="2" placeholder="未设置"> <input type="number" id="gen-frequency" step="0.01" min="-2" max="2"
placeholder="未设置">
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -250,7 +281,25 @@
<option value="assistant">Assistant</option> <option value="assistant">Assistant</option>
</select> </select>
</div> </div>
<div class="settings-field">
<label>单次最大总结(楼)</label>
<select id="trigger-max-per-run">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="150">150</option>
<option value="200">200</option>
</select>
</div> </div>
</div>
<!-- Auto Summary with sub-options -->
<div class="settings-checkbox-group">
<label class="settings-checkbox">
<input type="checkbox" id="trigger-enabled">
<span class="checkbox-mark"></span>
<span class="checkbox-label">启用自动总结</span>
</label>
<div class="settings-sub-options hidden" id="auto-summary-options">
<div class="settings-row"> <div class="settings-row">
<div class="settings-field"> <div class="settings-field">
<label>自动总结间隔(楼)</label> <label>自动总结间隔(楼)</label>
@@ -264,53 +313,45 @@
<option value="manual">仅手动</option> <option value="manual">仅手动</option>
</select> </select>
</div> </div>
<div class="settings-field">
<label>单次最大总结(楼)</label>
<select id="trigger-max-per-run">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="150">150</option>
<option value="200">200</option>
</select>
</div> </div>
</div> </div>
<div class="settings-row">
<div class="settings-field full">
<label>头部包裹词应对NoAss配置</label>
<input type="text" id="trigger-wrapper-head" placeholder="添加到开头">
</div>
</div>
<div class="settings-row">
<div class="settings-field full">
<label>尾部包裹词应对NoAss配置</label>
<input type="text" id="trigger-wrapper-tail" placeholder="添加到结尾">
</div>
</div>
<div class="settings-row">
<div class="settings-field-inline">
<input type="checkbox" id="trigger-enabled">
<label for="trigger-enabled">启用自动总结</label>
</div>
<div class="settings-field-inline">
<input type="checkbox" id="trigger-stream" checked>
<label for="trigger-stream">启用流式生成</label>
</div>
<div class="settings-field-inline">
<input type="checkbox" id="trigger-insert-at-end">
<label for="trigger-insert-at-end">强制插入到聊天最后</label>
</div>
</div>
<div class="settings-hint" style="margin-top:8px">若 API 不支持非流式请求,请勾选"启用流式生成"</div>
</div> </div>
<!-- Vector Settings --> <!-- Force Insert with wrapper options -->
<div class="settings-checkbox-group">
<label class="settings-checkbox">
<input type="checkbox" id="trigger-insert-at-end">
<span class="checkbox-mark"></span>
<span class="checkbox-label">强制插入到聊天最后(插件冲突用)</span>
</label>
<div class="settings-sub-options hidden" id="insert-wrapper-options">
<div class="settings-row">
<div class="settings-field full">
<label>头部包裹词</label>
<input type="text" id="trigger-wrapper-head" placeholder="添加到开头应对NoAss配置">
</div>
</div>
<div class="settings-row">
<div class="settings-field full">
<label>尾部包裹词</label>
<input type="text" id="trigger-wrapper-tail" placeholder="添加到结尾应对NoAss配置">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tab 2: Vector Settings -->
<div class="tab-pane" id="tab-vector">
<div class="settings-section"> <div class="settings-section">
<div class="settings-section-title">智能记忆(向量检索)</div> <div class="settings-section-title">智能记忆(向量检索)</div>
<div class="settings-row"> <div class="settings-checkbox-group">
<div class="settings-field-inline"> <label class="settings-checkbox">
<input type="checkbox" id="vector-enabled"> <input type="checkbox" id="vector-enabled">
<label for="vector-enabled">启用向量检索</label> <span class="checkbox-mark"></span>
</div> <span class="checkbox-label">启用向量检索</span>
</label>
</div> </div>
<div id="vector-config-area" class="hidden"> <div id="vector-config-area" class="hidden">
<div class="settings-row" style="margin-top:16px"> <div class="settings-row" style="margin-top:16px">
@@ -339,18 +380,24 @@
</select> </select>
</div> </div>
<div class="model-desc" id="local-model-desc">手机/低配适用</div> <div class="model-desc" id="local-model-desc">手机/低配适用</div>
<div class="engine-status-row">
<div class="engine-status" id="local-model-status"> <div class="engine-status" id="local-model-status">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="status-text">检查中...</span> <span class="status-text">检查中...</span>
</div> </div>
<div class="engine-progress hidden" id="local-model-progress">
<div class="progress-bar"><div class="progress-inner"></div></div>
<span class="progress-text">0%</span>
</div>
<div class="engine-actions" id="local-model-actions"> <div class="engine-actions" id="local-model-actions">
<button class="btn btn-sm btn-p" id="btn-download-model">下载模型</button> <button class="btn btn-sm btn-p" id="btn-download-model">下载</button>
<button class="btn btn-sm" id="btn-cancel-download" style="display:none">取消下载</button> <button class="btn btn-sm" id="btn-cancel-download"
<button class="btn btn-sm btn-del" id="btn-delete-model" style="display:none">删除缓存</button> style="display:none">取消</button>
<button class="btn btn-sm btn-del" id="btn-delete-model"
style="display:none">删除</button>
</div>
</div>
<div class="engine-progress hidden" id="local-model-progress" style="margin-top: 8px;">
<div class="progress-bar">
<div class="progress-inner"></div>
</div>
<span class="progress-text">0%</span>
</div> </div>
</div> </div>
@@ -385,32 +432,39 @@
<select id="vector-model-select" style="flex:1"> <select id="vector-model-select" style="flex:1">
<option value="">请选择模型</option> <option value="">请选择模型</option>
</select> </select>
<button class="btn btn-sm" id="btn-fetch-models" style="display:none">拉取</button> <button class="btn btn-sm" id="btn-fetch-models"
style="display:none">拉取</button>
</div> </div>
</div> </div>
</div> </div>
<div class="engine-status-row">
<div class="engine-status" id="online-api-status"> <div class="engine-status" id="online-api-status">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="status-text">未测试</span> <span class="status-text">未测试</span>
</div> </div>
<div class="settings-btn-row" style="justify-content:center">
<button class="btn btn-sm" id="btn-test-vector-api">测试连接</button> <button class="btn btn-sm" id="btn-test-vector-api">测试连接</button>
</div> </div>
<div class="provider-hint" id="provider-hint"> <div class="provider-hint" id="provider-hint">
💡 <a href="https://siliconflow.cn" target="_blank">硅基流动</a> 免费、速度快、质量好,推荐 BAAI/bge-m3 💡 <a href="https://siliconflow.cn" target="_blank">硅基流动</a> 免费、速度快、质量好,推荐
BAAI/bge-m3
</div> </div>
</div> </div>
<!-- 文本过滤规则 --> <!-- 文本过滤规则 - Redesigned for mobile -->
<div class="settings-row" style="margin-top:16px"> <div class="filter-rules-section">
<div class="settings-field full"> <div class="filter-rules-header">
<label>文本过滤规则</label> <label>文本过滤规则</label>
<p class="settings-hint" style="margin-bottom:8px"> <button class="btn btn-sm btn-add" id="btn-add-filter-rule">
遇到「起始」后跳过,直到「结束」。起始或结束可单独留空。用于过滤思考标签等干扰内容。 <svg viewBox="0 0 24 24" width="14" height="14" fill="none"
</p> stroke="currentColor" stroke-width="2">
<div id="filter-rules-list" style="display:flex;flex-direction:column;gap:6px"></div> <line x1="12" y1="5" x2="12" y2="19"></line>
<button class="btn btn-sm" id="btn-add-filter-rule" style="margin-top:8px"> 添加规则</button> <line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
添加
</button>
</div> </div>
<p class="settings-hint">过滤干扰内容(如思考标签):遇到「起始」跳过直到「结束」</p>
<div id="filter-rules-list" class="filter-rules-list"></div>
</div> </div>
<!-- Vector Stats --> <!-- Vector Stats -->
@@ -419,11 +473,26 @@
<div class="settings-field full"> <div class="settings-field full">
<label>当前聊天向量</label> <label>当前聊天向量</label>
<div class="vector-stats" id="vector-stats"> <div class="vector-stats" id="vector-stats">
<span>事件向量: <strong id="vector-event-count">0</strong>/<strong id="vector-event-total">0</strong></span> <div class="vector-stat-col">
<span>·</span> <span class="vector-stat-label">事件向量:</span>
<span>Chunks: <strong id="vector-chunk-count">0</strong> 个(<span id="vector-chunk-floors">0</span>/<span id="vector-chunk-total">0</span> 层)</span> <span class="vector-stat-value"><strong
<span>·</span> id="vector-event-count">0</strong>/<strong
<span>消息: <strong id="vector-message-count">0</strong></span> id="vector-event-total">0</strong></span>
</div>
<span class="vector-stat-sep">·</span>
<div class="vector-stat-col">
<span class="vector-stat-label">Chunks:</span>
<span class="vector-stat-value"><strong
id="vector-chunk-count">0</strong>
个(<span id="vector-chunk-floors">0</span>/<span
id="vector-chunk-total">0</span> 层)</span>
</div>
<span class="vector-stat-sep">·</span>
<div class="vector-stat-col">
<span class="vector-stat-label">消息:</span>
<span class="vector-stat-value"><strong
id="vector-message-count">0</strong></span>
</div>
</div> </div>
<div class="vector-mismatch-warning hidden" id="vector-mismatch-warning"> <div class="vector-mismatch-warning hidden" id="vector-mismatch-warning">
⚠ 引擎/模型已变更,需重新生成向量 ⚠ 引擎/模型已变更,需重新生成向量
@@ -432,12 +501,16 @@
</div> </div>
<div class="engine-progress hidden" id="vector-gen-progress-l1"> <div class="engine-progress hidden" id="vector-gen-progress-l1">
<div style="font-size:.75rem;color:var(--txt3);margin-bottom:4px">L1 片段</div> <div style="font-size:.75rem;color:var(--txt3);margin-bottom:4px">L1 片段</div>
<div class="progress-bar"><div class="progress-inner"></div></div> <div class="progress-bar">
<div class="progress-inner"></div>
</div>
<span class="progress-text">0/0</span> <span class="progress-text">0/0</span>
</div> </div>
<div class="engine-progress hidden" id="vector-gen-progress-l2"> <div class="engine-progress hidden" id="vector-gen-progress-l2">
<div style="font-size:.75rem;color:var(--txt3);margin-bottom:4px">L2 事件</div> <div style="font-size:.75rem;color:var(--txt3);margin-bottom:4px">L2 事件</div>
<div class="progress-bar"><div class="progress-inner"></div></div> <div class="progress-bar">
<div class="progress-inner"></div>
</div>
<span class="progress-text">0/0</span> <span class="progress-text">0/0</span>
</div> </div>
<div class="settings-hint" id="vector-perf-l1"></div> <div class="settings-hint" id="vector-perf-l1"></div>
@@ -447,14 +520,16 @@
<button class="btn btn-sm btn-del" id="btn-clear-vectors">清除向量</button> <button class="btn btn-sm btn-del" id="btn-clear-vectors">清除向量</button>
<button class="btn btn-sm hidden" id="btn-cancel-vectors">取消</button> <button class="btn btn-sm hidden" id="btn-cancel-vectors">取消</button>
</div> </div>
<div class="settings-hint" style="margin-top:8px">首次生成向量可能耗时较久,页面短暂卡顿属正常。若本地模型重进酒馆后需重下。</div> <div class="settings-hint" style="margin-top:8px">首次生成向量可能耗时较久,页面短暂卡顿属正常。若本地模型重进酒馆后需重下。
</div>
<!-- 向量导入导出 --> <!-- 向量导入导出 -->
<div class="vector-io-section" style="border-top:1px solid var(--bdr);padding-top:16px;margin-top:16px"> <div class="vector-io-section">
<div class="settings-row"> <div class="settings-row">
<div class="settings-field full"> <div class="settings-field full">
<label>向量迁移(跨设备 / 防清缓存)</label> <label>向量迁移(跨设备 / 防清缓存)</label>
<div class="settings-hint" style="margin-bottom:8px">导出/导入均为 zip 格式,勿解压</div> <div class="settings-hint" style="margin-bottom:8px">导出/导入均为 zip 格式,勿解压
</div>
<div class="settings-btn-row" id="vector-io-row" style="margin-top:8px"> <div class="settings-btn-row" id="vector-io-row" style="margin-top:8px">
<button class="btn btn-sm" id="btn-export-vectors">导出向量</button> <button class="btn btn-sm" id="btn-export-vectors">导出向量</button>
<button class="btn btn-sm" id="btn-import-vectors">导入向量</button> <button class="btn btn-sm" id="btn-import-vectors">导入向量</button>
@@ -467,6 +542,16 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Tab 3: Debug -->
<div class="tab-pane" id="tab-debug">
<div class="debug-log-header">
<div class="debug-title">🔧 记忆召回日志</div>
<div class="settings-hint">显示最近一次 AI 生成时的向量检索详情</div>
</div>
<pre id="recall-log-content" class="debug-log-viewer"></pre>
</div>
</div>
<div class="modal-foot"> <div class="modal-foot">
<button class="btn" id="settings-cancel">取消</button> <button class="btn" id="settings-cancel">取消</button>
<button class="btn btn-p" id="settings-save">保存</button> <button class="btn btn-p" id="settings-save">保存</button>
@@ -482,7 +567,8 @@
<h2>人物关系图</h2> <h2>人物关系图</h2>
<button class="modal-close" id="rel-fs-close"> <button class="modal-close" id="rel-fs-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -500,7 +586,8 @@
<h2>🤗 Hugging Face Space 部署指南</h2> <h2>🤗 Hugging Face Space 部署指南</h2>
<button class="modal-close" id="hf-guide-close"> <button class="modal-close" id="hf-guide-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -508,25 +595,10 @@
</div> </div>
</div> </div>
<!-- Recall Log Modal -->
<div class="modal" id="recall-log-modal">
<div class="modal-bg" id="recall-log-backdrop"></div>
<div class="modal-box" style="max-width:900px">
<div class="modal-head">
<h2>✨ 涌现 · 记忆召回日志</h2>
<button class="modal-close" id="recall-log-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="modal-body" style="padding:0">
<pre id="recall-log-content"></pre>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script src="story-summary-ui.js"></script> <script src="story-summary-ui.js"></script>
</body> </body>
</html> </html>

View File

@@ -60,6 +60,7 @@ import {
updateMeta, updateMeta,
saveEventVectors as saveEventVectorsToDb, saveEventVectors as saveEventVectorsToDb,
clearEventVectors, clearEventVectors,
deleteEventVectorsByIds,
clearAllChunks, clearAllChunks,
saveChunks, saveChunks,
saveChunkVectors, saveChunkVectors,
@@ -506,6 +507,91 @@ async function handleGenerateVectors(vectorCfg) {
xbLog.info(MODULE_ID, `向量生成完成: L1=${l1Vectors.filter(Boolean).length}, L2=${l2VectorItems.length}`); xbLog.info(MODULE_ID, `向量生成完成: L1=${l1Vectors.filter(Boolean).length}, L2=${l2VectorItems.length}`);
} }
// ═══════════════════════════════════════════════════════════════════════════
// L2 自动增量向量化(总结完成后调用)
// ═══════════════════════════════════════════════════════════════════════════
async function autoVectorizeNewEvents(newEventIds) {
if (!newEventIds?.length) return;
const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) return;
const { chatId } = getContext();
if (!chatId) return;
// 本地模型未加载时跳过(不阻塞总结流程)
if (vectorCfg.engine === "local") {
const modelId = vectorCfg.local?.modelId || DEFAULT_LOCAL_MODEL;
if (!isLocalModelLoaded(modelId)) {
xbLog.warn(MODULE_ID, "L2 自动向量化跳过:本地模型未加载");
return;
}
}
const store = getSummaryStore();
const events = store?.json?.events || [];
const newEventIdSet = new Set(newEventIds);
// 只取本次新增的 events
const newEvents = events.filter((e) => newEventIdSet.has(e.id));
if (!newEvents.length) return;
const pairs = newEvents
.map((e) => ({ id: e.id, text: `${e.title || ""} ${e.summary || ""}`.trim() }))
.filter((p) => p.text);
if (!pairs.length) return;
try {
const fingerprint = getEngineFingerprint(vectorCfg);
const batchSize = vectorCfg.engine === "local" ? 5 : 25;
for (let i = 0; i < pairs.length; i += batchSize) {
const batch = pairs.slice(i, i + batchSize);
const texts = batch.map((p) => p.text);
const vectors = await embed(texts, vectorCfg);
const items = batch.map((p, idx) => ({
eventId: p.id,
vector: vectors[idx],
}));
await saveEventVectorsToDb(chatId, items, fingerprint);
}
xbLog.info(MODULE_ID, `L2 自动增量完成: ${pairs.length} 个事件`);
await sendVectorStatsToFrame();
} catch (e) {
xbLog.error(MODULE_ID, "L2 自动向量化失败", e);
// 不抛出,不阻塞总结流程
}
}
// ═══════════════════════════════════════════════════════════════════════════
// L2 跟随编辑同步(用户编辑 events 时调用)
// ═══════════════════════════════════════════════════════════════════════════
async function syncEventVectorsOnEdit(oldEvents, newEvents) {
const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) return;
const { chatId } = getContext();
if (!chatId) return;
const oldIds = new Set((oldEvents || []).map((e) => e.id).filter(Boolean));
const newIds = new Set((newEvents || []).map((e) => e.id).filter(Boolean));
// 找出被删除的 eventIds
const deletedIds = [...oldIds].filter((id) => !newIds.has(id));
if (deletedIds.length > 0) {
await deleteEventVectorsByIds(chatId, deletedIds);
xbLog.info(MODULE_ID, `L2 同步删除: ${deletedIds.length} 个事件向量`);
await sendVectorStatsToFrame();
}
}
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// 向量完整性检测(仅提醒,不自动操作) // 向量完整性检测(仅提醒,不自动操作)
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
@@ -565,6 +651,7 @@ async function handleClearVectors() {
await clearAllChunks(chatId); await clearAllChunks(chatId);
await updateMeta(chatId, { lastChunkFloor: -1 }); await updateMeta(chatId, { lastChunkFloor: -1 });
await sendVectorStatsToFrame(); await sendVectorStatsToFrame();
await executeSlashCommand('/echo severity=info 向量数据已清除。如需恢复召回功能,请重新点击"生成向量"。');
xbLog.info(MODULE_ID, "向量数据已清除"); xbLog.info(MODULE_ID, "向量数据已清除");
} }
@@ -769,6 +856,11 @@ function openPanelForMessage(mesId) {
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
async function getHideBoundaryFloor(store) { async function getHideBoundaryFloor(store) {
// 没有总结时,不隐藏
if (store?.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) {
return -1;
}
const vectorCfg = getVectorConfig(); const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) { if (!vectorCfg?.enabled) {
return store?.lastSummarizedMesId ?? -1; return store?.lastSummarizedMesId ?? -1;
@@ -845,7 +937,7 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
const result = await runSummaryGeneration(targetMesId, configForRun, { const result = await runSummaryGeneration(targetMesId, configForRun, {
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }), onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }), onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
onComplete: ({ merged, endMesId }) => { onComplete: async ({ merged, endMesId, newEventIds }) => {
postToFrame({ postToFrame({
type: "SUMMARY_FULL_DATA", type: "SUMMARY_FULL_DATA",
payload: { payload: {
@@ -860,6 +952,9 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
applyHideStateDebounced(); applyHideStateDebounced();
updateFrameStatsAfterSummary(endMesId, merged); updateFrameStatsAfterSummary(endMesId, merged);
// L2 自动增量向量化
await autoVectorizeNewEvents(newEventIds);
}, },
}); });
@@ -1060,11 +1155,20 @@ function handleFrameMessage(event) {
const store = getSummaryStore(); const store = getSummaryStore();
if (!store) break; if (!store) break;
store.json ||= {}; store.json ||= {};
// 如果是 events先记录旧数据用于同步向量
const oldEvents = data.section === "events" ? [...(store.json.events || [])] : null;
if (VALID_SECTIONS.includes(data.section)) { if (VALID_SECTIONS.includes(data.section)) {
store.json[data.section] = data.data; store.json[data.section] = data.data;
} }
store.updatedAt = Date.now(); store.updatedAt = Date.now();
saveSummaryStore(); saveSummaryStore();
// 同步 L2 向量(删除被移除的事件)
if (data.section === "events" && oldEvents) {
syncEventVectorsOnEdit(oldEvents, data.data);
}
break; break;
} }
@@ -1133,7 +1237,7 @@ async function handleManualGenerate(mesId, config) {
await runSummaryGeneration(mesId, config, { await runSummaryGeneration(mesId, config, {
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }), onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }), onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
onComplete: ({ merged, endMesId }) => { onComplete: async ({ merged, endMesId, newEventIds }) => {
postToFrame({ postToFrame({
type: "SUMMARY_FULL_DATA", type: "SUMMARY_FULL_DATA",
payload: { payload: {
@@ -1148,6 +1252,9 @@ async function handleManualGenerate(mesId, config) {
applyHideStateDebounced(); applyHideStateDebounced();
updateFrameStatsAfterSummary(endMesId, merged); updateFrameStatsAfterSummary(endMesId, merged);
// L2 自动增量向量化
await autoVectorizeNewEvents(newEventIds);
}, },
}); });
@@ -1206,6 +1313,9 @@ async function handleMessageReceived() {
initButtonsForAll(); initButtonsForAll();
// 向量全量生成中时跳过 L1 sync避免竞争写入
if (vectorGenerating) return;
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig); await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
await maybeAutoBuildChunks(); await maybeAutoBuildChunks();
@@ -1289,7 +1399,8 @@ async function handleGenerationStarted(type, _params, isDryRun) {
if (boundary < 0) return; if (boundary < 0) return;
// 2) depth倒序插入从末尾往前数 // 2) depth倒序插入从末尾往前数
const depth = chatLen - boundary - 1; // 最小为 1避免插入到最底部导致 AI 看到的最后是总结
const depth = Math.max(1, chatLen - boundary - 1);
if (depth < 0) return; if (depth < 0) return;
// 3) 构建注入文本(保持原逻辑) // 3) 构建注入文本(保持原逻辑)

View File

@@ -335,6 +335,13 @@ export async function syncOnMessageReceived(chatId, lastFloor, message, vectorCo
if (!chatId || lastFloor < 0 || !message) return; if (!chatId || lastFloor < 0 || !message) return;
if (!vectorConfig?.enabled) return; if (!vectorConfig?.enabled) return;
// 本地模型未加载时跳过(避免意外触发下载或报错)
if (vectorConfig.engine === "local") {
const { isLocalModelLoaded, DEFAULT_LOCAL_MODEL } = await import("./embedder.js");
const modelId = vectorConfig.local?.modelId || DEFAULT_LOCAL_MODEL;
if (!isLocalModelLoaded(modelId)) return;
}
// 删除该楼层旧的 // 删除该楼层旧的
await deleteChunksAtFloor(chatId, lastFloor); await deleteChunksAtFloor(chatId, lastFloor);