Refine story summary prompts and vector sync
This commit is contained in:
@@ -36,7 +36,7 @@ Incremental_Summary_Requirements:
|
||||
- 转折: 改变某条线走向
|
||||
- 点睛: 有细节不影响主线
|
||||
- 氛围: 纯粹氛围片段
|
||||
- Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy),0-2个。只填 evt-数字 形式,必须指向“已存在事件”或“本次新输出事件”。不要写解释文字。
|
||||
- Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。
|
||||
- Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融)
|
||||
- Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0)
|
||||
- World_State_Tracking: 维护当前世界的硬性约束。解决"什么不能违反"。采用 KV 覆盖模型,追踪生死、物品归属、秘密知情、关系状态、环境规则等不可违背的事实。(覆盖式更新)
|
||||
@@ -215,10 +215,7 @@ Before generating, observe the USER and analyze carefully:
|
||||
- events.id 从 evt-{nextEventId} 开始编号
|
||||
- 仅输出【增量】内容,已有事件绝不重复
|
||||
- keywords 是全局关键词,综合已有+新增
|
||||
- causedBy 规则:
|
||||
- 数组,最多2个;无前因则 []
|
||||
- 只能填 evt-数字(例如 evt-12)
|
||||
- 必须引用“已存在事件”或“本次新输出事件”(允许引用本次 JSON 内较早出现的事件)
|
||||
- causedBy 仅在因果明确时填写,允许为[],0-2个,详见上方 Causal_Chain 规则
|
||||
- worldUpdate 可为空数组
|
||||
- 合法JSON,字符串值内部避免英文双引号
|
||||
- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象
|
||||
|
||||
@@ -91,11 +91,11 @@ function cleanSummary(summary) {
|
||||
function buildSystemPreamble() {
|
||||
return [
|
||||
"以上内容为因上下文窗口限制保留的可见历史",
|
||||
"【剧情记忆】为对以上可见、不可见历史的总结",
|
||||
"1) 【世界状态】属于硬约束",
|
||||
"2) 【事件/证据/碎片/人物弧光】可用于补全上下文与动机。",
|
||||
"以下【剧情记忆】是对可见与不可见历史的总结:",
|
||||
"• 【世界约束】记录着已确立的事实",
|
||||
"• 其余部分是过往经历的回忆碎片",
|
||||
"",
|
||||
"请阅读并内化以下剧情记忆:",
|
||||
"请内化这些记忆:",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ function buildNonVectorPrompt(store) {
|
||||
|
||||
if (data.world?.length) {
|
||||
const lines = formatWorldLines(data.world);
|
||||
sections.push(`[世界约束] 规则手册,请严格遵守\n${lines.join("\n")}`);
|
||||
sections.push(`[世界约束] 已确立的事实\n${lines.join("\n")}`);
|
||||
}
|
||||
|
||||
if (data.events?.length) {
|
||||
@@ -602,7 +602,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
|
||||
// 1. 世界约束
|
||||
if (assembled.world.lines.length) {
|
||||
sections.push(`[世界约束] 规则手册,请严格遵守\n${assembled.world.lines.join("\n")}`);
|
||||
sections.push(`[世界约束] 已确立的事实\n${assembled.world.lines.join("\n")}`);
|
||||
}
|
||||
|
||||
// 2. 核心经历
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// story-summary-ui.js
|
||||
// iframe 内 UI 逻辑
|
||||
|
||||
(function() {
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const $$ = sel => document.querySelectorAll(sel);
|
||||
const h = v => String(v ?? '').replace(/[&<>"']/g, c =>
|
||||
const h = v => String(v ?? '').replace(/[&<>"']/g, c =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]
|
||||
);
|
||||
const setHtml = (el, html) => {
|
||||
@@ -43,7 +43,7 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const PARENT_ORIGIN = (() => {
|
||||
try { return new URL(document.referrer).origin; }
|
||||
try { return new URL(document.referrer).origin; }
|
||||
catch { return window.location.origin; }
|
||||
})();
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
saveConfig();
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
function applyConfig(cfg) {
|
||||
@@ -299,16 +299,18 @@
|
||||
const items = rules?.length ? rules : [];
|
||||
|
||||
setHtml(list, items.map((r, i) => `
|
||||
<div class="filter-rule-item" data-idx="${i}" style="display:flex;gap:6px;align-items:center">
|
||||
<input type="text" class="filter-rule-start" placeholder="起始(可空)" value="${h(r.start || '')}" style="flex:1;padding:6px 8px;font-size:.8125rem">
|
||||
<span style="color:var(--txt3)">→</span>
|
||||
<input type="text" class="filter-rule-end" placeholder="结束(可空)" value="${h(r.end || '')}" style="flex:1;padding:6px 8px;font-size:.8125rem">
|
||||
<button class="btn btn-sm btn-del filter-rule-del" style="padding:4px 8px">✕</button>
|
||||
<div class="filter-rule-item" data-idx="${i}">
|
||||
<div class="filter-rule-inputs">
|
||||
<input type="text" class="filter-rule-start" placeholder="起始(可空)" value="${h(r.start || '')}">
|
||||
<span class="rule-arrow">⬇</span>
|
||||
<input type="text" class="filter-rule-end" placeholder="结束(可空)" value="${h(r.end || '')}">
|
||||
</div>
|
||||
<button class="btn-del-rule">✕</button>
|
||||
</div>
|
||||
`).join(''));
|
||||
|
||||
// 绑定删除
|
||||
list.querySelectorAll('.filter-rule-del').forEach(btn => {
|
||||
list.querySelectorAll('.btn-del-rule').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
btn.closest('.filter-rule-item')?.remove();
|
||||
};
|
||||
@@ -338,14 +340,15 @@
|
||||
const div = document.createElement('div');
|
||||
div.className = 'filter-rule-item';
|
||||
div.dataset.idx = idx;
|
||||
div.style.cssText = 'display:flex;gap:6px;align-items:center';
|
||||
setHtml(div, `
|
||||
<input type="text" class="filter-rule-start" placeholder="起始(可空)" value="" style="flex:1;padding:6px 8px;font-size:.8125rem">
|
||||
<span style="color:var(--txt3)">→</span>
|
||||
<input type="text" class="filter-rule-end" placeholder="结束(可空)" value="" style="flex:1;padding:6px 8px;font-size:.8125rem">
|
||||
<button class="btn btn-sm btn-del filter-rule-del" style="padding:4px 8px">✕</button>
|
||||
<div class="filter-rule-inputs">
|
||||
<input type="text" class="filter-rule-start" placeholder="起始(可空)" value="">
|
||||
<span class="rule-arrow">⬇</span>
|
||||
<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);
|
||||
}
|
||||
|
||||
@@ -550,7 +553,24 @@
|
||||
updateProviderUI(config.api.provider);
|
||||
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');
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -1578,7 +1587,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
renderTimeline([]);
|
||||
renderRelations(null);
|
||||
renderArcs([]);
|
||||
renderWorldState([]);
|
||||
renderWorldState([]);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1666,6 +1675,27 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
$('settings-cancel').onclick = () => closeSettings(false);
|
||||
$('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').onchange = e => {
|
||||
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-close').onclick = closeHfGuide;
|
||||
|
||||
// Recall log
|
||||
$('btn-recall').onclick = openRecallLog;
|
||||
$('recall-log-backdrop').onclick = closeRecallLog;
|
||||
$('recall-log-close').onclick = closeRecallLog;
|
||||
|
||||
// Character selector
|
||||
$('char-sel-trigger').onclick = e => {
|
||||
e.stopPropagation();
|
||||
@@ -1748,6 +1773,36 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
// Vector UI
|
||||
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
|
||||
window.onresize = () => {
|
||||
relationChart?.resize();
|
||||
@@ -1833,7 +1888,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}).join('');
|
||||
|
||||
setHtml(container, html || '<div class="empty">暂无世界状态</div>');
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ main {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.left, .right {
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
@@ -67,9 +68,11 @@ main {
|
||||
}
|
||||
|
||||
/* 关键词卡片:固定高度 */
|
||||
.left > .card:first-child {
|
||||
flex: 0 0 auto; /* 关键词:不伸缩 */
|
||||
.left>.card:first-child {
|
||||
flex: 0 0 auto;
|
||||
/* 关键词:不伸缩 */
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
Typography
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
@@ -153,9 +156,9 @@ h1 span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--bg3);
|
||||
border: 1px solid var(--bdr);
|
||||
padding: 4px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: .8125rem;
|
||||
color: var(--txt2);
|
||||
cursor: pointer;
|
||||
@@ -163,7 +166,7 @@ h1 span {
|
||||
}
|
||||
|
||||
.chk-label:hover {
|
||||
border-color: var(--acc);
|
||||
color: var(--txt);
|
||||
}
|
||||
|
||||
.chk-label input {
|
||||
@@ -290,33 +293,25 @@ h1 span {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.btn-recall {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border-color: #667eea;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.btn-debug {
|
||||
background: var(--bg2);
|
||||
color: var(--txt2);
|
||||
border: 1px solid var(--bdr);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-recall:hover {
|
||||
background: linear-gradient(135deg, #5a67d8 0%, #6b46a1 100%);
|
||||
border-color: #5a67d8;
|
||||
.btn-debug svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.btn-recall::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
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); }
|
||||
.btn-debug:hover {
|
||||
background: var(--bg3);
|
||||
border-color: var(--acc);
|
||||
color: var(--txt);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -700,13 +695,40 @@ h1 span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.trend-broken { background: rgba(68, 68, 68, .15); color: #444; }
|
||||
.trend-hate { 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; }
|
||||
.trend-broken {
|
||||
background: rgba(68, 68, 68, .15);
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.trend-hate {
|
||||
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
|
||||
@@ -787,8 +809,15 @@ h1 span {
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -1041,8 +1070,10 @@ h1 span {
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
|
||||
.settings-field input,
|
||||
.settings-field input:not([type="checkbox"]):not([type="radio"]),
|
||||
.settings-field select {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg3);
|
||||
border: 1px solid var(--bdr);
|
||||
@@ -1050,6 +1081,22 @@ h1 span {
|
||||
color: var(--txt);
|
||||
outline: none;
|
||||
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,
|
||||
@@ -1113,6 +1160,10 @@ h1 span {
|
||||
|
||||
.engine-option input {
|
||||
accent-color: var(--hl);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.engine-area {
|
||||
@@ -1132,19 +1183,35 @@ h1 span {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.engine-card-desc {
|
||||
font-size: .75rem;
|
||||
color: var(--txt3);
|
||||
margin-bottom: 12px;
|
||||
.engine-status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.engine-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
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 {
|
||||
@@ -1154,15 +1221,37 @@ h1 span {
|
||||
background: var(--txt3);
|
||||
}
|
||||
|
||||
.status-dot.ready { 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; }
|
||||
.status-dot.ready {
|
||||
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 {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .5; }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.engine-progress {
|
||||
@@ -1195,8 +1284,8 @@ h1 span {
|
||||
.engine-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.model-select-row {
|
||||
@@ -1218,8 +1307,8 @@ h1 span {
|
||||
.model-desc {
|
||||
font-size: .75rem;
|
||||
color: var(--txt3);
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
text-align: left;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vector-stats {
|
||||
@@ -1343,8 +1432,8 @@ h1 span {
|
||||
}
|
||||
|
||||
.hf-intro {
|
||||
background: linear-gradient(135deg, rgba(102,126,234,.08), rgba(118,75,162,.08));
|
||||
border: 1px solid rgba(102,126,234,.2);
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, .08), rgba(118, 75, 162, .08));
|
||||
border: 1px solid rgba(102, 126, 234, .2);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
@@ -1495,8 +1584,8 @@ h1 span {
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(255,255,255,.1);
|
||||
border: 1px solid rgba(255,255,255,.2);
|
||||
background: rgba(255, 255, 255, .1);
|
||||
border: 1px solid rgba(255, 255, 255, .2);
|
||||
color: #999;
|
||||
font-size: .6875rem;
|
||||
cursor: pointer;
|
||||
@@ -1505,14 +1594,14 @@ h1 span {
|
||||
}
|
||||
|
||||
.hf-code .copy-btn:hover {
|
||||
background: rgba(255,255,255,.2);
|
||||
background: rgba(255, 255, 255, .2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hf-status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
background: rgba(34,197,94,.15);
|
||||
background: rgba(34, 197, 94, .15);
|
||||
color: #22c55e;
|
||||
border-radius: 10px;
|
||||
font-size: .75rem;
|
||||
@@ -1684,8 +1773,7 @@ h1 span {
|
||||
|
||||
.btn-group {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@@ -1714,7 +1802,8 @@ h1 span {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.left, .right {
|
||||
.left,
|
||||
.right {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@@ -1882,7 +1971,9 @@ h1 span {
|
||||
font-size: .6875rem;
|
||||
}
|
||||
|
||||
main, .left, .right {
|
||||
main,
|
||||
.left,
|
||||
.right {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -2177,3 +2268,412 @@ h1 span {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- story-summary.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<link rel="stylesheet" href="story-summary.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
@@ -37,18 +39,19 @@
|
||||
<div class="controls">
|
||||
<label class="chk-label">
|
||||
<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>
|
||||
<span class="spacer"></span>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-icon" id="btn-settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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"/>
|
||||
<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" />
|
||||
</svg>
|
||||
<span>设置</span>
|
||||
</button>
|
||||
<button class="btn btn-recall" id="btn-recall">涌现</button>
|
||||
<button class="btn" id="btn-clear">清空</button>
|
||||
<button class="btn btn-p" id="btn-generate">总结</button>
|
||||
</div>
|
||||
@@ -92,8 +95,10 @@
|
||||
<div class="sec-title">人物关系</div>
|
||||
<div class="sec-actions">
|
||||
<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">
|
||||
<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 viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
|
||||
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>
|
||||
</button>
|
||||
<button class="sec-btn" data-section="characters">编辑</button>
|
||||
@@ -132,7 +137,8 @@
|
||||
<h2 id="editor-title">编辑</h2>
|
||||
<button class="modal-close" id="editor-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"/>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -152,319 +158,398 @@
|
||||
<!-- Settings Modal -->
|
||||
<div class="modal" id="settings-modal">
|
||||
<div class="modal-bg" id="settings-backdrop"></div>
|
||||
<div class="modal-box">
|
||||
<div class="modal-box settings-modal-box">
|
||||
<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">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- API Config -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">API 配置</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field">
|
||||
<label>渠道</label>
|
||||
<select id="api-provider">
|
||||
<option value="st">酒馆主 API(沿用当前)</option>
|
||||
<option value="openai">OpenAI 兼容</option>
|
||||
<option value="google">Google (Gemini)</option>
|
||||
<option value="claude">Claude (Anthropic)</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row hidden" id="api-url-row">
|
||||
<div class="settings-field full">
|
||||
<label>API URL</label>
|
||||
<input type="text" id="api-url" placeholder="https://api.openai.com 或代理地址">
|
||||
<div class="settings-hint">不同渠道默认端点:OpenAI 用 /v1,Gemini 用 /v1beta,Claude 用 /v1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row hidden" id="api-key-row">
|
||||
<div class="settings-field full">
|
||||
<label>API KEY</label>
|
||||
<input type="password" id="api-key" placeholder="仅保存在本地,不会上传">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row hidden" id="api-model-manual-row">
|
||||
<div class="settings-field full">
|
||||
<label>模型</label>
|
||||
<input type="text" id="api-model-text" placeholder="如 gemini-1.5-pro、claude-3-haiku">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row hidden" id="api-model-select-row">
|
||||
<div class="settings-field full">
|
||||
<label>可用模型</label>
|
||||
<select id="api-model-select">
|
||||
<option value="">请先拉取模型列表</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-btn-row hidden" id="api-connect-row">
|
||||
<button class="btn btn-sm btn-p" id="btn-connect">连接 / 拉取模型列表</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gen Params -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">生成参数</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field">
|
||||
<label>Temperature</label>
|
||||
<input type="number" id="gen-temp" step="0.01" min="0" max="2" placeholder="未设置">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>Top P</label>
|
||||
<input type="number" id="gen-top-p" step="0.01" min="0" max="1" placeholder="未设置">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>Top K</label>
|
||||
<input type="number" id="gen-top-k" step="1" min="1" placeholder="未设置">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field">
|
||||
<label>存在惩罚</label>
|
||||
<input type="number" id="gen-presence" step="0.01" min="-2" max="2" placeholder="未设置">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>频率惩罚</label>
|
||||
<input type="number" id="gen-frequency" step="0.01" min="-2" max="2" placeholder="未设置">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trigger Settings -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">总结设置</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field">
|
||||
<label>注入角色</label>
|
||||
<select id="trigger-role">
|
||||
<option value="system">System</option>
|
||||
<option value="user">User</option>
|
||||
<option value="assistant">Assistant</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field">
|
||||
<label>自动总结间隔(楼)</label>
|
||||
<input type="number" id="trigger-interval" min="1" max="30" step="1" value="20">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>触发时机</label>
|
||||
<select id="trigger-timing">
|
||||
<option value="after_ai">AI 回复后</option>
|
||||
<option value="before_user" selected>用户发送前</option>
|
||||
<option value="manual">仅手动</option>
|
||||
</select>
|
||||
</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 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>
|
||||
|
||||
<!-- Vector Settings -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">智能记忆(向量检索)</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field-inline">
|
||||
<input type="checkbox" id="vector-enabled">
|
||||
<label for="vector-enabled">启用向量检索</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="vector-config-area" class="hidden">
|
||||
<div class="settings-row" style="margin-top:16px">
|
||||
<div class="settings-field full">
|
||||
<label>Embedding 引擎</label>
|
||||
<div class="engine-selector">
|
||||
<label class="engine-option">
|
||||
<input type="radio" name="vector-engine" value="local">
|
||||
<span>本地模型</span>
|
||||
</label>
|
||||
<label class="engine-option">
|
||||
<input type="radio" name="vector-engine" value="online" checked>
|
||||
<span>在线服务</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Local Engine -->
|
||||
<div id="local-engine-area" class="engine-area hidden">
|
||||
<div class="model-select-row">
|
||||
<select id="local-model-select">
|
||||
<option value="bge-small-zh">中文轻量 (51MB)</option>
|
||||
<option value="bge-base-zh">中文标准 (102MB)</option>
|
||||
<option value="e5-small">多语言 (118MB)</option>
|
||||
<!-- 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-title">API 配置</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field">
|
||||
<label>渠道</label>
|
||||
<select id="api-provider">
|
||||
<option value="st">酒馆主 API(沿用当前)</option>
|
||||
<option value="openai">OpenAI 兼容</option>
|
||||
<option value="google">Google (Gemini)</option>
|
||||
<option value="claude">Claude (Anthropic)</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="model-desc" id="local-model-desc">手机/低配适用</div>
|
||||
<div class="engine-status" id="local-model-status">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">检查中...</span>
|
||||
</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">
|
||||
<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 btn-del" id="btn-delete-model" style="display:none">删除缓存</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Online Engine -->
|
||||
<div id="online-engine-area" class="engine-area">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>服务渠道</label>
|
||||
<select id="online-provider">
|
||||
<option value="siliconflow">硅基流动(推荐)</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
<option value="openai">OpenAI 兼容(可自建)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row" id="online-url-row">
|
||||
<div class="settings-field full">
|
||||
<label>API URL</label>
|
||||
<input type="text" id="vector-api-url" placeholder="https://api.siliconflow.cn">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="vector-api-key" placeholder="sk-xxx">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>模型</label>
|
||||
<div style="display:flex;gap:8px">
|
||||
<select id="vector-model-select" style="flex:1">
|
||||
<option value="">请选择模型</option>
|
||||
</select>
|
||||
<button class="btn btn-sm" id="btn-fetch-models" style="display:none">拉取</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="engine-status" id="online-api-status">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">未测试</span>
|
||||
</div>
|
||||
<div class="settings-btn-row" style="justify-content:center">
|
||||
<button class="btn btn-sm" id="btn-test-vector-api">测试连接</button>
|
||||
</div>
|
||||
<div class="provider-hint" id="provider-hint">
|
||||
💡 <a href="https://siliconflow.cn" target="_blank">硅基流动</a> 免费、速度快、质量好,推荐 BAAI/bge-m3
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文本过滤规则 -->
|
||||
<div class="settings-row" style="margin-top:16px">
|
||||
<div class="settings-row hidden" id="api-url-row">
|
||||
<div class="settings-field full">
|
||||
<label>文本过滤规则</label>
|
||||
<p class="settings-hint" style="margin-bottom:8px">
|
||||
遇到「起始」后跳过,直到「结束」。起始或结束可单独留空。用于过滤思考标签等干扰内容。
|
||||
</p>
|
||||
<div id="filter-rules-list" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||
<button class="btn btn-sm" id="btn-add-filter-rule" style="margin-top:8px">+ 添加规则</button>
|
||||
<label>API URL</label>
|
||||
<input type="text" id="api-url" placeholder="https://api.openai.com 或代理地址">
|
||||
<div class="settings-hint">默认端点:OpenAI:/v1,Gemini:/v1beta,Claude:/v1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row hidden" id="api-key-row">
|
||||
<div class="settings-field full">
|
||||
<label>API KEY</label>
|
||||
<input type="password" id="api-key" placeholder="仅保存在本地,不会上传">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row hidden" id="api-model-manual-row">
|
||||
<div class="settings-field full">
|
||||
<label>模型</label>
|
||||
<input type="text" id="api-model-text" placeholder="如 gemini-1.5-pro、claude-3-haiku">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row hidden" id="api-model-select-row">
|
||||
<div class="settings-field full">
|
||||
<label>可用模型</label>
|
||||
<select id="api-model-select">
|
||||
<option value="">请先拉取模型列表</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-btn-row hidden" id="api-connect-row"
|
||||
style="display: flex; gap: 12px; align-items: center; justify-content: space-between;">
|
||||
<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>
|
||||
|
||||
<!-- Vector Stats -->
|
||||
<div class="vector-chat-section">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>当前聊天向量</label>
|
||||
<div class="vector-stats" id="vector-stats">
|
||||
<span>事件向量: <strong id="vector-event-count">0</strong>/<strong id="vector-event-total">0</strong></span>
|
||||
<span>·</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>·</span>
|
||||
<span>消息: <strong id="vector-message-count">0</strong></span>
|
||||
</div>
|
||||
<div class="vector-mismatch-warning hidden" id="vector-mismatch-warning">
|
||||
⚠ 引擎/模型已变更,需重新生成向量
|
||||
</div>
|
||||
</div>
|
||||
<!-- Collapsible Gen Params -->
|
||||
<div class="settings-collapse">
|
||||
<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="engine-progress hidden" id="vector-gen-progress-l1">
|
||||
<div style="font-size:.75rem;color:var(--txt3);margin-bottom:4px">L1 片段</div>
|
||||
<div class="progress-bar"><div class="progress-inner"></div></div>
|
||||
<span class="progress-text">0/0</span>
|
||||
</div>
|
||||
<div class="engine-progress hidden" id="vector-gen-progress-l2">
|
||||
<div style="font-size:.75rem;color:var(--txt3);margin-bottom:4px">L2 事件</div>
|
||||
<div class="progress-bar"><div class="progress-inner"></div></div>
|
||||
<span class="progress-text">0/0</span>
|
||||
</div>
|
||||
<div class="settings-hint" id="vector-perf-l1"></div>
|
||||
<div class="settings-hint" id="vector-perf-l2"></div>
|
||||
<div class="settings-btn-row" id="vector-action-row">
|
||||
<button class="btn btn-sm btn-p" id="btn-gen-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>
|
||||
</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="settings-collapse-content hidden" id="gen-params-content">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>向量迁移(跨设备 / 防清缓存)</label>
|
||||
<div class="settings-hint" style="margin-bottom:8px">导出/导入均为 zip 格式,勿解压</div>
|
||||
<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-import-vectors">导入向量</button>
|
||||
<div class="settings-field">
|
||||
<label>Temperature</label>
|
||||
<input type="number" id="gen-temp" step="0.01" min="0" max="2"
|
||||
placeholder="未设置">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>Top P</label>
|
||||
<input type="number" id="gen-top-p" step="0.01" min="0" max="1"
|
||||
placeholder="未设置">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>Top K</label>
|
||||
<input type="number" id="gen-top-k" step="1" min="1" placeholder="未设置">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-hint" id="vector-io-status"></div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field">
|
||||
<label>存在惩罚</label>
|
||||
<input type="number" id="gen-presence" step="0.01" min="-2" max="2"
|
||||
placeholder="未设置">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>频率惩罚</label>
|
||||
<input type="number" id="gen-frequency" step="0.01" min="-2" max="2"
|
||||
placeholder="未设置">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trigger Settings -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">总结设置</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field">
|
||||
<label>注入角色</label>
|
||||
<select id="trigger-role">
|
||||
<option value="system">System</option>
|
||||
<option value="user">User</option>
|
||||
<option value="assistant">Assistant</option>
|
||||
</select>
|
||||
</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>
|
||||
|
||||
<!-- 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-field">
|
||||
<label>自动总结间隔(楼)</label>
|
||||
<input type="number" id="trigger-interval" min="1" max="30" step="1" value="20">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>触发时机</label>
|
||||
<select id="trigger-timing">
|
||||
<option value="after_ai">AI 回复后</option>
|
||||
<option value="before_user" selected>用户发送前</option>
|
||||
<option value="manual">仅手动</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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-title">智能记忆(向量检索)</div>
|
||||
<div class="settings-checkbox-group">
|
||||
<label class="settings-checkbox">
|
||||
<input type="checkbox" id="vector-enabled">
|
||||
<span class="checkbox-mark"></span>
|
||||
<span class="checkbox-label">启用向量检索</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="vector-config-area" class="hidden">
|
||||
<div class="settings-row" style="margin-top:16px">
|
||||
<div class="settings-field full">
|
||||
<label>Embedding 引擎</label>
|
||||
<div class="engine-selector">
|
||||
<label class="engine-option">
|
||||
<input type="radio" name="vector-engine" value="local">
|
||||
<span>本地模型</span>
|
||||
</label>
|
||||
<label class="engine-option">
|
||||
<input type="radio" name="vector-engine" value="online" checked>
|
||||
<span>在线服务</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Local Engine -->
|
||||
<div id="local-engine-area" class="engine-area hidden">
|
||||
<div class="model-select-row">
|
||||
<select id="local-model-select">
|
||||
<option value="bge-small-zh">中文轻量 (51MB)</option>
|
||||
<option value="bge-base-zh">中文标准 (102MB)</option>
|
||||
<option value="e5-small">多语言 (118MB)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="model-desc" id="local-model-desc">手机/低配适用</div>
|
||||
<div class="engine-status-row">
|
||||
<div class="engine-status" id="local-model-status">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">检查中...</span>
|
||||
</div>
|
||||
<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" id="btn-cancel-download"
|
||||
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>
|
||||
|
||||
<!-- Online Engine -->
|
||||
<div id="online-engine-area" class="engine-area">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>服务渠道</label>
|
||||
<select id="online-provider">
|
||||
<option value="siliconflow">硅基流动(推荐)</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
<option value="openai">OpenAI 兼容(可自建)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row" id="online-url-row">
|
||||
<div class="settings-field full">
|
||||
<label>API URL</label>
|
||||
<input type="text" id="vector-api-url" placeholder="https://api.siliconflow.cn">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="vector-api-key" placeholder="sk-xxx">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>模型</label>
|
||||
<div style="display:flex;gap:8px">
|
||||
<select id="vector-model-select" style="flex:1">
|
||||
<option value="">请选择模型</option>
|
||||
</select>
|
||||
<button class="btn btn-sm" id="btn-fetch-models"
|
||||
style="display:none">拉取</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="engine-status-row">
|
||||
<div class="engine-status" id="online-api-status">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">未测试</span>
|
||||
</div>
|
||||
<button class="btn btn-sm" id="btn-test-vector-api">测试连接</button>
|
||||
</div>
|
||||
<div class="provider-hint" id="provider-hint">
|
||||
💡 <a href="https://siliconflow.cn" target="_blank">硅基流动</a> 免费、速度快、质量好,推荐
|
||||
BAAI/bge-m3
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文本过滤规则 - Redesigned for mobile -->
|
||||
<div class="filter-rules-section">
|
||||
<div class="filter-rules-header">
|
||||
<label>文本过滤规则</label>
|
||||
<button class="btn btn-sm btn-add" id="btn-add-filter-rule">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<p class="settings-hint">过滤干扰内容(如思考标签):遇到「起始」跳过直到「结束」</p>
|
||||
<div id="filter-rules-list" class="filter-rules-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Vector Stats -->
|
||||
<div class="vector-chat-section">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>当前聊天向量</label>
|
||||
<div class="vector-stats" id="vector-stats">
|
||||
<div class="vector-stat-col">
|
||||
<span class="vector-stat-label">事件向量:</span>
|
||||
<span class="vector-stat-value"><strong
|
||||
id="vector-event-count">0</strong>/<strong
|
||||
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 class="vector-mismatch-warning hidden" id="vector-mismatch-warning">
|
||||
⚠ 引擎/模型已变更,需重新生成向量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="engine-progress hidden" id="vector-gen-progress-l1">
|
||||
<div style="font-size:.75rem;color:var(--txt3);margin-bottom:4px">L1 片段</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-inner"></div>
|
||||
</div>
|
||||
<span class="progress-text">0/0</span>
|
||||
</div>
|
||||
<div class="engine-progress hidden" id="vector-gen-progress-l2">
|
||||
<div style="font-size:.75rem;color:var(--txt3);margin-bottom:4px">L2 事件</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-inner"></div>
|
||||
</div>
|
||||
<span class="progress-text">0/0</span>
|
||||
</div>
|
||||
<div class="settings-hint" id="vector-perf-l1"></div>
|
||||
<div class="settings-hint" id="vector-perf-l2"></div>
|
||||
<div class="settings-btn-row" id="vector-action-row">
|
||||
<button class="btn btn-sm btn-p" id="btn-gen-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>
|
||||
</div>
|
||||
<div class="settings-hint" style="margin-top:8px">首次生成向量可能耗时较久,页面短暂卡顿属正常。若本地模型重进酒馆后需重下。
|
||||
</div>
|
||||
|
||||
<!-- 向量导入导出 -->
|
||||
<div class="vector-io-section">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field full">
|
||||
<label>向量迁移(跨设备 / 防清缓存)</label>
|
||||
<div class="settings-hint" style="margin-bottom:8px">导出/导入均为 zip 格式,勿解压
|
||||
</div>
|
||||
<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-import-vectors">导入向量</button>
|
||||
</div>
|
||||
<div class="settings-hint" id="vector-io-status"></div>
|
||||
</div>
|
||||
</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">
|
||||
@@ -482,7 +567,8 @@
|
||||
<h2>人物关系图</h2>
|
||||
<button class="modal-close" id="rel-fs-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"/>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -500,7 +586,8 @@
|
||||
<h2>🤗 Hugging Face Space 部署指南</h2>
|
||||
<button class="modal-close" id="hf-guide-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"/>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -508,25 +595,10 @@
|
||||
</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="story-summary-ui.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
updateMeta,
|
||||
saveEventVectors as saveEventVectorsToDb,
|
||||
clearEventVectors,
|
||||
deleteEventVectorsByIds,
|
||||
clearAllChunks,
|
||||
saveChunks,
|
||||
saveChunkVectors,
|
||||
@@ -506,6 +507,91 @@ async function handleGenerateVectors(vectorCfg) {
|
||||
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 updateMeta(chatId, { lastChunkFloor: -1 });
|
||||
await sendVectorStatsToFrame();
|
||||
await executeSlashCommand('/echo severity=info 向量数据已清除。如需恢复召回功能,请重新点击"生成向量"。');
|
||||
xbLog.info(MODULE_ID, "向量数据已清除");
|
||||
}
|
||||
|
||||
@@ -769,6 +856,11 @@ function openPanelForMessage(mesId) {
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async function getHideBoundaryFloor(store) {
|
||||
// 没有总结时,不隐藏
|
||||
if (store?.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const vectorCfg = getVectorConfig();
|
||||
if (!vectorCfg?.enabled) {
|
||||
return store?.lastSummarizedMesId ?? -1;
|
||||
@@ -845,7 +937,7 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
|
||||
const result = await runSummaryGeneration(targetMesId, configForRun, {
|
||||
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
|
||||
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
|
||||
onComplete: ({ merged, endMesId }) => {
|
||||
onComplete: async ({ merged, endMesId, newEventIds }) => {
|
||||
postToFrame({
|
||||
type: "SUMMARY_FULL_DATA",
|
||||
payload: {
|
||||
@@ -860,6 +952,9 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
|
||||
|
||||
applyHideStateDebounced();
|
||||
updateFrameStatsAfterSummary(endMesId, merged);
|
||||
|
||||
// L2 自动增量向量化
|
||||
await autoVectorizeNewEvents(newEventIds);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1060,11 +1155,20 @@ function handleFrameMessage(event) {
|
||||
const store = getSummaryStore();
|
||||
if (!store) break;
|
||||
store.json ||= {};
|
||||
|
||||
// 如果是 events,先记录旧数据用于同步向量
|
||||
const oldEvents = data.section === "events" ? [...(store.json.events || [])] : null;
|
||||
|
||||
if (VALID_SECTIONS.includes(data.section)) {
|
||||
store.json[data.section] = data.data;
|
||||
}
|
||||
store.updatedAt = Date.now();
|
||||
saveSummaryStore();
|
||||
|
||||
// 同步 L2 向量(删除被移除的事件)
|
||||
if (data.section === "events" && oldEvents) {
|
||||
syncEventVectorsOnEdit(oldEvents, data.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1133,7 +1237,7 @@ async function handleManualGenerate(mesId, config) {
|
||||
await runSummaryGeneration(mesId, config, {
|
||||
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
|
||||
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
|
||||
onComplete: ({ merged, endMesId }) => {
|
||||
onComplete: async ({ merged, endMesId, newEventIds }) => {
|
||||
postToFrame({
|
||||
type: "SUMMARY_FULL_DATA",
|
||||
payload: {
|
||||
@@ -1148,6 +1252,9 @@ async function handleManualGenerate(mesId, config) {
|
||||
|
||||
applyHideStateDebounced();
|
||||
updateFrameStatsAfterSummary(endMesId, merged);
|
||||
|
||||
// L2 自动增量向量化
|
||||
await autoVectorizeNewEvents(newEventIds);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1206,6 +1313,9 @@ async function handleMessageReceived() {
|
||||
|
||||
initButtonsForAll();
|
||||
|
||||
// 向量全量生成中时跳过 L1 sync(避免竞争写入)
|
||||
if (vectorGenerating) return;
|
||||
|
||||
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
|
||||
await maybeAutoBuildChunks();
|
||||
|
||||
@@ -1289,7 +1399,8 @@ async function handleGenerationStarted(type, _params, isDryRun) {
|
||||
if (boundary < 0) return;
|
||||
|
||||
// 2) depth:倒序插入,从末尾往前数
|
||||
const depth = chatLen - boundary - 1;
|
||||
// 最小为 1,避免插入到最底部导致 AI 看到的最后是总结
|
||||
const depth = Math.max(1, chatLen - boundary - 1);
|
||||
if (depth < 0) return;
|
||||
|
||||
// 3) 构建注入文本(保持原逻辑)
|
||||
|
||||
@@ -335,6 +335,13 @@ export async function syncOnMessageReceived(chatId, lastFloor, message, vectorCo
|
||||
if (!chatId || lastFloor < 0 || !message) 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user