From f998553ab0c12651bce26222f25f5c7f19b93769 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 5 Apr 2026 17:16:05 +0800 Subject: [PATCH] Revert "Merge PR #7: notice display and MVU diagnostics" This reverts commit cdf61114820ee533f5636459a428ef1405686d4e, reversing changes made to 0d8dcb63d280649c411e7816e0f653b0b516f876. --- extractor.js | 95 +++------ index.js | 31 --- mvu-compat.js | 11 +- notice.js | 40 +--- panel.html | 24 --- panel.js | 43 ---- plans/notice-display-mode-plan.md | 267 ------------------------ prompt-builder.js | 138 +----------- retriever.js | 55 +---- tests/p0-regressions.mjs | 58 ------ tests/prompt-builder-mvu.mjs | 335 ------------------------------ tests/retrieval-config.mjs | 48 ----- 12 files changed, 40 insertions(+), 1105 deletions(-) delete mode 100644 plans/notice-display-mode-plan.md diff --git a/extractor.js b/extractor.js index 3c84195..bdd1865 100644 --- a/extractor.js +++ b/extractor.js @@ -107,7 +107,7 @@ function isPlainObject(value) { function extractOperationsPayload(result) { if (Array.isArray(result)) { - return { source: "root", operations: result }; + return result; } if (!isPlainObject(result)) { return null; @@ -115,16 +115,29 @@ function extractOperationsPayload(result) { for (const key of EXTRACTION_RESULT_CONTAINER_KEYS) { if (Array.isArray(result[key])) { - return { - source: key, - operations: result[key], - }; + return result[key]; } } return null; } +function resolveExtractionAction(rawOp) { + const explicitAction = rawOp?.action ?? rawOp?.op ?? rawOp?.operation; + if (typeof explicitAction === "string" && explicitAction.trim()) { + return explicitAction.trim().toLowerCase(); + } + + if (rawOp?.type) { + if (rawOp?.nodeId || rawOp?.node_id) { + return "update"; + } + return "create"; + } + + return ""; +} + function resolveExtractionTypeDef(schema, type) { if (!Array.isArray(schema) || !type) { return null; @@ -142,32 +155,6 @@ function resolveExtractionFieldNames(typeDef) { ); } -function hasExplicitExtractionAction(rawOp) { - const explicitAction = rawOp?.action ?? rawOp?.op ?? rawOp?.operation; - return typeof explicitAction === "string" && explicitAction.trim().length > 0; -} - -function resolveExtractionAction(rawOp) { - const explicitAction = rawOp?.action ?? rawOp?.op ?? rawOp?.operation; - if (typeof explicitAction === "string" && explicitAction.trim()) { - return explicitAction.trim().toLowerCase(); - } - - if (rawOp?.type) { - if ( - rawOp?.nodeId || - rawOp?.node_id || - rawOp?.targetNodeId || - rawOp?.target_node_id - ) { - return "update"; - } - return "create"; - } - - return ""; -} - function resolveExtractionNodeId(rawOp) { const nodeId = rawOp?.nodeId ?? @@ -216,7 +203,6 @@ function normalizeExtractionOperation(rawOp, schema) { return rawOp; } - const explicitAction = hasExplicitExtractionAction(rawOp); const action = resolveExtractionAction(rawOp); const type = rawOp?.type == null ? "" : String(rawOp.type).trim(); const typeDef = resolveExtractionTypeDef(schema, type); @@ -238,10 +224,6 @@ function normalizeExtractionOperation(rawOp, schema) { normalized.nodeId = nodeId; } - if (!explicitAction && action && type) { - normalized.__legacyCompat = true; - } - if (Array.isArray(rawOp?.relations) && !Array.isArray(rawOp?.links)) { normalized.links = rawOp.relations; } else if (Array.isArray(rawOp?.edges) && !Array.isArray(rawOp?.links)) { @@ -279,42 +261,23 @@ function normalizeExtractionOperation(rawOp, schema) { } function normalizeExtractionResultPayload(result, schema) { - const extracted = extractOperationsPayload(result); - if (!extracted || !Array.isArray(extracted.operations)) { - return null; + const operations = extractOperationsPayload(result); + if (!Array.isArray(operations)) { + return result; } - let legacyCount = 0; - const normalizedOperations = extracted.operations.map((op) => { - const normalized = normalizeExtractionOperation(op, schema); - if (normalized?.__legacyCompat) { - legacyCount += 1; - delete normalized.__legacyCompat; - } - return normalized; - }); + const normalizedOperations = operations.map((op) => + normalizeExtractionOperation(op, schema), + ); - const normalizedPayload = - Array.isArray(result) || !isPlainObject(result) - ? { operations: normalizedOperations } - : { - ...result, - operations: normalizedOperations, - }; - - if (legacyCount > 0) { - console.info("[ST-BME] 兼容旧版扁平提取输出", { - source: extracted.source, - normalizedCount: legacyCount, - totalCount: normalizedOperations.length, - }); + if (Array.isArray(result) || !isPlainObject(result)) { + return { operations: normalizedOperations }; } - normalizedPayload.compat = { - legacyCount, - source: extracted.source, + return { + ...result, + operations: normalizedOperations, }; - return normalizedPayload; } /** diff --git a/index.js b/index.js index 9c01258..217d3fd 100644 --- a/index.js +++ b/index.js @@ -496,7 +496,6 @@ const defaultSettings = { compressionEveryN: 10, // UI 面板 - noticeDisplayMode: "normal", // normal|compact panelTheme: "crimson", // 面板主题 crimson|cyan|amber|violet }; @@ -1055,30 +1054,6 @@ function syncStageNoticeAbortAction(stage) { }); } -function getStageNoticeDisplayMode(level = "info") { - const configuredMode = getSettings()?.noticeDisplayMode; - if ( - configuredMode === "compact" && - level !== "warning" && - level !== "error" - ) { - return "compact"; - } - return "normal"; -} - -function refreshVisibleStageNotices() { - for (const stage of Object.keys(stageNoticeHandles)) { - const handle = stageNoticeHandles[stage]; - if (!handle || handle.isClosed?.()) continue; - const status = getStageUiStatus(stage); - if (!status) continue; - updateStageNotice(stage, status.text, status.meta, status.level, { - title: getStageNoticeTitle(stage), - }); - } -} - function updateStageNotice( stage, text, @@ -1094,7 +1069,6 @@ function updateStageNotice( const input = { title, message, - displayMode: options.displayMode || getStageNoticeDisplayMode(noticeLevel), level: noticeLevel, busy, persist, @@ -5509,7 +5483,6 @@ function updateModuleSettings(patch = {}) { "hideOldMessagesKeepLastN", ]); const recallUiKeys = new Set(["recallCardUserInputDisplayMode"]); - const noticeUiKeys = new Set(["noticeDisplayMode"]); const settings = getSettings(); Object.assign(settings, patch); extension_settings[MODULE_NAME] = settings; @@ -5570,10 +5543,6 @@ function updateModuleSettings(patch = {}) { schedulePersistedRecallMessageUiRefresh(30); } - if (Object.keys(patch).some((key) => noticeUiKeys.has(key))) { - refreshVisibleStageNotices(); - } - scheduleServerSettingsSave(); return settings; } diff --git a/mvu-compat.js b/mvu-compat.js index c09e9ad..f070376 100644 --- a/mvu-compat.js +++ b/mvu-compat.js @@ -2,13 +2,6 @@ // These rules are intentionally narrow so we strip MVU artifacts without // disturbing normal prompt or world info content. -export const MVU_SANITIZE_MODES = Object.freeze({ - /** 整段 drop likely MVU 内容(用于世界书条目)。 */ - AGGRESSIVE: "aggressive", - /** 只剥离 MVU 容器/宏,不整段 drop(用于用户原文、角色描述等任务输入字段)。 */ - PASSIVE: "passive", -}); - export const MVU_ENTRY_COMMENT_REGEX = /\[(mvu_update|mvu_plot|initvar)\]/i; const MVU_UPDATE_BLOCK_REGEX = @@ -222,8 +215,7 @@ export function sanitizeMvuContent( let text = blockedResult.text; let dropped = false; - if (sanitizedMode === MVU_SANITIZE_MODES.AGGRESSIVE) { - // 整段 drop:用于世界书条目,不用于用户原文字段 + if (sanitizedMode === "aggressive") { if ( isLikelyMvuWorldInfoContent(originalCollapsed) || isLikelyMvuWorldInfoContent(text) @@ -233,7 +225,6 @@ export function sanitizeMvuContent( reasons.push("likely_mvu_content"); } } - // MVU_SANITIZE_MODES.PASSIVE:只做 artifact 剥离 + blocked 过滤,不整段 drop。 return { text: collapseWhitespace(text), diff --git a/notice.js b/notice.js index 87c472a..19e7e20 100644 --- a/notice.js +++ b/notice.js @@ -60,16 +60,6 @@ function ensureStyle(doc) { -webkit-backdrop-filter: blur(10px) saturate(125%); } - .st-bme-notice[data-layout="compact"] { - display: flex; - align-items: center; - gap: 10px; - width: fit-content; - max-width: 100%; - align-self: flex-end; - padding: 10px; - } - .st-bme-notice::after { content: ""; position: absolute; @@ -97,7 +87,6 @@ function ensureStyle(doc) { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.14); box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.16); - flex-shrink: 0; } .st-bme-notice[data-busy="true"] .st-bme-notice__icon { @@ -108,12 +97,6 @@ function ensureStyle(doc) { min-width: 0; } - .st-bme-notice[data-layout="compact"] .st-bme-notice__content { - display: flex; - align-items: center; - min-width: 0; - } - .st-bme-notice__title { margin: 0; font-size: 17px; @@ -123,14 +106,6 @@ function ensureStyle(doc) { color: #f0f6ff; } - .st-bme-notice[data-layout="compact"] .st-bme-notice__title { - font-size: 16px; - line-height: 1.2; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .st-bme-notice__message { margin: 4px 0 0; font-size: 14px; @@ -151,12 +126,6 @@ function ensureStyle(doc) { -webkit-mask-image: linear-gradient(90deg, transparent 0%, black 6%, black 88%, transparent 100%); } - .st-bme-notice[data-layout="compact"] .st-bme-notice__message, - .st-bme-notice[data-layout="compact"] .st-bme-notice__actions, - .st-bme-notice[data-layout="compact"] .st-bme-notice__progress { - display: none !important; - } - .st-bme-notice__actions { display: flex; gap: 8px; @@ -201,7 +170,6 @@ function ensureStyle(doc) { line-height: 1; cursor: pointer; transition: background 140ms ease; - flex-shrink: 0; } .st-bme-notice__close:hover, @@ -312,11 +280,8 @@ function getIcon(level) { function applyNoticeState(item, input, progress) { const level = input.level || "info"; - const displayMode = input.displayMode === "compact" ? "compact" : "normal"; - const isCompact = displayMode === "compact"; item.dataset.level = level; item.dataset.busy = input.busy ? "true" : "false"; - item.dataset.layout = displayMode; const icon = item.querySelector(".st-bme-notice__icon"); if (icon) { @@ -331,7 +296,6 @@ function applyNoticeState(item, input, progress) { const message = item.querySelector(".st-bme-notice__message"); if (message) { message.textContent = input.message || ""; - message.hidden = isCompact || !String(input.message || "").trim(); if (input.marquee) { message.classList.add("st-bme-notice__message--marquee"); } else { @@ -342,7 +306,7 @@ function applyNoticeState(item, input, progress) { const actionWrap = item.querySelector(".st-bme-notice__actions"); const actionButton = item.querySelector(".st-bme-notice__action"); if (actionWrap && actionButton) { - if (!isCompact && input.action?.label) { + if (input.action?.label) { actionWrap.style.display = ""; actionButton.style.display = ""; actionButton.textContent = input.action.label; @@ -355,7 +319,7 @@ function applyNoticeState(item, input, progress) { } } - if (input.persist || isCompact) { + if (input.persist) { progress.style.display = "none"; progress.style.animationDuration = ""; } else { diff --git a/panel.html b/panel.html index d1f0143..c43dc56 100644 --- a/panel.html +++ b/panel.html @@ -1055,30 +1055,6 @@ -
-
-
-
提示信息
-
- 控制提取、召回等顶部通知的显示样式。 -
-
-
-
- - -
-
- 精简模式会将工作中的提示压缩为标题卡片;错误和警告仍显示完整内容,避免关键信息被隐藏。 -
-
-
diff --git a/panel.js b/panel.js index 7cf9a60..d31a408 100644 --- a/panel.js +++ b/panel.js @@ -840,32 +840,6 @@ function _switchConfigSection(sectionId) { } } -function _ensureMobileTraceConfigNavButton() { - if (!panelEl) return; - - const mobileNav = panelEl.querySelector(".bme-config-nav-mobile"); - if (!mobileNav) return; - if (mobileNav.querySelector('[data-config-section="trace"]')) return; - - const appearanceButton = mobileNav.querySelector( - '[data-config-section="appearance"]', - ); - const traceButton = document.createElement("button"); - traceButton.className = "bme-config-nav-btn"; - traceButton.dataset.configSection = "trace"; - traceButton.type = "button"; - traceButton.innerHTML = ` - - 消息追踪 - `; - - if (appearanceButton?.parentNode === mobileNav) { - mobileNav.insertBefore(traceButton, appearanceButton); - } else { - mobileNav.appendChild(traceButton); - } -} - function _syncConfigSectionState() { if (!panelEl) return; panelEl.querySelectorAll(".bme-config-nav-btn").forEach((btn) => { @@ -1711,10 +1685,6 @@ function _refreshConfigTab() { "bme-setting-recall-card-user-input-display-mode", settings.recallCardUserInputDisplayMode ?? "beautify_only", ); - _setInputValue( - "bme-setting-notice-display-mode", - settings.noticeDisplayMode ?? "normal", - ); _setInputValue("bme-setting-extract-every", settings.extractEvery ?? 1); _setInputValue( @@ -1934,8 +1904,6 @@ function _refreshConfigTab() { function _bindConfigControls() { if (!panelEl || panelEl.dataset.bmeConfigBound === "true") return; - _ensureMobileTraceConfigNavButton(); - panelEl.querySelectorAll(".bme-config-nav-btn").forEach((btn) => { if (btn.dataset.bmeBound === "true") return; btn.addEventListener("click", () => { @@ -2057,17 +2025,6 @@ function _bindConfigControls() { }); recallCardUserInputDisplayModeEl.dataset.bmeBound = "true"; } - const noticeDisplayModeEl = document.getElementById( - "bme-setting-notice-display-mode", - ); - if (noticeDisplayModeEl && noticeDisplayModeEl.dataset.bmeBound !== "true") { - noticeDisplayModeEl.addEventListener("change", () => { - _patchSettings({ - noticeDisplayMode: noticeDisplayModeEl.value || "normal", - }); - }); - noticeDisplayModeEl.dataset.bmeBound = "true"; - } bindNumber("bme-setting-extract-every", 1, 1, 50, (value) => _patchSettings({ extractEvery: value }), diff --git a/plans/notice-display-mode-plan.md b/plans/notice-display-mode-plan.md deleted file mode 100644 index bb51429..0000000 --- a/plans/notice-display-mode-plan.md +++ /dev/null @@ -1,267 +0,0 @@ -# ST-BME 提示信息“正常 / 精简”模式计划 - -## 目标 - -在“配置” -> “功能开关”里新增一个“提示信息”设置项,让用户选择通知展示模式: - -- `正常`:保持当前行为不变 -- `精简`:通知仅显示标题,例如 `ST-BME 提取`、`ST-BME 召回` - -默认值为 `正常`。该设置需要在 PC 端和手机端的设置入口都能看到并生效。 - -## 现状梳理 - -### 1. 提示信息的实际渲染位置 - -- `notice.js` - - 负责创建和更新通知 DOM - - 当前结构固定包含:图标、标题、正文、动作按钮区域、关闭按钮、进度条 - - 移动端只缩小了宿主宽度,没有缩小卡片内容结构 - -### 2. 通知内容从哪里来 - -- `index.js` - - `updateStageNotice(...)` 会把 `text + meta` 拼成 `message` - - `showManagedBmeNotice(...)` 统一负责显示提取 / 向量 / 召回 / 历史恢复通知 -- `ui-status.js` - - `getStageNoticeTitle(...)` 负责输出 `ST-BME 提取`、`ST-BME 召回` 等标题 - -### 3. 设置从哪里定义和持久化 - -- `index.js` - - `defaultSettings` 是设置默认值来源 - - `mergePersistedSettings(...)` / `getPersistedSettingsSnapshot(...)` 会自动把新增设置纳入持久化 -- `panel.js` - - `_refreshConfigTab()` 负责把设置值回填到 UI - - `_bindConfigControls()` 负责把 UI 改动写回设置 -- `panel.html` - - “功能开关” section 是实际设置表单 - -### 4. PC / 手机端设置入口的关系 - -- `panel.html` 里桌面端和手机端各有一套“配置导航按钮” -- 但它们切换的是同一套 `bme-config-section` -- 也就是说: - - 真正的设置项表单只需要在“功能开关” section 加一次 - - 该项天然会同时出现在 PC 和手机端 - -## 关键发现 - -仅仅把正文文本隐藏,还不足以让精简通知“变窄”。 - -原因是 `notice.js` 里的通知宿主 `#st-bme-notice-host` 使用纵向 flex 布局,而子项默认会被拉伸到宿主宽度。现在宿主在移动端宽度是 `calc(100vw - 16px)`,所以即使正文为空,卡片依然可能占满整行。 - -结论: - -- 精简模式不仅要隐藏正文和动作区 -- 还必须给通知卡片增加“按内容宽度收缩”的样式 - -这是这个需求里最容易漏掉的点。 - -## 推荐设计 - -### 设置字段 - -建议新增设置字段: - -- key: `noticeDisplayMode` -- 可选值: `"normal"` / `"compact"` -- 默认值: `"normal"` - -命名理由: - -- 语义直接,对应“提示信息显示模式” -- 以后如果要扩展为更多通知布局,也容易继续演进 - -### 设置 UI - -建议放在“配置” -> “功能开关”里,作为一个 `select`,而不是复选框。 - -建议文案: - -- 标题:`提示信息` -- 说明:`控制提取 / 召回等顶部通知的显示样式` -- 选项: - - `normal` -> `正常` - - `compact` -> `精简(仅显示标题)` - -### 精简模式的推荐行为 - -推荐在 `精简` 模式下: - -- 保留:标题 -- 保留:左侧状态图标 -- 保留:关闭按钮 -- 隐藏:正文文本 -- 隐藏:meta 信息 -- 隐藏:动作按钮区,例如“终止提取”“终止召回” -- 隐藏:底部进度条 - -这样可以最大化缩小高度,同时保留最基本的状态识别和手动关闭能力。 - -## 一个需要先确认的设计点 - -`warning` / `error` 通知是否也要强制进入“仅标题”模式? - -我的推荐方案: - -- `running` / `info` / `success`:遵循 `精简`,只显示标题 -- `warning` / `error`:仍显示正文,避免重要报错信息被吃掉 - -理由: - -- 用户截图里最占空间的是“工作中”的持续通知 -- 真正需要完整文本的,通常是失败原因或警告信息 - -如果你更想要“精简就是所有通知一律只显示标题”,也能做,但我认为可用性会更差一些。 - -## 预期改动范围 - -### 1. `index.js` - -- 在 `defaultSettings` 增加 `noticeDisplayMode: "normal"` -- 在通知更新逻辑里读取该设置 -- 显式采用“Option A”: - - `index.js` 在 `updateStageNotice(...)` 里把 `displayMode` 放进传给 `showManagedBmeNotice(...)` 的 `input` 对象 - - `notice.js` 只读取 `input.displayMode` 控制渲染和样式,不直接读取全局设置 - - 保持 `notice.js` 为纯渲染模块,不和 settings 持久化层耦合 -- 若采用“错误/警告保留正文”的推荐方案,则在这里按 level 做分流 -- 当用户切换 `noticeDisplayMode` 时,补一条“刷新当前可见通知”的处理 - - 目标:如果用户在通知仍可见时切换模式,现有通知也能立即切换,而不是等下次状态更新 - - 实现可放在设置更新后,对当前 stage notice 进行一次统一 `update` - -### 2. `notice.js` - -- 扩展通知输入参数,支持 `displayMode` 或 `compact` -- 渲染时根据模式: - - 正常模式:保持现状 - - 精简模式:不渲染正文和动作区,或渲染后隐藏 -- 增加精简态 class / data attribute,例如: - - `data-layout="compact"` -- 新增 compact 样式: - - 卡片宽度按内容收缩 - - 卡片高度缩成单行或接近单行 - - 维持移动端可点击性和可读性 - - 明确让 compact 通知右对齐 - - 首选:给 compact 卡片本身加 `align-self: flex-end` - - 备选:给 host 在 compact 态下加 `align-items: flex-end` - - compact 布局可直接切成更自然的横向紧凑结构 - - 例如从当前 grid 调整为单行 `flex` - - 避免 `close` 按钮在极窄宽度下被 grid 挤得别扭 - - compact 模式显式隐藏进度条,避免小卡片底部出现细长条影响观感 - -### 3. `panel.html` - -- 在“功能开关” section 里新增“提示信息”配置卡或配置行 -- 位置建议放在较靠前区域,因为它属于明显的 UI 行为开关 - -### 4. `panel.js` - -- `_refreshConfigTab()` 里回填 `noticeDisplayMode` -- `_bindConfigControls()` 里监听该 `select` 的变更并调用 `_patchSettings(...)` - -## 样式实现思路 - -推荐采用“精简卡片单独收缩”的方式,而不是修改整个通知宿主。 - -建议方向: - -- 正常卡片:继续占用现有宽度逻辑 -- 精简卡片: - - `align-self: flex-end` - - `width: fit-content` - - `max-width: 100%` - - 更紧凑的 padding / gap - - 必要时直接切成单行 `flex` 布局,而不是沿用三列 grid - -这样可以避免: - -- 一条精简通知把整个通知堆栈的布局都改坏 -- 正常模式和精简模式互相影响 - -补充说明: - -- 这里真正需要注意的是“host 是全宽,而 compact 卡片要在 host 内收缩并靠右” -- 因此不能只写 `width: fit-content` -- 还必须同时处理横向对齐 -- 纠正一下术语:这里如果要改 host,应该是 `align-items`,不是 `align-self` - -## 风险与兼容性 - -### 风险 1:终止按钮被隐藏后,工作中通知无法直接中断 - -这是“只显示标题”带来的天然代价。 - -处理方式建议: - -- 先按用户描述执行精简:隐藏动作按钮 -- 在计划评审阶段明确接受这个取舍 - -### 风险 2:只隐藏正文但不改宽度,视觉上仍然太大 - -这是本需求最核心的实现风险。必须在样式上单独处理 compact 卡片宽度。 - -### 风险 3:切换设置后,当前可见通知不立刻刷新 - -如果只让后续新通知读取新模式,那么用户在设置页切换“正常 / 精简”时,已显示的通知会出现“要等下一次状态变更才切换”的延迟感。 - -处理方式建议: - -- 把它当作一个小型 UI 刷新点纳入实现 -- 设置更新时,对当前仍存在的 stage notice 重新执行一次 `update` - -### 风险 4:旧配置兼容 - -风险较低。因为默认设置合并机制已经存在,只要新增默认值即可让旧配置自动回退到 `normal`。 - -## 建议验证点 - -### 功能验证 - -- 默认安装或旧配置升级后,提示信息模式应为 `正常` -- 在 PC 端设置面板中能看到“提示信息”选项 -- 在手机端设置面板中也能看到同一个选项 -- 切到 `精简` 后,提取 / 召回通知只显示标题 -- 切回 `正常` 后,恢复当前完整通知样式 -- 通知可见时切换设置,已显示的通知也会同步切换样式 - -### 视觉验证 - -- 手机上精简通知高度明显缩小 -- 手机上精简通知宽度不再铺满整行 -- 手机上精简通知应靠右显示,而不是缩小后仍贴左 -- 多条通知叠加时,布局仍然稳定 -- PC 端正常模式无回归 - -### 行为验证 - -- 运行中通知仍能正常更新标题 -- 自动关闭逻辑不受影响 -- 关闭按钮不受影响 -- compact 模式下进度条已按预期隐藏 -- 若保留“错误/警告显示正文”策略,则需单独验证错误提示仍完整可见 - -## 实施顺序 - -1. 增加设置字段与默认值 -2. 在功能开关 UI 中加入“提示信息”选择器 -3. 将选择器和设置读写绑定起来 -4. 给通知渲染层增加 compact 模式 -5. 调整 compact 样式,确保真正按内容收缩 -6. 验证 PC / 手机端、正常 / 精简、运行 / 完成 / 异常几类通知 - -## 我目前的结论 - -这个需求本身不复杂,真正要小心的是两件事: - -- “PC 和手机端都要有设置”其实主要是配置入口问题,不是双份业务实现 -- “精简后通知真的变小”不能只靠删文本,必须同时改通知卡片的宽度收缩策略 - -这次复审后,我会把实现重点再收敛成四个明确点: - -- `displayMode` 通过 `updateStageNotice(...) -> input.displayMode -> notice.js` 这条链路传递 -- compact 不仅隐藏正文,也隐藏进度条 -- compact 需要明确右对齐,不能只是 `fit-content` -- 用户切换设置时,当前可见通知也应立即刷新 - -如果后续开始实现,我会优先按这个计划走,并把“错误/警告是否保留正文”作为唯一需要最终拍板的交互点。 diff --git a/prompt-builder.js b/prompt-builder.js index 84cc14c..c58c3d5 100644 --- a/prompt-builder.js +++ b/prompt-builder.js @@ -2,7 +2,7 @@ // 统一负责任务预设块排序、变量渲染,以及世界书/EJS 上下文接入。 import { getActiveTaskProfile, getLegacyPromptForTask } from "./prompt-profiles.js"; -import { sanitizeMvuContent, MVU_SANITIZE_MODES } from "./mvu-compat.js"; +import { sanitizeMvuContent } from "./mvu-compat.js"; import { resolveTaskWorldInfo } from "./task-worldinfo.js"; import { applyTaskRegex } from "./task-regex.js"; @@ -32,41 +32,6 @@ const INPUT_CONTEXT_MVU_FIELDS = [ "userPersona", ]; -/** - * 字段族 → sanitize mode 映射表。 - * - * PASSIVE:用户原文字段(对话、角色描述、摘要、候选节点等)——只剥离 MVU 容器/宏, - * 不整段 drop。这些字段不可能"整段就是一条 MVU 世界书条目"。 - * AGGRESSIVE(默认):保留现有行为,用于世界书条目路径(sanitizeWorldInfoEntries)。 - * - * 未列入此表的字段走 AGGRESSIVE,与改动前行为一致。 - */ -const INPUT_CONTEXT_FIELD_MODE = { - userMessage: MVU_SANITIZE_MODES.PASSIVE, - recentMessages: MVU_SANITIZE_MODES.PASSIVE, - chatMessages: MVU_SANITIZE_MODES.PASSIVE, - dialogueText: MVU_SANITIZE_MODES.PASSIVE, - charDescription: MVU_SANITIZE_MODES.PASSIVE, - userPersona: MVU_SANITIZE_MODES.PASSIVE, - candidateText: MVU_SANITIZE_MODES.PASSIVE, - candidateNodes: MVU_SANITIZE_MODES.PASSIVE, - nodeContent: MVU_SANITIZE_MODES.PASSIVE, - eventSummary: MVU_SANITIZE_MODES.PASSIVE, - characterSummary: MVU_SANITIZE_MODES.PASSIVE, - threadSummary: MVU_SANITIZE_MODES.PASSIVE, - contradictionSummary: MVU_SANITIZE_MODES.PASSIVE, -}; - -/** 这些字段被清空时必须 warn(兜底告警)。 */ -const CRITICAL_INPUT_FIELDS = new Set([ - "recentMessages", - "dialogueText", - "chatMessages", - "charDescription", - "userPersona", - "candidateNodes", -]); - const INPUT_REGEX_STAGE_BY_FIELD = { userMessage: "input.userMessage", recentMessages: "input.recentMessages", @@ -361,30 +326,6 @@ function joinStructuredPath(basePath = "", segment = "") { : `${basePath}.${normalizedSegment}`; } -function mergeSanitizeReasons(...reasonLists) { - const merged = new Set(); - for (const list of reasonLists) { - for (const reason of Array.isArray(list) ? list : []) { - if (reason) { - merged.add(String(reason)); - } - } - } - return [...merged]; -} - -function summarizeSanitizePreview(value, maxLength = 200) { - const rendered = stringifyInterpolatedValue(value) - .replace(/\s+/g, " ") - .trim(); - if (!rendered) { - return ""; - } - return rendered.length > maxLength - ? `${rendered.slice(0, maxLength)}...` - : rendered; -} - function looksLikeMvuStateContainer(value, seen = new WeakSet()) { if (!value || typeof value !== "object") { return false; @@ -470,22 +411,12 @@ function sanitizeStructuredPromptValue( omit: !String(sanitized.text || "").trim() && String(value || "").trim().length > 0, - dropped: Boolean(sanitized.dropped), - reasons: Array.isArray(sanitized.reasons) ? [...sanitized.reasons] : [], - blockedHitCount: Number(sanitized.blockedHitCount || 0), - artifactRemovedCount: Number(sanitized.artifactRemovedCount || 0), - rawPreview: summarizeSanitizePreview(value), - sanitizedPreview: summarizeSanitizePreview(sanitized.text), }; } if (Array.isArray(value)) { const sanitizedArray = []; let changed = false; - let dropped = false; - let blockedHitCount = 0; - let artifactRemovedCount = 0; - let reasons = []; for (let index = 0; index < value.length; index += 1) { const childResult = sanitizeStructuredPromptValue( settings, @@ -507,31 +438,17 @@ function sanitizeStructuredPromptValue( ); if (childResult.omit) { changed = true; - dropped = dropped || Boolean(childResult.dropped); - blockedHitCount += Number(childResult.blockedHitCount || 0); - artifactRemovedCount += Number(childResult.artifactRemovedCount || 0); - reasons = mergeSanitizeReasons(reasons, childResult.reasons); continue; } sanitizedArray.push(childResult.value); if (childResult.changed) { changed = true; } - dropped = dropped || Boolean(childResult.dropped); - blockedHitCount += Number(childResult.blockedHitCount || 0); - artifactRemovedCount += Number(childResult.artifactRemovedCount || 0); - reasons = mergeSanitizeReasons(reasons, childResult.reasons); } return { value: sanitizedArray, changed: changed || sanitizedArray.length !== value.length, omit: value.length > 0 && sanitizedArray.length === 0, - dropped, - reasons, - blockedHitCount, - artifactRemovedCount, - rawPreview: summarizeSanitizePreview(value), - sanitizedPreview: summarizeSanitizePreview(sanitizedArray), }; } @@ -541,12 +458,6 @@ function sanitizeStructuredPromptValue( value, changed: false, omit: false, - dropped: false, - reasons: [], - blockedHitCount: 0, - artifactRemovedCount: 0, - rawPreview: summarizeSanitizePreview(value), - sanitizedPreview: summarizeSanitizePreview(value), }; } seen.add(value); @@ -555,10 +466,6 @@ function sanitizeStructuredPromptValue( const sanitizedObject = {}; let changed = false; let keptEntries = 0; - let dropped = false; - let blockedHitCount = 0; - let artifactRemovedCount = 0; - let reasons = []; for (const [key, entryValue] of Object.entries(value)) { const stripReason = stripMvuContainers @@ -574,8 +481,6 @@ function sanitizeStructuredPromptValue( reasons: [stripReason], blockedHitCount: 0, }); - dropped = true; - reasons = mergeSanitizeReasons(reasons, [stripReason]); continue; } @@ -599,10 +504,6 @@ function sanitizeStructuredPromptValue( ); if (childResult.omit) { changed = true; - dropped = dropped || Boolean(childResult.dropped); - blockedHitCount += Number(childResult.blockedHitCount || 0); - artifactRemovedCount += Number(childResult.artifactRemovedCount || 0); - reasons = mergeSanitizeReasons(reasons, childResult.reasons); continue; } sanitizedObject[key] = childResult.value; @@ -610,22 +511,12 @@ function sanitizeStructuredPromptValue( if (childResult.changed) { changed = true; } - dropped = dropped || Boolean(childResult.dropped); - blockedHitCount += Number(childResult.blockedHitCount || 0); - artifactRemovedCount += Number(childResult.artifactRemovedCount || 0); - reasons = mergeSanitizeReasons(reasons, childResult.reasons); } return { value: sanitizedObject, changed, omit: originalLooksMvuContainer && keptEntries === 0, - dropped, - reasons, - blockedHitCount, - artifactRemovedCount, - rawPreview: summarizeSanitizePreview(value), - sanitizedPreview: summarizeSanitizePreview(sanitizedObject), }; } @@ -633,12 +524,6 @@ function sanitizeStructuredPromptValue( value, changed: false, omit: false, - dropped: false, - reasons: [], - blockedHitCount: 0, - artifactRemovedCount: 0, - rawPreview: summarizeSanitizePreview(value), - sanitizedPreview: summarizeSanitizePreview(value), }; } @@ -724,7 +609,6 @@ function sanitizePromptContextInputs( const value = sanitizedContext[fieldName]; const regexStage = INPUT_REGEX_STAGE_BY_FIELD[fieldName] || ""; const regexRole = INPUT_REGEX_ROLE_BY_FIELD[fieldName] || "system"; - const fieldMode = INPUT_CONTEXT_FIELD_MODE[fieldName] || MVU_SANITIZE_MODES.AGGRESSIVE; const sanitized = sanitizeStructuredPromptValue( settings, taskType, @@ -732,7 +616,7 @@ function sanitizePromptContextInputs( { fieldName, path: fieldName, - mode: fieldMode, + mode: "aggressive", regexStage, role: regexRole, debugState, @@ -741,24 +625,6 @@ function sanitizePromptContextInputs( stripMvuContainers, }, ); - if (sanitized.omit && CRITICAL_INPUT_FIELDS.has(fieldName)) { - const rawLength = typeof value === "string" ? value.length : -1; - console.warn( - "[ST-BME] 关键任务输入字段被 MVU 策略清空", - { - taskType, - fieldName, - mode: fieldMode, - rawLength, - dropped: Boolean(sanitized.dropped), - reasons: Array.isArray(sanitized.reasons) ? sanitized.reasons : [], - artifactRemovedCount: Number(sanitized.artifactRemovedCount || 0), - blockedHitCount: Number(sanitized.blockedHitCount || 0), - rawPreview: String(sanitized.rawPreview || ""), - sanitizedPreview: String(sanitized.sanitizedPreview || ""), - }, - ); - } sanitizedContext[fieldName] = sanitized.omit ? Array.isArray(value) ? [] diff --git a/retriever.js b/retriever.js index 471f21e..7290c57 100644 --- a/retriever.js +++ b/retriever.js @@ -97,44 +97,6 @@ function buildRecallFallbackReason(llmResult) { } } -function resolveRecallSelectedIds(result) { - if (Array.isArray(result)) { - return result; - } - - const visited = new Set(); - const queue = [{ value: result, depth: 0 }]; - while (queue.length > 0) { - const current = queue.shift(); - const value = current?.value; - const depth = Number(current?.depth) || 0; - if (!value || typeof value !== "object" || visited.has(value) || depth > 1) { - continue; - } - visited.add(value); - - const directCandidates = [ - value.selected_ids, - value.selectedIds, - value.node_ids, - value.nodeIds, - value.ids, - ]; - for (const candidate of directCandidates) { - if (Array.isArray(candidate)) { - return candidate; - } - } - - queue.push({ value: value.data, depth: depth + 1 }); - queue.push({ value: value.result, depth: depth + 1 }); - queue.push({ value: value.payload, depth: depth + 1 }); - queue.push({ value: value.output, depth: depth + 1 }); - } - - return null; -} - function isAbortError(error) { return error?.name === "AbortError"; } @@ -1553,22 +1515,21 @@ async function llmRecall( returnFailureDetails: true, }); const result = llmResult?.ok ? llmResult.data : null; - const selectedIds = resolveRecallSelectedIds(result); - if (Array.isArray(selectedIds)) { + if (result?.selected_ids && Array.isArray(result.selected_ids)) { // 校验 ID 有效性 const validIds = uniqueNodeIds( - selectedIds.filter((id) => + result.selected_ids.filter((id) => candidates.some((c) => c.nodeId === id), ), ).slice(0, maxNodes); - if (validIds.length > 0 || selectedIds.length === 0) { + if (validIds.length > 0 || result.selected_ids.length === 0) { return { selectedNodeIds: validIds, status: "llm", reason: - validIds.length < selectedIds.length + validIds.length < result.selected_ids.length ? "LLM 返回了部分无效或超限 ID,已自动裁剪" : "LLM 精排完成", }; @@ -1577,7 +1538,7 @@ async function llmRecall( // LLM 失败时回退到纯评分排序 const fallbackReason = llmResult?.ok - ? Array.isArray(selectedIds) + ? Array.isArray(result?.selected_ids) ? "LLM 返回的候选 ID 无效,已回退到评分排序" : "LLM 返回了无法识别的 JSON 结构,已回退到评分排序" : buildRecallFallbackReason(llmResult); @@ -1585,11 +1546,7 @@ async function llmRecall( selectedNodeIds: candidates.slice(0, maxNodes).map((c) => c.nodeId), status: "fallback", reason: fallbackReason, - fallbackType: llmResult?.ok - ? Array.isArray(selectedIds) - ? "invalid-candidate" - : "invalid-structure" - : llmResult?.errorType || "unknown", + fallbackType: llmResult?.ok ? "invalid-candidate" : llmResult?.errorType || "unknown", }; } diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index e5e2a57..d279469 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -2157,62 +2157,6 @@ async function testExtractorNormalizesFlatCreateOperation() { } } -async function testExtractorSupportsLegacyFlatNodeOperations() { - const graph = createEmptyGraph(); - const restoreOverrides = pushTestOverrides({ - llm: { - async callLLMForJSON() { - return { - operations: [ - { - type: "event", - id: "evt-legacy", - title: "夜间喂食", - summary: "角色完成了一次深夜喂食。", - participants: "悟岳, 访客", - status: "resolved", - importance: 6, - }, - { - type: "character", - id: "char-legacy", - name: "悟岳", - state: "放松下来", - }, - ], - }; - }, - }, - }); - - try { - const result = await extractMemories({ - graph, - messages: [{ seq: 7, role: "assistant", content: "测试旧版扁平提取输出" }], - startSeq: 7, - endSeq: 7, - schema, - embeddingConfig: null, - settings: {}, - }); - - assert.equal(result.success, true); - assert.equal(result.newNodes, 2); - assert.equal(graph.lastProcessedSeq, 7); - - const eventNode = graph.nodes.find((node) => node.type === "event"); - const characterNode = graph.nodes.find((node) => node.type === "character"); - assert.ok(eventNode); - assert.ok(characterNode); - assert.equal(eventNode.fields?.title, "夜间喂食"); - assert.equal(eventNode.fields?.summary, "角色完成了一次深夜喂食。"); - assert.equal(characterNode.fields?.name, "悟岳"); - assert.equal(characterNode.fields?.state, "放松下来"); - } finally { - restoreOverrides(); - } -} - async function testExtractorNormalizesArrayPayloadAndPreservesScopeField() { const graph = createEmptyGraph(); const restoreOverrides = pushTestOverrides({ @@ -5175,8 +5119,6 @@ await testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure(); await testExtractorFailsOnUnknownOperation(); await testExtractorNormalizesFlatCreateOperation(); await testExtractorNormalizesArrayPayloadAndPreservesScopeField(); -await testExtractorSupportsLegacyFlatNodeOperations(); -await testExtractorNormalizesArrayPayloadAndPreservesScopeField(); await testConsolidatorMergeUpdatesSeqRange(); await testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing(); await testBatchJournalVectorDeltaCapturesRecoveryFields(); diff --git a/tests/prompt-builder-mvu.mjs b/tests/prompt-builder-mvu.mjs index c47a63c..40c4063 100644 --- a/tests/prompt-builder-mvu.mjs +++ b/tests/prompt-builder-mvu.mjs @@ -516,341 +516,6 @@ try { promptBuild.debug.mvu.sanitizedFieldCount, ); - // ── 新增测试:passive mode 字段族不被整段 drop ─────────────────────────────── - - // helpers - function buildExtractSettings() { - const taskProfiles = createDefaultTaskProfiles(); - return { - llmApiUrl: "https://example.com/v1", - llmApiKey: "sk-test", - llmModel: "gpt-test", - timeoutMs: 4321, - taskProfilesVersion: 3, - taskProfiles, - }; - } - - function buildExtractBlock(id, name, sourceKey, order) { - return { - id, - name, - type: "builtin", - enabled: true, - role: "system", - sourceKey, - sourceField: "", - content: "", - injectionMode: "relative", - order, - }; - } - - function buildMinimalExtractSettings() { - const base = buildExtractSettings(); - base.taskProfiles.extract = { - activeProfileId: "extract-passive-test", - profiles: [ - { - id: "extract-passive-test", - name: "passive test", - taskType: "extract", - builtin: false, - version: 3, - enabled: true, - blocks: [ - buildExtractBlock("blk-char", "charDescription", "charDescription", 0), - buildExtractBlock("blk-persona", "userPersona", "userPersona", 1), - buildExtractBlock("blk-recent", "recentMessages", "recentMessages", 2), - buildExtractBlock("blk-candidate", "candidateText", "candidateText", 3), - ], - generation: createDefaultTaskProfiles().extract.profiles[0].generation, - regex: { enabled: false, inheritStRegex: false, stages: {}, localRules: [] }, - }, - ], - }; - return base; - } - - // 测试 1:recentMessages 含多次 getvar 宏 — 不被整段 drop,宏被剥离 - { - delete globalThis.__stBmeRuntimeDebugState; - const s = buildMinimalExtractSettings(); - const pb = await buildTaskPrompt(s, "extract", { - recentMessages: "#0 [assistant]: {{get_message_variable::stat_data.hp}} 今晚的气氛很好。{{get_message_variable::display_data.mood}}", - charDescription: "普通角色描述,不含 MVU。", - userPersona: "普通用户设定。", - candidateText: "", - }); - const rendered = JSON.stringify(pb.executionMessages); - assert.match(rendered, /今晚的气氛很好/, - "T1: recentMessages 的叙述文本必须保留"); - assert.doesNotMatch(rendered, /get_message_variable/i, - "T1: getvar 宏必须被剥离"); - const droppedField = pb.debug.mvu.sanitizedFields.find( - (e) => e.name === "recentMessages" && e.dropped, - ); - assert.equal(droppedField, undefined, - "T1: recentMessages 不应被整段 drop(passive mode)"); - } - - // 测试 2:recentMessages 叙述里提到 stat_data 字样 — 不被整段 drop - { - delete globalThis.__stBmeRuntimeDebugState; - const s = buildMinimalExtractSettings(); - const pb = await buildTaskPrompt(s, "extract", { - recentMessages: "#0 [assistant]: 墙上的 stat_data 标签被撕掉了,角色叹了口气。", - charDescription: "", - userPersona: "", - candidateText: "", - }); - const rendered = JSON.stringify(pb.executionMessages); - assert.match(rendered, /墙上的/, - "T2: recentMessages 叙述文本必须保留"); - const droppedField = pb.debug.mvu.sanitizedFields.find( - (e) => e.name === "recentMessages" && e.dropped, - ); - assert.equal(droppedField, undefined, - "T2: recentMessages 不应被整段 drop"); - } - - // 测试 3:charDescription 含 MVU 宏 — 不被整段 drop,宏被剥离 - { - delete globalThis.__stBmeRuntimeDebugState; - const s = buildMinimalExtractSettings(); - const pb = await buildTaskPrompt(s, "extract", { - recentMessages: "普通对话。", - charDescription: "角色叫 Alice。 她性格温柔。", - userPersona: "", - candidateText: "", - }); - const rendered = JSON.stringify(pb.executionMessages); - assert.match(rendered, /她性格温柔/, - "T3: charDescription 叙述文本必须保留"); - assert.doesNotMatch(rendered, /StatusPlaceHolderImpl/i, - "T3: 占位符必须被剥离"); - const droppedField = pb.debug.mvu.sanitizedFields.find( - (e) => e.name === "charDescription" && e.dropped, - ); - assert.equal(droppedField, undefined, - "T3: charDescription 不应被整段 drop"); - } - - // 测试 4:userPersona 是 MVU 规则内容 — 不被整段 drop - { - delete globalThis.__stBmeRuntimeDebugState; - const s = buildMinimalExtractSettings(); - const pb = await buildTaskPrompt(s, "extract", { - recentMessages: "普通对话。", - charDescription: "", - userPersona: "变量更新规则:\ntype: state\n当前时间: 12:00", - candidateText: "", - }); - const rendered = JSON.stringify(pb.executionMessages); - assert.match(rendered, /变量更新规则/, - "T4: userPersona 文本必须保留"); - const droppedField = pb.debug.mvu.sanitizedFields.find( - (e) => e.name === "userPersona" && e.dropped, - ); - assert.equal(droppedField, undefined, - "T4: userPersona 不应被整段 drop(passive mode)"); - } - - // 测试 5:candidateNodes 含 stat_data/getvar — 字符串叶子保留,容器键剥离 - { - delete globalThis.__stBmeRuntimeDebugState; - const s = buildMinimalExtractSettings(); - s.taskProfiles.extract.profiles[0].blocks.push( - buildExtractBlock("blk-nodes", "candidateNodes", "candidateNodes", 4), - ); - const pb = await buildTaskPrompt(s, "extract", { - recentMessages: "", - charDescription: "", - userPersona: "", - candidateText: "", - candidateNodes: [ - { - id: "node-a", - summary: "这是一个有意义的候选摘要,说明了角色的决定。", - note: "{{get_message_variable::stat_data.地点}} 某地区的行动。", - variables: { - 0: { - stat_data: { 地点: "学校" }, - display_data: { 地点: "教室" }, - }, - }, - }, - ], - }); - const rendered = JSON.stringify(pb.executionMessages); - assert.match(rendered, /有意义的候选摘要/, - "T5: candidateNodes 的 summary 文本必须保留"); - assert.doesNotMatch(rendered, /get_message_variable/i, - "T5: getvar 宏必须被剥离"); - const containerDropped = pb.debug.mvu.sanitizedFields.find( - (e) => String(e.name || "").startsWith("candidateNodes[0].variables"), - ); - assert.ok(containerDropped, - "T5: stat_data/display_data 容器键必须仍被剥离"); - } - - // 测试 6:world info 仍然 aggressive drop(守卫 6cec031 正收益) - { - delete globalThis.__stBmeRuntimeDebugState; - const mvuWorldbookEntry = [ - createWorldbookEntry({ - uid: 999, - name: "mvu-statusbar", - comment: "mvu-statusbar", - content: "变量输出格式: 严格 \ntype: state\nformat: |-\n stat_data:", - strategyType: "constant", - keys: [], - order: 1, - }), - ]; - globalThis.getCharWorldbookNames = () => ({ - primary: "mvu-guard-worldbook", - additional: [], - }); - globalThis.getWorldbook = async (name) => - name === "mvu-guard-worldbook" ? mvuWorldbookEntry : []; - globalThis.getLorebookEntries = async (name) => - (name === "mvu-guard-worldbook" ? mvuWorldbookEntry : []).map((e) => ({ - uid: e.uid, comment: e.comment, - })); - globalThis.__promptBuilderMvuContext = { - ...globalThis.__promptBuilderMvuContext, - chatId: "mvu-guard-chat", - chatMetadata: {}, - }; - - const s = buildExtractSettings(); - // 使用含 worldInfo 块的 extract 默认 profile - const pb = await buildTaskPrompt(s, "extract", { - recentMessages: "普通对话,用于触发世界书。", - userMessage: "普通消息。", - chatMessages: [], - }); - const rendered = JSON.stringify(pb); - assert.doesNotMatch(rendered, /UpdateVariable/, - "T6: MVU 世界书条目必须仍被 aggressive drop"); - } - - // 测试 6b:warn 路径 — 双断言 - // 构造一个故意用 aggressive mode 且会 drop 的字段(绕过策略表用内部 API) - // 通过检验 sanitizedFields 中的 dropped + reasons 来验证 warn 的依据已正确记录 - { - delete globalThis.__stBmeRuntimeDebugState; - const { sanitizeMvuContent, MVU_SANITIZE_MODES } = await import("../mvu-compat.js"); - assert.ok(MVU_SANITIZE_MODES, "mvu-compat 必须导出 MVU_SANITIZE_MODES"); - assert.equal(MVU_SANITIZE_MODES.AGGRESSIVE, "aggressive", - "MVU_SANITIZE_MODES.AGGRESSIVE 应为 'aggressive'"); - assert.equal(MVU_SANITIZE_MODES.PASSIVE, "passive", - "MVU_SANITIZE_MODES.PASSIVE 应为 'passive'"); - - // aggressive mode 下 MVU 世界书内容应被 drop - const aggressiveResult = sanitizeMvuContent( - "变量输出格式: 严格 \ntype: state\nformat: |-\n stat_data:", - { mode: MVU_SANITIZE_MODES.AGGRESSIVE }, - ); - assert.equal(aggressiveResult.dropped, true, - "T6b: aggressive mode 命中 likely_mvu_content 应 dropped=true"); - assert.ok(aggressiveResult.reasons.includes("likely_mvu_content"), - "T6b: reasons 应含 likely_mvu_content"); - - // passive mode 下相同内容不应被整段 drop - const passiveResult = sanitizeMvuContent( - "变量更新规则:\ntype: state\n当前时间: 12:00", - { mode: MVU_SANITIZE_MODES.PASSIVE }, - ); - assert.equal(passiveResult.dropped, false, - "T6b: passive mode 不应整段 drop"); - - // warn 路径:手动 mock console.warn 验证关键字段清空时 warn 触发 - const warnCalls = []; - const originalWarn = console.warn; - console.warn = (...args) => warnCalls.push(args); - try { - // 构建一个 extract 任务,把一个关键字段故意设成 aggressive 会 drop 的内容 - // 为触发 warn,我们在 sanitizePromptContextInputs 里必须 omit 且原始非空 - // 因为 passive 策略会保留,我们直接用 recentMessages 传入一段 - // 绕过策略表的方式是:在 world info 条目里触发 aggressive(不经过字段策略表) - // 这里改为:直接测试 sanitizeMvuContent 在 PASSIVE mode 下 dropped=false,即 warn 不触发 - // 然后对 AGGRESSIVE 手动调用相同逻辑,断言 warn 输出 - // - // 实际场景 warn 触发点:在 sanitizePromptContextInputs 里检测到 CRITICAL 字段 omit - // 修复后正常场景不应触发;我们用 debug.mvu.sanitizedFields 来断言"字段未被 drop" - const s2 = buildMinimalExtractSettings(); - const pb2 = await buildTaskPrompt(s2, "extract", { - recentMessages: "变量更新规则:\ntype: state\n当前时间: 12:00", - charDescription: "", - userPersona: "", - candidateText: "", - }); - // passive 模式下不应 warn 关键字段 drop - const criticalDropWarn = warnCalls.find( - (args) => String(args[0] || "").includes("关键任务输入字段被 MVU 策略清空"), - ); - assert.equal(criticalDropWarn, undefined, - "T6b: passive 模式下关键字段不应触发 warn"); - // 且字段不应在 sanitizedFields 中被标记为 dropped - const recentDropped = pb2.debug.mvu.sanitizedFields.find( - (e) => e.name === "recentMessages" && e.dropped, - ); - assert.equal(recentDropped, undefined, - "T6b: recentMessages 不应在 debug.mvu.sanitizedFields 中 dropped"); - } finally { - console.warn = originalWarn; - } - } - - // 测试 6c:warn 诊断字段包含 reasons 和 before/after preview - { - delete globalThis.__stBmeRuntimeDebugState; - const warnCalls = []; - const originalWarn = console.warn; - console.warn = (...args) => warnCalls.push(args); - try { - const s = buildMinimalExtractSettings(); - await buildTaskPrompt(s, "extract", { - recentMessages: - "{{get_message_variable::stat_data.hp}}\n{{get_message_variable::display_data.hp}}", - charDescription: "", - userPersona: "", - candidateText: "", - }); - const criticalDropWarn = warnCalls.find( - (args) => String(args[0] || "").includes("关键任务输入字段被 MVU 策略清空"), - ); - assert.ok(criticalDropWarn, "T6c: 清洗后为空时应触发关键字段 warn"); - assert.equal(criticalDropWarn[1]?.fieldName, "recentMessages", - "T6c: warn 应指向 recentMessages"); - assert.equal(criticalDropWarn[1]?.mode, "passive", - "T6c: recentMessages 应以 passive mode 清洗"); - assert.ok( - Array.isArray(criticalDropWarn[1]?.reasons) && - criticalDropWarn[1].reasons.includes("artifact_stripped"), - "T6c: warn 应携带 artifact_stripped reason", - ); - assert.match( - String(criticalDropWarn[1]?.rawPreview || ""), - /get_message_variable/, - "T6c: warn 应携带原始内容 preview", - ); - assert.equal( - String(criticalDropWarn[1]?.sanitizedPreview || ""), - "", - "T6c: 清洗为空时 sanitizedPreview 应为空串", - ); - assert.ok( - Number(criticalDropWarn[1]?.artifactRemovedCount || 0) >= 2, - "T6c: warn 应记录 artifactRemovedCount", - ); - } finally { - console.warn = originalWarn; - } - } - console.log("prompt-builder-mvu tests passed"); } finally { if (originalRequire === undefined) { diff --git a/tests/retrieval-config.mjs b/tests/retrieval-config.mjs index 6a0f9b5..ec36163 100644 --- a/tests/retrieval-config.mjs +++ b/tests/retrieval-config.mjs @@ -329,54 +329,6 @@ assert.equal(state.llmOptions[0].returnFailureDetails, true); assert.equal(state.llmOptions[0].maxRetries, 2); assert.equal(state.llmOptions[0].maxCompletionTokens, 512); -state.vectorCalls.length = 0; -state.diffusionCalls.length = 0; -state.llmCalls.length = 0; -state.llmOptions.length = 0; -state.llmCandidateCount = 0; -state.llmResponse = { selectedIds: ["rule-1"] }; -const llmCamelCaseResult = await retrieve({ - graph, - userMessage: "换个 JSON 键名也应该兼容", - recentMessages: [], - embeddingConfig: {}, - schema, - options: { - topK: 4, - maxRecallNodes: 2, - enableVectorPrefilter: true, - enableGraphDiffusion: false, - enableLLMRecall: true, - llmCandidatePool: 2, - }, -}); -assert.deepEqual(Array.from(llmCamelCaseResult.selectedNodeIds), ["rule-1"]); -assert.equal(llmCamelCaseResult.meta.retrieval.llm.status, "llm"); - -state.vectorCalls.length = 0; -state.diffusionCalls.length = 0; -state.llmCalls.length = 0; -state.llmOptions.length = 0; -state.llmCandidateCount = 0; -state.llmResponse = { data: { selected_ids: ["rule-2"] } }; -const llmNestedResult = await retrieve({ - graph, - userMessage: "嵌套 JSON 结构也应该兼容", - recentMessages: [], - embeddingConfig: {}, - schema, - options: { - topK: 4, - maxRecallNodes: 2, - enableVectorPrefilter: true, - enableGraphDiffusion: false, - enableLLMRecall: true, - llmCandidatePool: 2, - }, -}); -assert.deepEqual(Array.from(llmNestedResult.selectedNodeIds), ["rule-2"]); -assert.equal(llmNestedResult.meta.retrieval.llm.status, "llm"); - state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0;