规则列表标出不可达行,「分流规则」改名「全局规则」
MATCH 的实现是无条件返回 true(rules/common/final.go:16-18),第一条 MATCH 之后的行永远轮不到。全局规则和附加规则的列表都把这类行标红、 拦路的 MATCH 标黄,hover 给原因;表底作兜底的 MATCH 不标黄,那是正常 用法。附加规则里用户自己的 MATCH 总会架空表底的自动兜底,兜底行跟着 标红。 规则页的「分流规则」改名「全局规则」,和「附加规则」对仗;术语表里 「分流规则」退为两者的统称。 顺手修 ini 导入的兜底判断:firstMatchIndex 没有 MATCH 时返回 -1, 之前误写成 === null,条件恒假,配方的兜底规则从来没被导入过。
This commit is contained in:
+5
-1
@@ -85,9 +85,13 @@ _Avoid_: 无效引用、悬空引用、坏引用
|
|||||||
## 规则
|
## 规则
|
||||||
|
|
||||||
**分流规则**:
|
**分流规则**:
|
||||||
配置文件 `rules:` 里的有序列表,自上而下匹配,决定一条连接走哪个出站。顺序本身是语义的一部分。
|
自上而下匹配、决定一条连接走哪个出站的有序规则列表,第一条命中即生效。顺序本身是语义的一部分。全局规则和附加规则都是分流规则。
|
||||||
_Avoid_: 路由规则、规则表
|
_Avoid_: 路由规则、规则表
|
||||||
|
|
||||||
|
**全局规则**:
|
||||||
|
配置文件顶层 `rules:` 里的分流规则表。只对没有附加规则的入站生效——带附加规则的入站整表改查自己的表,不再看它。
|
||||||
|
_Avoid_: 顶层规则、主规则、默认规则
|
||||||
|
|
||||||
**附加规则**:
|
**附加规则**:
|
||||||
挂在某一路入站上的一组分流规则,只对从该入站进来的流量生效。
|
挂在某一路入站上的一组分流规则,只对从该入站进来的流量生效。
|
||||||
_Avoid_: sub-rules、子规则、私有规则
|
_Avoid_: sub-rules、子规则、私有规则
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
proxyGroupName,
|
proxyGroupName,
|
||||||
subRuleName,
|
subRuleName,
|
||||||
} from '@shared/inbound-groups.ts'
|
} 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 type { GroupSpec, OutboundSetup, PanelGroup } from '@shared/inbound-groups.ts'
|
||||||
import { resolveChain } from '@shared/outbound-chain.ts'
|
import { resolveChain } from '@shared/outbound-chain.ts'
|
||||||
import { latestDelay } from '../api'
|
import { latestDelay } from '../api'
|
||||||
@@ -84,6 +84,12 @@ const foreignRuleSet = computed(
|
|||||||
/** 内网直连还缺几条。全齐了按钮就不出现 */
|
/** 内网直连还缺几条。全齐了按钮就不出现 */
|
||||||
const lanMissing = computed(() => missingLanRules(props.subRules).length)
|
const lanMissing = computed(() => missingLanRules(props.subRules).length)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户自己写的第一条 MATCH 的下标,没有时是 -1。它之后的行——包括表底的
|
||||||
|
* 自动兜底——永远轮不到,标红;它自己标黄,hover 说明原因。
|
||||||
|
*/
|
||||||
|
const subMatchAt = computed(() => firstMatchIndex(props.subRules))
|
||||||
|
|
||||||
/** 默认入站由配置顶层的端口字段声明,没有 proxy 字段可写,也不能删。 */
|
/** 默认入站由配置顶层的端口字段声明,没有 proxy 字段可写,也不能删。 */
|
||||||
const locked = computed(() => props.inbound.isDefault)
|
const locked = computed(() => props.inbound.isDefault)
|
||||||
|
|
||||||
@@ -717,8 +723,22 @@ const complete = computed(
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<ul class="srules">
|
<ul class="srules">
|
||||||
<li v-for="(rule, i) in subRules" :key="i" class="srule">
|
<li
|
||||||
<span class="mono s-text" :title="rule">{{ rule }}</span>
|
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">
|
<span class="s-acts">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -751,8 +771,12 @@ const complete = computed(
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="srule is-final"
|
:class="['srule is-final', subMatchAt >= 0 && 'is-dead']"
|
||||||
title="兜底:没被上面的规则分走的流量走这一路自己的出站。这一条由 Patchbay 自动维护"
|
:title="
|
||||||
|
subMatchAt >= 0
|
||||||
|
? '上面的 MATCH 已拦下所有流量,这条自动兜底不会再被使用'
|
||||||
|
: '兜底:没被上面的规则分走的流量走这一路自己的出站。这一条由 Patchbay 自动维护'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="mono s-text">MATCH,{{ proxyGroupName(inbound.name) }}</span>
|
<span class="mono s-text">MATCH,{{ proxyGroupName(inbound.name) }}</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -949,6 +973,16 @@ const complete = computed(
|
|||||||
color: var(--c-text-faint);
|
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 {
|
.s-acts {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|||||||
+41
-15
@@ -52,10 +52,21 @@ const emit = defineEmits<{ saved: [] }>()
|
|||||||
const clone = () => JSON.parse(JSON.stringify(props.data)) as ConfigObject
|
const clone = () => JSON.parse(JSON.stringify(props.data)) as ConfigObject
|
||||||
|
|
||||||
const rules = computed(() => rulesOf(props.data))
|
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(() =>
|
const providers = computed(() =>
|
||||||
Object.entries(ruleProvidersOf(props.data)).map(([name, decl]) => ({
|
Object.entries(ruleProvidersOf(props.data)).map(([name, decl]) => ({
|
||||||
name,
|
name,
|
||||||
@@ -88,7 +99,7 @@ const targetOptions = computed(() => [
|
|||||||
...outbounds.value.ownProxies,
|
...outbounds.value.ownProxies,
|
||||||
])
|
])
|
||||||
|
|
||||||
/** 第一条 MATCH 的下标。它之后的行标成不可达。 */
|
/** 第一条 MATCH 的下标,没有时是 -1。ini 导入拿它判断要不要收配方的兜底。 */
|
||||||
const matchAt = computed(() => firstMatchIndex(rules.value))
|
const matchAt = computed(() => firstMatchIndex(rules.value))
|
||||||
|
|
||||||
/** 断引用按行归好,模板里直接查。 */
|
/** 断引用按行归好,模板里直接查。 */
|
||||||
@@ -131,7 +142,7 @@ function withRules(next: string[]): ConfigObject {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 分流规则 ────────────────────────────────────
|
// ── 全局规则 ────────────────────────────────────
|
||||||
|
|
||||||
const ruleFormOpen = ref(false)
|
const ruleFormOpen = ref(false)
|
||||||
/** 编辑时是下标,新增时是 null */
|
/** 编辑时是下标,新增时是 null */
|
||||||
@@ -177,7 +188,7 @@ async function move(index: number, delta: -1 | 1) {
|
|||||||
async function removeRule(index: number) {
|
async function removeRule(index: number) {
|
||||||
const rule = rules.value[index]
|
const rule = rules.value[index]
|
||||||
if (rule === undefined || saving.value) return
|
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() {
|
async function applyLanPreset() {
|
||||||
@@ -265,8 +276,9 @@ async function submitImport(plan: IniImportPlan) {
|
|||||||
const next = clone()
|
const next = clone()
|
||||||
setRuleProviders(next, { ...ruleProvidersOf(next), ...plan.providers })
|
setRuleProviders(next, { ...ruleProvidersOf(next), ...plan.providers })
|
||||||
// 表里已有 MATCH 时丢弃配方的兜底:那是你自己安排的分流终点,配方不该
|
// 表里已有 MATCH 时丢弃配方的兜底:那是你自己安排的分流终点,配方不该
|
||||||
// 偷偷替换;追加在 MATCH 之后则永远是死行
|
// 偷偷替换;追加在 MATCH 之后则永远是死行。firstMatchIndex 没有时给 -1,
|
||||||
const keepFinals = matchAt.value === null
|
// 之前误写成 === null,配方兜底从来没被导入过
|
||||||
|
const keepFinals = matchAt.value < 0
|
||||||
const finals = keepFinals ? plan.finals : []
|
const finals = keepFinals ? plan.finals : []
|
||||||
setRules(next, [...insertBeforeMatch(rules.value, plan.rules), ...finals])
|
setRules(next, [...insertBeforeMatch(rules.value, plan.rules), ...finals])
|
||||||
const added = plan.rules.length + finals.length
|
const added = plan.rules.length + finals.length
|
||||||
@@ -350,7 +362,7 @@ async function updateNow(name: string) {
|
|||||||
<div class="sums">
|
<div class="sums">
|
||||||
<div class="sum">
|
<div class="sum">
|
||||||
<span class="sum-n">{{ rules.length }}</span>
|
<span class="sum-n">{{ rules.length }}</span>
|
||||||
<span class="sum-k">分流规则</span>
|
<span class="sum-k">全局规则</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sum">
|
<div class="sum">
|
||||||
<span class="sum-n">{{ providers.length }}</span>
|
<span class="sum-n">{{ providers.length }}</span>
|
||||||
@@ -361,7 +373,7 @@ async function updateNow(name: string) {
|
|||||||
|
|
||||||
<section class="sec">
|
<section class="sec">
|
||||||
<header class="sec-head">
|
<header class="sec-head">
|
||||||
<h2>分流规则</h2>
|
<h2>全局规则</h2>
|
||||||
<div class="sec-acts">
|
<div class="sec-acts">
|
||||||
<button
|
<button
|
||||||
v-if="missingLan.length > 0 && rules.length > 0"
|
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">
|
<ol v-if="rules.length > 0" class="rows">
|
||||||
<li v-for="row in rows" :key="row.index">
|
<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="idx mono">{{ row.index + 1 }}</span>
|
||||||
<span class="rtype mono">{{ row.type }}</span>
|
<span class="rtype mono">{{ row.type }}</span>
|
||||||
<span class="payload mono">
|
<span class="payload mono">
|
||||||
@@ -400,7 +421,6 @@ async function updateNow(name: string) {
|
|||||||
{{ row.target }}
|
{{ row.target }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="issuesByIndex.has(row.index)" class="tag tag-bad">断引用</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>
|
||||||
<span class="acts">
|
<span class="acts">
|
||||||
<button
|
<button
|
||||||
@@ -443,7 +463,7 @@ async function updateNow(name: string) {
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p v-else class="empty">没有分流规则,走规则的流量都会直连。</p>
|
<p v-else class="empty">没有全局规则,走规则的流量都会直连。</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="sec">
|
<section class="sec">
|
||||||
@@ -688,7 +708,13 @@ async function updateNow(name: string) {
|
|||||||
.is-dead .rtype,
|
.is-dead .rtype,
|
||||||
.is-dead .payload,
|
.is-dead .payload,
|
||||||
.is-dead .rtarget {
|
.is-dead .rtarget {
|
||||||
color: var(--c-text-faint);
|
color: var(--c-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拦路的那条 MATCH:它后面还有行,后面全是死行 */
|
||||||
|
.is-culprit .rtype,
|
||||||
|
.is-culprit .rtarget {
|
||||||
|
color: var(--c-warn);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bad {
|
.bad {
|
||||||
|
|||||||
Reference in New Issue
Block a user