fix: replace raw codes and internal IDs in the console with Chinese labels

Found in v0.2 visual QA (docs/plan/09-visual-qa.md):
- actor strings (admin_session:<uuid>, user:<uuid>, system) -> 管理员/系统/API Key
  in policy revisions, subscriptions, inbox, trash, business tasks, organize detail
- cache volume without key shows the downloader name instead of instance:<uuid>
- download timeline/plan: task and organize statuses, plan purpose, priority and
  status reason localized; audit gets labels for capability/files/site/feed actions
- organize record reason uses conflict detail (没有可入库的主视频) instead of
  blocked:no_main_video; slot/candidate reasons drop inflight:<uuid>
- identity decision summary no longer shows no_viable_candidate
- upstream test: 'Connection error.' classified as network, with a Chinese hint
- new policy draft prefills key default (placeholder looked like a value)
- dialog/sheet close sr-only text in Chinese

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 15:22:15 +08:00
1 parent d7eb6a45d0
commit 46d37d87fe
23 files changed
+176 -43

No files matched your search

@@ -15,6 +15,7 @@ import { useQueryState } from "~/components/console/use-query-state.ts";
import { Button } from "~/components/ui/button.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import { errorMessage, TTL_SECOND } from "~/lib/api.ts";
import { actorText } from "~/lib/labels.ts";
import { formatDateTime } from "~/lib/utils.ts";
// 业务任务(/tasks):引擎只负责推进,task 表是面向用户的投影;取消只停后续步骤,重试只对失败任务。
@@ -201,7 +202,7 @@ export function BusinessTasks() {
key: "by",
header: "触发",
hideBelow: "lg",
cell: (t) => <span className="text-muted-foreground">{t.triggeredBy ?? "—"}</span>,
cell: (t) => <span className="text-muted-foreground">{actorText(t.triggeredBy)}</span>,
},
{
key: "at",
@@ -294,7 +295,7 @@ export function TaskSummary({ t }: { t: Task }) {
<KvList
items={[
{ label: "尝试", value: `${t.attempts}/${t.maxAttempts}` },
{ label: "触发", value: t.triggeredBy },
{ label: "触发", value: actorText(t.triggeredBy) },
{ label: "创建", value: formatDateTime(t.createdAt) },
{ label: "开始", value: formatDateTime(t.startedAt) },
{ label: "结束", value: formatDateTime(t.finishedAt) },
@@ -3,6 +3,7 @@ import { Brush, HardDrive, SlidersHorizontal } from "lucide-react";
import { Link, useNavigate } from "react-router";
import { toast } from "sonner";
import { useCacheCleanup, useCacheVolumes } from "~/api/downloads.ts";
import { downloaders } from "~/api/instances.ts";
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { consolePath, settingsPath } from "~/components/console/nav.ts";
@@ -13,7 +14,18 @@ import { cn, formatBytes } from "~/lib/utils.ts";
type Volume = Dashboard["cache"]["volumes"][number];
/** 缓存卷显示名:没填缓存卷标识时服务端用 `instance:<id>`,换成下载器实例名,不露内部 ID */
export function useVolumeLabel() {
const list = downloaders.useList();
return (v: Pick<Volume, "volumeKey" | "instanceIds">) => {
if (!v.volumeKey.startsWith("instance:")) return v.volumeKey;
const names = v.instanceIds.map((id) => list.data?.items.find((i) => i.id === id)?.name).filter(Boolean);
return names.length ? names.join("、") : "下载器缓存";
};
}
function VolumeBar({ v }: { v: Volume }) {
const volumeLabel = useVolumeLabel();
const pct = v.usedPercent;
const tripped = pct !== null && pct >= v.hardThresholdPercent;
const nearing = pct !== null && !tripped && pct >= v.resumePercent;
@@ -22,7 +34,7 @@ function VolumeBar({ v }: { v: Volume }) {
<div className="flex flex-wrap items-baseline justify-between gap-2">
<span className="flex min-w-0 items-center gap-1.5 text-sm font-medium text-foreground">
<HardDrive className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{v.volumeKey}</span>
<span className="truncate">{volumeLabel(v)}</span>
</span>
<span className="text-meta text-muted-foreground tabular-nums">
{pct === null ? "使用率未知" : `${pct.toFixed(1)}%`}
@@ -20,10 +20,13 @@ import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Button } from "~/components/ui/button.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import { errorMessage } from "~/lib/api.ts";
import { planReasonText } from "~/lib/labels.ts";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
import { BusinessTimeline } from "./business-timeline.tsx";
import { OrganizeDialog } from "./organize-dialog.tsx";
const PLAN_PURPOSE_LABEL: Record<string, string> = { fill: "补缺", upgrade: "升级", replace_source: "换源" };
/** 进度:细线 + 文字;总量未知时不伪造百分比 */
export function ProgressLine({ t }: { t: DownloadTask }) {
const known = t.sizeBytes !== null;
@@ -166,9 +169,9 @@ export function DownloadDetail({ task }: { task: DownloadTask }) {
<KvList
items={[
{ label: "状态", value: PLAN_STATUS_LABEL[plan.data.status] },
{ label: "用途", value: plan.data.purpose },
{ label: "用途", value: PLAN_PURPOSE_LABEL[plan.data.purpose] ?? plan.data.purpose },
{ label: "预留空间", value: formatBytes(plan.data.reservedBytes, 1) },
{ label: "说明", value: plan.data.statusReason, wide: true },
{ label: "说明", value: planReasonText(plan.data.statusReason), wide: true },
]}
/>
<JsonView className="mt-2" label="评估依据" value={plan.data.evaluation} />
+16
View File
@@ -264,3 +264,19 @@ export const opts = <K extends string>(rec: Record<K, string | { label: string }
const v = rec[value];
return { value, label: typeof v === "string" ? v : v.label };
});
/** 版本目标 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:<id>`)→ 中文;不认识的原样显示 */
const SLOT_REASON: Record<string, string> = {
"blocked:no_main_video": "没有可入库的主视频",
"deferred:slot_busy": "已有计划在途,等它结束再评估",
verified: "已核验入库",
verified_partial: "部分核验入库",
hard_conditions_met: "硬条件全部满足",
};
export function slotReasonText(raw: string): string {
return raw
.split(";")
.filter((p) => !p.startsWith("inflight:"))
.map((p) => SLOT_REASON[p] ?? p)
.join(";");
}
@@ -2,7 +2,7 @@ import type { AcquisitionSlot, CandidateEvaluation } from "@umt/contracts";
import { Link2 } from "lucide-react";
import type { ReactNode } from "react";
import { formatDateTime } from "~/lib/utils.ts";
import { CANDIDATE_DECISION, SLOT_STATUS } from "./labels.ts";
import { CANDIDATE_DECISION, SLOT_STATUS, slotReasonText } from "./labels.ts";
import { Chip, ChipList, Tone } from "./parts.tsx";
import { summarizeCompletion, summarizeHard, summarizeSoft, summarizeUpgrade } from "./slot-rules.tsx";
@@ -45,7 +45,9 @@ export function SlotCard({
<span className="ml-auto text-meta text-muted-foreground tabular-nums">r{slot.revision}</span>
{actions}
</header>
{slot.statusReason ? <p className="mt-2 text-caption text-muted-foreground">{slot.statusReason}</p> : null}
{slot.statusReason ? (
<p className="mt-2 text-caption text-muted-foreground">{slotReasonText(slot.statusReason)}</p>
) : null}
<dl className="mt-3 grid gap-3 text-sm sm:grid-cols-2">
<div className="min-w-0">
<dt className="mb-1 text-meta text-muted-foreground">硬条件</dt>
@@ -94,7 +96,9 @@ export function SlotCard({
</ul>
) : null}
{c.reasons.length ? (
<p className="mt-1 text-meta break-words text-muted-foreground">{c.reasons.join(";")}</p>
<p className="mt-1 text-meta break-words text-muted-foreground">
{c.reasons.map(slotReasonText).filter(Boolean).join(";")}
</p>
) : null}
<p className="text-micro text-muted-foreground tabular-nums">评估于 {formatDateTime(c.evaluatedAt)}</p>
</li>
@@ -30,6 +30,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~
import { Switch } from "~/components/ui/switch.tsx";
import { Textarea } from "~/components/ui/textarea.tsx";
import { errorMessage } from "~/lib/api.ts";
import { actorText } from "~/lib/labels.ts";
import { cn, formatDateTime } from "~/lib/utils.ts";
import { CANDIDATE_DECISION, MEDIA_KIND, POLICY_STATUS } from "./labels.ts";
import { ChipList, ChipToggleGroup, Panel, Tone } from "./parts.tsx";
@@ -80,7 +81,8 @@ function draftFromRevision(r: PolicyRevision | null): PolicyDraft {
const rules = r ? AcquisitionPolicyRules.safeParse(r.rules).data : undefined;
const kinds = r ? z.array(z.string()).safeParse((r.scope as { mediaKinds?: unknown }).mediaKinds).data : undefined;
return {
policyKey: r?.policyKey ?? "",
// 新草案预填 default:占位符看起来像已填的值,留空提交会报"填写策略键"
policyKey: r?.policyKey ?? "default",
title: r?.title ?? "",
summary: r?.summary ?? "",
scopeKinds: kinds ?? [],
@@ -293,7 +295,7 @@ function PolicyGroup({
}
description={
published
? `当前生效 r${published.revision} · ${formatDateTime(published.publishedAt)} 由 ${published.publishedBy ?? "—"} 发布`
? `当前生效 r${published.revision} · ${formatDateTime(published.publishedAt)} 由 ${actorText(published.publishedBy)} 发布`
: "未发布,不会自动执行"
}
actions={
@@ -492,10 +494,12 @@ function PolicyDetailSheet({
items={[
{ label: "状态", value: <Tone v={POLICY_STATUS[rev.status]} /> },
{ label: "作用范围", value: scopeText(rev.scope) },
{ label: "创建", value: `${formatDateTime(rev.createdAt)} · ${rev.createdBy ?? "—"}` },
{ label: "创建", value: `${formatDateTime(rev.createdAt)} · ${actorText(rev.createdBy)}` },
{
label: "发布",
value: rev.publishedAt ? `${formatDateTime(rev.publishedAt)} · ${rev.publishedBy ?? "—"}` : "未发布",
value: rev.publishedAt
? `${formatDateTime(rev.publishedAt)} · ${actorText(rev.publishedBy)}`
: "未发布",
},
{ label: "说明", value: rev.summary, wide: true },
]}
+14 -2
View File
@@ -18,6 +18,15 @@ const ERROR_KIND: Record<string, string> = {
other: "失败",
};
const SOURCE_TEXT: Record<string, string> = { "stored credential": "已存凭证" };
/** 上游返回的多是英文原文:先给一句怎么办,原文留作排查 */
const ERROR_HINT: Record<string, string> = {
network: "连不上上游,检查 Base URL、端口和网络",
auth: "凭证被拒,检查 API Key 或重新订阅登录",
model: "模型不可用,换一个模型 id",
};
export function PiTestPanel({ upstream, model }: { upstream: PiUpstream; model?: string | null }) {
const test = useTestPiUpstream();
const [result, setResult] = useState<PiUpstreamTestResult | null>(null);
@@ -60,10 +69,13 @@ export function PiTestPanel({ upstream, model }: { upstream: PiUpstream; model?:
</StatusBadge>
<span className="text-muted-foreground">
{result.latencyMs} ms{result.model ? ` · ${result.model}` : ""}
{result.source ? ` · ${result.source}` : ""}
{result.source ? ` · ${SOURCE_TEXT[result.source] ?? result.source}` : ""}
</span>
{!result.ok && result.message ? (
<span className="w-full break-words text-destructive">{result.message}</span>
<span className="w-full break-words text-destructive">
{ERROR_HINT[result.errorKind ?? ""] ? `${ERROR_HINT[result.errorKind ?? ""]}。原始错误:` : ""}
{result.message}
</span>
) : null}
</div>
) : null}
+1 -1
View File
@@ -76,7 +76,7 @@ function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span"
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
<span className="sr-only">更多</span>
</span>
);
}
+2 -2
View File
@@ -59,7 +59,7 @@ function DialogContent({
className="absolute top-4 right-4 z-50 grid size-8 place-items-center rounded-full opacity-70 hover:bg-foreground/[0.06] ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
<span className="sr-only">关闭</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
@@ -94,7 +94,7 @@ function DialogFooter({
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
<Button variant="outline">关闭</Button>
</DialogPrimitive.Close>
)}
</div>
+1 -1
View File
@@ -66,7 +66,7 @@ function SheetContent({
{showCloseButton && (
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 grid size-8 place-items-center rounded-full opacity-70 hover:bg-foreground/[0.06] transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
<span className="sr-only">关闭</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>