style(copy): punctuation rules and site-wide fix with a CI check (QA U11)
- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing, full-width parens around Chinese, single ellipsis, 「」 quotes, period rules) - scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX text (not comments, types, keys, match/compare strings); --fix, --strict, --by-file; wired into deno task ci as check:copy in warn mode - --fix applied to web, server and contracts; files other lanes were editing (subscriptions, sites, browser, agent, auth/account, inventory) left for them - tests updated for the changed messages Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9b017906cf
commit
52402864d6
143 files changed
+985
-606
No files matched your search
@@ -100,7 +100,7 @@ export function RuleGroupEditor({
|
||||
ladderProblem(form.ladder),
|
||||
...form.excludes.map((e, i) => {
|
||||
const p = ladderProblem(e.state);
|
||||
return p ? `第 ${i + 1} 条硬排除:${p}` : null;
|
||||
return p ? `第 ${i + 1} 条硬排除:${p}` : null;
|
||||
}),
|
||||
].filter((p): p is string => !!p);
|
||||
const blocked = problems[0] ?? null;
|
||||
@@ -144,7 +144,7 @@ export function RuleGroupEditor({
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ApiRequestError && err.code === "conflict") {
|
||||
if (/revision/.test(err.message)) setConflict("这个规则组刚被别处改过,没有保存。载入最新版本后再改。");
|
||||
if (/revision/.test(err.message)) setConflict("这个规则组刚被别处改过,没有保存。载入最新版本后再改。");
|
||||
else setNameError(err.message);
|
||||
} else toast.error(errorMessage(err, "保存失败"));
|
||||
}
|
||||
@@ -173,7 +173,7 @@ export function RuleGroupEditor({
|
||||
) : null}
|
||||
</span>
|
||||
) : (
|
||||
"层从上到下依次匹配,先命中的层优先级高。"
|
||||
"层从上到下依次匹配,先命中的层优先级高。"
|
||||
)
|
||||
}
|
||||
footer={
|
||||
@@ -186,7 +186,7 @@ export function RuleGroupEditor({
|
||||
删除
|
||||
</Button>
|
||||
}
|
||||
title={`删除「${group.name}」?`}
|
||||
title={`删除「${group.name}」?`}
|
||||
description={`${SUPPRESS_NOTE}被版本目标或订阅引用时不能删除。`}
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
@@ -221,7 +221,7 @@ export function RuleGroupEditor({
|
||||
</div>
|
||||
) : null}
|
||||
{inDefault ? (
|
||||
<p className="text-meta text-muted-foreground">在默认链里的组不能删除,先在“默认值”里移出。</p>
|
||||
<p className="text-meta text-muted-foreground">在默认链里的组不能删除,先在「默认值」里移出。</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-x-4 sm:grid-cols-2">
|
||||
@@ -263,7 +263,7 @@ export function RuleGroupEditor({
|
||||
label="作用范围"
|
||||
hint={
|
||||
group && (group.scope.entityIds?.length || group.scope.siteIds?.length || group.scope.subscriptionId)
|
||||
? `另有:${scopeText({ ...group.scope, mediaTypes: undefined, mediaKinds: undefined } as RuleGroupScope)}(此处不改)`
|
||||
? `另有:${scopeText({ ...group.scope, mediaTypes: undefined, mediaKinds: undefined } as RuleGroupScope)}(此处不改)`
|
||||
: "不选 = 全部类型"
|
||||
}
|
||||
className="sm:col-span-2"
|
||||
@@ -279,7 +279,7 @@ export function RuleGroupEditor({
|
||||
<div className="min-w-0">
|
||||
<p className="text-caption font-medium text-foreground">一层都不中时拒绝</p>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
{form.ladderRequired ? "资源至少要命中一层" : "只影响排序,不中的资源 pri 为 0"}
|
||||
{form.ladderRequired ? "资源至少要命中一层" : "只影响排序,不中的资源 pri 为 0"}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -302,7 +302,7 @@ export function RuleGroupEditor({
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="硬排除">
|
||||
<p className="mb-2 text-meta text-muted-foreground">任一条命中即拒绝,每条写一层条件。</p>
|
||||
<p className="mb-2 text-meta text-muted-foreground">任一条命中即拒绝,每条写一层条件。</p>
|
||||
<ul className="space-y-2">
|
||||
{form.excludes.map((e, i) => (
|
||||
<li key={e.key} className="flex items-start gap-2">
|
||||
@@ -409,7 +409,7 @@ function Revisions({ id }: { id: string }) {
|
||||
</div>
|
||||
{r.changedFields.length ? (
|
||||
<p className="mt-1 text-meta text-muted-foreground">
|
||||
改动:{r.changedFields.map((f) => FIELD_LABEL[f] ?? f).join("、")}
|
||||
改动:{r.changedFields.map((f) => FIELD_LABEL[f] ?? f).join("、")}
|
||||
</p>
|
||||
) : null}
|
||||
</li>
|
||||
|
||||
@@ -22,7 +22,7 @@ function useToggle() {
|
||||
toggle: async (g: RuleGroup) => {
|
||||
try {
|
||||
await update.mutateAsync({ id: g.id, enabled: !g.enabled, expectedRevision: g.revision });
|
||||
toast.success(g.enabled ? "已停用,Agent 不会自动加回" : "已启用");
|
||||
toast.success(g.enabled ? "已停用,Agent 不会自动加回" : "已启用");
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err));
|
||||
}
|
||||
@@ -38,7 +38,7 @@ function RefsCell({ g, refs }: { g: RuleGroup; refs: RuleRefs | undefined }) {
|
||||
if (subs.length) parts.push(`${subs.length} 个订阅`);
|
||||
if (slots.length) parts.push(`${slots.length} 个版本目标`);
|
||||
if (!parts.length) return <span className="text-meta text-muted-foreground">{refs ? "未引用" : "—"}</span>;
|
||||
const title = [...subs.map((s) => `订阅:${s.title}`), ...slots.map((s) => `版本目标:${s.name}`)].join("\n");
|
||||
const title = [...subs.map((s) => `订阅:${s.title}`), ...slots.map((s) => `版本目标:${s.name}`)].join("\n");
|
||||
return (
|
||||
<span className="text-meta text-foreground" title={title || undefined}>
|
||||
{parts.join(" · ")}
|
||||
@@ -140,7 +140,7 @@ export function RuleGroupTable({
|
||||
empty={{
|
||||
icon: ListChecks,
|
||||
title: filtered ? "没有符合条件的规则组" : "还没有规则组",
|
||||
description: filtered ? undefined : "新建一个规则组,或在 Agent 会话里说明长期偏好。",
|
||||
description: filtered ? undefined : "新建一个规则组,或在 Agent 会话里说明长期偏好。",
|
||||
action: filtered ? undefined : (
|
||||
<Button size="sm" onClick={onCreate}>
|
||||
新建规则组
|
||||
@@ -197,7 +197,7 @@ function RuleTag({ g, onOpen }: { g: RuleGroup; onOpen: () => void }) {
|
||||
停用
|
||||
</Button>
|
||||
}
|
||||
title={`停用「${g.name}」?`}
|
||||
title={`停用「${g.name}」?`}
|
||||
description={SUPPRESS_NOTE}
|
||||
confirmLabel="停用"
|
||||
onConfirm={() => toggle(g)}
|
||||
@@ -213,14 +213,14 @@ function RuleTag({ g, onOpen }: { g: RuleGroup; onOpen: () => void }) {
|
||||
<Trash2 />
|
||||
</Button>
|
||||
}
|
||||
title={`删除「${g.name}」?`}
|
||||
title={`删除「${g.name}」?`}
|
||||
description={`${SUPPRESS_NOTE}被版本目标或订阅引用时不能删除。`}
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await del.mutateAsync(g.id);
|
||||
toast.success("已删除,Agent 不会自动加回");
|
||||
toast.success("已删除,Agent 不会自动加回");
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "删除失败"));
|
||||
throw err;
|
||||
@@ -237,7 +237,7 @@ export function RuleTags({ rows, onOpen }: { rows: RuleGroup[]; onOpen: (g: Rule
|
||||
return (
|
||||
<Panel
|
||||
title="偏好"
|
||||
description="单条规则,多由对话或 Agent 提出。停用或删除后 Agent 不会自动加回。"
|
||||
description="单条规则,多由对话或 Agent 提出。停用或删除后 Agent 不会自动加回。"
|
||||
actions={<span className="text-meta text-muted-foreground tabular-nums">{rows.length} 条</span>}
|
||||
>
|
||||
{rows.length === 0 ? (
|
||||
@@ -245,7 +245,7 @@ export function RuleTags({ rows, onOpen }: { rows: RuleGroup[]; onOpen: (g: Rule
|
||||
plain
|
||||
icon={Tags}
|
||||
title="还没有偏好"
|
||||
description="在 Agent 会话里说明长期偏好,会出现在这里。"
|
||||
description="在 Agent 会话里说明长期偏好,会出现在这里。"
|
||||
className="py-6"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -10,7 +10,7 @@ export const RULE_KIND: Record<RuleGroupKind, { label: string; hint: string }> =
|
||||
search: { label: "搜索", hint: "手动搜索与 Agent 搜索资源时用" },
|
||||
};
|
||||
|
||||
export const SUPPRESS_NOTE = "停用或删除会留下抑制记录,Agent 不会自动加回;重新启用或手动新建同样的规则后解除。";
|
||||
export const SUPPRESS_NOTE = "停用或删除会留下抑制记录,Agent 不会自动加回;重新启用或手动新建同样的规则后解除。";
|
||||
|
||||
export const RULE_SOURCE: Record<RuleGroupSource, string> = {
|
||||
manual: "手动",
|
||||
@@ -59,5 +59,5 @@ export function ruleSummary(g: RuleGroup): string {
|
||||
parts.push(`${g.ladderRequired ? "要求" : "偏好"} ${g.layers.map((l) => exprToString(l.expr)).join(" > ")}`);
|
||||
for (const h of g.hardExcludes) parts.push(`排除 ${exprToString(h)}`);
|
||||
if (parts.length) return parts.join(";");
|
||||
return g.legacyKind ? "旧偏好标签,没有可评估的条件" : "没有条件";
|
||||
return g.legacyKind ? "旧偏好标签,没有可评估的条件" : "没有条件";
|
||||
}
|
||||
@@ -180,7 +180,7 @@ export function useRuleTextParser(
|
||||
if (single && r.layers.length > 1)
|
||||
errors.push({
|
||||
code: "unexpected_token",
|
||||
message: "硬排除只能写一层,不能用 >",
|
||||
message: "硬排除只能写一层,不能用 >",
|
||||
position: text.indexOf(">"),
|
||||
layer: 1,
|
||||
});
|
||||
@@ -253,7 +253,7 @@ export function RuleStringField({
|
||||
<li key={`${e.code}-${e.position}-${i}`} className="text-meta text-destructive">
|
||||
<ErrorCaret text={state.text} position={e.position} />
|
||||
<span>
|
||||
第 {e.position + 1} 个字符{single ? "" : `(第 ${e.layer + 1} 层)`}:{e.message}
|
||||
第 {e.position + 1} 个字符{single ? "" : `(第 ${e.layer + 1} 层)`}:{e.message}
|
||||
</span>
|
||||
{e.suggestions?.length ? (
|
||||
<span className="ml-1 inline-flex flex-wrap gap-1 align-middle">
|
||||
@@ -396,7 +396,7 @@ function LayerCardView({
|
||||
<Input
|
||||
value={card.name ?? ""}
|
||||
onChange={(e) => onChange({ ...card, name: e.target.value || undefined })}
|
||||
placeholder="层名称(可选)"
|
||||
placeholder="层名称(可选)"
|
||||
aria-label={`第 ${index + 1} 层名称`}
|
||||
maxLength={40}
|
||||
className="h-7 w-36 text-meta"
|
||||
@@ -530,11 +530,11 @@ export function LadderEditor({ state, onChange }: { state: LadderState; onChange
|
||||
<div className="space-y-3">
|
||||
<RuleStringField state={state} onChange={onChange} ariaLabel="规则串" />
|
||||
{state.errors.length ? (
|
||||
<p className="text-meta text-muted-foreground">规则串改对后同步到下面的层;现在显示的是上次解析成功的结果。</p>
|
||||
<p className="text-meta text-muted-foreground">规则串改对后同步到下面的层;现在显示的是上次解析成功的结果。</p>
|
||||
) : null}
|
||||
{state.cards === null ? (
|
||||
<p className="rounded-lg border border-border/70 px-3 py-2 text-meta text-muted-foreground">
|
||||
嵌套超过两层,只能在规则串里改。
|
||||
嵌套超过两层,只能在规则串里改。
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
@@ -559,7 +559,7 @@ export function LadderEditor({ state, onChange }: { state: LadderState; onChange
|
||||
))}
|
||||
</ol>
|
||||
) : state.errors.length ? null : (
|
||||
<p className="text-meta text-muted-foreground">没有层:只用硬排除过滤,不影响排序。</p>
|
||||
<p className="text-meta text-muted-foreground">没有层:只用硬排除过滤,不影响排序。</p>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -88,13 +88,13 @@ export function RulePreviewPanel({
|
||||
<Textarea
|
||||
value={titles}
|
||||
onChange={(e) => setTitles(e.target.value)}
|
||||
placeholder={"每行一个资源标题,最多 50 行\nDune.Part.Two.2024.2160p.UHD.BluRay.REMUX.HDR.DV-FraMeSToR"}
|
||||
placeholder={"每行一个资源标题,最多 50 行\nDune.Part.Two.2024.2160p.UHD.BluRay.REMUX.HDR.DV-FraMeSToR"}
|
||||
aria-label="资源标题"
|
||||
rows={4}
|
||||
className="font-mono text-meta"
|
||||
/>
|
||||
) : null}
|
||||
{blocked ? <p className="text-meta text-muted-foreground">{blocked},改好后再试算。</p> : null}
|
||||
{blocked ? <p className="text-meta text-muted-foreground">{blocked},改好后再试算。</p> : null}
|
||||
{result ? <PreviewResult result={result} /> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -139,10 +139,10 @@ function AtomDialog({
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{atom ? `编辑条件 ${atom.key}` : "新建自定义条件"}</DialogTitle>
|
||||
<DialogDescription>字段同 MoviePilot 自定义规则。正则不区分大小写,多条之间是“或”。</DialogDescription>
|
||||
<DialogDescription>字段同 MoviePilot 自定义规则。正则不区分大小写,多条之间是「或」。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-x-4 sm:grid-cols-2">
|
||||
<FormField label="关键字" htmlFor={`${uid}-key`} required hint="大写字母和数字;不能与内置关键字同名">
|
||||
<FormField label="关键字" htmlFor={`${uid}-key`} required hint="大写字母和数字;不能与内置关键字同名">
|
||||
<Input
|
||||
id={`${uid}-key`}
|
||||
value={f.key}
|
||||
@@ -158,7 +158,7 @@ function AtomDialog({
|
||||
<FormField label="说明" htmlFor={`${uid}-desc`} className="sm:col-span-2">
|
||||
<Input id={`${uid}-desc`} value={f.description} onChange={(e) => set("description", e.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="包含(正则,每行一条)" htmlFor={`${uid}-inc`}>
|
||||
<FormField label="包含(正则,每行一条)" htmlFor={`${uid}-inc`}>
|
||||
<Textarea
|
||||
id={`${uid}-inc`}
|
||||
rows={3}
|
||||
@@ -167,7 +167,7 @@ function AtomDialog({
|
||||
className="font-mono text-meta"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="排除(正则,每行一条)" htmlFor={`${uid}-exc`}>
|
||||
<FormField label="排除(正则,每行一条)" htmlFor={`${uid}-exc`}>
|
||||
<Textarea
|
||||
id={`${uid}-exc`}
|
||||
rows={3}
|
||||
@@ -184,7 +184,7 @@ function AtomDialog({
|
||||
onChange={(v) => set("matchFields", v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="大小(MB,剧集按每集)">
|
||||
<FormField label="大小(MB,剧集按每集)">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
inputMode="numeric"
|
||||
@@ -203,7 +203,7 @@ function AtomDialog({
|
||||
/>
|
||||
</div>
|
||||
</FormField>
|
||||
<FormField label="发布后(分钟)">
|
||||
<FormField label="发布后(分钟)">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
inputMode="numeric"
|
||||
@@ -322,7 +322,7 @@ export function AtomsTab() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-caption text-muted-foreground">
|
||||
内置关键字不够用时,用正则、大小、做种数等定义新条件,在规则串里按关键字引用。
|
||||
内置关键字不够用时,用正则、大小、做种数等定义新条件,在规则串里按关键字引用。
|
||||
</p>
|
||||
<Button size="sm" onClick={() => setDialog({ open: true, atom: null })}>
|
||||
<Plus />
|
||||
@@ -339,7 +339,7 @@ export function AtomsTab() {
|
||||
onRowClick={(a) => setDialog({ open: true, atom: a })}
|
||||
empty={{ icon: FlaskConical, title: "还没有自定义条件", description: "内置关键字覆盖不到时再新建。" }}
|
||||
/>
|
||||
<Panel title="内置关键字" description="对齐 MoviePilot;判定依据按解析字段优先">
|
||||
<Panel title="内置关键字" description="对齐 MoviePilot;判定依据按解析字段优先">
|
||||
{keywords.isPending ? (
|
||||
<LoadingState rows={3} />
|
||||
) : keywords.isError ? (
|
||||
@@ -350,7 +350,7 @@ export function AtomsTab() {
|
||||
<li key={k.id} className="flex min-w-0 items-baseline gap-2 text-caption">
|
||||
<span className="w-16 shrink-0 font-mono font-medium text-foreground">{k.id}</span>
|
||||
<span className="min-w-0 text-muted-foreground">
|
||||
{k.supported ? k.description || k.name : "不支持,规则串里出现会报错"}
|
||||
{k.supported ? k.description || k.name : "不支持,规则串里出现会报错"}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -385,7 +385,7 @@ export function DefaultsTab({ groups }: { groups: RuleGroup[] | undefined }) {
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-caption text-muted-foreground">
|
||||
订阅和版本目标没指定规则组时用这里的链。链上的组按顺序串联,任一组拒绝即拒绝,优先级取最后一个带层的组。
|
||||
订阅和版本目标没指定规则组时用这里的链。链上的组按顺序串联,任一组拒绝即拒绝,优先级取最后一个带层的组。
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{dirty ? <StatusBadge tone="warning">未保存</StatusBadge> : null}
|
||||
@@ -419,7 +419,7 @@ export function DefaultsTab({ groups }: { groups: RuleGroup[] | undefined }) {
|
||||
return (
|
||||
<Panel key={k} title={DEFAULT_CHAIN_LABEL[k]} description={RULE_KIND[k].hint}>
|
||||
{ids.length === 0 ? (
|
||||
<p className="text-meta text-muted-foreground">链为空:不按规则组过滤。</p>
|
||||
<p className="text-meta text-muted-foreground">链为空:不按规则组过滤。</p>
|
||||
) : (
|
||||
<ol className="space-y-1.5">
|
||||
{ids.map((id, i) => {
|
||||
@@ -434,7 +434,7 @@ export function DefaultsTab({ groups }: { groups: RuleGroup[] | undefined }) {
|
||||
{id === lastLadder ? (
|
||||
<span className="text-micro text-muted-foreground">决定优先级</span>
|
||||
) : null}
|
||||
{g && !g.enabled ? <span className="text-micro text-warning">已停用,评估时跳过</span> : null}
|
||||
{g && !g.enabled ? <span className="text-micro text-warning">已停用,评估时跳过</span> : null}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -514,7 +514,8 @@ export function SuppressionsTab() {
|
||||
return (
|
||||
<Panel title="抑制记录" description={sups.data ? `${active} 条生效中` : undefined}>
|
||||
<p className="mb-3 text-caption text-muted-foreground">
|
||||
停用、删除,或改掉对话 / Agent 提出的规则,都会留下抑制记录。Agent 再提出同样的规则会被直接拒绝,解除后才能再提。
|
||||
停用、删除,或改掉对话 / Agent 提出的规则,都会留下抑制记录。Agent
|
||||
再提出同样的规则会被直接拒绝,解除后才能再提。
|
||||
</p>
|
||||
{sups.isPending ? (
|
||||
<LoadingState rows={2} />
|
||||
@@ -549,8 +550,8 @@ export function SuppressionsTab() {
|
||||
解除抑制
|
||||
</Button>
|
||||
}
|
||||
title="解除这条抑制?"
|
||||
description="解除后 Agent 可以再次提出这条规则,仍需审批。规则组本身不会恢复。"
|
||||
title="解除这条抑制?"
|
||||
description="解除后 Agent 可以再次提出这条规则,仍需审批。规则组本身不会恢复。"
|
||||
confirmLabel="解除"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user