From 05482f361c89bf7824a0dfce090c795d454f101a Mon Sep 17 00:00:00 2001 From: magicalastrogy Date: Mon, 22 Jun 2026 18:07:01 +0800 Subject: [PATCH 1/3] Add worldbook entry regex filters --- src/function/request/entry_comment_regex.ts | 52 ++++++ src/function/request/filter_entries.ts | 55 ++++++ src/panel/update/Request.vue | 63 ++++++- src/store.ts | 2 + tests/prompt_filter.test.ts | 187 ++++++++++++++++++++ tests/store_response_format.test.ts | 23 +++ 6 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 src/function/request/entry_comment_regex.ts diff --git a/src/function/request/entry_comment_regex.ts b/src/function/request/entry_comment_regex.ts new file mode 100644 index 0000000..13828fa --- /dev/null +++ b/src/function/request/entry_comment_regex.ts @@ -0,0 +1,52 @@ +export type EntryCommentRegexCompileResult = { + regex?: RegExp; + error?: string; +}; + +function isEscaped(value: string, index: number): boolean { + let backslash_count = 0; + for (let i = index - 1; i >= 0 && value[i] === '\\'; i--) { + backslash_count++; + } + return backslash_count % 2 === 1; +} + +function findClosingSlash(value: string): number { + for (let i = value.length - 1; i > 0; i--) { + if (value[i] === '/' && !isEscaped(value, i)) { + return i; + } + } + return -1; +} + +export function compileEntryCommentRegex(value: string): EntryCommentRegexCompileResult { + const trimmed = value.trim(); + if (trimmed === '') { + return {}; + } + + try { + if (trimmed.startsWith('/')) { + const closing_slash = findClosingSlash(trimmed); + if (closing_slash <= 0) { + throw new Error('JS 风格正则需要以 /pattern/flags 形式书写'); + } + return { + regex: new RegExp( + trimmed.slice(1, closing_slash), + trimmed.slice(closing_slash + 1) + ), + }; + } + + return { regex: new RegExp(trimmed) }; + } catch (error) { + return { error: error instanceof Error ? error.message : String(error) }; + } +} + +export function testEntryCommentRegex(regex: RegExp, comment: string): boolean { + regex.lastIndex = 0; + return regex.test(comment); +} diff --git a/src/function/request/filter_entries.ts b/src/function/request/filter_entries.ts index d978952..9d8b75c 100644 --- a/src/function/request/filter_entries.ts +++ b/src/function/request/filter_entries.ts @@ -1,5 +1,9 @@ import { isExtraModelSupported } from '@/function/is_extra_model_supported'; import { isFunctionCallingSupported } from '@/function/is_function_calling_supported'; +import { + compileEntryCommentRegex, + testEntryCommentRegex, +} from '@/function/request/entry_comment_regex'; import { useDataStore } from '@/store'; import { PLOT_REGEX, UPDATE_REGEX } from '@/variable_def'; @@ -75,4 +79,55 @@ export async function filterEntries(lores: { .value(); store.runtimes.unsupported_warnings = Array.from(removed_worlds).join(', '); + + if (!store.runtimes.is_during_extra_analysis) { + return; + } + + const compile_filter_regex = (label: string, value: string) => { + const result = compileEntryCommentRegex(value); + if (result.error) { + toastr.warning( + `${label}无效,已在本轮世界书条目过滤中忽略:${result.error}`, + '[MVU]世界书条目过滤正则无效', + { timeOut: 5000 } + ); + } + return result.regex; + }; + + const white_regex = compile_filter_regex( + '白名单正则', + store.settings.额外模型解析配置.世界书条目白名单正则 + ); + const black_regex = compile_filter_regex( + '黑名单正则', + store.settings.额外模型解析配置.世界书条目黑名单正则 + ); + + if (!white_regex && !black_regex) { + return; + } + + const should_remove_by_comment_regex = (entry: Record) => { + const comment = String(entry.comment ?? ''); + if (UPDATE_REGEX.test(comment)) { + return false; + } + if (white_regex && !testEntryCommentRegex(white_regex, comment)) { + return true; + } + if (black_regex && testEntryCommentRegex(black_regex, comment)) { + return true; + } + return false; + }; + + const apply_comment_regex_filter = (lore: Record[]) => { + _.remove(lore, should_remove_by_comment_regex); + }; + apply_comment_regex_filter(lores.characterLore); + apply_comment_regex_filter(lores.globalLore); + apply_comment_regex_filter(lores.chatLore); + apply_comment_regex_filter(lores.personaLore); } diff --git a/src/panel/update/Request.vue b/src/panel/update/Request.vue index 358ece8..7e26f27 100644 --- a/src/panel/update/Request.vue +++ b/src/panel/update/Request.vue @@ -37,10 +37,45 @@ 启用 + + + + +
+ {{ whitelist_regex_error }} +
+
+ + + + +
+ {{ blacklist_regex_error }} +
+
+ + diff --git a/src/store.ts b/src/store.ts index e7881cc..d232537 100644 --- a/src/store.ts +++ b/src/store.ts @@ -109,6 +109,8 @@ const NewSettings = z ]) .default('依次请求,失败后重试'), 请求次数: z.number().default(3), + 世界书条目白名单正则: z.string().default(''), + 世界书条目黑名单正则: z.string().default(''), 模型来源: z.enum(['与插头相同', '自定义']).default('与插头相同'), api地址: z.string().default('http://localhost:1234/v1'), diff --git a/tests/prompt_filter.test.ts b/tests/prompt_filter.test.ts index 9736f51..a25cd09 100644 --- a/tests/prompt_filter.test.ts +++ b/tests/prompt_filter.test.ts @@ -13,6 +13,8 @@ describe('filterEntries', () => { const store = useDataStore(); store.settings.更新方式 = '额外模型解析'; store.settings.额外模型解析配置.应答格式 = '聊天消息'; + store.settings.额外模型解析配置.世界书条目白名单正则 = ''; + store.settings.额外模型解析配置.世界书条目黑名单正则 = ''; store.runtimes.unsupported_warnings = ''; store.runtimes.is_during_extra_analysis = false; @@ -203,4 +205,189 @@ describe('filterEntries', () => { ]); expect(store.runtimes.unsupported_warnings).toBe('UntaggedWorld'); }); + + test('applies whitelist regex to entry comments during extra analysis', async () => { + const store = useDataStore(); + + store.runtimes.is_during_extra_analysis = true; + store.settings.额外模型解析配置.世界书条目白名单正则 = '角色A'; + + const lores = { + globalLore: [ + makeEntry('PlotWorld', '[mvu_plot]'), + makeEntry('PlotWorld', '角色A设定'), + makeEntry('PlotWorld', '地点B设定'), + ], + characterLore: [makeEntry('WorldA', '[mvu_update]')], + chatLore: [], + personaLore: [], + }; + + mockGetLorebookEntries.mockResolvedValue(cloneEntries(lores.characterLore)); + + await filterEntries(lores); + + expect(lores.globalLore).toEqual([makeEntry('PlotWorld', '角色A设定')]); + }); + + test('applies blacklist regex to entry comments during extra analysis', async () => { + const store = useDataStore(); + + store.runtimes.is_during_extra_analysis = true; + store.settings.额外模型解析配置.世界书条目黑名单正则 = '禁用|临时'; + + const lores = { + globalLore: [ + makeEntry('PlotWorld', '[mvu_plot]'), + makeEntry('PlotWorld', '常驻设定'), + makeEntry('PlotWorld', '临时设定'), + ], + characterLore: [makeEntry('WorldA', '[mvu_update]')], + chatLore: [], + personaLore: [], + }; + + mockGetLorebookEntries.mockResolvedValue(cloneEntries(lores.characterLore)); + + await filterEntries(lores); + + expect(lores.globalLore).toEqual([makeEntry('PlotWorld', '常驻设定')]); + }); + + test('requires whitelist match and blacklist miss when both regexes are configured', async () => { + const store = useDataStore(); + + store.runtimes.is_during_extra_analysis = true; + store.settings.额外模型解析配置.世界书条目白名单正则 = '角色|地点'; + store.settings.额外模型解析配置.世界书条目黑名单正则 = '地点'; + + const lores = { + globalLore: [ + makeEntry('PlotWorld', '[mvu_plot]'), + makeEntry('PlotWorld', '角色设定'), + makeEntry('PlotWorld', '地点设定'), + makeEntry('PlotWorld', '物品设定'), + ], + characterLore: [makeEntry('WorldA', '[mvu_update]')], + chatLore: [], + personaLore: [], + }; + + mockGetLorebookEntries.mockResolvedValue(cloneEntries(lores.characterLore)); + + await filterEntries(lores); + + expect(lores.globalLore).toEqual([makeEntry('PlotWorld', '角色设定')]); + }); + + test('lets update entries bypass comment whitelist and blacklist filters', async () => { + const store = useDataStore(); + + store.runtimes.is_during_extra_analysis = true; + store.settings.额外模型解析配置.世界书条目白名单正则 = '角色A'; + store.settings.额外模型解析配置.世界书条目黑名单正则 = 'mvu_update|禁用'; + + const lores = { + globalLore: [ + makeEntry('PlotWorld', '[mvu_plot]'), + makeEntry('PlotWorld', '[mvu_update] 禁用'), + makeEntry('PlotWorld', '禁用设定'), + ], + characterLore: [makeEntry('WorldA', '[mvu_update]')], + chatLore: [], + personaLore: [], + }; + + mockGetLorebookEntries.mockResolvedValue(cloneEntries(lores.characterLore)); + + await filterEntries(lores); + + expect(lores.globalLore).toEqual([makeEntry('PlotWorld', '[mvu_update] 禁用')]); + expect(lores.characterLore).toEqual([makeEntry('WorldA', '[mvu_update]')]); + }); + + test('does not apply comment whitelist and blacklist filters outside extra analysis', async () => { + const store = useDataStore(); + + store.runtimes.is_during_extra_analysis = false; + store.settings.额外模型解析配置.世界书条目白名单正则 = '保留'; + store.settings.额外模型解析配置.世界书条目黑名单正则 = '排除'; + + const lores = { + globalLore: [ + makeEntry('PlotWorld', '[mvu_plot]'), + makeEntry('PlotWorld', '排除设定'), + makeEntry('PlotWorld', '其他设定'), + ], + characterLore: [makeEntry('WorldA', '[mvu_plot]')], + chatLore: [], + personaLore: [], + }; + + mockGetLorebookEntries.mockResolvedValue(cloneEntries(lores.characterLore)); + + await filterEntries(lores); + + expect(lores.globalLore).toEqual([ + makeEntry('PlotWorld', '[mvu_plot]'), + makeEntry('PlotWorld', '排除设定'), + makeEntry('PlotWorld', '其他设定'), + ]); + }); + + test('supports JS-style slash regex with flags', async () => { + const store = useDataStore(); + + store.runtimes.is_during_extra_analysis = true; + store.settings.额外模型解析配置.世界书条目白名单正则 = '/ALLOWED/i'; + store.settings.额外模型解析配置.世界书条目黑名单正则 = '/drop/i'; + + const lores = { + globalLore: [ + makeEntry('PlotWorld', '[mvu_plot]'), + makeEntry('PlotWorld', 'allowed entry'), + makeEntry('PlotWorld', 'ALLOWED drop entry'), + makeEntry('PlotWorld', 'other entry'), + ], + characterLore: [makeEntry('WorldA', '[mvu_update]')], + chatLore: [], + personaLore: [], + }; + + mockGetLorebookEntries.mockResolvedValue(cloneEntries(lores.characterLore)); + + await filterEntries(lores); + + expect(lores.globalLore).toEqual([makeEntry('PlotWorld', 'allowed entry')]); + }); + + test('ignores invalid regexes without interrupting valid filters', async () => { + const store = useDataStore(); + + store.runtimes.is_during_extra_analysis = true; + store.settings.额外模型解析配置.世界书条目白名单正则 = '['; + store.settings.额外模型解析配置.世界书条目黑名单正则 = 'drop'; + + const lores = { + globalLore: [ + makeEntry('PlotWorld', '[mvu_plot]'), + makeEntry('PlotWorld', 'keep entry'), + makeEntry('PlotWorld', 'drop entry'), + ], + characterLore: [makeEntry('WorldA', '[mvu_update]')], + chatLore: [], + personaLore: [], + }; + + mockGetLorebookEntries.mockResolvedValue(cloneEntries(lores.characterLore)); + + await expect(filterEntries(lores)).resolves.toBeUndefined(); + + expect(lores.globalLore).toEqual([makeEntry('PlotWorld', 'keep entry')]); + expect((globalThis as any).toastr.warning).toHaveBeenCalledWith( + expect.stringContaining('白名单正则无效'), + '[MVU]世界书条目过滤正则无效', + { timeOut: 5000 } + ); + }); }); diff --git a/tests/store_response_format.test.ts b/tests/store_response_format.test.ts index a0995f5..6aa8c51 100644 --- a/tests/store_response_format.test.ts +++ b/tests/store_response_format.test.ts @@ -64,6 +64,29 @@ describe('extra model response format settings', () => { expect(store.settings.额外模型解析配置.max_chat_history).toBe(2); }); + test('defaults worldbook entry comment filters to disabled empty regexes', () => { + const store = useDataStore(); + + expect(store.settings.额外模型解析配置.世界书条目白名单正则).toBe(''); + expect(store.settings.额外模型解析配置.世界书条目黑名单正则).toBe(''); + }); + + test('keeps configured worldbook entry comment filters when loading settings', () => { + (globalThis as any).SillyTavern.extensionSettings = { + mvu_settings: { + 额外模型解析配置: { + 世界书条目白名单正则: '角色|地点', + 世界书条目黑名单正则: '/临时/i', + }, + }, + }; + + const store = useDataStore(); + + expect(store.settings.额外模型解析配置.世界书条目白名单正则).toBe('角色|地点'); + expect(store.settings.额外模型解析配置.世界书条目黑名单正则).toBe('/临时/i'); + }); + test('defaults v4 compatible thinking override to disabled state off', () => { const store = useDataStore(); From c9936defe58c91b35f9749402992be3f44e930c7 Mon Sep 17 00:00:00 2001 From: magicalastrogy Date: Mon, 22 Jun 2026 18:36:54 +0800 Subject: [PATCH 2/3] Show worldbook filter results in UI --- src/function/request/entry_comment_regex.ts | 16 +++++ src/function/request/filter_entries.ts | 49 +++++++++++---- src/panel/update/Request.vue | 61 ++++++++++++++++++ src/store.ts | 10 +++ tests/prompt_filter.test.ts | 68 +++++++++++++++++++++ 5 files changed, 193 insertions(+), 11 deletions(-) diff --git a/src/function/request/entry_comment_regex.ts b/src/function/request/entry_comment_regex.ts index 13828fa..d7d6937 100644 --- a/src/function/request/entry_comment_regex.ts +++ b/src/function/request/entry_comment_regex.ts @@ -3,6 +3,22 @@ export type EntryCommentRegexCompileResult = { error?: string; }; +export type EntryCommentFilterResult = { + lore: 'globalLore' | 'characterLore' | 'chatLore' | 'personaLore'; + world: string; + comment: string; + reason: '白名单' | '黑名单'; +}; + +export const ENTRY_COMMENT_FILTER_LOG_TITLE = '[MVU]世界书条目黑/白名单筛选结果'; + +export function logEntryCommentFilterResult(result: EntryCommentFilterResult[]) { + console.log( + ENTRY_COMMENT_FILTER_LOG_TITLE, + result.map(entry => ({ ...entry })) + ); +} + function isEscaped(value: string, index: number): boolean { let backslash_count = 0; for (let i = index - 1; i >= 0 && value[i] === '\\'; i--) { diff --git a/src/function/request/filter_entries.ts b/src/function/request/filter_entries.ts index 9d8b75c..7d97ef4 100644 --- a/src/function/request/filter_entries.ts +++ b/src/function/request/filter_entries.ts @@ -2,6 +2,8 @@ import { isExtraModelSupported } from '@/function/is_extra_model_supported'; import { isFunctionCallingSupported } from '@/function/is_function_calling_supported'; import { compileEntryCommentRegex, + EntryCommentFilterResult, + logEntryCommentFilterResult, testEntryCommentRegex, } from '@/function/request/entry_comment_regex'; import { useDataStore } from '@/store'; @@ -15,6 +17,9 @@ export async function filterEntries(lores: { }) { const store = useDataStore(); store.runtimes.unsupported_warnings = ''; + if (store.runtimes.is_during_extra_analysis) { + store.runtimes.上次世界书条目过滤结果 = []; + } //在这个回调中,会将所有lore的条目传入,此处可以去除所有 [mvu_update] 相关的条目,避免在非更新的轮次中输出相关内容。 if (store.settings.更新方式 === '随AI输出') { @@ -109,25 +114,47 @@ export async function filterEntries(lores: { return; } - const should_remove_by_comment_regex = (entry: Record) => { + const filtered_entries: EntryCommentFilterResult[] = []; + + const get_comment_filter_reason = (entry: Record) => { const comment = String(entry.comment ?? ''); if (UPDATE_REGEX.test(comment)) { - return false; + return undefined; } if (white_regex && !testEntryCommentRegex(white_regex, comment)) { - return true; + return '白名单'; } if (black_regex && testEntryCommentRegex(black_regex, comment)) { - return true; + return '黑名单'; } - return false; + return undefined; }; - const apply_comment_regex_filter = (lore: Record[]) => { - _.remove(lore, should_remove_by_comment_regex); + const apply_comment_regex_filter = ( + lore_name: EntryCommentFilterResult['lore'], + lore: Record[] + ) => { + _.remove(lore, entry => { + const reason = get_comment_filter_reason(entry); + if (!reason) { + return false; + } + filtered_entries.push({ + lore: lore_name, + world: String(entry.world ?? ''), + comment: String(entry.comment ?? ''), + reason, + }); + return true; + }); }; - apply_comment_regex_filter(lores.characterLore); - apply_comment_regex_filter(lores.globalLore); - apply_comment_regex_filter(lores.chatLore); - apply_comment_regex_filter(lores.personaLore); + apply_comment_regex_filter('characterLore', lores.characterLore); + apply_comment_regex_filter('globalLore', lores.globalLore); + apply_comment_regex_filter('chatLore', lores.chatLore); + apply_comment_regex_filter('personaLore', lores.personaLore); + + store.runtimes.上次世界书条目过滤结果 = filtered_entries; + if (filtered_entries.length > 0) { + logEntryCommentFilterResult(filtered_entries); + } } diff --git a/src/panel/update/Request.vue b/src/panel/update/Request.vue index 7e26f27..87e23d9 100644 --- a/src/panel/update/Request.vue +++ b/src/panel/update/Request.vue @@ -71,6 +71,15 @@ {{ blacklist_regex_error }} + +
+ +
@@ -101,6 +110,48 @@ const blacklist_regex_error = computed(() => getRegexError(store.settings.额外模型解析配置.世界书条目黑名单正则) ); +function showLastFilteredEntriesPopup() { + const result = store.runtimes.上次世界书条目过滤结果; + const content = + result.length === 0 + ? '

上次分析被筛选的条目

上次分析没有被黑/白名单筛选掉的条目。

' + : ` +
+

上次分析被筛选的条目

+ + + + + + + + + + + ${result + .map( + entry => ` + + + + + + + ` + ) + .join('')} + +
来源世界书原因comment
${_.escape(entry.lore)}${_.escape(entry.world)}${_.escape(entry.reason)}${_.escape(entry.comment)}
+
+ `; + + SillyTavern.callGenericPopup(content, SillyTavern.POPUP_TYPE.TEXT, '', { + allowVerticalScrolling: true, + leftAlign: true, + wide: true, + }); +} + watch( () => store.settings.额外模型解析配置.请求方式, value => { @@ -127,4 +178,14 @@ watch( line-height: 1.35; word-break: break-word; } + +.mvu-regex-actions { + display: flex; + padding: 0 0.6rem 0.45rem; +} + +.mvu-regex-actions__button { + min-height: 2rem; + white-space: normal; +} diff --git a/src/store.ts b/src/store.ts index d232537..c556930 100644 --- a/src/store.ts +++ b/src/store.ts @@ -193,6 +193,16 @@ const Runtimes = z unsupported_warnings: z.string().default(''), is_during_extra_analysis: z.boolean().default(false), is_function_call_enabled: z.boolean().default(false), + 上次世界书条目过滤结果: z + .array( + z.object({ + lore: z.enum(['globalLore', 'characterLore', 'chatLore', 'personaLore']), + world: z.string(), + comment: z.string(), + reason: z.enum(['白名单', '黑名单']), + }) + ) + .default([]), debug: z .object({ 首次额外请求必失败: z.boolean().default(false), diff --git a/tests/prompt_filter.test.ts b/tests/prompt_filter.test.ts index a25cd09..5137f32 100644 --- a/tests/prompt_filter.test.ts +++ b/tests/prompt_filter.test.ts @@ -1,3 +1,4 @@ +import { ENTRY_COMMENT_FILTER_LOG_TITLE } from '@/function/request/entry_comment_regex'; import { filterEntries } from '@/function/request/filter_entries'; import { useDataStore } from '@/store'; @@ -7,6 +8,7 @@ const cloneEntries = (entries: Array<{ world: string; comment: string }>) => let mockGetCurrentCharPrimaryLorebook: jest.MockedFunction<() => string | undefined>; let mockGetLorebookEntries: jest.MockedFunction<(name: string) => Promise>; +let consoleLogSpy: jest.SpyInstance; describe('filterEntries', () => { beforeEach(() => { @@ -18,6 +20,8 @@ describe('filterEntries', () => { store.runtimes.unsupported_warnings = ''; store.runtimes.is_during_extra_analysis = false; + store.runtimes.上次世界书条目过滤结果 = []; + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); (globalThis as any).toastr = { warning: jest.fn(), @@ -42,6 +46,7 @@ describe('filterEntries', () => { afterEach(() => { useDataStore().runtimes.is_during_extra_analysis = false; + consoleLogSpy.mockRestore(); }); // 场景: 更新方式为随AI输出时,不进行任何过滤处理 @@ -228,6 +233,22 @@ describe('filterEntries', () => { await filterEntries(lores); expect(lores.globalLore).toEqual([makeEntry('PlotWorld', '角色A设定')]); + expect(store.runtimes.上次世界书条目过滤结果).toEqual([ + { + lore: 'globalLore', + world: 'PlotWorld', + comment: '地点B设定', + reason: '白名单', + }, + ]); + expect(consoleLogSpy).toHaveBeenCalledWith(ENTRY_COMMENT_FILTER_LOG_TITLE, [ + { + lore: 'globalLore', + world: 'PlotWorld', + comment: '地点B设定', + reason: '白名单', + }, + ]); }); test('applies blacklist regex to entry comments during extra analysis', async () => { @@ -278,6 +299,53 @@ describe('filterEntries', () => { await filterEntries(lores); expect(lores.globalLore).toEqual([makeEntry('PlotWorld', '角色设定')]); + expect(store.runtimes.上次世界书条目过滤结果).toEqual([ + { + lore: 'globalLore', + world: 'PlotWorld', + comment: '地点设定', + reason: '黑名单', + }, + { + lore: 'globalLore', + world: 'PlotWorld', + comment: '物品设定', + reason: '白名单', + }, + ]); + }); + + test('keeps empty last filter result and does not log when no entry is removed', async () => { + const store = useDataStore(); + + store.runtimes.is_during_extra_analysis = true; + store.runtimes.上次世界书条目过滤结果 = [ + { + lore: 'globalLore', + world: 'OldWorld', + comment: 'old', + reason: '黑名单', + }, + ]; + store.settings.额外模型解析配置.世界书条目白名单正则 = '角色'; + + const lores = { + globalLore: [makeEntry('PlotWorld', '[mvu_plot]'), makeEntry('PlotWorld', '角色设定')], + characterLore: [makeEntry('WorldA', '[mvu_update]')], + chatLore: [], + personaLore: [], + }; + + mockGetLorebookEntries.mockResolvedValue(cloneEntries(lores.characterLore)); + + await filterEntries(lores); + + expect(lores.globalLore).toEqual([makeEntry('PlotWorld', '角色设定')]); + expect(store.runtimes.上次世界书条目过滤结果).toEqual([]); + expect(consoleLogSpy).not.toHaveBeenCalledWith( + ENTRY_COMMENT_FILTER_LOG_TITLE, + expect.anything() + ); }); test('lets update entries bypass comment whitelist and blacklist filters', async () => { From 2e7944b6ef11efb8f08cea6d13db5cdbb55a3b88 Mon Sep 17 00:00:00 2001 From: MagicalAstrogy <103271693+MagicalAstrogy@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:01:14 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++ src/panel/update/Prompt.vue | 119 ++++++++++++++++++++++++++++++++++- src/panel/update/Request.vue | 119 +---------------------------------- 3 files changed, 125 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e73a0e3..ceb4899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2026-06-22 +## 体验改进 + - 现在支持关闭内置破限的随机头部(使用Gemini系模型时,需要打开),以获得更好的缓存效果。 + - 现在支持使用 白名单/黑名单 筛选参与额外解析的世界书。 + - (请注意,这个功能主要面向玩家,角色卡作者请使用 `[mvu_update]` `[mvu_plot]` 达到相同的效果。 + # 2026-06-20 ## 体验改进 - 现在支持调整在额外模型解析过程中,提供的消息楼层数。默认为 2。 diff --git a/src/panel/update/Prompt.vue b/src/panel/update/Prompt.vue index 6b52f01..5f19679 100644 --- a/src/panel/update/Prompt.vue +++ b/src/panel/update/Prompt.vue @@ -51,12 +51,56 @@ 启用 + + + + +
+ {{ whitelist_regex_error }} +
+
+ + + + +
+ {{ blacklist_regex_error }} +
+
+ +
+ +
+ + diff --git a/src/panel/update/Request.vue b/src/panel/update/Request.vue index 87e23d9..952d486 100644 --- a/src/panel/update/Request.vue +++ b/src/panel/update/Request.vue @@ -37,54 +37,10 @@ 启用 - - - - -
- {{ whitelist_regex_error }} -
-
- - - - -
- {{ blacklist_regex_error }} -
-
- -
- -
- -