规则列表标出不可达行,「分流规则」改名「全局规则」

MATCH 的实现是无条件返回 true(rules/common/final.go:16-18),第一条
MATCH 之后的行永远轮不到。全局规则和附加规则的列表都把这类行标红、
拦路的 MATCH 标黄,hover 给原因;表底作兜底的 MATCH 不标黄,那是正常
用法。附加规则里用户自己的 MATCH 总会架空表底的自动兜底,兜底行跟着
标红。

规则页的「分流规则」改名「全局规则」,和「附加规则」对仗;术语表里
「分流规则」退为两者的统称。

顺手修 ini 导入的兜底判断:firstMatchIndex 没有 MATCH 时返回 -1,
之前误写成 === null,条件恒假,配方的兜底规则从来没被导入过。
This commit is contained in:
2026-08-12 13:43:38 +08:00
parent f57eb55328
commit 57690a159c
3 changed files with 85 additions and 21 deletions
+39 -5
View File
@@ -20,7 +20,7 @@ import {
proxyGroupName,
subRuleName,
} from '@shared/inbound-groups.ts'
import { missingLanRules } from '@shared/rules.ts'
import { firstMatchIndex, missingLanRules } from '@shared/rules.ts'
import type { GroupSpec, OutboundSetup, PanelGroup } from '@shared/inbound-groups.ts'
import { resolveChain } from '@shared/outbound-chain.ts'
import { latestDelay } from '../api'
@@ -84,6 +84,12 @@ const foreignRuleSet = computed(
/** 内网直连还缺几条。全齐了按钮就不出现 */
const lanMissing = computed(() => missingLanRules(props.subRules).length)
/**
* 用户自己写的第一条 MATCH 的下标,没有时是 -1。它之后的行——包括表底的
* 自动兜底——永远轮不到,标红;它自己标黄,hover 说明原因。
*/
const subMatchAt = computed(() => firstMatchIndex(props.subRules))
/** 默认入站由配置顶层的端口字段声明,没有 proxy 字段可写,也不能删。 */
const locked = computed(() => props.inbound.isDefault)
@@ -717,8 +723,22 @@ const complete = computed(
</h3>
<ul class="srules">
<li v-for="(rule, i) in subRules" :key="i" class="srule">
<span class="mono s-text" :title="rule">{{ rule }}</span>
<li
v-for="(rule, i) in subRules"
:key="i"
:class="['srule', subMatchAt >= 0 && i > subMatchAt && 'is-dead', i === subMatchAt && 'is-culprit']"
>
<span
class="mono s-text"
:title="
i === subMatchAt
? 'MATCH 命中所有流量,它之后的规则永远不会被检查'
: subMatchAt >= 0 && i > subMatchAt
? '排在 MATCH 之后,永远不会被检查'
: rule
"
>{{ rule }}</span
>
<span class="s-acts">
<button
type="button"
@@ -751,8 +771,12 @@ const complete = computed(
</span>
</li>
<li
class="srule is-final"
title="兜底:没被上面的规则分走的流量走这一路自己的出站。这一条由 Patchbay 自动维护"
:class="['srule is-final', subMatchAt >= 0 && 'is-dead']"
:title="
subMatchAt >= 0
? '上面的 MATCH 已拦下所有流量,这条自动兜底不会再被使用'
: '兜底:没被上面的规则分走的流量走这一路自己的出站。这一条由 Patchbay 自动维护'
"
>
<span class="mono s-text">MATCH,{{ proxyGroupName(inbound.name) }}</span>
</li>
@@ -949,6 +973,16 @@ const complete = computed(
color: var(--c-text-faint);
}
/* 不可达:在用户自己的 MATCH 之后(含自动兜底),永远轮不到 */
.srule.is-dead .s-text {
color: var(--c-danger);
}
/* 拦路的那条 MATCH:它后面的行全是死行 */
.srule.is-culprit .s-text {
color: var(--c-warn);
}
/* 操作按钮悬停到行上才现身,规则文本是主角 */
.s-acts {
flex: none;
+41 -15
View File
@@ -52,10 +52,21 @@ const emit = defineEmits<{ saved: [] }>()
const clone = () => JSON.parse(JSON.stringify(props.data)) as ConfigObject
const rules = computed(() => rulesOf(props.data))
/** 每行拆好的样子,模板直接读,不在渲染里反复拆。 */
const rows = computed(() =>
rules.value.map((rule, index) => ({ index, raw: rule, ...parseRule(rule) })),
)
/**
* 每行拆好的样子,模板直接读,不在渲染里反复拆。
* culprit 是拦路的那条 MATCH(后面还有行时才算,作为表底兜底是正常用法);
* dead 是被它拦住、永远轮不到的行。
*/
const rows = computed(() => {
const at = firstMatchIndex(rules.value)
return rules.value.map((rule, index) => ({
index,
raw: rule,
...parseRule(rule),
culprit: index === at && index < rules.value.length - 1,
dead: at >= 0 && index > at,
}))
})
const providers = computed(() =>
Object.entries(ruleProvidersOf(props.data)).map(([name, decl]) => ({
name,
@@ -88,7 +99,7 @@ const targetOptions = computed(() => [
...outbounds.value.ownProxies,
])
/** 第一条 MATCH 的下标。它之后的行标成不可达。 */
/** 第一条 MATCH 的下标,没有时是 -1。ini 导入拿它判断要不要收配方的兜底。 */
const matchAt = computed(() => firstMatchIndex(rules.value))
/** 断引用按行归好,模板里直接查。 */
@@ -131,7 +142,7 @@ function withRules(next: string[]): ConfigObject {
return data
}
// ── 分流规则 ────────────────────────────────────
// ── 全局规则 ────────────────────────────────────
const ruleFormOpen = ref(false)
/** 编辑时是下标,新增时是 null */
@@ -177,7 +188,7 @@ async function move(index: number, delta: -1 | 1) {
async function removeRule(index: number) {
const rule = rules.value[index]
if (rule === undefined || saving.value) return
await commit(withRules(rules.value.filter((_, i) => i !== index)), `已删除规则 [ ${rule} ]`)
await commit(withRules(rules.value.filter((_, i) => i !== index)), `已删除全局规则 [ ${rule} ]`)
}
async function applyLanPreset() {
@@ -265,8 +276,9 @@ async function submitImport(plan: IniImportPlan) {
const next = clone()
setRuleProviders(next, { ...ruleProvidersOf(next), ...plan.providers })
// 表里已有 MATCH 时丢弃配方的兜底:那是你自己安排的分流终点,配方不该
// 偷偷替换;追加在 MATCH 之后则永远是死行
const keepFinals = matchAt.value === null
// 偷偷替换;追加在 MATCH 之后则永远是死行。firstMatchIndex 没有时给 -1,
// 之前误写成 === null,配方兜底从来没被导入过
const keepFinals = matchAt.value < 0
const finals = keepFinals ? plan.finals : []
setRules(next, [...insertBeforeMatch(rules.value, plan.rules), ...finals])
const added = plan.rules.length + finals.length
@@ -350,7 +362,7 @@ async function updateNow(name: string) {
<div class="sums">
<div class="sum">
<span class="sum-n">{{ rules.length }}</span>
<span class="sum-k">分流规则</span>
<span class="sum-k">全局规则</span>
</div>
<div class="sum">
<span class="sum-n">{{ providers.length }}</span>
@@ -361,7 +373,7 @@ async function updateNow(name: string) {
<section class="sec">
<header class="sec-head">
<h2>分流规则</h2>
<h2>全局规则</h2>
<div class="sec-acts">
<button
v-if="missingLan.length > 0 && rules.length > 0"
@@ -382,7 +394,16 @@ async function updateNow(name: string) {
<ol v-if="rules.length > 0" class="rows">
<li v-for="row in rows" :key="row.index">
<div :class="['line', matchAt >= 0 && row.index > matchAt && 'is-dead']">
<div
:class="['line', row.dead && 'is-dead', row.culprit && 'is-culprit']"
:title="
row.culprit
? 'MATCH 命中所有流量,它之后的规则永远不会被检查'
: row.dead
? '排在 MATCH 之后,永远不会被检查'
: undefined
"
>
<span class="idx mono">{{ row.index + 1 }}</span>
<span class="rtype mono">{{ row.type }}</span>
<span class="payload mono">
@@ -400,7 +421,6 @@ async function updateNow(name: string) {
{{ row.target }}
</span>
<span v-if="issuesByIndex.has(row.index)" class="tag tag-bad">断引用</span>
<span v-if="matchAt >= 0 && row.index > matchAt" class="tag">不可达</span>
</span>
<span class="acts">
<button
@@ -443,7 +463,7 @@ async function updateNow(name: string) {
</div>
</li>
</ol>
<p v-else class="empty">没有分流规则,走规则的流量都会直连。</p>
<p v-else class="empty">没有全局规则,走规则的流量都会直连。</p>
</section>
<section class="sec">
@@ -688,7 +708,13 @@ async function updateNow(name: string) {
.is-dead .rtype,
.is-dead .payload,
.is-dead .rtarget {
color: var(--c-text-faint);
color: var(--c-danger);
}
/* 拦路的那条 MATCH:它后面还有行,后面全是死行 */
.is-culprit .rtype,
.is-culprit .rtarget {
color: var(--c-warn);
}
.bad {