merge: copy/c2

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-07 02:04:48 +08:00
commit f344937026
36 files changed
+103 -101

No files matched your search

@@ -40,7 +40,7 @@ function ChangePasswordDialog({ open, onOpenChange }: { open: boolean; onOpenCha
setErrors({});
try {
const r = await change.mutateAsync({ currentPassword: form.currentPassword, newPassword: form.newPassword });
toast.success(r.revokedSessions ? `密码已修改,已注销其它 ${r.revokedSessions} 个会话` : "密码已修改");
toast.success(r.revokedSessions ? `密码已修改,其他 ${r.revokedSessions} 个会话已退出` : "密码已修改");
setForm({ currentPassword: "", newPassword: "", confirm: "" });
onOpenChange(false);
} catch (err) {
@@ -58,7 +58,7 @@ function ChangePasswordDialog({ open, onOpenChange }: { open: boolean; onOpenCha
<DialogContent>
<DialogHeader>
<DialogTitle>修改密码</DialogTitle>
<DialogDescription>修改后其它设备上的会话会被注销,当前会话保留。</DialogDescription>
<DialogDescription>其他设备上的会话会退出登录,当前会话保留。</DialogDescription>
</DialogHeader>
<form id="pwd-form" onSubmit={submit} className="grid gap-1" noValidate>
<FormField label="当前密码" htmlFor="pwd-cur" error={errors.currentPassword}>
+5 -5
View File
@@ -51,7 +51,7 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console",
label: "首页",
icon: House,
description: "在忙什么、要我处理什么、最近整理了什么",
description: "进行中的任务、等你批准的操作、最近的整理",
},
{
to: "/console/agent",
@@ -68,11 +68,11 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/sites",
label: "站点",
icon: Globe,
description: "PT 站、公开 BT 站与 RSS 源:账号、资源与 RSS",
description: "PT 站、公开 BT 站和自定义 RSS 源",
},
{ to: "/console/discover", label: "发现", icon: Compass, description: "bgm.tv 每日放送与豆瓣热门" },
{ to: "/console/subscriptions", label: "订阅", icon: CalendarClock, description: "单次收录与持续追踪" },
{ to: "/console/rules", label: "规则", icon: ListChecks, description: "收录策略与偏好标签" },
{ to: "/console/rules", label: "规则", icon: ListChecks, description: "收录策略与规则组" },
],
},
{
@@ -106,7 +106,7 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/logs",
label: "日志",
icon: ScrollText,
description: "任务、定时与自动任务、登录与操作留痕,可实时查看",
description: "任务、定时任务、登录与操作记录,可实时查看",
},
{
to: "/console/status",
@@ -131,7 +131,7 @@ const NAV_ALIASES: Record<string, string> = {
/** 不在侧栏但有页面的路由:顶栏标题用 */
const EXTRA_TITLES: Record<string, string> = {
"/console/preferences": "偏好标签",
"/console/preferences": "规则",
"/console/search": "搜索",
};
@@ -11,7 +11,7 @@ export function WorkspacePlaceholder({ milestone = "W3" }: { milestone?: string
return (
<>
<PageHeader title={item?.label ?? "未命名页面"} description={item?.description} />
<EmptyState icon={item?.icon ?? Construction} title="尚未接入" description={`计划在 ${milestone} 接入。`} />
<EmptyState icon={item?.icon ?? Construction} title="还没接入" description={`计划在 ${milestone} 接入。`} />
</>
);
}
+6 -6
View File
@@ -70,10 +70,10 @@ const ACTION_LABEL: Record<string, string> = {
"pi_upstream.oauth_start": "开始订阅登录",
"pi_upstream.oauth_login": "完成订阅登录",
"pi_upstream.oauth_logout": "退出订阅登录",
"schedule.run_now": "立即执行定时任务",
"schedule.run_now": "立即运行定时任务",
"schedule.skip": "跳过定时任务",
"schedule.update": "修改定时任务",
"storage.cleanup": "立即清理存储",
"storage.cleanup": "清理存储",
"acquisition_plan.manual_submit": "手动提交下载",
"acquisition_plan.cancel": "取消下载",
"capability.invoke": "智能体操作",
@@ -81,7 +81,7 @@ const ACTION_LABEL: Record<string, string> = {
"site.created": "登记站点",
"site.updated": "修改站点",
"feed_source.created": "新建 RSS",
"identity.release_assigned": "指定资源身份",
"identity.release_assigned": "为资源指定作品",
"files.mkdir": "新建目录",
"files.rename": "重命名",
"files.move": "移动文件",
@@ -149,13 +149,13 @@ const SCHEDULE_STATUS: Record<string, { label: string; tone: StatusTone }> = {
running: { label: "进行中", tone: "neutral" },
};
const AGENT_STATUS: Record<string, { label: string; tone: StatusTone }> = {
queued: { label: "排队", tone: "neutral" },
waiting: { label: "等待", tone: "neutral" },
queued: { label: "排队中", tone: "neutral" },
waiting: { label: "等待中", tone: "neutral" },
cancelled: { label: "已停止", tone: "warning" },
budget_exceeded: { label: "超出预算", tone: "warning" },
};
const ENGINE_STATUS: Record<string, { label: string; tone: StatusTone }> = {
pending: { label: "排队", tone: "neutral" },
pending: { label: "排队中", tone: "neutral" },
canceled: { label: "已取消", tone: "warning" },
};
+2 -2
View File
@@ -115,7 +115,7 @@ function ScheduleRunBody({ e }: { e: LogEntry }) {
{ to: logsPath({ tab: "schedules", job: jobId }), label: "定时任务设置" },
{
to: objectLogsPath("scheduled_job", jobId),
label: "这个定时任务的全部运行",
label: "这个定时任务的全部运行记录",
icon: <History className="h-3.5 w-3.5" />,
},
]
@@ -240,7 +240,7 @@ function RunSheet({
<Button asChild variant="ghost" size="sm">
<Link to={`/console/tasks/${encodeURIComponent(r.id)}`}>
<ExternalLink />
独立页面
打开任务页
</Link>
</Button>
{entry ? <AskAgentButton entries={[entry]} /> : null}
@@ -199,7 +199,7 @@ export function AgentCheckCard({
<footer className="mt-auto flex flex-wrap items-center gap-1.5">
<Button size="sm" disabled={!a.canRun} onClick={a.run}>
{a.runNow.isPending ? <LoaderCircle className="animate-spin" /> : <Play />}
立即执行
立即运行
</Button>
<Button size="sm" variant="ghost" disabled={!a.canSkip} onClick={a.skipNext}>
<SkipForward />
@@ -337,7 +337,7 @@ function AgentCheckDetail({
</Field>
<Field label="触发条件">
{CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]}
{c.reportAsDecision ? <span className="ml-1 text-meta text-muted-foreground">· 结果写入待处理</span> : null}
{c.reportAsDecision ? <span className="ml-1 text-meta text-muted-foreground">· 生成巡检报告</span> : null}
</Field>
<Field label="预算">{budgetParts.length ? budgetParts.join(" · ") : "默认"}</Field>
<Field label="模型">
@@ -169,11 +169,11 @@ export function AgentCheckDialog({
rows={4}
value={d.goal}
aria-invalid={!!errors.goal}
placeholder="检查 {{openDecisions}} 条待处理事项,给出处理建议"
placeholder="检查 {{openDecisions}} 条需要处理的事项,给出处理建议"
onChange={(e) => set("goal", e.target.value)}
/>
</FormField>
<FormField label="能力白名单" hint="不选则放行全部只读能力">
<FormField label="能力白名单" hint="不选就允许全部只读能力">
<div className="grid max-h-48 gap-1 overflow-y-auto rounded-tight border border-border/70 px-2.5 py-2 sm:grid-cols-2">
{READONLY_CAPS.map((c) => {
const on = d.caps.includes(c.id);
@@ -264,15 +264,17 @@ export function AgentCheckDialog({
<div className="grid gap-2 pb-2">
<label htmlFor="ac-report" className="flex items-center justify-between gap-3 text-caption">
<span>
写入待处理
<span className="ml-2 text-meta text-muted-foreground">结果汇总成一条待处理事项</span>
生成巡检报告
<span className="ml-2 text-meta text-muted-foreground">运行成功后把结果汇总成一条报告</span>
</span>
<Switch id="ac-report" checked={d.reportAsDecision} onCheckedChange={(v) => set("reportAsDecision", v)} />
</label>
<label htmlFor="ac-recover" className="flex items-center justify-between gap-3 text-caption">
<span>
允许授权内恢复
<span className="ml-2 text-meta text-muted-foreground">需有效持续授权,否则只读</span>
<span className="ml-2 text-meta text-muted-foreground">
可重试任务、重新校验、确认识别;没有有效的持续授权时只读
</span>
</span>
<Switch id="ac-recover" checked={d.allowRecover} onCheckedChange={(v) => set("allowRecover", v)} />
</label>
@@ -60,7 +60,7 @@ export function useJobActions(job: ScheduledJob) {
runNow.mutate(
{ id: job.id },
{
onSuccess: (r) => toast.success(r.queued ? "已排队执行" : "已在排队或运行中"),
onSuccess: (r) => toast.success(r.queued ? "已加入队列" : "已在排队或运行中"),
onError: (e) => toast.error(errorMessage(e)),
},
),
@@ -149,8 +149,8 @@ function AutoTaskRow({ job, now, highlight }: { job: ScheduledJob; now: number;
<Button
size="icon-sm"
variant="ghost"
aria-label={`立即执行${job.name}`}
title="立即执行"
aria-label={`立即运行${job.name}`}
title="立即运行"
disabled={!a.canRun}
onClick={a.run}
>
@@ -21,7 +21,7 @@ import { errorMessage } from "~/lib/api.ts";
// 对话里给「检查并启用」卡片,用户在计划页检查、修改后启用。
const EXAMPLES = [
"每天早上 9 点前看看有没有失败或卡住的下载,有就写成待处理",
"每天早上 9 点前看看有没有失败或卡住的下载,有就汇报",
"每隔 6 小时检查一下站点登录有没有失效",
"每周汇报一次订阅追更情况和缺集",
];
@@ -14,7 +14,7 @@ describe("计划页标签", () => {
{ text: "巡检待处理(" },
{ placeholder: "now", label: "当前时间" },
{ text: "):有 " },
{ placeholder: "openDecisions", label: "待处理事项数" },
{ placeholder: "openDecisions", label: "需要处理的事项数" },
{ text: " 条,{{unknown}}" },
]);
});
+3 -3
View File
@@ -48,8 +48,8 @@ export const TRIGGER_LABEL: Record<ScheduleTrigger, string> = {
export const CHECK_TRIGGER_LABEL: Record<AgentCheckTrigger, string> = {
always: "每次运行",
open_decisions: "有待处理时",
stalled_downloads: "有下载无进展时",
open_decisions: "有需要处理的事项时",
stalled_downloads: "有下载无进展或失败时",
subscriptions_active: "有追踪订阅时",
};
@@ -135,7 +135,7 @@ export function formatClock(iso: string | null | undefined): string {
export const GOAL_PLACEHOLDER_LABEL: Record<string, string> = {
now: "当前时间",
date: "今天日期",
openDecisions: "待处理事项数",
openDecisions: "需要处理的事项数",
stalledDownloads: "异常下载数",
activeSubscriptions: "追踪中的订阅数",
};
@@ -55,7 +55,7 @@ export function RunsSheet({ job, onClose }: { job: ScheduledJob | null; onClose:
to={consolePath("agent", r.agentRunId)}
className="w-fit text-meta text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
>
查看智能体会话{r.agentStatus ? `(${agentRunLabel(r.agentStatus).label})` : ""}
查看智能体会话{r.agentStatus ? `(${agentRunLabel(r.agentStatus).label})` : ""}
</Link>
) : null}
</li>
@@ -101,7 +101,7 @@ function PasskeyRow({ item }: { item: PasskeyInfo }) {
}
title={`删除 Passkey「${item.name}」?`}
description="删除后这把钥匙不能再登录控制台;设备上的钥匙要在系统设置里另外删。"
confirmLabel="删除"
confirmLabel="删除 Passkey"
destructive
onConfirm={async () => {
try {
+5 -5
View File
@@ -41,7 +41,7 @@ function PasswordBlock({ disabled }: { disabled: boolean }) {
setErrors({});
try {
const r = await change.mutateAsync({ currentPassword: form.currentPassword, newPassword: form.newPassword });
toast.success(r.revokedSessions ? `密码已修改,注销了其它 ${r.revokedSessions} 个会话` : "密码已修改");
toast.success(r.revokedSessions ? `密码已修改,其他 ${r.revokedSessions} 个会话已退出` : "密码已修改");
setForm(EMPTY_PWD);
} catch (err) {
const msg = errorMessage(err);
@@ -58,7 +58,7 @@ function PasswordBlock({ disabled }: { disabled: boolean }) {
section="account"
anchor="password"
title="修改密码"
description="修改后其它设备上的会话全部注销,当前会话保留。"
description="其他设备上的会话会退出登录,当前会话保留。"
dirty={dirty}
>
<form onSubmit={submit} noValidate className="grid gap-x-4 gap-y-1 sm:grid-cols-3">
@@ -136,13 +136,13 @@ function SessionBlock() {
</dd>
<dt className="text-muted-foreground">当前会话</dt>
<dd>{s?.expiresAt ? `到 ${formatDateTime(s.expiresAt)} 失效` : "未使用登录会话"}</dd>
<dt className="text-muted-foreground">其它会话</dt>
<dd className="text-muted-foreground">修改密码时全部注销</dd>
<dt className="text-muted-foreground">其他会话</dt>
<dd className="text-muted-foreground">修改密码时全部退出登录</dd>
</dl>
<div className="mt-4 border-t border-border/60 pt-4">
<p className="text-caption font-medium">本机 Token</p>
<p className="mb-2 text-meta text-muted-foreground">
只存在这个浏览器,有值时请求会带上它。{hasToken ? "已设置。" : "未设置。"}
只保存在这个浏览器,有值时每个请求都会带上。{hasToken ? "已设置。" : "未设置。"}
</p>
<div className="flex max-w-md flex-wrap items-center gap-2">
<SecretInput
+4 -4
View File
@@ -88,20 +88,20 @@ function SearchSitesBlock() {
export const IDENTITY_CARD: CardSpec = {
anchor: "identity",
title: "资源识别",
description: "默认只在资源命中订阅、作品别名或新作品规则,或你手动识别和搜索时查 TMDB;其余放进未识别,不进待处理。",
description: "默认只在资源命中订阅、作品别名或新作品规则,或你手动识别和搜索时查 TMDB;其余资源留在未识别。",
fields: [
{
name: "identity.eagerTmdbLookup",
label: "每条新资源都查 TMDB",
kind: "switch",
hint: "每条新资源都查一次 TMDB。RSS 量大时每天可能上千次请求,站点独有的短剧和综艺多半查不到",
hint: "RSS 量大时每天可能上千次请求;站点独有的短剧和综艺多半查不到",
wide: true,
},
{
name: "identity.detailIdsPerSiteHour",
label: "识别时每站每小时最多抓详情页",
kind: "number",
hint: "标题认不准时,抓站点详情页取 IMDb、TMDB 或豆瓣 ID 再识别;只在搜索、订阅相关资源、补全和手动重新识别时抓,RSS 收进来的资源不抓。0 = 不抓",
hint: "标题认不准时,抓站点详情页取 IMDb、TMDB 或豆瓣 ID 再识别;只在搜索、命中订阅、补全和手动重新识别时抓,RSS 收进来的资源不抓。0 = 不抓",
},
{
name: "identity.autoEnrich",
@@ -125,7 +125,7 @@ export const IDENTITY_CARD: CardSpec = {
},
{
name: "identity.enrichSnippetAllSites",
label: "也用其它网站的搜索摘要",
label: "也用其他网站的搜索摘要",
kind: "switch",
hint: "短剧多半只有聚合站收录。打开后,白名单以外网站的搜索摘要也用来抽年份、集数、主演、简介(只看搜索引擎给的摘要,不打开这些网页)",
wide: true,
@@ -37,7 +37,7 @@ import { SettingsBlock } from "./settings-block.tsx";
export const KIND_LABEL: Record<CapabilityKind, string> = {
read: "只读",
preview: "预览",
propose: "提案",
propose: "方案",
approve: "审批",
execute: "执行",
};
@@ -49,7 +49,7 @@ const RISK: Record<RiskLevel, { label: string; tone: StatusTone }> = {
};
const AUTH: Record<CapabilityAuthState, { label: string; tone: StatusTone; hint: string }> = {
read_only: { label: "无需授权", tone: "neutral", hint: "只读或预览,不落变更,Key 有对应 scope 即可调用" },
read_only: { label: "不需要授权", tone: "neutral", hint: "只读或预览,不改数据;Key 有对应 scope 就能调用" },
granted: { label: "有持续授权", tone: "success", hint: "智能体、MCP、API Key 调用按持续授权放行" },
needs_approval: {
label: "需审批",
+1 -1
View File
@@ -105,7 +105,7 @@ export const BUDGET_CARD: CardSpec = {
export const INVESTIGATE_CARD: CardSpec = {
anchor: "investigate",
title: "自动处理",
description: "整理时识别不到作品就叫智能体识别;下载失败、集数对不上等问题出现时,自动发起一次有界调查。",
description: "整理时识别不到作品就叫智能体识别;下载失败、集数对不上时自动发起一次调查,受下面的上限约束。",
fields: [
{
name: "agent.autoIdentifyOrganize",
@@ -50,7 +50,7 @@ function Verified({ p }: { p: BrowserProviderView }) {
function CapabilityTable({ p }: { p: BrowserProviderView }) {
const caps = p.capabilities;
if (!caps) return <p className="mt-4 text-caption text-muted-foreground">还没有探测过。点"测试连通"读取能力。</p>;
if (!caps) return <p className="mt-4 text-caption text-muted-foreground">还没有探测过。点「测试连通」读取能力。</p>;
return (
<div className="mt-5 space-y-2">
<h3 className="text-caption font-semibold">
@@ -81,7 +81,7 @@ function CapabilityTable({ p }: { p: BrowserProviderView }) {
})}
</ul>
<p className="text-meta text-muted-foreground">
"已核验"只来自对真实实例的逐项核验记录(见运行手册),探测只能判断接口在不在。未核验的能力在原站视图里标出。
「已核验」只来自对真实实例的逐项核验记录(见运行手册),探测只能判断接口在不在。未核验的能力在原站视图里标出。
</p>
</div>
);
@@ -124,7 +124,7 @@ export function BrowserProvidersBlock() {
},
{
key: "key",
header: "访问凭据",
header: "访问凭证",
hideBelow: "md",
cell: (r) => (r.hasAccessKey ? "已设置" : "未设置"),
},
@@ -148,7 +148,7 @@ export function BrowserProvidersBlock() {
const auth = (v.health?.detail as { error?: string } | undefined)?.error === "auth";
return {
ok: v.health?.status === "ok",
message: auth ? "访问凭据不对" : v.health?.status === "degraded" ? "浏览器服务正在恢复" : "连不上",
message: auth ? "访问凭证不对" : v.health?.status === "degraded" ? "浏览器服务正在恢复" : "连不上",
version: v.capabilities?.upstreamVersion ?? null,
};
}}
+1 -1
View File
@@ -279,7 +279,7 @@ export const TTL_CARD: CardSpec = {
fields: [
ttlField("tmdbSearchDays", "TMDB 搜索与识别结果"),
ttlField("tmdbDetailDays", "TMDB 详情"),
ttlField("tmdbMissDays", "TMDB 空结果", "搜索无结果、未识别时的负缓存"),
ttlField("tmdbMissDays", "TMDB 空结果", "搜索无结果或没识别出的结果也缓存,期内不重复查"),
ttlField("imageDays", "TMDB 人物与图片"),
ttlField("metaSearchDays", "豆瓣 / Bangumi 搜索"),
ttlField("metaDetailDays", "豆瓣 / Bangumi 条目"),
@@ -118,7 +118,7 @@ export function DownloadersTable() {
config={{
noun: "下载器实例",
description:
"只接管带管理标签的任务。可添加多个下载器:订阅或手动下载指定了下载器就用指定的,否则用站点指定的,再否则用默认下载器。多个实例共用一块缓存盘时填相同的缓存卷标识,水位合并计算。点行编辑实例和路径映射。",
"只管理带管理标签的任务。选下载器的顺序:订阅或手动下载指定的、站点指定的、默认下载器。多个实例共用一块下载盘时填相同的缓存卷标识,水位合并计算。点行编辑实例和路径映射。",
emptyHint: "添加 qBittorrent 后才能提交下载。",
drawerExtra: (r) => <PathMappingsPanel viewKind="downloader" instanceId={r.id} />,
columns: [
@@ -180,7 +180,7 @@ export function DownloadersTable() {
placeholder: "qbt_…",
visible: (v) => v.authKind === "api_key",
},
{ name: "managedTag", label: "管理标签", kind: "text", placeholder: "umt", hint: "只接管带此标签的任务" },
{ name: "managedTag", label: "管理标签", kind: "text", placeholder: "umt", hint: "只管理带这个标签的任务" },
{ name: "cacheVolumeKey", label: "缓存卷标识", kind: "text", placeholder: "cache-1" },
{
name: "hardPct",
@@ -357,7 +357,7 @@ const MEDIA_KIND = [
{ value: "series", label: "剧集" },
{ value: "variety", label: "综艺" },
{ value: "mixed", label: "混合" },
{ value: "other", label: "其它" },
{ value: "other", label: "其他" },
];
/** 库根路径 ↔ 存储 + 路径:Alist 上的库根存成 /@alist/<存储 id>/<Alist 内路径> */
+1 -1
View File
@@ -23,7 +23,7 @@ export function LibrarySection() {
section="library"
anchor="roots"
title="媒体库目录"
description="整理把作品放进这些目录;媒体服务器扫的也是它们。"
description="整理后的作品放在这些目录里,媒体服务器也扫描这些目录。"
plain
>
<LibraryRootsTable />
@@ -69,7 +69,7 @@ export const LANGUAGE_CARD: CardSpec = {
label: "语言回退链",
kind: "text",
placeholder: DEFAULT_LANGUAGES.join(", "),
hint: "标题和每集信息按顺序取第一个有值的语言,简介默认只取链里的中文;中文名取链中第一个中文值。只认简繁中文、英文和作品原语言(如日语作品的 ja-JP),其它语言跳过。逗号分隔,如 zh-CN, zh-SG",
hint: "逗号分隔,如 zh-CN, zh-SG。标题和每集信息取链上第一个有值的语言,简介默认只取中文。只认简繁中文、英文和作品原语言(如日语作品的 ja-JP),其他语言跳过",
wide: true,
},
{
+1 -1
View File
@@ -190,7 +190,7 @@ function RootNamingEditor({ root }: { root: LibraryRoot }) {
<div className="flex flex-wrap items-center gap-2">
{KINDS.map((k) => (
<StatusBadge key={k} tone={saved[k] ? "success" : "neutral"}>
{NAMING_KIND_LABEL[k]}:{saved[k] ? "媒体库目录覆盖" : "沿用全局"}
{NAMING_KIND_LABEL[k]}:{saved[k] ? "媒体库目录覆盖" : "沿用全局"}
</StatusBadge>
))}
</div>
@@ -59,7 +59,7 @@ export function ClassificationSection() {
})
}
>
让智能体协助
问智能体
</Button>
}
>
@@ -159,7 +159,7 @@ function SourceEditor({ initial, onClose }: { initial: OrganizeSourceView | null
<DialogHeader>
<DialogTitle>{initial ? "编辑整理来源" : "新增整理来源"}</DialogTitle>
<DialogDescription>
首次保存或更换来源目录先建立当前文件基线,只自动处理后续新增文件。历史文件请使用「整理存量」。
首次保存或更换来源目录时记下现有文件作为基线,之后只自动处理新增文件。已有文件用「整理存量」整理。
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 sm:grid-cols-2">
@@ -316,7 +316,7 @@ function SourceEditor({ initial, onClose }: { initial: OrganizeSourceView | null
},
{
onSuccess: () => {
toast.success("来源已保存,当前存量不会自动整理");
toast.success("来源已保存,已有文件不会自动整理");
onClose();
},
onError: (e) => toast.error(filesErrorText(e, "保存来源失败")),
@@ -324,7 +324,7 @@ function SourceEditor({ initial, onClose }: { initial: OrganizeSourceView | null
);
}}
>
{save.isPending ? "保存并建立基线…" : "保存来源"}
{save.isPending ? "保存中…" : "保存来源"}
</Button>
</div>
</DialogContent>
@@ -360,7 +360,7 @@ export function OrganizeSourcesSection() {
section="library"
anchor="sources"
title="整理来源"
description="每条配置绑定一个来源与一个媒体库目标。新增来源建立基线,历史存量由你预览后执行。"
description="每条配置把一个来源目录对应到一个媒体库目录。新增的来源只自动处理之后出现的文件,已有文件用「整理存量」预览后整理。"
actions={
<Button size="sm" onClick={() => setEditor(null)}>
新增来源
@@ -369,13 +369,13 @@ export function OrganizeSourcesSection() {
>
<div className="space-y-3">
{sources.isPending ? (
<p>读取来源中…</p>
<p>加载中…</p>
) : sources.error ? (
<p role="alert" className="text-destructive">
{filesErrorText(sources.error, "读取来源失败")}
</p>
) : !sources.data?.items.length ? (
<p className="text-caption text-muted-foreground">尚未配置整理来源。</p>
<p className="text-caption text-muted-foreground">还没有整理来源。</p>
) : (
paged.items?.map((p) => (
<div key={p.id} className="space-y-2 rounded-xl border border-border p-3">
@@ -389,7 +389,7 @@ export function OrganizeSourcesSection() {
const { id, baselineAt: _, lastScanAt: _s, lastError: _e, fileCount: _c, ...body } = p;
save.mutate(
{ id, body: { ...body, enabled } },
{ onError: (e) => toast.error(filesErrorText(e, "切换启用失败")) },
{ onError: (e) => toast.error(filesErrorText(e, "切换失败")) },
);
}}
/>
@@ -408,7 +408,7 @@ export function OrganizeSourcesSection() {
r.items.length
? setWizard(stockWizardSource(p, r))
: toast.info(
`没有可整理文件;跳过 ${r.skipped},不可读取 ${r.unavailable}${r.truncated ? ",扫描达到上限" : ""}`,
`没有可整理的文件;跳过 ${r.skipped} 个,读不到 ${r.unavailable} 个${r.truncated ? ",已达扫描上限" : ""}`,
),
onError: (e) => toast.error(filesErrorText(e, "扫描存量失败")),
},
@@ -435,8 +435,8 @@ export function OrganizeSourcesSection() {
: "手动"}
</p>
<p className="text-meta text-muted-foreground">
基线:{p.baselineAt ? formatDateTime(p.baselineAt) : "尚未建立"} · 最近扫描:
{p.lastScanAt ? formatDateTime(p.lastScanAt) : "尚未扫描"} · 快照 {p.fileCount} 个文件
基线:{p.baselineAt ? formatDateTime(p.baselineAt) : "未建立"} · 最近扫描:
{p.lastScanAt ? formatDateTime(p.lastScanAt) : "未扫描"} · 快照 {p.fileCount} 个文件
</p>
{p.lastError ? <p className="text-caption text-destructive">{p.lastError}</p> : null}
</div>
@@ -451,7 +451,7 @@ export function OrganizeSourcesSection() {
<DialogContent>
<DialogHeader>
<DialogTitle>删除来源「{deleting.name}」?</DialogTitle>
<DialogDescription>删除配置及监控快照,文件仍保留在原目录。</DialogDescription>
<DialogDescription>配置和监控快照会删除,目录里的文件不动。</DialogDescription>
</DialogHeader>
<Button
variant="outline"
@@ -463,7 +463,7 @@ export function OrganizeSourcesSection() {
})
}
>
确认删除来源
删除来源
</Button>
</DialogContent>
</Dialog>
@@ -61,7 +61,7 @@ export const SCRAPE_CARD: CardSpec = {
name: "organize.scrapeAfterImport",
label: "整理后自动刮削",
kind: "switch",
hint: "只对认出作品(有 TMDB ID 或本地元数据)的整理排刮削;没刮削的在整理记录上标「未刮削」",
hint: "只刮削认出作品的整理(有 TMDB ID 或本地元数据);没刮削的整理记录标「未刮削」",
},
{
name: "organize.scrapeMode",
@@ -102,7 +102,7 @@ export function PiOAuthLogin({
abortRef.current = ac;
while (!ac.signal.aborted) {
if (Date.now() > new Date(s.expiresAt).getTime()) {
finish("expired", "授权已超时。");
finish("expired", "授权已超时");
return;
}
try {
@@ -258,7 +258,7 @@ export function ModelFetchBar({
: remote
? `来源:远程 · ${formatRelative(data?.fetchedAt)}更新`
: unsupported
? "来源:静态(这个 provider 没有模型列表接口)"
? "来源:静态(这个提供方没有模型列表接口)"
: data
? "来源:静态"
: null}
@@ -70,7 +70,7 @@ export function PiUpstreamWizard({ open, onClose }: { open: boolean; onClose: ()
: (created?.name ?? "");
const description =
step === "provider"
? "第 1 步,共 3 步:选 provider"
? "第 1 步,共 3 步:选提供方"
: step === "config"
? "第 2 步,共 3 步:凭证与模型"
: created?.authKind === "oauth"
@@ -116,7 +116,7 @@ function ProviderPicker({ onPick }: { onPick: (p: PiProviderInfo) => void }) {
<div className="relative">
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
aria-label="筛选 provider"
aria-label="筛选提供方"
placeholder="筛选,例如 openai"
value={filter}
onChange={(e) => setFilter(e.target.value)}
@@ -148,7 +148,7 @@ function ProviderPicker({ onPick }: { onPick: (p: PiProviderInfo) => void }) {
</DetailSection>
);
})}
{list.length === 0 ? <p className="text-caption text-muted-foreground">没有匹配的 provider。</p> : null}
{list.length === 0 ? <p className="text-caption text-muted-foreground">没有匹配的提供方。</p> : null}
</div>
);
}
@@ -252,7 +252,7 @@ function ConfigStep({
<div className="sm:col-span-2">
<Button type="button" variant="ghost" size="xs" onClick={onBack} className="-ml-2 mb-2">
<ChevronLeft />
换一个 provider
换一个提供方
</Button>
{provider.note && !custom ? <p className="mb-3 text-caption text-muted-foreground">{provider.note}</p> : null}
</div>
@@ -356,7 +356,7 @@ function ConfigStep({
htmlFor="pw-model"
error={errors.defaultModel}
required={custom}
hint={custom ? "端点上的模型 id" : "留空用 provider 的默认模型"}
hint={custom ? "端点上的模型 id" : "留空用提供方的默认模型"}
className="sm:col-span-2"
>
<ModelFetchBar
+1 -1
View File
@@ -94,7 +94,7 @@ export const NETWORK_CARD: CardSpec = {
label: "User-Agent",
kind: "text",
placeholder: "跟随浏览器",
hint: "留空跟随浏览器(推荐),站点请求和浏览器用同一个 UA。填了则账号没单独填 UA 时都用它,和浏览器不一致",
hint: "留空时站点请求和浏览器用同一个 UA(推荐)。填写后,没单独设 UA 的账号都用这个值,与浏览器的 UA 不一致",
wide: true,
},
{
@@ -123,7 +123,7 @@ export const STORAGE_FIELDS: FieldSpec[] = [
name: "uploadAsTask",
label: "上传交给 Alist 后台任务",
kind: "switch",
hint: "开启后上传由 Alist 排队完成,本工具等任务结束再继续;缺省关闭(同 MP)",
hint: "开启后上传由 Alist 排队完成,本工具等任务结束再继续;缺省关闭",
},
{
name: "snapshotCheckFolderModtime",
@@ -197,7 +197,7 @@ export function StoragesTable() {
description:
"登记 Alist / OpenList 后,媒体库目录、整理来源和文件管理都能用它。网盘方向的上传、下载、复制和移动以接口成功、目标存在为准,不比对大小和哈希。",
emptyHint: "还没有登记 Alist / OpenList。",
deleteHint: "只删除登记和保存的凭据;被媒体库目录使用时不能删除。",
deleteHint: "只删除登记和保存的凭证;被媒体库目录使用时不能删除。",
columns: [
{
key: "name",
+5 -5
View File
@@ -59,9 +59,9 @@ const BUDGET_KEYS = ["pgDataGb", "pgDataWarnGb", "imagesGb", "backupsGb", "sessi
export const STORAGE_BUDGET_CARD: CardSpec = {
anchor: "storage",
title: "存储预算",
description: "超过预算时健康页告警;图片缓存超出容量按最久未访问删除。单位 GB。",
description: "超过预算时运行状态页告警;图片缓存超出容量按最久未访问删除。单位 GB。",
fields: [
gbField("pgDataGb", "数据库预算", "超过为严重"),
gbField("pgDataGb", "数据库预算", "超过时标为需要处理"),
gbField("pgDataWarnGb", "数据库告警线", "不高于数据库预算"),
gbField("imagesGb", "图片缓存容量"),
gbField("backupsGb", "备份目录预算"),
@@ -104,11 +104,11 @@ export const STORAGE_RETENTION_CARD: CardSpec = {
"未识别资源归档",
"未命中的条目这么久没有新证据就归档,仍可手动指定",
),
dayField("retentionDays", "engineRun", "后台运行记录", "仍被任务记录引用的跟随任务保留"),
dayField("retentionDays", "engineRun", "后台运行记录", "被任务记录引用的随任务一起保留"),
dayField("retentionDays", "outboxEvent", "已投递的内部事件"),
dayField("retentionDays", "resolutionAttempt", "识别尝试记录", "每个资源至少留最近 3 次"),
dayField("retentionDays", "identityResolved", "已识别的待识别条目", "忽略和归档的不删"),
dayField("retentionDays", "siteSnapshot", "站点快照", "每个站点最近一次成功的保留"),
dayField("retentionDays", "identityResolved", "未识别中已识别的条目", "忽略和归档的不删"),
dayField("retentionDays", "siteSnapshot", "站点抓取结果", "每个站点保留最近一次成功的抓取"),
],
fromSettings: (s) =>
Object.fromEntries(RETENTION_KEYS.map((k) => [`storage.retentionDays.${k}`, n(s.storage.retentionDays[k])])),
@@ -54,7 +54,7 @@ export function Sparkline({
if (!has)
return (
<div className="flex items-center text-meta text-muted-foreground" style={{ height }}>
正在采样…
采样中…
</div>
);
return (
@@ -118,7 +118,7 @@ function CleanupTable({ rows }: { rows: CleanupRow[] }) {
const run = (t: StorageCleanupTarget) =>
clean.mutate(t, {
onSuccess: (r) =>
r.queued ? toast.success(`已排入清理:${CLEANUP_LABEL[t]}`) : toast.info(`${CLEANUP_LABEL[t]}已在排队或运行中`),
r.queued ? toast.success(`已加入队列:${CLEANUP_LABEL[t]}`) : toast.info(`${CLEANUP_LABEL[t]}已在排队或运行中`),
onError: (e) => toast.error(errorMessage(e)),
});
return (
@@ -387,7 +387,7 @@ export function StorageSection({ health }: { health: SystemHealth }) {
</Panel>
</TechInfo>
<div className="space-y-6">
<Panel title="缓存" note="条数含尚未清理的过期条目">
<Panel title="缓存" note="条数含还没清理的过期条目">
<DataTable<KvRow>
rows={s.kvCache}
rowKey={(r) => r.region}
@@ -470,7 +470,7 @@ export function StorageSection({ health }: { health: SystemHealth }) {
<StatementsInfo st={s.db.statements} />
<Panel title="清理任务" note="立即清理会把对应的维护任务排入执行,结果看运行记录">
<Panel title="清理任务" note="立即清理会把维护任务加入队列,结果在运行记录里看">
<CleanupTable rows={s.cleanup} />
</Panel>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@ import type * as React from "react";
import { cn } from "~/lib/utils.ts";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
return <nav aria-label="页面路径" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+1 -1
View File
@@ -18,6 +18,6 @@ export function revealFirstError(root?: HTMLElement | null) {
/** toast 用的一句话:"有 3 处需要修改:填写模板名称" */
export function formErrorSummary(errors: Record<string, string>): string {
const msgs = Object.values(errors).filter(Boolean);
if (msgs.length === 0) return "表单有错误";
if (msgs.length === 0) return "有内容需要修改";
return msgs.length === 1 ? `需要修改:${msgs[0]}` : `有 ${msgs.length} 处需要修改:${msgs[0]}`;
}
+10 -10
View File
@@ -24,7 +24,7 @@ const HEALTH: Record<string, Labeled> = {
export const healthLabel = (v: string | null | undefined) => pick(HEALTH, v);
const DOWNLOAD: Record<string, Labeled> = {
queued: { label: "排队", tone: "neutral" },
queued: { label: "排队中", tone: "neutral" },
submitting: { label: "提交中", tone: "neutral" },
downloading: { label: "下载中", tone: "neutral" },
paused: { label: "已暂停", tone: "warning" },
@@ -46,17 +46,17 @@ const DECISION_KIND: Record<string, string> = {
download_failed: "下载失败",
path_conflict: "整理冲突",
space_insufficient: "空间不足",
approval_required: "待审批",
approval_required: "等你批准",
metadata_conflict: "元数据冲突",
source_unreachable: "来源不可达",
source_unreachable: "来源连不上",
downloader_offline: "下载器连不上",
cache_volume_unknown: "下载目录读不到容量",
other: "其它",
other: "其他",
};
export const decisionKindLabel = (v: string) => DECISION_KIND[v] ?? v;
const ORG: Record<string, Labeled> = {
queued: { label: "排队", tone: "neutral" },
queued: { label: "排队中", tone: "neutral" },
running: { label: "进行中", tone: "neutral" },
succeeded: { label: "成功", tone: "success" },
partial: { label: "部分成功", tone: "warning" },
@@ -91,8 +91,8 @@ export const agentRunLabel = (v: string | null | undefined) => pick(AGENT_RUN, v
const ENGINE_RUN: Record<string, Labeled> = {
pending: { label: "待执行", tone: "neutral" },
running: { label: "运行中", tone: "neutral" },
sleeping: { label: "休眠(旧)", tone: "neutral" },
succeeded: { label: "完成(旧)", tone: "success" },
sleeping: { label: "休眠", tone: "neutral" },
succeeded: { label: "完成", tone: "success" },
completed: { label: "完成", tone: "success" },
failed: { label: "失败", tone: "destructive" },
canceled: { label: "已取消", tone: "warning" },
@@ -156,11 +156,11 @@ export const actorTypeLabel = (t: string) => ACTOR_TYPES.find((a) => a.value ===
/** 计划 statusReason(`added`、`organize:succeeded` 等)→ 中文;不认识的原样显示 */
const PLAN_REASON: Record<string, string> = {
added: "已添加到下载器",
"organize:succeeded": "整理成功",
"organize:partial": "部分整理成功",
"organize:succeeded": "已整理",
"organize:partial": "部分整理",
"organize:failed": "整理失败",
authorized: "已批准",
"reserved:agent": "智能体已预留空间",
"reserved:agent": "智能体已批准下载",
};
/** 不认识的内部码走原因码表,仍不认识就不显示(原码在技术信息里) */
export const planReasonText = (v: string | null | undefined) =>