docs(copy): 设置页接入、媒体库、整理、系统分区文案按 human-copy 重写

- 下载器说明去掉「接管」,选下载器顺序写成一句;缓存盘统一为下载盘
- 「凭据」统一为「凭证」;去掉说明里的「同 MP」「待处理」等旧称
- 整理来源的基线、存量说法改成用户能懂的话,确认按钮写动作
- 数据保留里「站点快照」「待识别条目」按术语表改名

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 01:48:45 +08:00
1 parent c57411f9a5
commit ea42018d3f
12 files changed
+35 -35

No files matched your search

+4 -4
View File
@@ -88,20 +88,20 @@ function SearchSitesBlock() {
export const IDENTITY_CARD: CardSpec = { export const IDENTITY_CARD: CardSpec = {
anchor: "identity", anchor: "identity",
title: "资源识别", title: "资源识别",
description: "默认只在资源命中订阅、作品别名或新作品规则,或你手动识别和搜索时查 TMDB;其余放进未识别,不进待处理。", description: "默认只在资源命中订阅、作品别名或新作品规则,或你手动识别和搜索时查 TMDB;其余资源留在未识别。",
fields: [ fields: [
{ {
name: "identity.eagerTmdbLookup", name: "identity.eagerTmdbLookup",
label: "每条新资源都查 TMDB", label: "每条新资源都查 TMDB",
kind: "switch", kind: "switch",
hint: "每条新资源都查一次 TMDB。RSS 量大时每天可能上千次请求,站点独有的短剧和综艺多半查不到", hint: "RSS 量大时每天可能上千次请求;站点独有的短剧和综艺多半查不到",
wide: true, wide: true,
}, },
{ {
name: "identity.detailIdsPerSiteHour", name: "identity.detailIdsPerSiteHour",
label: "识别时每站每小时最多抓详情页", label: "识别时每站每小时最多抓详情页",
kind: "number", kind: "number",
hint: "标题认不准时,抓站点详情页取 IMDb、TMDB 或豆瓣 ID 再识别;只在搜索、订阅相关资源、补全和手动重新识别时抓,RSS 收进来的资源不抓。0 = 不抓", hint: "标题认不准时,抓站点详情页取 IMDb、TMDB 或豆瓣 ID 再识别;只在搜索、命中订阅、补全和手动重新识别时抓,RSS 收进来的资源不抓。0 = 不抓",
}, },
{ {
name: "identity.autoEnrich", name: "identity.autoEnrich",
@@ -125,7 +125,7 @@ export const IDENTITY_CARD: CardSpec = {
}, },
{ {
name: "identity.enrichSnippetAllSites", name: "identity.enrichSnippetAllSites",
label: "也用其它网站的搜索摘要", label: "也用其他网站的搜索摘要",
kind: "switch", kind: "switch",
hint: "短剧多半只有聚合站收录。打开后,白名单以外网站的搜索摘要也用来抽年份、集数、主演、简介(只看搜索引擎给的摘要,不打开这些网页)", hint: "短剧多半只有聚合站收录。打开后,白名单以外网站的搜索摘要也用来抽年份、集数、主演、简介(只看搜索引擎给的摘要,不打开这些网页)",
wide: true, wide: true,
@@ -50,7 +50,7 @@ function Verified({ p }: { p: BrowserProviderView }) {
function CapabilityTable({ p }: { p: BrowserProviderView }) { function CapabilityTable({ p }: { p: BrowserProviderView }) {
const caps = p.capabilities; 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 ( return (
<div className="mt-5 space-y-2"> <div className="mt-5 space-y-2">
<h3 className="text-caption font-semibold"> <h3 className="text-caption font-semibold">
@@ -81,7 +81,7 @@ function CapabilityTable({ p }: { p: BrowserProviderView }) {
})} })}
</ul> </ul>
<p className="text-meta text-muted-foreground"> <p className="text-meta text-muted-foreground">
"已核验"只来自对真实实例的逐项核验记录(见运行手册),探测只能判断接口在不在。未核验的能力在原站视图里标出。 「已核验」只来自对真实实例的逐项核验记录(见运行手册),探测只能判断接口在不在。未核验的能力在原站视图里标出。
</p> </p>
</div> </div>
); );
@@ -124,7 +124,7 @@ export function BrowserProvidersBlock() {
}, },
{ {
key: "key", key: "key",
header: "访问凭据", header: "访问凭证",
hideBelow: "md", hideBelow: "md",
cell: (r) => (r.hasAccessKey ? "已设置" : "未设置"), cell: (r) => (r.hasAccessKey ? "已设置" : "未设置"),
}, },
@@ -148,7 +148,7 @@ export function BrowserProvidersBlock() {
const auth = (v.health?.detail as { error?: string } | undefined)?.error === "auth"; const auth = (v.health?.detail as { error?: string } | undefined)?.error === "auth";
return { return {
ok: v.health?.status === "ok", ok: v.health?.status === "ok",
message: auth ? "访问凭据不对" : v.health?.status === "degraded" ? "浏览器服务正在恢复" : "连不上", message: auth ? "访问凭证不对" : v.health?.status === "degraded" ? "浏览器服务正在恢复" : "连不上",
version: v.capabilities?.upstreamVersion ?? null, version: v.capabilities?.upstreamVersion ?? null,
}; };
}} }}
+1 -1
View File
@@ -279,7 +279,7 @@ export const TTL_CARD: CardSpec = {
fields: [ fields: [
ttlField("tmdbSearchDays", "TMDB 搜索与识别结果"), ttlField("tmdbSearchDays", "TMDB 搜索与识别结果"),
ttlField("tmdbDetailDays", "TMDB 详情"), ttlField("tmdbDetailDays", "TMDB 详情"),
ttlField("tmdbMissDays", "TMDB 空结果", "搜索无结果、未识别时的负缓存"), ttlField("tmdbMissDays", "TMDB 空结果", "搜索无结果或没识别出的结果也缓存,期内不重复查"),
ttlField("imageDays", "TMDB 人物与图片"), ttlField("imageDays", "TMDB 人物与图片"),
ttlField("metaSearchDays", "豆瓣 / Bangumi 搜索"), ttlField("metaSearchDays", "豆瓣 / Bangumi 搜索"),
ttlField("metaDetailDays", "豆瓣 / Bangumi 条目"), ttlField("metaDetailDays", "豆瓣 / Bangumi 条目"),
@@ -118,7 +118,7 @@ export function DownloadersTable() {
config={{ config={{
noun: "下载器实例", noun: "下载器实例",
description: description:
"只接管带管理标签的任务。可添加多个下载器:订阅或手动下载指定了下载器就用指定的,否则用站点指定的,再否则用默认下载器。多个实例共用一块缓存盘时填相同的缓存卷标识,水位合并计算。点行编辑实例和路径映射。", "只管理带管理标签的任务。选下载器的顺序:订阅或手动下载指定的、站点指定的、默认下载器。多个实例共用一块下载盘时填相同的缓存卷标识,水位合并计算。点行编辑实例和路径映射。",
emptyHint: "添加 qBittorrent 后才能提交下载。", emptyHint: "添加 qBittorrent 后才能提交下载。",
drawerExtra: (r) => <PathMappingsPanel viewKind="downloader" instanceId={r.id} />, drawerExtra: (r) => <PathMappingsPanel viewKind="downloader" instanceId={r.id} />,
columns: [ columns: [
@@ -180,7 +180,7 @@ export function DownloadersTable() {
placeholder: "qbt_…", placeholder: "qbt_…",
visible: (v) => v.authKind === "api_key", 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: "cacheVolumeKey", label: "缓存卷标识", kind: "text", placeholder: "cache-1" },
{ {
name: "hardPct", name: "hardPct",
@@ -357,7 +357,7 @@ const MEDIA_KIND = [
{ value: "series", label: "剧集" }, { value: "series", label: "剧集" },
{ value: "variety", label: "综艺" }, { value: "variety", label: "综艺" },
{ value: "mixed", label: "混合" }, { value: "mixed", label: "混合" },
{ value: "other", label: "其它" }, { value: "other", label: "其他" },
]; ];
/** 库根路径 ↔ 存储 + 路径:Alist 上的库根存成 /@alist/<存储 id>/<Alist 内路径> */ /** 库根路径 ↔ 存储 + 路径:Alist 上的库根存成 /@alist/<存储 id>/<Alist 内路径> */
+1 -1
View File
@@ -23,7 +23,7 @@ export function LibrarySection() {
section="library" section="library"
anchor="roots" anchor="roots"
title="媒体库目录" title="媒体库目录"
description="整理把作品放进这些目录;媒体服务器扫的也是它们。" description="整理后的作品放在这些目录里,媒体服务器也扫描这些目录。"
plain plain
> >
<LibraryRootsTable /> <LibraryRootsTable />
@@ -69,7 +69,7 @@ export const LANGUAGE_CARD: CardSpec = {
label: "语言回退链", label: "语言回退链",
kind: "text", kind: "text",
placeholder: DEFAULT_LANGUAGES.join(", "), placeholder: DEFAULT_LANGUAGES.join(", "),
hint: "标题和每集信息按顺序取第一个有值的语言,简介默认只取链里的中文;中文名取链中第一个中文值。只认简繁中文、英文和作品原语言(如日语作品的 ja-JP),其它语言跳过。逗号分隔,如 zh-CN, zh-SG", hint: "逗号分隔,如 zh-CN, zh-SG。标题和每集信息取链上第一个有值的语言,简介默认只取中文。只认简繁中文、英文和作品原语言(如日语作品的 ja-JP),其他语言跳过",
wide: true, wide: true,
}, },
{ {
+1 -1
View File
@@ -190,7 +190,7 @@ function RootNamingEditor({ root }: { root: LibraryRoot }) {
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{KINDS.map((k) => ( {KINDS.map((k) => (
<StatusBadge key={k} tone={saved[k] ? "success" : "neutral"}> <StatusBadge key={k} tone={saved[k] ? "success" : "neutral"}>
{NAMING_KIND_LABEL[k]}:{saved[k] ? "媒体库目录覆盖" : "沿用全局"} {NAMING_KIND_LABEL[k]}:{saved[k] ? "媒体库目录覆盖" : "沿用全局"}
</StatusBadge> </StatusBadge>
))} ))}
</div> </div>
@@ -59,7 +59,7 @@ export function ClassificationSection() {
}) })
} }
> >
让智能体协助 问智能体
</Button> </Button>
} }
> >
@@ -159,7 +159,7 @@ function SourceEditor({ initial, onClose }: { initial: OrganizeSourceView | null
<DialogHeader> <DialogHeader>
<DialogTitle>{initial ? "编辑整理来源" : "新增整理来源"}</DialogTitle> <DialogTitle>{initial ? "编辑整理来源" : "新增整理来源"}</DialogTitle>
<DialogDescription> <DialogDescription>
首次保存或更换来源目录先建立当前文件基线,只自动处理后续新增文件。历史文件请使用「整理存量」。 首次保存或更换来源目录时记下现有文件作为基线,之后只自动处理新增文件。已有文件用「整理存量」整理。
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
@@ -316,7 +316,7 @@ function SourceEditor({ initial, onClose }: { initial: OrganizeSourceView | null
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("来源已保存,当前存量不会自动整理"); toast.success("来源已保存,已有文件不会自动整理");
onClose(); onClose();
}, },
onError: (e) => toast.error(filesErrorText(e, "保存来源失败")), onError: (e) => toast.error(filesErrorText(e, "保存来源失败")),
@@ -324,7 +324,7 @@ function SourceEditor({ initial, onClose }: { initial: OrganizeSourceView | null
); );
}} }}
> >
{save.isPending ? "保存并建立基线…" : "保存来源"} {save.isPending ? "保存中…" : "保存来源"}
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>
@@ -360,7 +360,7 @@ export function OrganizeSourcesSection() {
section="library" section="library"
anchor="sources" anchor="sources"
title="整理来源" title="整理来源"
description="每条配置绑定一个来源与一个媒体库目标。新增来源建立基线,历史存量由你预览后执行。" description="每条配置把一个来源目录对应到一个媒体库目录。新增的来源只自动处理之后出现的文件,已有文件用「整理存量」预览后整理。"
actions={ actions={
<Button size="sm" onClick={() => setEditor(null)}> <Button size="sm" onClick={() => setEditor(null)}>
新增来源 新增来源
@@ -369,13 +369,13 @@ export function OrganizeSourcesSection() {
> >
<div className="space-y-3"> <div className="space-y-3">
{sources.isPending ? ( {sources.isPending ? (
<p>读取来源中…</p> <p>加载中…</p>
) : sources.error ? ( ) : sources.error ? (
<p role="alert" className="text-destructive"> <p role="alert" className="text-destructive">
{filesErrorText(sources.error, "读取来源失败")} {filesErrorText(sources.error, "读取来源失败")}
</p> </p>
) : !sources.data?.items.length ? ( ) : !sources.data?.items.length ? (
<p className="text-caption text-muted-foreground">尚未配置整理来源。</p> <p className="text-caption text-muted-foreground">还没有整理来源。</p>
) : ( ) : (
paged.items?.map((p) => ( paged.items?.map((p) => (
<div key={p.id} className="space-y-2 rounded-xl border border-border p-3"> <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; const { id, baselineAt: _, lastScanAt: _s, lastError: _e, fileCount: _c, ...body } = p;
save.mutate( save.mutate(
{ id, body: { ...body, enabled } }, { 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 r.items.length
? setWizard(stockWizardSource(p, r)) ? setWizard(stockWizardSource(p, r))
: toast.info( : toast.info(
`没有可整理文件;跳过 ${r.skipped},不可读取 ${r.unavailable}${r.truncated ? ",扫描达到上限" : ""}`, `没有可整理的文件;跳过 ${r.skipped} 个,读不到 ${r.unavailable} 个${r.truncated ? ",已达扫描上限" : ""}`,
), ),
onError: (e) => toast.error(filesErrorText(e, "扫描存量失败")), onError: (e) => toast.error(filesErrorText(e, "扫描存量失败")),
}, },
@@ -451,7 +451,7 @@ export function OrganizeSourcesSection() {
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>删除来源「{deleting.name}」?</DialogTitle> <DialogTitle>删除来源「{deleting.name}」?</DialogTitle>
<DialogDescription>删除配置及监控快照,文件仍保留在原目录。</DialogDescription> <DialogDescription>配置和监控快照会删除,目录里的文件不动。</DialogDescription>
</DialogHeader> </DialogHeader>
<Button <Button
variant="outline" variant="outline"
@@ -463,7 +463,7 @@ export function OrganizeSourcesSection() {
}) })
} }
> >
确认删除来源 删除来源
</Button> </Button>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -61,7 +61,7 @@ export const SCRAPE_CARD: CardSpec = {
name: "organize.scrapeAfterImport", name: "organize.scrapeAfterImport",
label: "整理后自动刮削", label: "整理后自动刮削",
kind: "switch", kind: "switch",
hint: "只对认出作品(有 TMDB ID 或本地元数据)的整理排刮削;没刮削的在整理记录上标「未刮削」", hint: "只刮削认出作品的整理(有 TMDB ID 或本地元数据);没刮削的整理记录标「未刮削」",
}, },
{ {
name: "organize.scrapeMode", name: "organize.scrapeMode",
+1 -1
View File
@@ -94,7 +94,7 @@ export const NETWORK_CARD: CardSpec = {
label: "User-Agent", label: "User-Agent",
kind: "text", kind: "text",
placeholder: "跟随浏览器", placeholder: "跟随浏览器",
hint: "留空跟随浏览器(推荐),站点请求和浏览器用同一个 UA。填了则账号没单独填 UA 时都用它,和浏览器不一致", hint: "留空时站点请求和浏览器用同一个 UA(推荐)。填写后,没单独设 UA 的账号都用这个值,与浏览器的 UA 不一致",
wide: true, wide: true,
}, },
{ {
@@ -123,7 +123,7 @@ export const STORAGE_FIELDS: FieldSpec[] = [
name: "uploadAsTask", name: "uploadAsTask",
label: "上传交给 Alist 后台任务", label: "上传交给 Alist 后台任务",
kind: "switch", kind: "switch",
hint: "开启后上传由 Alist 排队完成,本工具等任务结束再继续;缺省关闭(同 MP)", hint: "开启后上传由 Alist 排队完成,本工具等任务结束再继续;缺省关闭",
}, },
{ {
name: "snapshotCheckFolderModtime", name: "snapshotCheckFolderModtime",
@@ -197,7 +197,7 @@ export function StoragesTable() {
description: description:
"登记 Alist / OpenList 后,媒体库目录、整理来源和文件管理都能用它。网盘方向的上传、下载、复制和移动以接口成功、目标存在为准,不比对大小和哈希。", "登记 Alist / OpenList 后,媒体库目录、整理来源和文件管理都能用它。网盘方向的上传、下载、复制和移动以接口成功、目标存在为准,不比对大小和哈希。",
emptyHint: "还没有登记 Alist / OpenList。", emptyHint: "还没有登记 Alist / OpenList。",
deleteHint: "只删除登记和保存的凭据;被媒体库目录使用时不能删除。", deleteHint: "只删除登记和保存的凭证;被媒体库目录使用时不能删除。",
columns: [ columns: [
{ {
key: "name", key: "name",
+5 -5
View File
@@ -59,9 +59,9 @@ const BUDGET_KEYS = ["pgDataGb", "pgDataWarnGb", "imagesGb", "backupsGb", "sessi
export const STORAGE_BUDGET_CARD: CardSpec = { export const STORAGE_BUDGET_CARD: CardSpec = {
anchor: "storage", anchor: "storage",
title: "存储预算", title: "存储预算",
description: "超过预算时健康页告警;图片缓存超出容量按最久未访问删除。单位 GB。", description: "超过预算时运行状态页告警;图片缓存超出容量按最久未访问删除。单位 GB。",
fields: [ fields: [
gbField("pgDataGb", "数据库预算", "超过为严重"), gbField("pgDataGb", "数据库预算", "超过时标为需要处理"),
gbField("pgDataWarnGb", "数据库告警线", "不高于数据库预算"), gbField("pgDataWarnGb", "数据库告警线", "不高于数据库预算"),
gbField("imagesGb", "图片缓存容量"), gbField("imagesGb", "图片缓存容量"),
gbField("backupsGb", "备份目录预算"), gbField("backupsGb", "备份目录预算"),
@@ -104,11 +104,11 @@ export const STORAGE_RETENTION_CARD: CardSpec = {
"未识别资源归档", "未识别资源归档",
"未命中的条目这么久没有新证据就归档,仍可手动指定", "未命中的条目这么久没有新证据就归档,仍可手动指定",
), ),
dayField("retentionDays", "engineRun", "后台运行记录", "仍被任务记录引用的跟随任务保留"), dayField("retentionDays", "engineRun", "后台运行记录", "被任务记录引用的随任务一起保留"),
dayField("retentionDays", "outboxEvent", "已投递的内部事件"), dayField("retentionDays", "outboxEvent", "已投递的内部事件"),
dayField("retentionDays", "resolutionAttempt", "识别尝试记录", "每个资源至少留最近 3 次"), dayField("retentionDays", "resolutionAttempt", "识别尝试记录", "每个资源至少留最近 3 次"),
dayField("retentionDays", "identityResolved", "已识别的待识别条目", "忽略和归档的不删"), dayField("retentionDays", "identityResolved", "未识别中已识别的条目", "忽略和归档的不删"),
dayField("retentionDays", "siteSnapshot", "站点快照", "每个站点最近一次成功的保留"), dayField("retentionDays", "siteSnapshot", "站点抓取结果", "每个站点保留最近一次成功的抓取"),
], ],
fromSettings: (s) => fromSettings: (s) =>
Object.fromEntries(RETENTION_KEYS.map((k) => [`storage.retentionDays.${k}`, n(s.storage.retentionDays[k])])), Object.fromEntries(RETENTION_KEYS.map((k) => [`storage.retentionDays.${k}`, n(s.storage.retentionDays[k])])),