Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 2026-06-22
## 体验改进
- 现在支持关闭内置破限的随机头部(使用Gemini系模型时,需要打开),以获得更好的缓存效果。
- 现在支持使用 白名单/黑名单 筛选参与额外解析的世界书。
- (请注意,这个功能主要面向玩家,角色卡作者请使用 `[mvu_update]` `[mvu_plot]` 达到相同的效果。

# 2026-06-20
## 体验改进
- 现在支持调整在额外模型解析过程中,提供的消息楼层数。默认为 2。
Expand Down
68 changes: 68 additions & 0 deletions src/function/request/entry_comment_regex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
export type EntryCommentRegexCompileResult = {
regex?: RegExp;
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--) {
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);
}
82 changes: 82 additions & 0 deletions src/function/request/filter_entries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
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';
import { PLOT_REGEX, UPDATE_REGEX } from '@/variable_def';

Expand All @@ -11,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输出') {
Expand Down Expand Up @@ -75,4 +84,77 @@ 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 filtered_entries: EntryCommentFilterResult[] = [];

const get_comment_filter_reason = (entry: Record<string, any>) => {
const comment = String(entry.comment ?? '');
if (UPDATE_REGEX.test(comment)) {
return undefined;
}
if (white_regex && !testEntryCommentRegex(white_regex, comment)) {
return '白名单';
}
if (black_regex && testEntryCommentRegex(black_regex, comment)) {
return '黑名单';
}
return undefined;
};

const apply_comment_regex_filter = (
lore_name: EntryCommentFilterResult['lore'],
lore: Record<string, any>[]
) => {
_.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('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);
}
}
119 changes: 118 additions & 1 deletion src/panel/update/Prompt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,56 @@
<span>启用</span>
</Checkbox>
</Field>

<Field label="世界书条目白名单正则">
<template #label-suffix>
<HelpIcon
help="留空关闭;非空时,额外模型解析阶段只保留 comment 匹配该正则的世界书条目。支持 角色|地点 或 /角色|地点/i。"
/>
</template>
<input
v-model="store.settings.额外模型解析配置.世界书条目白名单正则"
type="text"
class="text_pole"
placeholder="角色|地点 或 /角色|地点/i"
/>
<div v-if="whitelist_regex_error" class="mvu-regex-error">
{{ whitelist_regex_error }}
</div>
</Field>

<Field label="世界书条目黑名单正则">
<template #label-suffix>
<HelpIcon
help="留空关闭;非空时,额外模型解析阶段会排除 comment 匹配该正则的世界书条目。支持 临时|禁用 或 /临时|禁用/i。"
/>
</template>
<input
v-model="store.settings.额外模型解析配置.世界书条目黑名单正则"
type="text"
class="text_pole"
placeholder="临时|禁用 或 /临时|禁用/i"
/>
<div v-if="blacklist_regex_error" class="mvu-regex-error">
{{ blacklist_regex_error }}
</div>
</Field>

<div class="mvu-regex-actions">
<input
class="mvu-regex-actions__button menu_button menu_button_icon interactable"
type="button"
value="查看上次分析被筛选的条目"
@click="showLastFilteredEntriesPopup"
/>
</div>
</Detail>
</template>

<script setup lang="ts">
import { getAvailableExtraModelPresetNames } from '@/function/update/extra_model_preset';
import { compileEntryCommentRegex } from '@/function/request/entry_comment_regex';
import { getFunctionCallingApiVersionUnsupportedMessage } from '@/function/is_function_calling_supported';
import { getAvailableExtraModelPresetNames } from '@/function/update/extra_model_preset';
import Checkbox from '@/panel/component/Checkbox.vue';
import Detail from '@/panel/component/Detail.vue';
import Field from '@/panel/component/Field.vue';
Expand All @@ -71,6 +115,18 @@ const store = useDataStore();
const available_preset_names = computed(() => getAvailableExtraModelPresetNames());
const response_format_options = [...EXTRA_MODEL_RESPONSE_FORMATS];

function getRegexError(value: string) {
const error = compileEntryCommentRegex(value).error;
return error ? `正则无效:${error}` : '';
}

const whitelist_regex_error = computed(() =>
getRegexError(store.settings.额外模型解析配置.世界书条目白名单正则)
);
const blacklist_regex_error = computed(() =>
getRegexError(store.settings.额外模型解析配置.世界书条目黑名单正则)
);

function ensureValidPresetSelection() {
if (store.settings.额外模型解析配置.破限方案 !== '使用其他预设') {
return;
Expand All @@ -86,6 +142,48 @@ function ensureValidPresetSelection() {
}
}

function showLastFilteredEntriesPopup() {
const result = store.runtimes.上次世界书条目过滤结果;
const content =
result.length === 0
? '<div><h3>上次分析被筛选的条目</h3><p>上次分析没有被黑/白名单筛选掉的条目。</p></div>'
: `
<div>
<h3>上次分析被筛选的条目</h3>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align: left; border-bottom: 1px solid currentColor; padding: 0.35rem;">来源</th>
<th style="text-align: left; border-bottom: 1px solid currentColor; padding: 0.35rem;">世界书</th>
<th style="text-align: left; border-bottom: 1px solid currentColor; padding: 0.35rem;">原因</th>
<th style="text-align: left; border-bottom: 1px solid currentColor; padding: 0.35rem;">comment</th>
</tr>
</thead>
<tbody>
${result
.map(
entry => `
<tr>
<td style="vertical-align: top; padding: 0.35rem;">${_.escape(entry.lore)}</td>
<td style="vertical-align: top; padding: 0.35rem;">${_.escape(entry.world)}</td>
<td style="vertical-align: top; padding: 0.35rem;">${_.escape(entry.reason)}</td>
<td style="vertical-align: top; padding: 0.35rem; word-break: break-word;">${_.escape(entry.comment)}</td>
</tr>
`
)
.join('')}
</tbody>
</table>
</div>
`;

SillyTavern.callGenericPopup(content, SillyTavern.POPUP_TYPE.TEXT, '', {
allowVerticalScrolling: true,
leftAlign: true,
wide: true,
});
}

watch(available_preset_names, ensureValidPresetSelection, { immediate: true });
watch(
() => store.settings.额外模型解析配置.破限方案,
Expand Down Expand Up @@ -134,3 +232,22 @@ watch(
}
);
</script>

<style scoped>
.mvu-regex-error {
color: var(--SmartThemeQuoteColor, #ff6b6b);
font-size: calc(var(--mainFontSize, 1rem) * 0.9);
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;
}
</style>
5 changes: 4 additions & 1 deletion src/panel/update/Request.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ const store = useDataStore();
watch(
() => store.settings.额外模型解析配置.请求方式,
value => {
if (value !== '依次请求,失败后重试' && compare(store.versions.tavernhelper, '4.4.3', '<')) {
if (
value !== '依次请求,失败后重试' &&
compare(store.versions.tavernhelper, '4.4.3', '<')
) {
toastr.warning(
'请升级酒馆助手到 4.4.3 或更高版本,否则批量请求功能可能让预设的「流式传输」设置失效',
'[MVU]批量请求可能有问题',
Expand Down
12 changes: 12 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -191,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),
Expand Down
Loading
Loading