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:
rootandClaude Opus 5.5 committed 2026-09-28 20:09:29 +08:00
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>
+8 -8
View File
@@ -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"
/>
) : (
+2 -2
View File
@@ -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>
);
+16 -15
View File
@@ -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 {