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

@@ -19,8 +19,8 @@ import { errorMessage } from "~/lib/api.ts";
import { formatBytes } from "~/lib/utils.ts";
const STEPS = [
"生成下载计划,预留版本目标和预算;每个版本目标同时只有一个在途计划",
"已有相同资源或相同 hash 的任务时直接接管,不重复提交",
"生成下载计划,预留版本目标和预算;每个版本目标同时只有一个在途计划",
"已有相同资源或相同 hash 的任务时直接接管,不重复提交",
"缓存卷超过硬阈值或读不到磁盘状态时无法提交",
"提交前复核免费、做种等易变字段",
];
@@ -56,7 +56,7 @@ export function DownloadDialog({
if (r.submitted && r.plan) {
setRejected(null);
setDone(r.plan.id);
toast.success(`已提交${slotName(r.slotId) ? `(${slotName(r.slotId)})` : ""},进度见“下载队列”`);
toast.success(`已提交${slotName(r.slotId) ? `(${slotName(r.slotId)})` : ""},进度见「下载队列」`);
} else {
setRejected(r.rejected);
toast.warning("没有生成下载计划");
@@ -102,12 +102,14 @@ export function DownloadDialog({
</div>
) : (
<p className="text-meta text-muted-foreground">
{release.entity ? "这个作品没有版本目标。先在订阅里新建。" : "资源未关联本地作品。先在“身份与元数据”指定。"}
{release.entity
? "这个作品没有版本目标。先在订阅里新建。"
: "资源未关联本地作品。先在「身份与元数据」指定。"}
</p>
)}
{rejected ? (
<div className="rounded-xl border border-warning/40 bg-warning/10 px-3 py-2.5 text-caption text-warning">
<p className="mb-1">没有生成下载计划:</p>
<p className="mb-1">没有生成下载计划:</p>
<ul className="list-disc space-y-0.5 pl-5">
{rejected.map((r, i) => (
<li key={`${r.slotId}-${r.code}-${i}`} className="break-words">
@@ -132,11 +134,11 @@ export function DownloadDialog({
onClick={() => {
onOpenChange(false);
openAgentDrawer({
label: `资源:${release.title}`,
label: `资源:${release.title}`,
releaseId: release.id,
entityId: release.entity?.id,
siteId: release.siteId,
goal: "判断这个资源是否值得下载;合适就按正常流程提交",
goal: "判断这个资源是否值得下载;合适就按正常流程提交",
});
}}
>
@@ -130,7 +130,7 @@ export function FeedFormDialog({
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>{feed ? "编辑 Feed" : "新建 Feed"}</DialogTitle>
<DialogDescription>URL 加密保存,之后只显示脱敏地址。</DialogDescription>
<DialogDescription>URL 加密保存,之后只显示脱敏地址。</DialogDescription>
</DialogHeader>
<form
className="grid gap-1"
@@ -152,7 +152,7 @@ export function FeedFormDialog({
htmlFor="feed-url"
required={!feed}
error={errors.url}
hint={feed ? `当前:${feed.urlDisplay};留空表示不变` : "RSS / Atom 地址"}
hint={feed ? `当前:${feed.urlDisplay};留空表示不变` : "RSS / Atom 地址"}
>
<Input
id="feed-url"
@@ -179,7 +179,7 @@ export function FeedFormDialog({
</SelectContent>
</Select>
</FormField>
<FormField label="轮询间隔(分钟)" htmlFor="feed-poll" error={errors.pollMin}>
<FormField label="轮询间隔(分钟)" htmlFor="feed-poll" error={errors.pollMin}>
<Input
id="feed-poll"
type="number"
@@ -233,7 +233,7 @@ export function FeedFormDialog({
</FormField>
</div>
<FormField
label="过滤条件(JSON,可选)"
label="过滤条件(JSON,可选)"
htmlFor="feed-filter"
error={errors.filter}
hint='如 {"include":["2160p"],"exclude":["CAM"]}'
@@ -248,7 +248,7 @@ export function FeedFormDialog({
/>
</FormField>
<div className="flex items-center justify-between gap-3 py-1 text-caption text-foreground">
<label htmlFor="feed-enabled">启用(关闭后停止轮询)</label>
<label htmlFor="feed-enabled">启用(关闭后停止轮询)</label>
<Switch id="feed-enabled" checked={d.enabled} onCheckedChange={(v) => set("enabled", v)} />
</div>
<DialogFooter className="mt-2 gap-2">
@@ -59,7 +59,7 @@ function FeedActions({ f }: { f: FeedSource }) {
<Button
variant="ghost"
size="icon-sm"
title="立即抓取(受站点限流)"
title="立即抓取(受站点限流)"
aria-label="立即抓取"
disabled={!f.enabled || fetchNow.isPending}
onKeyDown={(e) => e.stopPropagation()}
@@ -207,7 +207,7 @@ export function FeedTable({
empty={{
icon: Rss,
title: filtered ? "没有符合筛选的 Feed" : "还没有 Feed",
description: filtered ? undefined : "新建 Feed 后按轮询间隔抓取,新资源进入识别和收录评估。",
description: filtered ? undefined : "新建 Feed 后按轮询间隔抓取,新资源进入识别和收录评估。",
}}
/>
@@ -19,12 +19,12 @@ import { categoryLabel, hrOf, ORIGIN_LABEL, relTime } from "./release-fields.ts"
import { InventoryOverlay, ReleaseBadges } from "./release-list.tsx";
const DECISION_LABEL = {
accept: { label: "符合,可收录", tone: "success" },
accept: { label: "符合,可收录", tone: "success" },
upgrade: { label: "可升级", tone: "success" },
reject: { label: "不收录", tone: "destructive" },
unknown_field: { label: "缺字段,待复核", tone: "warning" },
unknown_field: { label: "缺字段,待复核", tone: "warning" },
skipped: { label: "被跳过", tone: "neutral" },
deferred: { label: "延后:已有计划在途", tone: "warning" },
deferred: { label: "延后:已有计划在途", tone: "warning" },
} as const;
/** 本地库存 + 候选理由(来自作品详情:Slot / CandidateEvaluation),不从标题猜 */
@@ -62,7 +62,7 @@ function LocalInventory({ release, entry }: { release: WorkbenchRelease; entry:
{detail.isPending ? (
<LoadingState rows={2} />
) : detail.error ? (
<p className="text-meta text-destructive">作品详情读取失败:{errorMessage(detail.error)}</p>
<p className="text-meta text-destructive">作品详情读取失败:{errorMessage(detail.error)}</p>
) : (
<>
<p className="text-meta text-muted-foreground">
@@ -140,7 +140,7 @@ export function ReleaseDetailSheet({
size="sm"
onClick={() =>
openAgentDrawer({
label: `资源:${r.title}`,
label: `资源:${r.title}`,
releaseId: r.id,
entityId: r.entity?.id,
siteId,
@@ -177,7 +177,7 @@ export function ReleaseDetailSheet({
{
onSuccess: (d) =>
d.refreshError
? toast.warning(`复核失败,显示上次数据:${d.refreshError}`)
? toast.warning(`复核失败,显示上次数据:${d.refreshError}`)
: toast.success("已复核"),
onError: (e) => toast.error(errorMessage(e)),
},
@@ -197,7 +197,7 @@ export function ReleaseDetailSheet({
<>
{detail.data.refreshError ? (
<p className="mb-2 rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
最近一次复核失败,以下是过期数据:{detail.data.refreshError}
最近一次复核失败,以下是过期数据:{detail.data.refreshError}
</p>
) : null}
<KvList
@@ -206,7 +206,7 @@ export function ReleaseDetailSheet({
{ label: "促销截止", value: formatDateTime(detail.data.volatile.promotionExpiresAt) },
{
label: "做种 / 下载",
value: `${r.seeders ?? "?"} / ${r.leechers ?? "?"}(${relTime(detail.data.volatile.peersObservedAt)}观察)`,
value: `${r.seeders ?? "?"} / ${r.leechers ?? "?"}(${relTime(detail.data.volatile.peersObservedAt)}观察)`,
},
{ label: "详情抓取于", value: formatDateTime(detail.data.volatile.detailFetchedAt) },
]}
@@ -225,7 +225,7 @@ export function ReleaseDetailSheet({
items={[
{ label: "分类", value: categoryLabel(r) },
{ label: "大小", value: r.sizeBytes === null ? "未知" : formatBytes(r.sizeBytes, 2) },
{ label: "H&R", value: { yes: "有", no: "无", unknown: "未知(按有义务处理)" }[hrOf(r)] },
{ label: "H&R", value: { yes: "有", no: "无", unknown: "未知(按有义务处理)" }[hrOf(r)] },
{ label: "站点上传", value: formatDateTime(r.sourcePublishedAt) },
{ label: "首次发现", value: formatDateTime(r.firstSeenAt) },
{ label: "最近确认", value: `${formatDateTime(r.dataUpdatedAt)}(${ORIGIN_LABEL[r.origin]})` },
@@ -91,6 +91,6 @@ export function accountSummary(accounts: SiteAccount[]): { label: string; tone:
export function circuitLabel(site: Pick<Site, "circuitState" | "circuitOpenUntil">): string | null {
if (site.circuitState === "closed") return null;
if (site.circuitState === "half_open") return "熔断半开(试探中)";
if (site.circuitState === "half_open") return "熔断半开(试探中)";
return `熔断中${site.circuitOpenUntil ? `,${relTime(site.circuitOpenUntil)}恢复` : ""}`;
}
@@ -14,7 +14,7 @@ export function ReleaseBadges({ r }: { r: WorkbenchRelease }) {
{promo ? (
<StatusBadge tone={promo.expired ? "neutral" : promo.free ? "success" : "neutral"}>
{promo.label}
{promo.expired ? "(已过期)" : r.promotionExpiresAt ? ` · ${relTime(r.promotionExpiresAt)}截止` : ""}
{promo.expired ? "(已过期)" : r.promotionExpiresAt ? ` · ${relTime(r.promotionExpiresAt)}截止` : ""}
</StatusBadge>
) : null}
{hr === "yes" ? <StatusBadge tone="warning">H&R</StatusBadge> : null}
@@ -114,7 +114,7 @@ export function ReleaseRow({
{r.leechers ?? "?"}
</span>
<span title="站点上传时间">上传 {relTime(r.sourcePublishedAt)}</span>
<span className="inline-flex items-center gap-1" title={`数据最近确认:${r.dataUpdatedAt}`}>
<span className="inline-flex items-center gap-1" title={`数据最近确认:${r.dataUpdatedAt}`}>
<Clock className="h-3 w-3" />
{ORIGIN_LABEL[r.origin]} · {relTime(r.dataUpdatedAt)}
</span>
@@ -64,7 +64,7 @@ export function SnapshotPanel({ site }: { site: Site }) {
snap.mutate(
{ id: site.id, maxPages: 2 },
{
onSuccess: () => toast.success(`${site.name}:快照已排队`),
onSuccess: () => toast.success(`${site.name}:快照已排队`),
onError: (e) => toast.error(errorMessage(e)),
},
)
@@ -90,7 +90,7 @@ export function SnapshotPanel({ site }: { site: Site }) {
{s.query ? <span className="min-w-0 truncate text-muted-foreground">“{s.query}”</span> : null}
<span className="text-muted-foreground tabular-nums">
{s.pages.length ? `第 ${s.pages.join(",")} 页 · ` : ""}
{s.itemCount} 条(新 {s.newCount})
{s.itemCount} 条(新 {s.newCount})
</span>
<span className="ml-auto text-meta text-muted-foreground" title={formatDateTime(s.startedAt)}>
{relTime(s.startedAt)}