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
@@ -87,7 +87,7 @@ export function IdentityDialog({
|
||||
setErrors({});
|
||||
set.mutate(r.data, {
|
||||
onSuccess: () => {
|
||||
toast.success(`已改为 TMDB ${r.data.entityType}:${r.data.externalId}${r.data.lock ? ",并加人工锁" : ""}`);
|
||||
toast.success(`已改为 TMDB ${r.data.entityType}:${r.data.externalId}${r.data.lock ? ",并加人工锁" : ""}`);
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (err) => toast.error(errorMessage(err, "修改失败")),
|
||||
@@ -102,7 +102,7 @@ export function IdentityDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>修改外部身份</DialogTitle>
|
||||
<DialogDescription>
|
||||
同类型的旧 TMDB 身份会退役,保留修订记录。加人工锁后,自动匹配和刷新不再改动。
|
||||
同类型的旧 TMDB 身份会退役,保留修订记录。加人工锁后,自动匹配和刷新不再改动。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={submit} className="grid gap-3">
|
||||
@@ -249,7 +249,7 @@ export function FieldLockDialog({
|
||||
<Dialog open={!!field} onOpenChange={(o) => !lock.isPending && onOpenChange(o)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>锁定字段:{field ? (ENTITY_FIELD_LABEL[field] ?? field) : ""}</DialogTitle>
|
||||
<DialogTitle>锁定字段:{field ? (ENTITY_FIELD_LABEL[field] ?? field) : ""}</DialogTitle>
|
||||
<DialogDescription>锁定后刷新元数据不改这个字段。已绑定的路径不变。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={submit} className="grid gap-3">
|
||||
@@ -383,7 +383,7 @@ export function MergeDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>合并作品</DialogTitle>
|
||||
<DialogDescription>
|
||||
「{entity.title}」会标为已合并,旧 ID 跳转到目标作品,订阅、资产和历史记录一并转过去。合并错了可以再修订。
|
||||
「{entity.title}」会标为已合并,旧 ID 跳转到目标作品,订阅、资产和历史记录一并转过去。合并错了可以再修订。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<EntityPicker value={target} onChange={setTarget} excludeId={entity.id} />
|
||||
@@ -411,10 +411,10 @@ export function MergeDialog({
|
||||
setConfirm(o);
|
||||
if (!o && !merge.isSuccess) return;
|
||||
}}
|
||||
title="合并作品?"
|
||||
title="合并作品?"
|
||||
description={
|
||||
target
|
||||
? `把「${entity.title}」合并到「${target.title}${target.year ? ` (${target.year})` : ""}」,完成后跳转到目标作品。`
|
||||
? `把「${entity.title}」合并到「${target.title}${target.year ? ` (${target.year})` : ""}」,完成后跳转到目标作品。`
|
||||
: ""
|
||||
}
|
||||
confirmLabel="合并"
|
||||
|
||||
@@ -80,7 +80,7 @@ function AddAlias({ entity }: { entity: PickedEntity }) {
|
||||
value={f.alias}
|
||||
aria-invalid={!!errors.alias}
|
||||
onChange={(e) => setF((x) => ({ ...x, alias: e.target.value }))}
|
||||
placeholder="如:沙丘2"
|
||||
placeholder="如:沙丘 2"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="类型" htmlFor="alias-kind">
|
||||
@@ -170,8 +170,8 @@ export function AliasesPanel() {
|
||||
<Trash2 />
|
||||
</Button>
|
||||
}
|
||||
title={`删除别名「${a.alias}」?`}
|
||||
description="删除后不再用这个别名匹配作品,已确认的外部身份不受影响。"
|
||||
title={`删除别名「${a.alias}」?`}
|
||||
description="删除后不再用这个别名匹配作品,已确认的外部身份不受影响。"
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
onConfirm={async () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ export type PickedEntity = { id: string; title: string; year: number | null; kin
|
||||
export function EntityPicker({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "搜索本地作品(标题/原名)",
|
||||
placeholder = "搜索本地作品(标题/原名)",
|
||||
id,
|
||||
}: {
|
||||
value: PickedEntity | null;
|
||||
|
||||
@@ -127,7 +127,7 @@ export function PoolSheet({ item, onClose }: { item: IdentityPoolItem | null; on
|
||||
>
|
||||
{queued ? (
|
||||
<p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
|
||||
已进待处理,在那里选择候选或忽略。
|
||||
已进待处理,在那里选择候选或忽略。
|
||||
</p>
|
||||
) : null}
|
||||
<KvList
|
||||
@@ -156,7 +156,7 @@ export function PoolSheet({ item, onClose }: { item: IdentityPoolItem | null; on
|
||||
)}
|
||||
</DetailSection>
|
||||
|
||||
<JsonView label="解析条件(原始)" value={item.query} defaultOpen={false} />
|
||||
<JsonView label="解析条件(原始)" value={item.query} defaultOpen={false} />
|
||||
</DetailSheet>
|
||||
);
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export function PoolTab() {
|
||||
<div className="min-w-0">
|
||||
<p className="font-mono text-caption break-all text-foreground">{r.release.title}</p>
|
||||
<p className="mt-0.5 truncate text-meta text-muted-foreground">
|
||||
{r.release.subtitle ? `${r.release.subtitle} · ` : ""}解析:{querySummary(r.query)}
|
||||
{r.release.subtitle ? `${r.release.subtitle} · ` : ""}解析:{querySummary(r.query)}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
@@ -149,7 +149,7 @@ export function PoolTab() {
|
||||
empty={{
|
||||
icon: SearchX,
|
||||
title: filtered ? "没有符合条件的待识别资源" : "没有待识别资源",
|
||||
description: "证据不足或冲突的资源会出现在这里,有新证据时自动重试。",
|
||||
description: "证据不足或冲突的资源会出现在这里,有新证据时自动重试。",
|
||||
}}
|
||||
hasMore={pool.hasNextPage}
|
||||
onLoadMore={() => pool.fetchNextPage()}
|
||||
|
||||
@@ -128,7 +128,7 @@ export const CANDIDATE_DECISION: Record<string, ToneLabel> = {
|
||||
reject: { label: "拒绝", tone: "destructive" },
|
||||
unknown_field: { label: "字段未知", tone: "warning" },
|
||||
skipped: { label: "跳过", tone: "neutral" },
|
||||
deferred: { label: "延后:已有计划在途", tone: "warning" },
|
||||
deferred: { label: "延后:已有计划在途", tone: "warning" },
|
||||
};
|
||||
|
||||
export const DECISION_KIND: Record<DecisionKind, string> = {
|
||||
@@ -174,7 +174,7 @@ export const POOL_STATUS: Record<string, ToneLabel> = {
|
||||
};
|
||||
|
||||
export const PREF_KIND: Record<PreferenceKind, { label: string; hint: string }> = {
|
||||
soft: { label: "软偏好", hint: "影响排序,不排除" },
|
||||
soft: { label: "软偏好", hint: "影响排序,不排除" },
|
||||
hard: { label: "硬规则", hint: "命中即排除" },
|
||||
version: { label: "版本偏好", hint: "同一作品内选哪种版本" },
|
||||
target: { label: "版本目标", hint: "为作品新建一个版本目标" },
|
||||
@@ -214,7 +214,7 @@ export const PRIORITY: Record<AcquisitionPriority, string> = {
|
||||
export const SUB_STATUS: Record<SubscriptionStatus, ToneLabel> = {
|
||||
active: { label: "进行中", tone: "success" },
|
||||
paused: { label: "已暂停", tone: "warning" },
|
||||
pending: { label: "待定(元数据未就绪)", tone: "warning" },
|
||||
pending: { label: "待定(元数据未就绪)", tone: "warning" },
|
||||
completed: { label: "已完成", tone: "neutral" },
|
||||
cancelled: { label: "已取消", tone: "neutral" },
|
||||
};
|
||||
@@ -268,7 +268,7 @@ export const opts = <K extends string>(rec: Record<K, string | { label: string }
|
||||
/** 版本目标 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:<id>`)→ 中文;不认识的原样显示 */
|
||||
const SLOT_REASON: Record<string, string> = {
|
||||
"blocked:no_main_video": "没有可入库的主视频",
|
||||
"deferred:slot_busy": "已有计划在途,等它结束再评估",
|
||||
"deferred:slot_busy": "已有计划在途,等它结束再评估",
|
||||
verified: "已核验入库",
|
||||
verified_partial: "部分核验入库",
|
||||
hard_conditions_met: "硬条件全部满足",
|
||||
|
||||
@@ -31,7 +31,7 @@ const jsonObject = (name: string) =>
|
||||
} catch {
|
||||
// 落到下面的错误
|
||||
}
|
||||
ctx.addIssue({ code: "custom", message: `${name}必须是 JSON 对象,如 {"mediaKinds":["movie"]}` });
|
||||
ctx.addIssue({ code: "custom", message: `${name}必须是 JSON 对象,如 {"mediaKinds":["movie"]}` });
|
||||
return z.NEVER;
|
||||
});
|
||||
|
||||
@@ -108,8 +108,8 @@ export function PreferenceFormDialog({
|
||||
<DialogTitle>{tag ? "编辑偏好" : "添加偏好"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{tag
|
||||
? "修改作用范围或规则会留下旧语义的抑制记录,Agent 不会按旧证据加回。"
|
||||
: "手动添加等同对话确认。之前取消过的偏好,重新添加会解除抑制。"}
|
||||
? "修改作用范围或规则会留下旧语义的抑制记录,Agent 不会按旧证据加回。"
|
||||
: "手动添加等同对话确认。之前取消过的偏好,重新添加会解除抑制。"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={submit} className="grid gap-2" noValidate>
|
||||
@@ -120,7 +120,7 @@ export function PreferenceFormDialog({
|
||||
value={draft.label}
|
||||
aria-invalid={!!errors.label}
|
||||
onChange={(e) => set("label", e.target.value)}
|
||||
placeholder="如:偏爱科幻"
|
||||
placeholder="如:偏爱科幻"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="类型" htmlFor="pref-kind" hint={PREF_KIND[draft.kind].hint}>
|
||||
@@ -157,7 +157,7 @@ export function PreferenceFormDialog({
|
||||
onChange={(e) => set("scope", e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="规则(JSON)" htmlFor="pref-rule" error={errors.rule} hint="结构化条件,由代码执行">
|
||||
<FormField label="规则(JSON)" htmlFor="pref-rule" error={errors.rule} hint="结构化条件,由代码执行">
|
||||
<Textarea
|
||||
id="pref-rule"
|
||||
rows={4}
|
||||
|
||||
@@ -78,7 +78,7 @@ export function SlotCard({
|
||||
</dl>
|
||||
{latest.length ? (
|
||||
<div className="mt-3 border-t border-border/60 pt-3">
|
||||
<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">
|
||||
{latest.map((c) => (
|
||||
<li key={c.sourceReleaseId} className="min-w-0">
|
||||
|
||||
@@ -42,8 +42,8 @@ export const CUTS = [
|
||||
] as const;
|
||||
export const CONTENT_TYPES = ["main", "extra", "behind_the_scenes", "performance_only", "preview", "reaction"] as const;
|
||||
|
||||
const SOURCE_LABEL: Record<string, string> = { BluRay: "BluRay(压制)", "BluRay-Disc": "原盘" };
|
||||
const CUT_LABEL: Record<string, string> = { default: "默认(无剪辑标记)" };
|
||||
const SOURCE_LABEL: Record<string, string> = { BluRay: "BluRay(压制)", "BluRay-Disc": "原盘" };
|
||||
const CUT_LABEL: Record<string, string> = { default: "默认(无剪辑标记)" };
|
||||
const CT_LABEL: Record<string, string> = {
|
||||
main: "正片",
|
||||
extra: "花絮",
|
||||
@@ -52,7 +52,7 @@ const CT_LABEL: Record<string, string> = {
|
||||
preview: "预告",
|
||||
reaction: "reaction",
|
||||
};
|
||||
const UNKNOWN_LABEL = { hold: "等待补充(不下载)", reject: "拒绝", accept: "放行" } as const;
|
||||
const UNKNOWN_LABEL = { hold: "等待补充(不下载)", reject: "拒绝", accept: "放行" } as const;
|
||||
const GB = 1024 ** 3;
|
||||
|
||||
const o = <T extends string>(list: readonly T[], labels: Record<string, string> = {}) =>
|
||||
@@ -235,7 +235,7 @@ export function summarizeHard(j: JsonObject | undefined, fallback?: JsonObject):
|
||||
if (!h) return ["规则无法解析"];
|
||||
if (h.resolutions?.length) out.push(`分辨率 ${h.resolutions.join(" / ")}`);
|
||||
if (h.sources?.length) out.push(`来源 ${h.sources.map((s) => SOURCE_LABEL[s] ?? s).join(" / ")}`);
|
||||
if (h.requireEncode) out.push("仅蓝光压制(不含原盘/Remux)");
|
||||
if (h.requireEncode) out.push("仅蓝光压制(不含原盘/Remux)");
|
||||
if (h.minSizeBytes !== undefined || h.maxSizeBytes !== undefined)
|
||||
out.push(
|
||||
`体积 ${h.minSizeBytes !== undefined ? formatBytes(h.minSizeBytes, 1) : "0"} – ${
|
||||
@@ -282,7 +282,7 @@ export function summarizeUpgrade(j: JsonObject | undefined): string[] {
|
||||
export function summarizeCompletion(j: JsonObject | undefined): string {
|
||||
const c = Completion.safeParse(j ?? {}).data;
|
||||
if (!c) return "未设置";
|
||||
return c.kind === "movie" ? "按内容版本核对(电影)" : "按内容单元覆盖核对(剧集/综艺)";
|
||||
return c.kind === "movie" ? "按内容版本核对(电影)" : "按内容单元覆盖核对(剧集/综艺)";
|
||||
}
|
||||
|
||||
// ── 编辑器 ──
|
||||
@@ -312,7 +312,7 @@ export function SlotRulesEditor({
|
||||
onChange={(v) => set("resolutions", v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="交付来源" hint="BluRay 指蓝光压制;原盘与 Remux 单列">
|
||||
<FormField label="交付来源" hint="BluRay 指蓝光压制;原盘与 Remux 单列">
|
||||
<ChipToggleGroup
|
||||
label="交付来源"
|
||||
options={o(SOURCES, SOURCE_LABEL)}
|
||||
@@ -354,7 +354,7 @@ export function SlotRulesEditor({
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<FormField label="必需字幕语言" htmlFor={`${id}-subs`} hint="逗号分隔;zh 可被 zh-Hans/zh-Hant 满足">
|
||||
<FormField label="必需字幕语言" htmlFor={`${id}-subs`} hint="逗号分隔;zh 可被 zh-Hans/zh-Hant 满足">
|
||||
<Input
|
||||
id={`${id}-subs`}
|
||||
value={value.requiredSubtitles}
|
||||
@@ -370,7 +370,7 @@ export function SlotRulesEditor({
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="内容版本(剪辑)" hint="不选即不限">
|
||||
<FormField label="内容版本(剪辑)" hint="不选即不限">
|
||||
<ChipToggleGroup
|
||||
label="内容版本"
|
||||
options={o(CUTS, CUT_LABEL)}
|
||||
@@ -386,7 +386,7 @@ export function SlotRulesEditor({
|
||||
onChange={(v) => set("contentTypes", v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="字段未知时" hint="资源缺少判断字段(如字幕)时">
|
||||
<FormField label="字段未知时" hint="资源缺少判断字段(如字幕)时">
|
||||
<Select
|
||||
value={value.unknownPolicy || "__none"}
|
||||
onValueChange={(v) => set("unknownPolicy", v === "__none" ? "" : (v as SlotRulesDraft["unknownPolicy"]))}
|
||||
@@ -408,7 +408,7 @@ export function SlotRulesEditor({
|
||||
|
||||
<fieldset className="space-y-3 border-t border-border/60 pt-4">
|
||||
<legend className="mb-1 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
||||
软偏好 · 通过硬条件后排序,越靠前越优先
|
||||
软偏好 · 通过硬条件后排序,越靠前越优先
|
||||
</legend>
|
||||
<FormField label="HDR">
|
||||
<ChipToggleGroup
|
||||
@@ -437,14 +437,14 @@ export function SlotRulesEditor({
|
||||
onChange={(v) => set("softCodecs", v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="偏好发布组" htmlFor={`${id}-grp`} hint="逗号分隔,越靠前越好">
|
||||
<FormField label="偏好发布组" htmlFor={`${id}-grp`} hint="逗号分隔,越靠前越好">
|
||||
<Input id={`${id}-grp`} value={value.softGroups} onChange={(e) => set("softGroups", e.target.value)} />
|
||||
</FormField>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="space-y-3 border-t border-border/60 pt-4">
|
||||
<legend className="mb-1 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
||||
升级 · 生成替换计划,不直接覆盖旧文件
|
||||
升级 · 生成替换计划,不直接覆盖旧文件
|
||||
</legend>
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2">
|
||||
<p className="text-caption font-medium text-foreground">允许自动升级</p>
|
||||
@@ -459,7 +459,7 @@ export function SlotRulesEditor({
|
||||
label="最小来源等级提升"
|
||||
htmlFor={`${id}-tier`}
|
||||
error={errors.minTierSteps}
|
||||
hint="默认 1;同等级不反复更换"
|
||||
hint="默认 1;同等级不反复更换"
|
||||
>
|
||||
<Input
|
||||
id={`${id}-tier`}
|
||||
@@ -481,7 +481,7 @@ export function SlotRulesEditor({
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="合格压制组名单" htmlFor={`${id}-allow`} hint="设置后,名单外的压制不触发自动升级">
|
||||
<FormField label="合格压制组名单" htmlFor={`${id}-allow`} hint="设置后,名单外的压制不触发自动升级">
|
||||
<Input
|
||||
id={`${id}-allow`}
|
||||
value={value.encodeGroupAllowlist}
|
||||
@@ -511,8 +511,8 @@ export function SlotRulesEditor({
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="__none">未设置</SelectItem>
|
||||
<SelectItem value="movie">按内容版本核对(电影)</SelectItem>
|
||||
<SelectItem value="units">按内容单元覆盖核对(剧集/综艺)</SelectItem>
|
||||
<SelectItem value="movie">按内容版本核对(电影)</SelectItem>
|
||||
<SelectItem value="units">按内容单元覆盖核对(剧集/综艺)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.form ? <p className="text-meta text-destructive">{errors.form}</p> : null}
|
||||
|
||||
Reference in new issue
Block a user