Files
UHD_Media_Tool/apps/web/app/components/rules/expr.ts
T
rootandClaude Opus 5.5 b908b4a0c9 feat(web): rules page with layer cards, rule-string sync, preview and preferences as tags (FE-3)
- /console/rules tabs: rule groups, custom conditions, defaults, acquisition policies, suppressions
- editor: rule string parsed live via /rules/parse (position + suggestions) and synced both ways with layer cards; hard excludes; expectedRevision conflicts keep local edits
- preview against recent releases or pasted titles with pri / level / reasons / trace
- single-rule groups shown as preference tags; disable/delete writes suppression records
- /console/preferences redirects to /console/rules (origin passed through)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 16:06:16 +08:00

116 lines
4.0 KiB
TypeScript

import type { RuleExpr, RuleLayer } from "@umt/contracts";
// 规则组层的编辑模型:一层 = 顶层 且/或 + 若干条目;条目是条件(可取反)或一个括号组(组内同一种 且/或)。
// 覆盖 MP 规则串的常见写法;更深的嵌套只能在规则串里改(cardsOf 返回 null)。
export type Op = "and" | "or";
export type AtomItem = { kind: "atom"; id: string; neg: boolean };
export type GroupItem = { kind: "group"; op: Op; neg: boolean; items: AtomItem[] };
export type LayerItem = AtomItem | GroupItem;
export type LayerCard = { name?: string; op: Op; items: LayerItem[] };
/** 规范化:原子大写,层内 ! > & > |,只加必要括号(与后端 /rules/serialize 同口径) */
export function exprToString(e: RuleExpr, parent: "top" | "and" | "or" | "not" = "top"): string {
switch (e.op) {
case "atom":
return e.id.toUpperCase();
case "not":
return `!${exprToString(e.arg, "not")}`;
case "and": {
const s = e.args.map((a) => exprToString(a, "and")).join(" & ");
return parent === "not" ? `(${s})` : s;
}
case "or": {
const s = e.args.map((a) => exprToString(a, "or")).join(" | ");
return parent === "and" || parent === "not" ? `(${s})` : s;
}
}
}
export function layersToString(layers: RuleLayer[]): string {
return layers.map((l) => exprToString(l.expr)).join(" > ");
}
function atomOf(e: RuleExpr): AtomItem | null {
if (e.op === "atom") return { kind: "atom", id: e.id.toUpperCase(), neg: false };
if (e.op === "not" && e.arg.op === "atom") return { kind: "atom", id: e.arg.id.toUpperCase(), neg: true };
return null;
}
function groupOf(e: RuleExpr): GroupItem | null {
let neg = false;
let inner = e;
if (inner.op === "not") {
neg = true;
inner = inner.arg;
}
if (inner.op !== "and" && inner.op !== "or") return null;
const items: AtomItem[] = [];
for (const a of inner.args) {
const at = atomOf(a);
if (!at) return null;
items.push(at);
}
return { kind: "group", op: inner.op, neg, items };
}
/** 表达式 → 卡片;表达不了(嵌套超过两层)返回 null */
export function cardOf(layer: RuleLayer): LayerCard | null {
const e = layer.expr;
const single = atomOf(e);
if (single) return { name: layer.name, op: "and", items: [single] };
if (e.op === "not") {
const g = groupOf(e);
return g ? { name: layer.name, op: "and", items: [g] } : null;
}
if (e.op !== "and" && e.op !== "or") return null;
const items: LayerItem[] = [];
for (const a of e.args) {
const it = atomOf(a) ?? groupOf(a);
if (!it) return null;
items.push(it);
}
return { name: layer.name, op: e.op, items };
}
export function cardsOf(layers: RuleLayer[]): LayerCard[] | null {
const out: LayerCard[] = [];
for (const l of layers) {
const c = cardOf(l);
if (!c) return null;
out.push(c);
}
return out;
}
function atomExpr(a: AtomItem): RuleExpr {
const e: RuleExpr = { op: "atom", id: a.id };
return a.neg ? { op: "not", arg: e } : e;
}
function itemExpr(it: LayerItem): RuleExpr | null {
if (it.kind === "atom") return atomExpr(it);
if (it.items.length === 0) return null;
const inner: RuleExpr =
it.items.length === 1 ? atomExpr(it.items[0] as AtomItem) : { op: it.op, args: it.items.map(atomExpr) };
return it.neg ? { op: "not", arg: inner } : inner;
}
/** 卡片 → 层;空层返回 null 的 expr(调用方据此提示) */
export function cardToLayer(c: LayerCard): RuleLayer | null {
const args = c.items.map(itemExpr).filter((x): x is RuleExpr => !!x);
if (args.length === 0) return null;
const expr: RuleExpr = args.length === 1 ? (args[0] as RuleExpr) : { op: c.op, args };
return c.name ? { name: c.name, expr } : { expr };
}
/** 表达式里引用的全部条件 id */
export function atomsIn(e: RuleExpr, out = new Set<string>()): Set<string> {
if (e.op === "atom") out.add(e.id.toUpperCase());
else if (e.op === "not") atomsIn(e.arg, out);
else for (const a of e.args) atomsIn(a, out);
return out;
}
export const OP_LABEL: Record<Op, string> = { and: "且", or: "或" };