refactor(copy): 术语统一——收录版本、模型服务、智能体等(DEAI-2)

- 版本目标 → 收录版本(前后端用户可见字符串、能力描述、给模型的提示)
- Agent 上游 / 上游 → 模型服务;界面上的 Agent / Agent 会话 → 智能体 / 智能体会话
- 身份歧义 / 识别有歧义 / 无法确定作品 → 没识别出作品;待识别(池) → 未识别
- 建档 → 加入媒体库;资产 → 文件;库根 → 媒体库目录;纳管 → 本工具 / 外部任务;
  哨兵 → 标记文件 / 检查目录;站点快照 → 抓取;Feed / RSS 订阅 / 来源健康 → RSS
- 代码标识符、API 字段名、迁移文件不改;glossary.md 同步;smoke 选择器跟随

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 20:35:43 +08:00
1 parent d282212a6b
commit 4480fc346d
212 files changed
+741 -715

No files matched your search

@@ -249,8 +249,7 @@ export function ToolCallCard({
<dd className="min-w-0 font-mono break-all text-foreground/80">{call.toolCallId}</dd>
<dt className="text-muted-foreground">幂等键</dt>
<dd className="min-w-0 font-mono break-all text-foreground/80">
{audit?.idempotencyKey ??
(meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读能力不记操作账本")}
{audit?.idempotencyKey ?? (meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读操作不记录")}
</dd>
{audit?.grantId || audit?.approvalId ? (
<>
@@ -652,7 +651,7 @@ export function AgentConversation({
at: m.at,
role: "user",
foldable: false,
node: <UserMessage text={m.text} at={m.at} note={m.pending ? "已送达,等待 Agent 读取" : undefined} animateIn />,
node: <UserMessage text={m.text} at={m.at} note={m.pending ? "已送达,等待智能体读取" : undefined} animateIn />,
});
entries.sort((a, b) => a.at.localeCompare(b.at));
const blocks = groupConversation(entries);
@@ -678,13 +677,13 @@ export function AgentConversation({
b.kind === "single" ? (
<div key={b.entry.key}>{b.entry.node}</div>
) : (
<Message key={b.key} from="assistant" aria-label="Agent 回合">
<Message key={b.key} from="assistant" aria-label="智能体回合">
<MessageAvatar>
<Bot />
</MessageAvatar>
<MessageContent className="gap-2">
<MessageHeader>
<span className="font-medium text-foreground/80">Agent</span>
<span className="font-medium text-foreground/80">智能体</span>
<time dateTime={b.at}>{clockText(b.at)}</time>
</MessageHeader>
{b.parts.map((p) =>
@@ -72,7 +72,7 @@ function DrawerTabs({ tab }: { tab: AgentDrawerTab }) {
return (
<div
role="tablist"
aria-label="Agent 视图"
aria-label="智能体视图"
className="mt-2 inline-flex w-fit gap-1 rounded-full border border-border/70 p-0.5"
>
{TABS.map((t) => (
@@ -161,10 +161,10 @@ export function AgentDrawer() {
<SheetHeader className="gap-1 border-b border-[var(--shell-border)] px-5 pt-4 pb-3 pr-24">
<SheetTitle className="flex items-center gap-2 text-lead">
<Bot className="h-4 w-4" />
Agent
智能体
</SheetTitle>
<SheetDescription className={cn("break-words", history && "sr-only")}>
{context?.label ? `当前对象:${context.label}` : "交代目标,Agent 在授权范围内执行并汇报。"}
{context?.label ? `当前对象:${context.label}` : "交代目标,智能体在授权范围内执行并汇报。"}
<span className="ml-1 hidden text-muted-foreground/75 sm:inline">
({agentShortcutLabel()} 开关,Esc 关闭)
</span>
@@ -281,7 +281,7 @@ function AgentMiniDock({ runId }: { runId: string }) {
: null;
return (
<section
aria-label="Agent 小窗"
aria-label="智能体小窗"
className="pg-float fixed right-4 bottom-4 z-40 w-80 max-w-[calc(100vw-2rem)] rounded-2xl p-3 animate-in fade-in-0 slide-in-from-bottom-2 duration-base ease-pop motion-reduce:animate-none"
>
<div className="flex items-center gap-2">
@@ -290,7 +290,7 @@ function AgentMiniDock({ runId }: { runId: string }) {
<span className="min-w-0 flex-1 truncate text-meta text-muted-foreground">
{run.data ? splitContextGoal(run.data.run.goal).text : ""}
</span>
<Button variant="ghost" size="icon-xs" aria-label="展开 Agent" title="展开" onClick={restoreAgentDrawer}>
<Button variant="ghost" size="icon-xs" aria-label="展开智能体" title="展开" onClick={restoreAgentDrawer}>
<Maximize2 />
</Button>
<Button
@@ -325,7 +325,7 @@ export function AgentDrawerTrigger({ className }: { className?: string }) {
<Button
variant="ghost"
size="icon-sm"
aria-label="打开 Agent"
aria-label="打开智能体"
title={`Agent(${agentShortcutLabel()})`}
className={className}
onClick={() =>
@@ -107,7 +107,7 @@ export function OptionsCard({ runId, view, waiting }: { runId: string; view: Opt
const optionId = picked === NONE ? null : picked;
if (!picked) return;
if (optionId === null && !reason.trim()) {
toast.error("写一句不要的原因,Agent 按它调整");
toast.error("写一句不要的原因,智能体按它调整");
return;
}
choose.mutate(
@@ -115,7 +115,7 @@ export function OptionsCard({ runId, view, waiting }: { runId: string; view: Opt
{
onSuccess: () => {
setSent({ optionId, reason: reason.trim() });
toast.success(optionId ? "已选定,Agent 继续执行" : "已退回,Agent 调整方案");
toast.success(optionId ? "已选定,智能体继续执行" : "已退回,智能体调整方案");
},
onError: (e) => toast.error(errorMessage(e)),
},
@@ -129,8 +129,8 @@ export function OptionsCard({ runId, view, waiting }: { runId: string; view: Opt
: `都不要${resolved.reason ? `:${resolved.reason}` : ""}`
: sent
? sent.optionId
? `已选择:${view.options.find((o) => o.id === sent.optionId)?.title ?? sent.optionId},等待 Agent 继续`
: `都不要:${sent.reason},等待 Agent 调整`
? `已选择:${view.options.find((o) => o.id === sent.optionId)?.title ?? sent.optionId},等待智能体继续`
: `都不要:${sent.reason},等待智能体调整`
: !waiting
? "会话已不在等待这个选择"
: null;
@@ -232,7 +232,7 @@ export function OptionsCard({ runId, view, waiting }: { runId: string; view: Opt
</Button>
<span className="text-meta text-muted-foreground">
{formatDateTime(view.expiresAt)} 前没选,
{view.timeoutAction === "recommended" ? "自动选推荐项" : "Agent 停止"}。
{view.timeoutAction === "recommended" ? "自动选推荐项" : "智能体停止"}。
</span>
</div>
)}
@@ -285,7 +285,7 @@ export function InlineApproval({ view, stepTitle }: { view: ApprovalView; stepTi
...(s === "rejected" && reason.trim() ? { reason: reason.trim() } : {}),
},
{
onSuccess: () => toast.success(s === "approved" ? "已批准,Agent 继续执行" : "已拒绝,Agent 按理由调整"),
onSuccess: () => toast.success(s === "approved" ? "已批准,智能体继续执行" : "已拒绝,智能体按理由调整"),
onError: (e) => toast.error(errorMessage(e)),
},
);
@@ -332,7 +332,7 @@ export function InlineApproval({ view, stepTitle }: { view: ApprovalView; stepTi
value={reason}
maxLength={2000}
aria-label="拒绝理由"
placeholder="拒绝时写理由或建议(可选),Agent 按它调整"
placeholder="拒绝时写理由或建议(可选),智能体按它调整"
onChange={(e) => setReason(e.target.value)}
/>
) : null}
@@ -114,8 +114,8 @@ export function GoalComposer({
/>
<p className="px-1 text-meta text-muted-foreground" aria-live="polite">
{proposeFirst
? "Agent 先调查并给出方案,你选定后才执行。"
: "Agent 列出步骤后直接执行,需要审批的动作会停在对应步骤等你批准。"}{" "}
? "智能体先调查并给出方案,你选定后才执行。"
: "智能体列出步骤后直接执行,需要审批的动作会停在对应步骤等你批准。"}{" "}
Enter 发送,Shift+Enter 换行。
</p>
{suggestions?.length ? (
@@ -400,7 +400,7 @@ export function AgentRunPanel({
viewportClassName="pr-1"
contentClassName={column}
busy={streaming}
label="Agent 会话记录"
label="智能体会话记录"
>
<AgentConversation
run={r}
@@ -196,7 +196,7 @@ export function AgentSessionList({
}, [canLoadMore, isFetchingNextPage, fetchNextPage]);
return (
<nav aria-label="Agent 会话列表" className={cn("flex min-h-0 flex-col", className)}>
<nav aria-label="智能体会话列表" className={cn("flex min-h-0 flex-col", className)}>
<div className="mb-2 space-y-1.5">
<Input
type="search"
+2 -2
View File
@@ -48,7 +48,7 @@ function script(goal: string, opts: AgentSessionOptions): Step[] {
for (const c of chunks(text)) push(60, { type: "text_delta", delta: c });
};
const steps: AgentPlanStep[] = [
{ id: "s1", title: "查本地库存和版本目标", status: "running", note: null },
{ id: "s1", title: "查本地库存和收录版本", status: "running", note: null },
{ id: "s2", title: "在站点搜 4K 候选", status: "pending", note: null },
{ id: "s3", title: "选资源并生成下载计划", status: "pending", note: null },
{ id: "s4", title: "提交下载", status: "pending", note: null },
@@ -63,7 +63,7 @@ function script(goal: string, opts: AgentSessionOptions): Step[] {
};
push(0, { type: "run_started", goal, resumed: false });
push(100, { type: "plan_updated", steps: steps.map((x) => ({ ...x })), revision: ++revision });
say("先确认本地库存与版本目标,再去站点找候选。");
say("先确认本地库存与收录版本,再去站点找候选。");
push(400, {
type: "tool_call_start",
toolCallId: "t1",
+3 -3
View File
@@ -42,15 +42,15 @@ const RULES: Rule[] = [
},
{
match: /^\/console\/identity/,
goals: ["识别待识别的文件", "找出外部身份可能错误的作品", "补全缺少元数据的作品"],
goals: ["识别未识别的文件", "找出外部身份可能错误的作品", "补全缺少元数据的作品"],
},
{
match: /^\/console\/subscriptions/,
goals: ["帮我新建一个订阅:", "哪些订阅很久没有新收录了", "检查订阅的版本目标是否合理"],
goals: ["帮我新建一个订阅:", "哪些订阅很久没有新收录了", "检查订阅的收录版本是否合理"],
},
{
match: /^\/console\/sources/,
goals: ["哪些来源不新鲜了,原因是什么", "检查所有 Feed 的抓取状态", "最近一天各来源抓到了什么"],
goals: ["哪些来源不新鲜了,原因是什么", "检查所有 RSS 的抓取状态", "最近一天各来源抓到了什么"],
},
{
match: /^\/console\/engine/,
+1 -1
View File
@@ -18,7 +18,7 @@ export interface MessageProps extends ComponentPropsWithRef<"article"> {
animateIn?: boolean;
}
const FROM_LABEL: Record<MessageFrom, string> = { user: "用户消息", assistant: "Agent 消息", system: "系统消息" };
const FROM_LABEL: Record<MessageFrom, string> = { user: "用户消息", assistant: "智能体消息", system: "系统消息" };
export function Message({ from, animateIn = false, className, children, ...props }: MessageProps) {
return (
@@ -32,11 +32,11 @@ describe("原站视图坐标换算", () => {
describe("当前控制文案", () => {
const now = Date.parse("2026-09-29T10:00:00Z");
it("自动化且没有最近动作 → 「自动化」;两分钟内有自动化动作 → 「Agent」", () => {
it("自动化且没有最近动作 → 「自动化」;两分钟内有自动化动作 → 「智能体」", () => {
expect(controllerLabel({ controller: "automation", lastAutomationAt: null }, now)).toBe("自动化");
expect(controllerLabel({ controller: "automation" }, now)).toBe("自动化");
expect(controllerLabel({ controller: "automation", lastAutomationAt: "2026-09-29T09:50:00Z" }, now)).toBe("自动化");
expect(controllerLabel({ controller: "automation", lastAutomationAt: "2026-09-29T09:59:10Z" }, now)).toBe("Agent");
expect(controllerLabel({ controller: "automation", lastAutomationAt: "2026-09-29T09:59:10Z" }, now)).toBe("智能体");
expect(agentActive({ controller: "automation", lastAutomationAt: "2026-09-29T09:59:10Z" }, now)).toBe(true);
});
it("人工接管 / 等待处理不看自动化时间", () => {
+2 -2
View File
@@ -41,7 +41,7 @@ export function controllerLabel(
s: { controller: BrowserController; lastAutomationAt?: string | null },
now = Date.now(),
): string {
return agentActive(s, now) ? "Agent" : CONTROLLER_LABEL[s.controller];
return agentActive(s, now) ? "智能体" : CONTROLLER_LABEL[s.controller];
}
const WAITING_TEXT: Record<string, string> = {
@@ -51,7 +51,7 @@ const WAITING_TEXT: Record<string, string> = {
challenge_unsolved: "自动处理页面挑战未成功",
login_required: "登录已失效:接管后在画面里登录一次,交还后全站生效",
lease_expired: "接管超时未交还",
agent_request: "Agent 请求人工处理",
agent_request: "智能体请求人工处理",
human_released_waiting: "交还时选择继续等待",
};
@@ -229,7 +229,7 @@ function DesktopView({
control.release.mutate(s.id, {
onSuccess: () => {
refreshRole();
toast.success("已交还,Agent 会重新读取页面");
toast.success("已交还,智能体会重新读取页面");
},
onError: (e) => toast.error(errorMessage(e)),
})
@@ -276,8 +276,8 @@ function DesktopView({
{mine
? "直接在画面里点击和输入。中文、日文用本机输入法输入;复制粘贴用 Ctrl+C / Ctrl+V。"
: agentActive(s)
? "只读观看,Agent 正在操作。需要自己点或输入时先接管,Agent 会暂停。"
: "只读观看,目前没有 Agent 在操作。需要自己点或输入时先接管。"}
? "只读观看,智能体正在操作。需要自己点或输入时先接管,智能体会暂停。"
: "只读观看,目前没有智能体在操作。需要自己点或输入时先接管。"}
</p>
<div className="pg-slab relative min-w-0 overflow-hidden rounded-xl">
@@ -174,7 +174,7 @@ export function SiteOriginView({ site }: { site: SiteListItem }) {
disabled={busy}
onClick={() =>
control.release.mutate(s.id, {
onSuccess: () => toast.success("已交还,Agent 会重新读取页面"),
onSuccess: () => toast.success("已交还,智能体会重新读取页面"),
onError: (e) => toast.error(errorMessage(e)),
})
}
@@ -145,7 +145,7 @@ export function AgentLauncher() {
const onAgentPage = /^\/console\/agent(\/|$)/.test(pathname);
if (!host || (onAgentPage && !open) || (minimized && runId && !open)) return null;
const label = open ? "收起 Agent" : count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent";
const label = open ? "收起智能体" : count ? `打开智能体(${count} 个会话进行中)` : "打开智能体";
return createPortal(
<button
type="button"
@@ -171,7 +171,7 @@ export function AgentLauncher() {
</span>
<span className="hidden min-w-0 flex-col items-start text-left md:flex">
<span className="flex items-center gap-2 text-control leading-5 font-semibold">
{open ? "收起" : "Agent"}
{open ? "收起" : "智能体"}
{count && !open ? <PulseDot /> : null}
{count && !open ? <CountBadge count={count} /> : null}
<kbd className="rounded-tight border border-border/70 px-1 font-sans text-label leading-4 font-medium text-muted-foreground">
@@ -205,7 +205,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
{...{ [AGENT_LAUNCHER_ATTR]: "" }}
onClick={activate}
title={title}
aria-label={count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent"}
aria-label={count ? `打开智能体(${count} 个会话进行中)` : "打开智能体"}
aria-keyshortcuts="Control+J Meta+J"
className="prism-ring relative mb-2 grid h-10 w-10 shrink-0 cursor-pointer place-items-center rounded-xl text-foreground outline-none transition-transform duration-micro active:scale-[0.95] focus-visible:ring-2 focus-visible:ring-ring"
>
@@ -228,7 +228,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
</span>
<span className="flex min-w-0 flex-1 flex-col">
<span className="flex items-center gap-2 text-sm font-semibold">
Agent
智能体
{count ? <PulseDot /> : null}
</span>
<span className="truncate text-meta text-muted-foreground">
@@ -250,8 +250,8 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
export function AgentShortcutsHelp() {
const shortcut = agentShortcutLabel();
const rows: [string, string][] = [
[shortcut, "打开 / 收起 Agent,附带当前页面"],
["Esc", "关闭 Agent"],
[shortcut, "打开 / 收起智能体,附带当前页面"],
["Esc", "关闭智能体"],
["Enter", "发送目标;Shift+Enter 换行"],
];
return (
@@ -6,15 +6,15 @@ import { groupRows, readCollapsed, toggleCollapsed, writeCollapsed } from "./dat
type Row = { id: string; status: string; name: string };
const rows: Row[] = [
{ id: "1", status: "待识别", name: "a.mkv" },
{ id: "1", status: "未识别", name: "a.mkv" },
{ id: "2", status: "已登记", name: "b.mkv" },
{ id: "3", status: "待识别", name: "c.mkv" },
{ id: "3", status: "未识别", name: "c.mkv" },
];
describe("DataTable groupBy:分组逻辑", () => {
it("按首次出现顺序分组,组内保持原顺序", () => {
expect(groupRows(rows, (r) => r.status).map((g) => [g.key, g.rows.map((r) => r.id)])).toEqual([
["待识别", ["1", "3"]],
["未识别", ["1", "3"]],
["已登记", ["2"]],
]);
});
@@ -24,7 +24,7 @@ describe("DataTable groupBy:分组逻辑", () => {
(r) => r.status,
(a, b) => a.localeCompare(b, "zh-CN"),
);
expect(g.map((x) => x.key)).toEqual(["待识别", "已登记"].sort((a, b) => a.localeCompare(b, "zh-CN")));
expect(g.map((x) => x.key)).toEqual(["未识别", "已登记"].sort((a, b) => a.localeCompare(b, "zh-CN")));
});
it("折叠切换返回新集合", () => {
const a = new Set<string>();
@@ -72,8 +72,8 @@ describe("DataTable groupBy:渲染", () => {
}),
);
expect(html.match(/aria-expanded="true"/g)).toHaveLength(2);
expect(html).toContain("待识别");
expect(html).toMatch(/待识别<\/span><span[^>]*>2<\/span>/);
expect(html).toContain("未识别");
expect(html).toMatch(/未识别<\/span><span[^>]*>2<\/span>/);
expect(html).toMatch(/已登记<\/span><span[^>]*>1<\/span>/);
for (const n of ["a.mkv", "b.mkv", "c.mkv"]) expect(html).toContain(n);
});
+2 -2
View File
@@ -19,8 +19,8 @@ describe("控制台导航", () => {
it("IA-3:5 组,按定稿清单逐项列出,路径唯一且都在 /console 下", () => {
expect(NAV_GROUPS.map((g) => g.title)).toEqual(["概览", "资源", "订阅", "整理", "系统"]);
expect(NAV_GROUPS.map((g) => g.items.map((i) => i.label))).toEqual([
["首页", "任务", "计划", "Agent 会话"],
["站点", "RSS 订阅"],
["首页", "任务", "计划", "智能体会话"],
["站点", "RSS"],
["订阅", "规则"],
["下载面板", "整理记录", "文件管理", "媒体概览"],
["设置", "审计", "运行状态"],
+8 -8
View File
@@ -52,19 +52,19 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/tasks",
label: "任务",
icon: ListTodo,
description: "每次运行都是一条任务:定时、事件、手动、Agent 触发都在这里",
description: "每次运行都是一条任务:定时、事件、手动、智能体触发都在这里",
},
{
to: "/console/schedules",
label: "计划",
icon: CalendarCog,
description: "定时任务的开关、间隔和下次运行:自动任务与 Agent 巡检",
description: "定时任务的开关、间隔和下次运行:自动任务与智能体巡检",
},
{
to: "/console/agent",
label: "Agent 会话",
label: "智能体会话",
icon: Bot,
description: "交代目标,Agent 在授权内执行并汇报",
description: "交代目标,智能体在授权内执行并汇报",
},
],
},
@@ -75,9 +75,9 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/sites",
label: "站点",
icon: Globe,
description: "站点、凭证、账号数据与健康;资源、快照与 Feed",
description: "站点、凭证、账号数据与健康;资源、抓取与 RSS",
},
{ to: "/console/sources", label: "RSS 订阅", icon: Rss, description: "全部 Feed 与抓取状态" },
{ to: "/console/sources", label: "RSS", icon: Rss, description: "全部 RSS 与抓取状态" },
],
},
{
@@ -107,7 +107,7 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/library",
label: "媒体概览",
icon: Library,
description: "作品、季 / 集、版本目标、外部身份与待识别资源",
description: "作品、季 / 集、收录版本、外部身份与未识别资源",
},
],
},
@@ -118,7 +118,7 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/settings",
label: "设置",
icon: Settings,
description: "下载器、媒体服务器、库根、站点账号、Agent 上游与自动化",
description: "下载器、媒体服务器、媒体库目录、站点账号、模型服务与自动化",
},
{
to: "/console/audit",
@@ -30,10 +30,10 @@ export const TASK_KIND_LABEL: Record<TaskKind, string> = {
submitDownload: "提交下载",
verifyAndImport: "核验入库",
refreshMetadata: "刷新元数据",
reconcileLibrary: "资产对账",
reconcileLibrary: "文件核对",
cleanupDownloadCache: "清理缓存",
resolveDecision: "处理待处理事项",
agentRun: "Agent 运行",
agentRun: "智能体运行",
};
export const TASK_STATUS: Record<
@@ -54,15 +54,15 @@ export const ACTIVE_TASK: TaskStatus[] = ["pending", "running", "waiting"];
export const SUBJECT_LABEL: Record<string, string> = {
download_task: "下载任务",
acquisition_plan: "下载计划",
acquisition_slot: "版本目标",
acquisition_slot: "收录版本",
organization_record: "整理记录",
organize_request: "整理请求",
media_entity: "作品",
media_asset: "资产",
media_asset: "文件",
source_release: "资源",
feed_source: "Feed",
feed_source: "RSS",
site: "站点",
library_root: "库根",
library_root: "媒体库目录",
cache_volume: "缓存卷",
};
@@ -13,7 +13,7 @@ export const STAGE_LABEL: Record<TimelineStage, string> = {
plan: "计划",
download: "下载",
organization: "整理",
agent: "Agent",
agent: "智能体",
engine: "任务",
};
@@ -113,7 +113,7 @@ export function CacheVolumes() {
</Button>
}
title="按规则清理下载缓存?"
description="只清理已入库、且满足保留期与 H&R 的纳管任务,其余不动。"
description="只清理已入库、且满足保留期与 H&R 的本工具任务,其余不动。"
confirmLabel="清理"
onConfirm={() =>
cleanup.mutateAsync(undefined).then(
@@ -51,7 +51,7 @@ export function DownloadStatusBadges({ t }: { t: DownloadTask }) {
) : t.pauseReason ? (
<StatusBadge>{PAUSE_REASON_LABEL[t.pauseReason]}</StatusBadge>
) : null}
{!t.managed ? <StatusBadge>未纳管</StatusBadge> : null}
{!t.managed ? <StatusBadge>外部任务</StatusBadge> : null}
{t.origin === "adopted" ? <StatusBadge>接管</StatusBadge> : null}
</span>
);
@@ -123,7 +123,8 @@ export function DownloadDetail({ task }: { task: DownloadTask }) {
})
}
>
<Bot />问 Agent
<Bot />
问智能体
</Button>
</div>
</div>
@@ -131,7 +132,7 @@ export function DownloadDetail({ task }: { task: DownloadTask }) {
{task.pauseReason === "threshold" ? (
<p className="flex items-start gap-2 rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
<ShieldAlert className="mt-0.5 h-3.5 w-3.5 shrink-0" />
缓存卷达到硬阈值,任务已暂停,水位回落到恢复线后自动恢复。手动恢复和 Agent 都不能绕过阈值。
缓存卷达到硬阈值,任务已暂停,水位回落到恢复线后自动恢复。手动恢复和智能体都不能绕过阈值。
</p>
) : null}
{task.noProgressSince ? (
+14 -9
View File
@@ -186,7 +186,12 @@ export function MkdirDialog({
const SUGGEST_REASON: Record<string, string> = {
identity_uncertain: "识别不可信,没有推荐名",
};
const TEMPLATE_SOURCE: Record<string, string> = { entity: "作品覆盖", root: "库根", global: "全局", override: "覆写" };
const TEMPLATE_SOURCE: Record<string, string> = {
entity: "作品覆盖",
root: "媒体库目录",
global: "全局",
override: "覆写",
};
function suggestReasonText(r: string | null): string {
if (!r) return "没有推荐名";
@@ -434,7 +439,7 @@ export function RenameDialog({
))}
</ul>
{pv.assetLocations > 0 ? (
<Notice tone="neutral">同步 {pv.assetLocations} 条资产位置,写一条整理记录。</Notice>
<Notice tone="neutral">同步 {pv.assetLocations} 条文件位置,写一条整理记录。</Notice>
) : null}
{pv.bindings.length > 0 ? (
<Notice tone="warning">这是作品或季的绑定目录。改名后旧绑定标为已替代,之后入库用新目录。</Notice>
@@ -582,7 +587,7 @@ export function TransferDialog({
}}
className="max-h-72 md:max-h-96"
/>
<p className="text-meta text-muted-foreground">只能选可写库根下的目录。</p>
<p className="text-meta text-muted-foreground">只能选可写媒体库目录下的目录。</p>
</div>
<div className="min-w-0 space-y-3">
{target && targetRoot ? (
@@ -616,7 +621,7 @@ export function TransferDialog({
<StatusBadge tone="warning">冲突 {conflicts.length}(跳过,不覆盖)</StatusBadge>
) : null}
{plan.pairs.some((p) => p.registered) ? (
<StatusBadge>含已登记资产,执行后同步位置</StatusBadge>
<StatusBadge>含已入库文件,执行后同步位置</StatusBadge>
) : null}
</div>
{hnrBlocked ? (
@@ -777,7 +782,7 @@ export function DeleteDialog({
<DialogContent className="max-h-[92dvh] grid-rows-[auto_minmax(0,1fr)_auto] sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{single ? `删除 ${single}?` : `删除 ${items.length} 项?`}</DialogTitle>
<DialogDescription>已检查做种任务、已登记资产和路径绑定。下载器里的任务不受影响。</DialogDescription>
<DialogDescription>已检查做种任务、已入库文件和路径绑定。下载器里的任务不受影响。</DialogDescription>
</DialogHeader>
<div className="min-h-0 space-y-3 overflow-y-auto">
{check.isPending ? (
@@ -824,7 +829,7 @@ export function DeleteDialog({
</p>
))}
{c.assets.length ? (
<p>已登记资产 {c.assets.length} 个(删除后退役,已满足的版本目标退回待处理)</p>
<p>已入库文件 {c.assets.length} 个(删除后从媒体库移除,已满足的收录版本退回待处理)</p>
) : null}
{c.bindings.map((bd) => (
<p key={bd.bindingId}>
@@ -843,9 +848,9 @@ export function DeleteDialog({
<label className="flex items-start gap-2.5 text-caption">
<input type="checkbox" className="mt-1" checked={retire} onChange={(e) => setRetire(e.target.checked)} />
<span>
同时退役资产
同时从媒体库移除
<span className="block text-meta text-muted-foreground">
资产位置标为已移除,资产退役;已满足的版本目标退回待处理。
这些文件从媒体库记录中移除;已满足的收录版本退回待处理。
</span>
</span>
</label>
@@ -1042,7 +1047,7 @@ export function RefreshMetadataDialog({ items, onClose }: { items: FileEntry[];
<p key={s.ref} className="text-meta text-warning">
{s.ref}:
{s.reason === "not_registered"
? "没有已登记资产"
? "没有已入库文件"
: s.reason === "no_entity"
? "不属于任何已登记作品"
: s.reason === "not_found"
+1 -1
View File
@@ -29,7 +29,7 @@ export function DirPicker({
onSelect={onChange}
/>
) : (
<p className="px-2 py-3 text-caption text-muted-foreground">没有可写的库根。在设置里把库根设为纳管后再选。</p>
<p className="px-2 py-3 text-caption text-muted-foreground">没有可写入的媒体库目录。先在设置里添加。</p>
)}
</div>
);
+1 -1
View File
@@ -148,7 +148,7 @@ function RootNode({
)}
{root.pendingCount ? (
<span className="shrink-0 text-warning">
待识别 {root.pendingCount}
未识别 {root.pendingCount}
{root.pendingPartial ? "+" : ""}
</span>
) : null}
@@ -78,7 +78,7 @@ function RecordsSection({ entityId }: { entityId: string }) {
);
}
/** 选中单项的详情抽屉:文件信息、作品/版本目标/下载任务、识别、整理记录、附属文件、媒体信息、路径绑定与小文件预览 */
/** 选中单项的详情抽屉:文件信息、作品/收录版本/下载任务、识别、整理记录、附属文件、媒体信息、路径绑定与小文件预览 */
export function FileDetail({
root,
entry,
@@ -148,7 +148,7 @@ export function FileDetail({
</DetailSection>
{e.asset ? (
<DetailSection title="已登记资产">
<DetailSection title="已入库文件">
<KvList
items={[
{
@@ -165,16 +165,16 @@ export function FileDetail({
},
{ label: "季集", value: e.asset.contentUnit ?? "整部" },
{
label: "版本目标",
label: "收录版本",
value: e.asset.slotId ? (
<Link className="underline underline-offset-2" to={`/console/library/${e.asset.mediaEntityId}#slots`}>
{e.asset.slotStatus ? (SLOT_STATUS[e.asset.slotStatus]?.label ?? e.asset.slotStatus) : "查看"}
</Link>
) : (
"未满足任何版本目标"
"未满足任何收录版本"
),
},
{ label: "主资产", value: e.asset.isPrimary ? "是" : "否" },
{ label: "主文件", value: e.asset.isPrimary ? "是" : "否" },
]}
/>
</DetailSection>
+1 -1
View File
@@ -707,7 +707,7 @@ function doMove(body: {
const tRoot = rootOf(body.target.root);
const target = find(body.target.root, body.target.path).node;
if (!target.dir || !tRoot.writable)
throw new ApiRequestError("bad_request", "not_writable:目标必须是可写库根下的目录");
throw new ApiRequestError("bad_request", "not_writable:目标必须是可写媒体库目录下的目录");
const pairs = body.items.map((it) => {
const { node } = find(it.root, it.path);
const name = node.name;
+3 -3
View File
@@ -32,7 +32,7 @@ export function entryStatusView(e: Pick<FileEntry, "status" | "download" | "esca
? { label: "做种中·H&R 未满足", tone: "warning", hint: "H&R 满足前不能改名、删除或移动" }
: { label: "做种中", tone: "neutral" };
case "unidentified":
return { label: "待识别", tone: "warning" };
return { label: "未识别", tone: "warning" };
default:
return null;
}
@@ -41,7 +41,7 @@ export function entryStatusView(e: Pick<FileEntry, "status" | "download" | "esca
export const FILTER_CHIPS = [
{ value: "", label: "全部" },
{ value: "video", label: "视频" },
{ value: "unidentified", label: "待识别" },
{ value: "unidentified", label: "未识别" },
{ value: "registered", label: "已登记" },
{ value: "downloading", label: "下载中" },
{ value: "temp", label: "临时文件" },
@@ -309,7 +309,7 @@ export function deleteBlockText(b: string | null): { text: string; soft: boolean
case "is_root":
return { text: "不能删除登记目录本身", soft: false };
case "needs_retire":
return { text: "含已登记资产,勾选「同时退役资产」后才能删除", soft: true };
return { text: "含已入库文件,勾选「同时从媒体库移除」后才能删除", soft: true };
case "needs_unbind":
return { text: "是作品或季的绑定目录,勾选「解除路径绑定」后才能删除", soft: true };
case "root_unavailable":
@@ -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} />
@@ -392,7 +392,7 @@ export function MergeDialog({
id="merge-reason"
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="重复建档"
placeholder="重复添加"
/>
</FormField>
<DialogFooter>
@@ -68,7 +68,7 @@ function FieldRow({ k, s, enrichmentId }: { k: EnrichFieldName; s: Suggestion; e
{k !== "posterUrl" ? <p className="text-sm break-words text-foreground">{String(v.value)}</p> : null}
<div className="flex flex-wrap items-center gap-1.5">
<Chip>{EVIDENCE_LEVEL[v.evidenceLevel] ?? v.evidenceLevel}</Chip>
{v.verbatim ? null : <Chip>Agent 归纳</Chip>}
{v.verbatim ? null : <Chip>智能体归纳</Chip>}
<SourceLink url={v.sourceUrl} />
</div>
</div>
@@ -142,7 +142,7 @@ export function EnrichSection({ item }: { item: IdentityPoolItem }) {
{out.notice}
{" · "}
{out.cached ? "7 天内的缓存结果" : `搜索 ${out.usage.searchCalls} 次,读取网页 ${out.usage.fetchCalls} 次`}
{s ? ` · ${s.extractor === "model" ? `Agent 抽取${s.model ? `(${s.model})` : ""}` : "规则抽取"}` : ""}
{s ? ` · ${s.extractor === "model" ? `智能体抽取${s.model ? `(${s.model})` : ""}` : "规则抽取"}` : ""}
{` · 今日 ${out.budget.usedToday}/${out.budget.dailyLimit}`}
</p>
{out.error ? <p className="text-caption text-warning">{out.error}</p> : null}
@@ -23,7 +23,7 @@ const ACTION: Record<IdentityRevision["action"], string> = {
locked: "加人工锁",
unlocked: "解除人工锁",
};
const ACTOR: Record<string, string> = { user: "用户", agent: "Agent", system: "系统", api_key: "API Key" };
const ACTOR: Record<string, string> = { user: "用户", agent: "智能体", system: "系统", api_key: "API Key" };
export const identityRef = (i: Pick<ExternalIdentity, "provider" | "entityType" | "externalId">) =>
`${i.provider}:${i.entityType}:${i.externalId}`;
@@ -76,7 +76,7 @@ function CandidateRow({ c, index }: { c: PoolCandidate; index: number }) {
<ExternalLink className="h-3 w-3" />
</Link>
) : (
<span className="text-meta text-muted-foreground">本地未建档</span>
<span className="text-meta text-muted-foreground">不在媒体库</span>
)}
</div>
{c.evidence.length ? (
@@ -148,7 +148,7 @@ export function PoolTab() {
selectedKey={openId}
empty={{
icon: SearchX,
title: filtered ? "没有符合条件的待识别资源" : "没有待识别资源",
title: filtered ? "没有符合条件的未识别资源" : "没有未识别资源",
description: "未命中订阅、证据不足或冲突的资源会出现在这里;新增相近别名时自动重试,也可手动指定。",
}}
hasMore={pool.hasNextPage}
@@ -249,7 +249,7 @@ export function ExternalTab() {
empty={{
icon: Fingerprint,
title: "没有外部身份",
description: "没有外部身份的作品也能建档入库。",
description: "没有外部编号的作品也能入库。",
}}
hasMore={list.hasNextPage}
onLoadMore={() => list.fetchNextPage()}
+10 -10
View File
@@ -34,7 +34,7 @@ export const MEDIA_KIND: Record<MediaKind, string> = {
};
export const ENTITY_STATUS: Record<MediaEntityStatus, ToneLabel> = {
provisional: { label: "临时建档", tone: "warning" },
provisional: { label: "临时作品", tone: "warning" },
active: { label: "正常", tone: "success" },
merged: { label: "已合并", tone: "neutral" },
archived: { label: "已归档", tone: "neutral" },
@@ -132,8 +132,8 @@ export const CANDIDATE_DECISION: Record<string, ToneLabel> = {
};
export const DECISION_KIND: Record<DecisionKind, string> = {
identity_ambiguous: "身份歧义",
identity_unresolved: "待识别",
identity_ambiguous: "没识别出作品",
identity_unresolved: "未识别",
download_stalled: "下载无进展",
download_failed: "下载失败",
path_conflict: "路径冲突",
@@ -146,7 +146,7 @@ export const DECISION_KIND: Record<DecisionKind, string> = {
export const DECISION_STATUS: Record<DecisionStatus, ToneLabel> = {
open: { label: "待处理", tone: "warning" },
investigating: { label: "Agent 调查中", tone: "neutral" },
investigating: { label: "智能体调查中", tone: "neutral" },
awaiting_user: { label: "等你决定", tone: "warning" },
resolved: { label: "已处理", tone: "success" },
dismissed: { label: "已忽略", tone: "neutral" },
@@ -168,7 +168,7 @@ export const POOL_REASON: Record<IdentityCandidateReason, ToneLabel> = {
};
export const POOL_STATUS: Record<string, ToneLabel> = {
pending: { label: "待识别", tone: "warning" },
pending: { label: "未识别", tone: "warning" },
resolved: { label: "已识别", tone: "success" },
dismissed: { label: "已忽略", tone: "neutral" },
archived: { label: "已归档", tone: "neutral" },
@@ -178,13 +178,13 @@ export const PREF_KIND: Record<PreferenceKind, { label: string; hint: string }>
soft: { label: "软偏好", hint: "影响排序,不排除" },
hard: { label: "硬规则", hint: "命中即排除" },
version: { label: "版本偏好", hint: "同一作品内选哪种版本" },
target: { label: "版本目标", hint: "为作品新建一个版本目标" },
target: { label: "收录版本", hint: "为作品新建一个收录版本" },
};
export const PREF_ORIGIN: Record<PreferenceOrigin, string> = {
manual: "手动",
conversation: "对话确认",
agent_inferred: "Agent 推断",
agent_inferred: "智能体推断",
};
export const PREF_STATUS: Record<PreferenceStatus, ToneLabel> = {
@@ -219,12 +219,12 @@ export const SUB_STATUS: Record<SubscriptionStatus, ToneLabel> = {
completed: { label: "已完成", tone: "neutral" },
cancelled: { label: "已取消", tone: "neutral" },
};
export const SUB_ORIGIN: Record<string, string> = { user: "用户", agent: "Agent", policy: "策略", import: "导入" };
export const SUB_ORIGIN: Record<string, string> = { user: "用户", agent: "智能体", policy: "策略", import: "导入" };
export const METADATA_SOURCE: Record<string, string> = {
tmdb: "TMDB",
user: "人工",
agent: "Agent",
agent: "智能体",
nfo: "NFO",
site: "站点",
import: "导入",
@@ -316,7 +316,7 @@ export const opts = <K extends string>(rec: Record<K, string | { label: string }
return { value, label: typeof v === "string" ? v : v.label };
});
/** 版本目标 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:<id>`)→ 中文;不认识的原样显示 */
/** 收录版本 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:<id>`)→ 中文;不认识的原样显示 */
const SLOT_REASON: Record<string, string> = {
"blocked:no_main_video": "没有可入库的主视频",
"deferred:slot_busy": "已有计划在途,等它结束再评估",
@@ -108,7 +108,7 @@ export function PreferenceFormDialog({
<DialogTitle>{tag ? "编辑偏好" : "添加偏好"}</DialogTitle>
<DialogDescription>
{tag
? "修改作用范围或规则会留下旧语义的抑制记录,Agent 不会按旧证据加回。"
? "修改作用范围或规则会留下旧语义的抑制记录,智能体不会按旧证据加回。"
: "手动添加等同对话确认。之前取消过的偏好,重新添加会解除抑制。"}
</DialogDescription>
</DialogHeader>
@@ -39,7 +39,7 @@ export function SlotCard({
{slot.requireDistinctAsset ? (
<Chip>
<Link2 className="h-3 w-3" />
需不同物理资产{slot.distinctGroup ? ` · ${slot.distinctGroup}` : ""}
需独立文件{slot.distinctGroup ? ` · ${slot.distinctGroup}` : ""}
</Chip>
) : null}
<span className="ml-auto text-meta text-muted-foreground tabular-nums">r{slot.revision}</span>
@@ -72,7 +72,7 @@ export function SlotCard({
<dd className="text-caption text-foreground">{summarizeCompletion(slot.completion)}</dd>
</div>
<div className="min-w-0 sm:col-span-2">
<dt className="mb-1 text-meta text-muted-foreground">当前资产</dt>
<dt className="mb-1 text-meta text-muted-foreground">当前文件</dt>
<dd className="text-caption break-words text-foreground">{currentAsset ?? "无"}</dd>
</div>
</dl>
@@ -353,8 +353,8 @@ function RulesSummary({ rules, valid }: { rules: AcquisitionPolicyRules | undefi
return (
<div className="space-y-2 text-caption">
<div>
<span className="text-muted-foreground">默认版本目标 </span>
<ChipList items={(rules.defaultSlots ?? []).map(tplLine)} empty="无(订阅没有版本目标时不自动创建)" />
<span className="text-muted-foreground">默认收录版本 </span>
<ChipList items={(rules.defaultSlots ?? []).map(tplLine)} empty="无(订阅没有收录版本时不自动创建)" />
</div>
<div>
<span className="text-muted-foreground">新作品收录 </span>
@@ -718,7 +718,7 @@ function PolicyEditorSheet({
</DetailSection>
<DetailSection
title="默认版本目标模板"
title="默认收录版本模板"
actions={
<Button
variant="ghost"
@@ -730,7 +730,7 @@ function PolicyEditorSheet({
</Button>
}
>
<p className="mb-2 text-meta text-muted-foreground">订阅的作品没有版本目标时,按这些模板创建。</p>
<p className="mb-2 text-meta text-muted-foreground">订阅的作品没有收录版本时,按这些模板创建。</p>
<TemplateList list={d.defaultSlots} onChange={(v) => set("defaultSlots", v)} errors={errors} />
</DetailSection>
@@ -768,7 +768,7 @@ function PolicyEditorSheet({
</FormField>
</div>
<div className="flex items-center justify-between gap-2">
<p className="text-meta text-muted-foreground">新作品的版本目标模板</p>
<p className="text-meta text-muted-foreground">新作品的收录版本模板</p>
<Button
variant="ghost"
size="xs"
@@ -51,10 +51,10 @@ export function SlotsPanel({
return (
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-meta text-muted-foreground">每个版本目标单独判定和升级。这里只改规则。</p>
<p className="text-meta text-muted-foreground">每个收录版本单独判定和升级。这里只改规则。</p>
<Button variant="outline" size="sm" onClick={() => setEditing("new")}>
<Plus />
新建版本目标
新建收录版本
</Button>
</div>
{q.isPending ? (
@@ -65,7 +65,7 @@ export function SlotsPanel({
<EmptyState
plain
icon={Target}
title="还没有版本目标"
title="还没有收录版本"
description="没有时按已发布收录策略的默认模板创建,也可以手动新建。"
className="py-8"
/>
@@ -140,7 +140,7 @@ function SlotEditorSheet({
const errs: Record<string, string> = {};
if (!meta.success) for (const i of meta.error.issues) errs[String(i.path[0])] ??= i.message;
if (!rules.ok) Object.assign(errs, rules.errors);
if (requireDistinct && !distinctGroup.trim()) errs.distinctGroup = "要求不同物理资产时须填写分组";
if (requireDistinct && !distinctGroup.trim()) errs.distinctGroup = "要求独立文件时须填写分组";
setErrors(errs);
if (Object.keys(errs).length || !meta.success || !rules.ok) return;
const common = {
@@ -178,7 +178,7 @@ function SlotEditorSheet({
open
onOpenChange={(o) => !o && !pending && onClose()}
width="lg"
title={slot ? `编辑版本目标 ${slot.name}` : "新建版本目标"}
title={slot ? `编辑收录版本 ${slot.name}` : "新建收录版本"}
description={
slot ? `当前 r${slot.revision}。保存时若已被修改,会提示刷新。` : "不满足硬条件的资源不收;软偏好决定先收哪个。"
}
@@ -225,10 +225,10 @@ function SlotEditorSheet({
</div>
<div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2">
<div className="min-w-0">
<p className="text-caption font-medium text-foreground">要求不同物理资产</p>
<p className="text-caption font-medium text-foreground">要求独立文件</p>
<p className="text-meta text-muted-foreground">同一个文件不能同时满足两个目标</p>
</div>
<Switch checked={requireDistinct} onCheckedChange={setRequireDistinct} aria-label="要求不同物理资产" />
<Switch checked={requireDistinct} onCheckedChange={setRequireDistinct} aria-label="要求独立文件" />
</div>
<SlotRulesEditor value={draft} onChange={setDraft} errors={errors} />
</DetailSheet>
+1 -1
View File
@@ -3,7 +3,7 @@ import type { NamingKind, NamingOverride } from "@umt/contracts";
// 命名模板表单的纯函数:保存错误拆分、覆盖请求体、来源文案。
export const NAMING_KINDS: NamingKind[] = ["movie", "tv", "variety"];
export const SOURCE_LABEL = { global: "全局", root: "库根", entity: "作品" } as const;
export const SOURCE_LABEL = { global: "全局", root: "媒体库目录", entity: "作品" } as const;
/** 422 消息 "movie: 1:3 …; tv: …" → 按类型拆开,行列号改写成中文 */
export function splitNamingErrors(message: string): Partial<Record<NamingKind, string>> | null {
@@ -358,7 +358,7 @@ export const FILE_KIND: Record<string, string> = {
export const TEMPLATE_SOURCE: Record<string, string> = {
entity: "作品覆盖",
root: "库根",
root: "媒体库目录",
global: "全局",
override: "保留原文件名",
};
@@ -194,11 +194,11 @@ function SlotSelect({
const items = slots.data?.items ?? [];
return (
<select
aria-label="版本目标"
aria-label="收录版本"
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={!id}
title={entity && !id ? "新建的作品还没有版本目标" : undefined}
title={entity && !id ? "新建的作品还没有收录版本" : undefined}
className="h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2 text-caption disabled:opacity-50"
>
<option value="">自动</option>
@@ -368,7 +368,7 @@ export function StepFiles(p: StepFilesProps) {
<th className="w-[15rem] px-2 py-2 font-medium">作品</th>
<th className="w-16 px-2 py-2 font-medium">季</th>
<th className="w-24 px-2 py-2 font-medium">集</th>
<th className="w-40 px-2 py-2 font-medium">版本目标</th>
<th className="w-40 px-2 py-2 font-medium">收录版本</th>
<th className="w-20 px-2 py-2">
<span className="sr-only">操作</span>
</th>
@@ -144,7 +144,7 @@ export function StepOptions({
checked={t.mode === "auto"}
onSelect={() => set({ target: { mode: "auto" } })}
title="自动"
hint="已绑定的作品沿用原目录,否则按作品类型选库根"
hint="已绑定的作品沿用原目录,否则按作品类型选媒体库目录"
/>
{libRoots.map((r) => (
<Choice
@@ -186,7 +186,7 @@ export function StepOptions({
title="按命名模板"
hint={
<>
模板按 作品覆盖 → 库根 → 全局 取第一个,预览里显示实际来源。
模板按 作品覆盖 → 媒体库目录 → 全局 取第一个,预览里显示实际来源。
<Link to="/console/settings#library/naming" className="ml-1 underline underline-offset-2">
去设置
</Link>
@@ -35,7 +35,7 @@ const ERROR_TEXT: Record<string, string> = {
/** 服务端原因码(message 以 code: 开头)→ 文案 */
const REASON_PREFIX: Record<string, string> = {
library_root_offline: "目标库根离线",
library_root_offline: "目标媒体库目录离线",
root_unavailable: "目录离线",
not_writable: "目标不可写",
path_escape: "路径越界",
@@ -187,7 +187,7 @@ export function RuleGroupEditor({
</Button>
}
title={`删除「${group.name}」?`}
description={`${SUPPRESS_NOTE}被版本目标或订阅引用时不能删除。`}
description={`${SUPPRESS_NOTE}被收录版本或订阅引用时不能删除。`}
confirmLabel="删除"
destructive
onConfirm={async () => {
@@ -403,7 +403,7 @@ function Revisions({ id }: { id: string }) {
<span className="font-medium text-foreground">{REV_ACTION[action] ?? action}</span>
{r.revision ? <span className="text-muted-foreground tabular-nums">r{r.revision}</span> : null}
<span className="text-muted-foreground">
{r.actorType === "agent" ? "Agent" : r.actorType === "api_key" ? "API Key" : "用户"}
{r.actorType === "agent" ? "智能体" : r.actorType === "api_key" ? "API Key" : "用户"}
</span>
<span className="ml-auto text-meta text-muted-foreground tabular-nums">{formatDateTime(r.at)}</span>
</div>
+8 -8
View File
@@ -22,7 +22,7 @@ function useToggle() {
toggle: async (g: RuleGroup) => {
try {
await update.mutateAsync({ id: g.id, enabled: !g.enabled, expectedRevision: g.revision });
toast.success(g.enabled ? "已停用,Agent 不会自动加回" : "已启用");
toast.success(g.enabled ? "已停用,智能体不会自动加回" : "已启用");
} catch (err) {
toast.error(errorMessage(err));
}
@@ -36,9 +36,9 @@ function RefsCell({ g, refs }: { g: RuleGroup; refs: RuleRefs | undefined }) {
const parts: string[] = [];
if (g.defaultOrder !== null) parts.push("默认链");
if (subs.length) parts.push(`${subs.length} 个订阅`);
if (slots.length) parts.push(`${slots.length} 个版本目标`);
if (slots.length) parts.push(`${slots.length} 个收录版本`);
if (!parts.length) return <span className="text-meta text-muted-foreground">{refs ? "未引用" : "—"}</span>;
const title = [...subs.map((s) => `订阅:${s.title}`), ...slots.map((s) => `版本目标:${s.name}`)].join("\n");
const title = [...subs.map((s) => `订阅:${s.title}`), ...slots.map((s) => `收录版本:${s.name}`)].join("\n");
return (
<span className="text-meta text-foreground" title={title || undefined}>
{parts.join(" · ")}
@@ -140,7 +140,7 @@ export function RuleGroupTable({
empty={{
icon: ListChecks,
title: filtered ? "没有符合条件的规则组" : "还没有规则组",
description: filtered ? undefined : "新建一个规则组,或在 Agent 会话里说明长期偏好。",
description: filtered ? undefined : "新建一个规则组,或在智能体会话里说明长期偏好。",
action: filtered ? undefined : (
<Button size="sm" onClick={onCreate}>
新建规则组
@@ -214,13 +214,13 @@ function RuleTag({ g, onOpen }: { g: RuleGroup; onOpen: () => void }) {
</Button>
}
title={`删除「${g.name}」?`}
description={`${SUPPRESS_NOTE}被版本目标或订阅引用时不能删除。`}
description={`${SUPPRESS_NOTE}被收录版本或订阅引用时不能删除。`}
confirmLabel="删除"
destructive
onConfirm={async () => {
try {
await del.mutateAsync(g.id);
toast.success("已删除,Agent 不会自动加回");
toast.success("已删除,智能体不会自动加回");
} catch (err) {
toast.error(errorMessage(err, "删除失败"));
throw err;
@@ -237,7 +237,7 @@ export function RuleTags({ rows, onOpen }: { rows: RuleGroup[]; onOpen: (g: Rule
return (
<Panel
title="偏好"
description="单条规则,多由对话或 Agent 提出。停用或删除后 Agent 不会自动加回。"
description="单条规则,多由对话或智能体提出。停用或删除后智能体不会自动加回。"
actions={<span className="text-meta text-muted-foreground tabular-nums">{rows.length} 条</span>}
>
{rows.length === 0 ? (
@@ -245,7 +245,7 @@ export function RuleTags({ rows, onOpen }: { rows: RuleGroup[]; onOpen: (g: Rule
plain
icon={Tags}
title="还没有偏好"
description="在 Agent 会话里说明长期偏好,会出现在这里。"
description="在智能体会话里说明长期偏好,会出现在这里。"
className="py-6"
/>
) : (
+5 -5
View File
@@ -5,17 +5,17 @@ import { exprToString } from "./expr.ts";
// 规则页文案:类型 / 来源 / 作用范围 / 默认链 / 抑制原因
export const RULE_KIND: Record<RuleGroupKind, { label: string; hint: string }> = {
acquisition: { label: "订阅", hint: "订阅与版本目标挑资源时用" },
upgrade: { label: "洗版", hint: "版本目标开了升级时额外叠加" },
search: { label: "搜索", hint: "手动搜索与 Agent 搜索资源时用" },
acquisition: { label: "订阅", hint: "订阅与收录版本挑资源时用" },
upgrade: { label: "洗版", hint: "收录版本开了升级时额外叠加" },
search: { label: "搜索", hint: "手动搜索与智能体搜索资源时用" },
};
export const SUPPRESS_NOTE = "停用或删除会留下抑制记录,Agent 不会自动加回;重新启用或手动新建同样的规则后解除。";
export const SUPPRESS_NOTE = "停用或删除会留下抑制记录,智能体不会自动加回;重新启用或手动新建同样的规则后解除。";
export const RULE_SOURCE: Record<RuleGroupSource, string> = {
manual: "手动",
chat: "对话",
agent: "Agent",
agent: "智能体",
};
export const DEFAULT_CHAIN_LABEL: Record<RuleGroupKind, string> = {
+3 -3
View File
@@ -385,7 +385,7 @@ export function DefaultsTab({ groups }: { groups: RuleGroup[] | undefined }) {
<div className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-caption text-muted-foreground">
订阅和版本目标没指定规则组时用这里的链。链上的组按顺序串联,任一组拒绝即拒绝,优先级取最后一个带层的组。
订阅和收录版本没指定规则组时用这里的链。链上的组按顺序串联,任一组拒绝即拒绝,优先级取最后一个带层的组。
</p>
<div className="flex items-center gap-2">
{dirty ? <StatusBadge tone="warning">未保存</StatusBadge> : null}
@@ -514,7 +514,7 @@ export function SuppressionsTab() {
return (
<Panel title="抑制记录" description={sups.data ? `${active} 条生效中` : undefined}>
<p className="mb-3 text-caption text-muted-foreground">
停用、删除,或改掉对话 / Agent 提出的规则,都会留下抑制记录。Agent
停用、删除,或改掉对话 / 智能体提出的规则,都会留下抑制记录。智能体
再提出同样的规则会被直接拒绝,解除后才能再提。
</p>
{sups.isPending ? (
@@ -551,7 +551,7 @@ export function SuppressionsTab() {
</Button>
}
title="解除这条抑制?"
description="解除后 Agent 可以再次提出这条规则,仍需审批。规则组本身不会恢复。"
description="解除后智能体可以再次提出这条规则,仍需审批。规则组本身不会恢复。"
confirmLabel="解除"
onConfirm={async () => {
try {
@@ -147,7 +147,7 @@ export function AgentCheckCard({
{c.allowRecover ? <span className="ml-1 text-meta text-muted-foreground">· 允许授权内恢复</span> : null}
</Field>
<Field label="预算">{budgetParts.length ? budgetParts.join(" · ") : "默认"}</Field>
<Field label="上游">
<Field label="模型服务">
{stats?.lastModel ? (
<span className="break-words" title={modelsTitle(stats.models)}>
{stats.lastProvider ? `${stats.lastProvider} · ` : ""}
@@ -161,15 +161,15 @@ export function AgentCheckCard({
className="break-words"
title={
upstream.modelFromCatalog
? "上游没有指定默认模型,运行时使用模型目录里的第一个"
: "还没有运行记录,按默认上游显示"
? "模型服务没有指定默认模型,运行时使用模型目录里的第一个"
: "还没有运行记录,按默认模型服务显示"
}
>
默认上游 {upstream.name} · {upstream.model ?? "模型未指定"}
默认模型服务 {upstream.name} · {upstream.model ?? "模型未指定"}
{upstream.modelFromCatalog ? <span className="text-muted-foreground">(目录首个)</span> : null}
</span>
) : (
<span className="text-muted-foreground">未配置 Agent 上游</span>
<span className="text-muted-foreground">未配置模型服务</span>
)}
</Field>
<Field label="触发条件">
@@ -283,7 +283,7 @@ export function AgentCheckCard({
</Button>
}
title={`删除「${job.name}」?`}
description="运行记录一并删除,已产生的 Agent 会话保留。"
description="运行记录一并删除,已产生的智能体会话保留。"
confirmLabel="删除巡检"
destructive
onConfirm={() =>
@@ -142,7 +142,7 @@ export function AgentCheckDialog({
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-xl">
<DialogHeader>
<DialogTitle>{job ? "编辑巡检" : "新建巡检"}</DialogTitle>
<DialogDescription>到点按目标发起一次 Agent 会话,只在白名单和预算内执行。</DialogDescription>
<DialogDescription>到点按目标发起一次智能体会话,只在白名单和预算内执行。</DialogDescription>
</DialogHeader>
<form
className="grid gap-1"
+1 -1
View File
@@ -29,7 +29,7 @@ export const GROUP_LABEL: Record<ScheduleGroup, string> = {
download: "下载",
organize: "整理",
maintain: "维护",
agent: "Agent 巡检",
agent: "智能体巡检",
};
export const RUN_STATUS: Record<ScheduleRunStatus, { label: string; tone: StatusTone }> = {
@@ -52,7 +52,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"
>
查看 Agent 会话{r.agentStatus ? `(${agentRunLabel(r.agentStatus).label})` : ""}
查看智能体会话{r.agentStatus ? `(${agentRunLabel(r.agentStatus).label})` : ""}
</Link>
) : null}
</li>
@@ -143,7 +143,7 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
</div>
</div>
<div className="grid gap-1.5">
<p className="px-1 text-meta font-medium text-muted-foreground">Agent 巡检</p>
<p className="px-1 text-meta font-medium text-muted-foreground">智能体巡检</p>
<div className="grid grid-cols-3 gap-2">
<StatTile
label="已启用"
@@ -202,8 +202,8 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
<section aria-labelledby="checks">
<SectionHead
id="checks"
title="Agent 巡检"
hint="到点按目标发起一次 Agent 会话,只在能力白名单和预算内执行;触发条件不满足时跳过,不调用模型。"
title="智能体巡检"
hint="到点按目标发起一次智能体会话,只在能力白名单和预算内执行;触发条件不满足时跳过,不调用模型。"
actions={
<Button size="sm" onClick={() => setEditing({ job: null, key: Date.now() })}>
<Plus />
@@ -212,7 +212,7 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
}
/>
{checks.length === 0 ? (
<EmptyState icon={Bot} title="还没有巡检" description="新建一个巡检,到点由 Agent 按目标检查并汇报。" />
<EmptyState icon={Bot} title="还没有巡检" description="新建一个巡检,到点由智能体按目标检查并汇报。" />
) : (
<div className="grid items-start gap-4 md:grid-cols-2 2xl:grid-cols-3">
{checks.map((j) => (
@@ -426,7 +426,7 @@ function ReleaseRow({ r, onDownload }: { r: SearchRelease; onDownload: () => voi
) : null}
<Button variant="ghost" size="sm" onClick={() => delegateToAgent(r)}>
<Bot />
委托 Agent
委托智能体
</Button>
<Button size="sm" onClick={onDownload}>
<Download />
@@ -23,7 +23,7 @@ import { formatBytes } from "~/lib/utils.ts";
// 与站点工作台同一套下载说明;请求走 POST /search/releases/{siteId}/{siteReleaseId}/plan(同 /acquisition-plans 流程)
const STEPS = [
"生成下载计划,预留版本目标和预算;每个版本目标同时只有一个在途计划",
"生成下载计划,预留收录版本和预算;每个收录版本同时只有一个在途计划",
"已有相同资源或相同 hash 的任务时直接接管,不重复提交",
"缓存卷超过硬阈值或读不到磁盘状态时无法提交",
"提交前复核免费、做种等易变字段",
@@ -109,11 +109,11 @@ export function SearchDownloadDialog({
{!entityId ? (
<p className="text-meta text-muted-foreground">资源未关联本地作品。先在「身份与元数据」指定。</p>
) : slots.isPending ? (
<p className="text-meta text-muted-foreground">正在读取版本目标…</p>
<p className="text-meta text-muted-foreground">正在读取收录版本…</p>
) : slotList.length ? (
<div className="flex flex-wrap items-center gap-2">
<FilterSelect
label="版本目标"
label="收录版本"
value={slotId}
onChange={setSlotId}
allLabel="自动匹配"
@@ -124,7 +124,7 @@ export function SearchDownloadDialog({
/>
</div>
) : (
<p className="text-meta text-muted-foreground">这个作品没有版本目标。先在订阅里新建。</p>
<p className="text-meta text-muted-foreground">这个作品没有收录版本。先在订阅里新建。</p>
)}
{release.entityMatch === "alias" ? (
<p className="text-meta text-warning">作品关联来自标题别名,尚未确认。</p>
@@ -159,7 +159,7 @@ export function SearchDownloadDialog({
}}
>
<Bot />
委托 Agent
委托智能体
</Button>
<Button onClick={go} disabled={plan.isPending || !!done || !entityId}>
<Download />
@@ -15,7 +15,7 @@ export function slotSummary(h: LocalMediaHit): string {
if (h.versionCount) parts.push(`${h.versionCount} 个版本`);
if (h.downloadingCount) parts.push(`${h.downloadingCount} 个下载中`);
const gaps = c.empty + c.attention + c.upgrade_pending;
if (gaps) parts.push(`${gaps} 个版本目标未满足`);
if (gaps) parts.push(`${gaps} 个收录版本未满足`);
return parts.join(" · ") || "未订阅";
}
@@ -77,14 +77,14 @@ describe("站点选择", () => {
});
describe("库内状态文案", () => {
it("未收录 / 已建档 / 已收录 / 正在下载;版本目标满足与缺失", () => {
expect(inventoryBadge(inv({})).label).toBe("未收录");
expect(inventoryBadge(inv({ state: "tracked" })).label).toBe("已建档 · 未入库");
expect(inventoryBadge(inv({ state: "owned", versionCount: 2 })).label).toBe("已收录 · 2 个版本");
it("未收录 / 已建档 / 已收录 / 正在下载;收录版本满足与缺失", () => {
expect(inventoryBadge(inv({})).label).toBe("不在媒体库");
expect(inventoryBadge(inv({ state: "tracked" })).label).toBe("在媒体库 · 未下载");
expect(inventoryBadge(inv({ state: "owned", versionCount: 2 })).label).toBe("已入库 · 2 个版本");
expect(inventoryBadge(inv({ state: "downloading", downloadingCount: 1 })).label).toBe("正在下载 1 个");
expect(
slotLine(inv({ state: "owned", slotTotal: 2, slotSatisfied: 1, missing: [{ slotId: ID, name: "2160p" }] })),
).toBe("版本目标满足 1/2 · 缺「2160p」");
).toBe("收录版本满足 1/2 · 缺「2160p」");
expect(slotLine(inv({}))).toBeNull();
});
+5 -5
View File
@@ -33,20 +33,20 @@ export function releaseSearchPath(w: { title: string; ref: string }, siteIds: st
export function inventoryBadge(inv: WorkInventory): { label: string; tone: StatusTone } {
switch (inv.state) {
case "not_collected":
return { label: "未收录", tone: "neutral" };
return { label: "不在媒体库", tone: "neutral" };
case "downloading":
return { label: `正在下载 ${inv.downloadingCount} 个`, tone: "warning" };
case "owned":
return { label: `已收录 · ${inv.versionCount} 个版本`, tone: "success" };
return { label: `已入库 · ${inv.versionCount} 个版本`, tone: "success" };
default:
return { label: "已建档 · 未入库", tone: "neutral" };
return { label: "在媒体库 · 未下载", tone: "neutral" };
}
}
/** 版本目标满足情况一行:满足 a/b · 缺「x」「y」 */
/** 收录版本满足情况一行:满足 a/b · 缺「x」「y」 */
export function slotLine(inv: WorkInventory): string | null {
if (inv.state === "not_collected" || inv.slotTotal === 0) return null;
const parts = [`版本目标满足 ${inv.slotSatisfied}/${inv.slotTotal}`];
const parts = [`收录版本满足 ${inv.slotSatisfied}/${inv.slotTotal}`];
if (inv.missing.length) parts.push(`缺${inv.missing.map((m) => `「${m.name}」`).join("")}`);
return parts.join(" · ");
}
@@ -50,17 +50,17 @@ 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 即可调用" },
granted: { label: "有持续授权", tone: "success", hint: "Agent、MCP、API Key 调用按持续授权放行" },
granted: { label: "有持续授权", tone: "success", hint: "智能体、MCP、API Key 调用按持续授权放行" },
needs_approval: {
label: "需审批",
tone: "warning",
hint: "没有覆盖它的持续授权,Agent、MCP、API Key 调用需要单次审批",
hint: "没有覆盖它的持续授权,智能体、MCP、API Key 调用需要单次审批",
},
agent_only: { label: "仅 Agent 会话", tone: "neutral", hint: "只在 Agent 会话内可用,不经 MCP 暴露" },
agent_only: { label: "仅智能体会话", tone: "neutral", hint: "只在智能体会话内可用,不经 MCP 暴露" },
};
export const CALLER_LABEL: Record<CapabilityCaller, string> = {
agent: "内置 Agent",
agent: "内置智能体",
mcp: "MCP",
rest: "REST",
other: "其他",
@@ -86,7 +86,7 @@ function McpServerBlock() {
section="agent"
anchor="mcp"
title="MCP 服务端"
description="外部 Agent 用 API Key 经 MCP 调用同一套能力。授权、审计、幂等与控制台一致,有副作用的工具没有持续授权或单次审批时被拒绝。"
description="外部智能体用 API Key 经 MCP 调用同一套能力。授权、审计、幂等与控制台一致,有副作用的工具没有持续授权或单次审批时被拒绝。"
>
{status.isPending ? (
<LoadingState rows={3} />
@@ -318,13 +318,13 @@ function CapabilitiesBlock() {
section="agent"
anchor="capabilities"
title="能力清单"
description="Agent、MCP、REST 共用这些能力。调用数与失败率为最近 7 天,含被拒绝的调用。"
description="智能体、MCP、REST 共用这些能力。调用数与失败率为最近 7 天,含被拒绝的调用。"
plain
>
{automationOff ? (
<p className="mb-3 rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-caption text-warning">
{tools.data?.readOnlyReconcile ? "只读对账模式" : "自动化总开关已关闭"}
:执行类能力对 Agent、MCP、API Key 一律拒绝,持续授权暂不生效。
:执行类能力对智能体、MCP、API Key 一律拒绝,持续授权暂不生效。
</p>
) : null}
<DataTable
@@ -14,7 +14,7 @@ export const WEBSEARCH_CARD: CardSpec = {
anchor: "websearch",
title: "网页搜索",
description:
"开启后 Agent 可以用 web.search 搜索站外公开信息,用 web.fetch 读取搜索结果里的网页。结果和正文按不可信数据交给模型,web.fetch 不能读内网地址和结果以外的链接。",
"开启后智能体可以用 web.search 搜索站外公开信息,用 web.fetch 读取搜索结果里的网页。结果和正文按不可信数据交给模型,web.fetch 不能读内网地址和结果以外的链接。",
fields: [
{
name: "agent.webSearch.provider",
@@ -63,7 +63,7 @@ export const WEBSEARCH_CARD: CardSpec = {
name: "agent.webSearch.maxResults",
label: "每次条数",
kind: "number",
hint: "1–10,Agent 可以要更少",
hint: "1–10,智能体可以要更少",
visible: (v) => v["agent.webSearch.provider"] !== "none",
},
{
+4 -4
View File
@@ -85,7 +85,7 @@ const BUDGET_CARD: CardSpec = {
const INVESTIGATE_CARD: CardSpec = {
anchor: "investigate",
title: "自动调查",
description: "待处理事项(识别有歧义、下载失败等)创建时,自动发起一次有界调查。",
description: "待处理事项(没识别出作品、下载失败等)创建时,自动发起一次有界调查。",
fields: [
{ name: "agent.autoInvestigate", label: "自动调查", kind: "switch", wide: true },
{
@@ -155,7 +155,7 @@ const ADVANCED_CARD: CardSpec = {
name: "agent.optionsTimeoutSec",
label: "方案选择等待(秒)",
kind: "number",
hint: "Agent 给出方案后等你选择的最长时间",
hint: "智能体给出方案后等你选择的最长时间",
},
{
name: "agent.optionsTimeoutAction",
@@ -163,7 +163,7 @@ const ADVANCED_CARD: CardSpec = {
kind: "select",
options: [
{ value: "recommended", label: "自动选推荐项" },
{ value: "stop", label: "不选,Agent 收尾" },
{ value: "stop", label: "不选,智能体收尾" },
],
},
{
@@ -184,7 +184,7 @@ const ADVANCED_CARD: CardSpec = {
name: "agent.fakeScript",
label: "fake 脚本(JSON)",
kind: "textarea",
hint: "只对 fake 上游生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
hint: "只对 fake 模型服务生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
},
],
fromSettings: (s) => ({
@@ -16,7 +16,7 @@ const GiB = 1024 ** 3;
const CACHE_CARD: CardSpec = {
anchor: "cache",
title: "缓存水位",
description: "缓存卷使用率到硬阈值时停止新增下载并暂停纳管任务;降到恢复水位以下只恢复因阈值暂停的任务。",
description: "缓存卷使用率到硬阈值时停止新增下载并暂停本工具的下载任务;降到恢复水位以下只恢复因阈值暂停的任务。",
fields: [
{ name: "cache.hardThresholdPercent", label: "硬阈值(%)", kind: "number" },
{ name: "cache.resumePercent", label: "恢复水位(%)", kind: "number", hint: "低于硬阈值" },
@@ -115,7 +115,7 @@ const STORAGE_BUDGET_CARD: CardSpec = {
gbField("pgDataWarnGb", "数据库告警线", "不高于数据库预算"),
gbField("imagesGb", "图片缓存容量"),
gbField("backupsGb", "备份目录预算"),
gbField("sessionsEvidenceGb", "Agent 会话与截图证据预算"),
gbField("sessionsEvidenceGb", "智能体会话与截图证据预算"),
],
fromSettings: (s) => Object.fromEntries(BUDGET_KEYS.map((k) => [`storage.budgets.${k}`, n(s.storage.budgets[k])])),
toPatch: (v) => ({ storage: { budgets: pick(v, "storage.budgets", BUDGET_KEYS) } }),
@@ -136,17 +136,17 @@ const STORAGE_RETENTION_CARD: CardSpec = {
title: "流水数据保留",
description: "到期由定时清理任务分批删除。单位天,心跳为小时。",
fields: [
dayField("retentionDays", "observation", "Feed 观察"),
dayField("retentionDays", "observation", "RSS 抓取记录"),
dayField("retentionDays", "sseEvent", "实时事件"),
dayField("retentionDays", "heartbeatHours", "进程心跳(小时)"),
dayField("retentionDays", "taskRun", "任务运行记录"),
dayField("retentionDays", "agentEvent", "Agent 会话事件", "只删已结束的会话"),
dayField("retentionDays", "agentEvent", "智能体会话事件", "只删已结束的会话"),
dayField("retentionDays", "audit", "审计日志", "能力调用审计除外"),
dayField("retentionDays", "capabilityStats", "能力调用统计"),
dayField(
"retentionDays",
"identityPoolArchive",
"待识别池归档",
"未识别资源归档",
"未命中的条目这么久没有新证据就归档,仍可手动指定",
),
],
@@ -97,7 +97,7 @@ export function BrowserProvidersBlock() {
section="sites"
anchor="browser"
title="浏览器服务"
description="原站视图和 Agent 打开站点网页用的 camofox-browser。部署自带的一台由环境变量登记(camofox-env)。"
description="原站视图和智能体打开站点网页用的 camofox-browser。部署自带的一台由环境变量登记(camofox-env)。"
plain
>
<ResourceSection<BrowserProviderView>
@@ -257,9 +257,8 @@ export function LibraryRootsTable() {
<ResourceSection<LibraryRoot>
hooks={{ list, create, update, remove } as never}
config={{
noun: "库根",
description:
"登记或改路径时会在目录里写哨兵文件 .uhd-root,据此判断库根是否在线;目录不存在或不可写时无法保存。",
noun: "媒体库目录",
description: "保存时会在目录里放一个标记文件,用来判断目录是否挂载;目录不存在或不可写时无法保存。",
deleteHint: "只删除登记,磁盘上的文件不动。",
columns: [
{
@@ -286,7 +285,7 @@ export function LibraryRootsTable() {
},
{ key: "health", header: "在线", cell: (r) => <Health status={r.healthStatus} at={r.lastHealthAt} /> },
],
rowActions: (r) => <TestButton label="检查哨兵" pending={test.isPending} run={() => test.mutateAsync(r.id)} />,
rowActions: (r) => <TestButton label="检查目录" pending={test.isPending} run={() => test.mutateAsync(r.id)} />,
fields: [
{ name: "name", label: "名称", kind: "text", required: true, placeholder: "例如:电影库" },
{ name: "mediaKind", label: "内容类型", kind: "select", options: MEDIA_KIND },
+1 -1
View File
@@ -42,7 +42,7 @@ const ORGANIZE_CARD: CardSpec = {
export function LibrarySection() {
return (
<div className="grid grid-cols-1 gap-8">
<SettingsBlock section="library" anchor="roots" title="库根" plain>
<SettingsBlock section="library" anchor="roots" title="媒体库目录" plain>
<LibraryRootsTable />
</SettingsBlock>
<SettingsGate>{(s) => <SettingsCard section="library" spec={ORGANIZE_CARD} settings={s} />}</SettingsGate>
@@ -203,7 +203,7 @@ const IDENTITY_CARD: CardSpec = {
anchor: "identity",
title: "资源识别",
description:
"缺省只在资源命中订阅或本地作品别名、新作品收录规则前置条件通过、你手动识别或搜索时查 TMDB;其余进待识别池,不进待处理。",
"缺省只在资源命中订阅或本地作品别名、新作品收录规则前置条件通过、你手动识别或搜索时查 TMDB;其余进未识别资源,不进待处理。",
fields: [
{
name: "identity.eagerTmdbLookup",
@@ -216,7 +216,7 @@ const IDENTITY_CARD: CardSpec = {
name: "identity.autoEnrich",
label: "TMDB 查不到时自动补全元数据",
kind: "switch",
hint: "资源命中订阅或新作品收录规则、TMDB 却没有结果时,自动搜索网页出一份补全建议(每条资源一次,不建作品)。每次约 1–2 次网页搜索、最多读 3 个网页、1 次 Agent 上游调用",
hint: "资源命中订阅或新作品收录规则、TMDB 却没有结果时,自动搜索网页出一份补全建议(每条资源一次,不建作品)。每次约 1–2 次网页搜索、最多读 3 个网页、1 次 模型服务调用",
wide: true,
},
{
+5 -5
View File
@@ -120,7 +120,7 @@ function RootNaming() {
<SettingsBlock
section="library"
anchor="naming-roots"
title="库根命名覆盖"
title="媒体库目录命名覆盖"
description="按类型覆盖全局模板;留空的类型沿用全局。"
>
{roots.isPending ? (
@@ -128,12 +128,12 @@ function RootNaming() {
) : roots.isError ? (
<ErrorState plain error={roots.error} onRetry={() => roots.refetch()} />
) : !root ? (
<p className="text-caption text-muted-foreground">还没有库根。先在上方「库根」添加。</p>
<p className="text-caption text-muted-foreground">还没有媒体库目录。先在上方「媒体库目录」添加。</p>
) : (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
<Select value={root.id} onValueChange={setRootId}>
<SelectTrigger size="sm" aria-label="库根" className="max-w-full min-w-48">
<SelectTrigger size="sm" aria-label="媒体库目录" className="max-w-full min-w-48">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
@@ -177,7 +177,7 @@ function RootNamingEditor({ root }: { root: LibraryRoot }) {
try {
await save.mutateAsync({ id: root.id, naming: body });
setDraft({ ...(body ?? {}) });
toast.success(body ? "库根命名覆盖已保存" : "已清除库根命名覆盖");
toast.success(body ? "媒体库目录命名覆盖已保存" : "已清除媒体库目录命名覆盖");
} catch (err) {
const split = splitNamingErrors(errorMessage(err));
if (split) setErrors(split);
@@ -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>
@@ -104,7 +104,7 @@ export function PathMappingsPanel({
<p className="truncate">{m.remotePrefix}</p>
<p className="truncate text-muted-foreground">→ {m.localPrefix}</p>
{rootName(m.libraryRootId) ? (
<p className="font-sans text-muted-foreground">库根:{rootName(m.libraryRootId)}</p>
<p className="font-sans text-muted-foreground">媒体库目录:{rootName(m.libraryRootId)}</p>
) : null}
</div>
<StatusBadge tone={h.tone}>{h.label}</StatusBadge>
@@ -175,7 +175,7 @@ export function PathMappingsPanel({
disabled={create.isPending}
/>
<FieldControl
spec={{ name: "libraryRootId", label: "关联库根", kind: "select", options: rootOpts }}
spec={{ name: "libraryRootId", label: "关联媒体库目录", kind: "select", options: rootOpts }}
value={v.libraryRootId}
onChange={(x) => setV((s) => ({ ...s, libraryRootId: x }))}
disabled={create.isPending}
+1 -1
View File
@@ -22,7 +22,7 @@ const SOURCE_TEXT: Record<string, string> = { "stored credential": "已存凭证
/** 上游返回的多是英文原文:先给一句怎么办,原文留作排查 */
const ERROR_HINT: Record<string, string> = {
network: "连不上上游,检查 Base URL、端口和网络",
network: "连不上模型服务,检查 Base URL、端口和网络",
auth: "凭证被拒,检查 API Key 或重新订阅登录",
model: "模型不可用,换一个模型 id",
};
@@ -49,13 +49,13 @@ export function PiUpstreamsBlock() {
<SettingsBlock
section="agent"
anchor="upstream"
title="上游"
description="没指定上游的运行用默认上游。密钥和登录凭证保存后不回显。"
title="模型服务"
description="没指定模型服务的运行用默认模型服务。密钥和登录凭证保存后不回显。"
plain
actions={
<Button size="sm" onClick={() => setWizard(true)}>
<Plus />
新建上游
新建模型服务
</Button>
}
>
@@ -66,7 +66,7 @@ export function PiUpstreamsBlock() {
error={list.error}
onRetry={() => list.refetch()}
onRowClick={(r) => setOpenId(r.id)}
empty={{ title: "还没有上游" }}
empty={{ title: "还没有模型服务" }}
columns={[
{
key: "name",
@@ -181,7 +181,7 @@ function UpstreamDrawer({
const makeDefault = async () => {
const model = v.model.trim();
if (!model) {
setErrors((e) => ({ ...e, defaultModel: "设为默认上游前先选默认模型" }));
setErrors((e) => ({ ...e, defaultModel: "设为默认模型服务前先选默认模型" }));
return;
}
try {
@@ -189,7 +189,7 @@ function UpstreamDrawer({
id: upstream.id,
body: { makeDefault: true, ...(model !== (upstream.defaultModel ?? "") ? { defaultModel: model } : {}) },
});
toast.success(`${upstream.name}已设为默认上游`);
toast.success(`${upstream.name}已设为默认模型服务`);
} catch (err) {
toast.error(errorMessage(err, "设置失败"));
}
@@ -204,7 +204,7 @@ function UpstreamDrawer({
description={
<span className="flex flex-wrap items-center gap-2">
<UpstreamStatusBadge u={upstream} />
{upstream.isDefault ? <StatusBadge tone="success">默认上游</StatusBadge> : null}
{upstream.isDefault ? <StatusBadge tone="success">默认模型服务</StatusBadge> : null}
<span>
{provider?.name ?? upstream.provider} · {AUTH_LABEL[upstream.authKind]}
</span>
@@ -402,11 +402,13 @@ function UpstreamDrawer({
<ConfirmDialog
open={deleting}
onOpenChange={setDeleting}
title={`删除上游「${upstream.name}」?`}
title={`删除模型服务「${upstream.name}」?`}
description={
upstream.isDefault ? "它是默认上游,删除后没有默认上游;凭证一并删除,无法恢复。" : "凭证一并删除,无法恢复。"
upstream.isDefault
? "它是默认模型服务,删除后没有默认模型服务;凭证一并删除,无法恢复。"
: "凭证一并删除,无法恢复。"
}
confirmLabel="删除上游"
confirmLabel="删除模型服务"
destructive
onConfirm={async () => {
try {
@@ -60,9 +60,9 @@ export function PiUpstreamWizard({ open, onClose }: { open: boolean; onClose: ()
const title =
step === "provider"
? "新建 Agent 上游"
? "新建模型服务"
: step === "config"
? `新建上游 · ${provider?.name ?? ""}`
? `新建模型服务 · ${provider?.name ?? ""}`
: (created?.name ?? "");
const description =
step === "provider"
@@ -70,8 +70,8 @@ export function PiUpstreamWizard({ open, onClose }: { open: boolean; onClose: ()
: step === "config"
? "第 2 步,共 3 步:凭证与模型"
: created?.authKind === "oauth"
? "第 3 步,共 3 步:登录并测试。上游已创建。"
: "第 3 步,共 3 步:测试。上游已创建。";
? "第 3 步,共 3 步:登录并测试。模型服务已创建。"
: "第 3 步,共 3 步:测试。模型服务已创建。";
return (
<DetailSheet open={open} onOpenChange={(o) => !o && close()} title={title} description={description} width="lg">
@@ -413,7 +413,7 @@ function VerifyStep({
const finish = async () => {
if (makeDefault && !model.trim()) {
setModelError("设为默认上游前先选默认模型");
setModelError("设为默认模型服务前先选默认模型");
return;
}
const body = {
@@ -422,7 +422,7 @@ function VerifyStep({
};
try {
if (Object.keys(body).length) await update.mutateAsync({ id: upstream.id, body });
if (makeDefault && !upstream.isDefault) toast.success(`${upstream.name}已设为默认上游`);
if (makeDefault && !upstream.isDefault) toast.success(`${upstream.name}已设为默认模型服务`);
onDone();
} catch (err) {
toast.error(errorMessage(err, "保存失败"));
@@ -476,7 +476,7 @@ function VerifyStep({
onCheckedChange={setMakeDefault}
disabled={upstream.isDefault}
/>
{upstream.isDefault ? "已是默认上游" : "设为默认上游"}
{upstream.isDefault ? "已是默认模型服务" : "设为默认模型服务"}
</label>
<Button onClick={finish} disabled={update.isPending}>
{update.isPending ? <LoaderCircle className="animate-spin" /> : null}
+4 -4
View File
@@ -23,15 +23,15 @@ const DEFAULTS_CARD: CardSpec = {
{ name: "sites.defaultRateLimit.concurrency", label: "并发", kind: "number", hint: "同一站点同时进行的请求" },
{
name: "sites.defaultSnapshotIntervalSec",
label: "快照周期(分钟)",
label: "抓取周期(分钟)",
kind: "number",
hint: "站点开了定时快照但没设周期时使用;5–10080",
hint: "站点开了定时抓取但没设周期时使用;5–10080",
},
{
name: "sites.defaultSnapshotMaxPages",
label: "快照页数",
label: "抓取页数",
kind: "number",
hint: "定时和手动快照最多抓几页列表;站点定义更少时按站点;1–20",
hint: "定时和手动抓取最多抓几页列表;站点定义更少时按站点;1–20",
},
{
name: "sites.defaultHealthCheckIntervalSec",
@@ -258,7 +258,7 @@ export function SiteAddDialog({ onClose }: { onClose: () => void }) {
onClick={() => pick(CUSTOM)}
className="w-full rounded-lg border border-dashed border-border px-3 py-2 text-left text-caption text-muted-foreground hover:border-foreground/40 hover:text-foreground"
>
自定义 RSS 站点:只抓 Feed,不登录
自定义 RSS 站点:只抓 RSS,不登录
</button>
</section>
</>
@@ -335,7 +335,7 @@ export function SiteAddDialog({ onClose }: { onClose: () => void }) {
</>
) : (
<p className="text-caption text-muted-foreground">
不需要登录凭证。添加后在站点详情的"快照与 Feed"里新建 Feed。
不需要登录凭证。添加后在站点详情的"抓取与 RSS"里新建 RSS。
</p>
)}
</form>
+5 -5
View File
@@ -141,11 +141,11 @@ export function SiteMoreMenu({ site, onDialog }: { site: SiteListItem; onDialog:
{item(<Pencil />, "编辑站点", () => onDialog({ kind: "edit", site }))}
{item(
<Camera />,
"抓取快照",
"抓取最新资源",
() =>
snap.mutate(
{ id: site.id },
{ onSuccess: () => toast.success("快照已排队"), onError: (e) => toast.error(errorMessage(e)) },
{ onSuccess: () => toast.success("已开始抓取"), onError: (e) => toast.error(errorMessage(e)) },
),
{ disabled: !site.enabled },
)}
@@ -314,13 +314,13 @@ export function SiteCard({ site, checks, onDialog }: Props) {
</dd>
</div>
<div className="min-w-0">
<dt className="text-meta text-muted-foreground">快照</dt>
<dt className="text-meta text-muted-foreground">抓取</dt>
<dd className={cn("text-foreground", stale && "text-warning")}>
{!site.snapshotEnabled
? "未开启"
: site.lastSnapshotAt
? `${relTime(site.lastSnapshotAt)}${stale ? " · 已过期" : ""}`
: "还没有快照"}
: "还没抓取过"}
</dd>
</div>
<div className="min-w-0">
@@ -455,7 +455,7 @@ export function SiteTable({
},
{
key: "snapshot",
header: "快照",
header: "抓取",
hideBelow: "lg",
cell: (s) => (
<span className={s.snapshotEnabled && snapshotStale(s) ? "text-warning" : "text-muted-foreground"}>
@@ -380,7 +380,7 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
open
onOpenChange={(o) => !o && !busy && onClose()}
title={`编辑 · ${site.name}`}
description="限流和快照周期留空时用「设置 → 站点默认」"
description="限流和抓取周期留空时用「设置 → 站点默认」"
footer={
<>
<Button variant="outline" onClick={onClose} disabled={busy}>
@@ -434,11 +434,11 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
disabled={busy}
/>
<label htmlFor="se-snap" className="text-caption text-foreground">
定时抓取快照
定时抓取
</label>
</div>
{v.snapshotEnabled ? (
<FormField label="快照周期(分钟)" htmlFor="se-int" error={errors.intervalMin} hint="留空用站点默认">
<FormField label="抓取周期(分钟)" htmlFor="se-int" error={errors.intervalMin} hint="留空用站点默认">
<Input
id="se-int"
inputMode="numeric"
@@ -508,7 +508,7 @@ export function DeleteSiteDialog({
<ul className="list-disc space-y-1 pl-5 text-caption text-muted-foreground">
<li>站点、{site.accounts?.length ?? 0} 个账号和全部凭证删除</li>
<li>已抓到的资源保留,标为"站点已关闭",不再产生新候选</li>
<li>该站 Feed 停用;已有下载任务按原策略继续</li>
<li>该站 RSS 停用;已有下载任务按原策略继续</li>
<li>同一站点以后可以重新添加</li>
</ul>
</ConfirmDialog>
@@ -216,7 +216,7 @@ export function SiteFeedDialog({
<p className="text-caption text-muted-foreground">
{account ? `账号 ${account.label}` : "该站没有账号,按地址里的 passkey 抓取"} · 解析器按站点定义 · 每 30
分钟抓一次,可在 Feed 详情里改
分钟抓一次,可在 RSS 详情里改
</p>
</form>
)}
@@ -279,7 +279,7 @@ function FirstFetch({
</p>
) : timedOut ? (
<p className="rounded-lg border border-border px-3 py-2 text-muted-foreground">
已保存。抓取还在排队(站点限流),结果稍后显示在 Feed 列表里。
已保存。抓取还在排队(站点限流),结果稍后显示在 RSS 列表里。
</p>
) : (
<p className="flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-muted-foreground">
@@ -99,9 +99,9 @@ export function SiteOverview({ site }: { site: SiteListItem }) {
),
},
{
label: "快照",
label: "抓取",
value: site.snapshotEnabled
? `${site.lastSnapshotAt ? relTime(site.lastSnapshotAt) : "还没有快照"} · ${site.snapshotIntervalSec ? `每 ${Math.round(site.snapshotIntervalSec / 60)} 分钟` : "默认周期"}`
? `${site.lastSnapshotAt ? relTime(site.lastSnapshotAt) : "还没抓取过"} · ${site.snapshotIntervalSec ? `每 ${Math.round(site.snapshotIntervalSec / 60)} 分钟` : "默认周期"}`
: "未开启",
},
{ label: "限流", value: rateLimitText(site.rateLimit) },
@@ -30,14 +30,14 @@ const LEVEL: Record<HealthLevel, { label: string; tone: StatusTone }> = {
/** 清理目标的界面名称 */
export const CLEANUP_LABEL: Record<StorageCleanupTarget, string> = {
observation: "Feed 观察记录",
observation: "RSS 抓取记录",
sse_event: "SSE 补读事件",
heartbeat: "进程心跳",
task_run: "任务运行",
agent_event: "Agent 事件",
agent_event: "智能体事件",
audit: "审计事件",
capability_stats: "能力调用统计",
identity_pool: "待识别池归档",
identity_pool: "未识别资源归档",
analyze: "统计信息更新",
images: "图片缓存容量",
kv_cache: "过期缓存",
@@ -216,7 +216,7 @@ export function DeleteSubscriptionsDialog({
open={open}
onOpenChange={onOpenChange}
title={ids.length > 1 ? `删除 ${ids.length} 个订阅?` : "删除这个订阅?"}
description="不再搜索和追更。已入库文件、版本目标和下载记录保留,可在订阅历史里重新订阅。"
description="不再搜索和追更。已入库文件、收录版本和下载记录保留,可在订阅历史里重新订阅。"
confirmLabel="删除"
destructive
onConfirm={() =>
@@ -22,7 +22,7 @@ import {
seasonTitle,
} from "./model.ts";
// 覆盖矩阵(设计 §2.1):列 = 集(电影 = 内容版本),行 = 版本目标,首行"库内任一版本"。
// 覆盖矩阵(设计 §2.1):列 = 集(电影 = 内容版本),行 = 收录版本,首行"库内任一版本"。
// 底色无彩,只有状态用状态色:已入库 success、缺失 destructive、可升级/延后 warning;在途用斜纹。
type Row = SubscriptionCoverage["rows"][number];
@@ -120,7 +120,7 @@ export function CoverageMatrix({
scope="col"
className="sticky left-0 z-10 bg-surface-raised pr-2 text-left font-normal text-muted-foreground"
>
版本目标
收录版本
</th>
{cols.map((c) => (
<th
@@ -238,7 +238,7 @@ export function CellSheet({
}
}}
title={sel ? `${sel.season} · ${columnLabel(sel.col)}` : ""}
description={sel ? `${sel.row.name}${sel.row.slotId === null ? "(作品全部资产)" : ""}` : null}
description={sel ? `${sel.row.name}${sel.row.slotId === null ? "(作品全部文件)" : ""}` : null}
footer={
sel && c ? (
<>
@@ -284,7 +284,7 @@ export function CellSheet({
{ label: "pri", value: c.pri !== null ? String(c.pri) : null },
{ label: "可升级", value: c.state === "have" ? (c.upgradable ? "是,低于目标档位" : "否") : null },
{
label: "资产",
label: "文件",
value: c.assetId ? (
mediaEntityId ? (
<Link
@@ -316,7 +316,7 @@ export function CellSheet({
</p>
) : null}
{sel.row.slotId === null && c.state === "missing" ? (
<p className="text-meta text-muted-foreground">这一行汇总全部版本。要补全,点具体版本目标那一行的格。</p>
<p className="text-meta text-muted-foreground">这一行汇总全部版本。要补全,点具体收录版本那一行的格。</p>
) : null}
</>
) : null}
@@ -86,7 +86,7 @@ describe("覆盖矩阵格", () => {
expect(moveItem(["a", "b", "c"], 2, 0)).toEqual(["c", "a", "b"]);
expect(moveItem(["a", "b"], 0, 5)).toEqual(["a", "b"]);
});
it("升级策略取自版本目标", () => {
it("升级策略取自收录版本", () => {
expect(upgradeOf([{ upgradeThreshold: {} }], "series")).toBe("off");
expect(upgradeOf([{ upgradeThreshold: { enabled: true, mode: "full_pack" } }], "series")).toBe("full_pack");
expect(upgradeOf([{ upgradeThreshold: { enabled: true } }], "movie")).toBe("movie");
@@ -55,7 +55,7 @@ export function seasonLabel(
return x.seasonScope === "following_seasons" && x.seasonNumber > 0 ? `${base} 起` : base;
}
/** 版本目标徽标:洗版中 → 在途 → 缺 N → 已齐;都没有 → 已入库 N */
/** 收录版本徽标:洗版中 → 在途 → 缺 N → 已齐;都没有 → 已入库 N */
export function slotBadge(s: SubscriptionSlotSummary): ToneLabel {
if (s.upgrading)
return {
@@ -136,7 +136,7 @@ export const PLAN_STAGE: Record<string, string> = { reserved: "已预留", downl
export const SKIP_REASON: Record<string, string> = {
out_of_range: "集范围外",
not_aired: "未播出",
not_required: "该版本目标不要求这个内容版本",
not_required: "该收录版本不要求这个内容版本",
suspected: "总集数未知,按已见最高集推断",
deferred: "有延后的资源",
};
@@ -155,7 +155,7 @@ export function seasonTitle(s: SubscriptionCoverage["seasons"][number], kind: Su
return s.seasonNumber === 0 ? "特别篇" : `第 ${s.seasonNumber} 季`;
}
// ── 版本目标 ──
// ── 收录版本 ──
export type UpgradeStrategy = "off" | "per_unit" | "full_pack" | "movie";
export const UPGRADE_LABEL: Record<UpgradeStrategy, string> = {
@@ -165,7 +165,7 @@ export const UPGRADE_LABEL: Record<UpgradeStrategy, string> = {
movie: "洗版",
};
/** 订阅版本目标当前的升级策略:任一开启取其 mode */
/** 订阅收录版本当前的升级策略:任一开启取其 mode */
export function upgradeOf(
slots: Pick<AcquisitionSlot, "upgradeThreshold">[],
kind: SubscriptionMediaKind,
@@ -231,7 +231,7 @@ export type SubscriptionDraft = {
include: string;
exclude: string;
keywords: string;
/** MP 分辨率 / 资源质量 / 特效预设(写到该订阅自己的版本目标) */
/** MP 分辨率 / 资源质量 / 特效预设(写到该订阅自己的收录版本) */
resolution: SubscriptionResolution | "";
source: SubscriptionSourcePreset | "";
effect: SubscriptionEffect | "";
@@ -334,7 +334,7 @@ const isInt = (s: string) => /^\d+$/.test(s.trim());
/**
* 草稿 → params(PATCH 整体替换,所以以现有 params 为底保留未知键)。
* upgradeChanged:只有升级策略改了才带 best_version(否则每次保存都会 bump 版本目标 revision)。
* upgradeChanged:只有升级策略改了才带 best_version(否则每次保存都会 bump 收录版本 revision)。
*/
export function draftToParams(
d: SubscriptionDraft,
@@ -26,7 +26,9 @@ function Flags({ x }: { x: Item }) {
return (
<>
{x.group === "paused" ? <StatusBadge tone="neutral">暂停</StatusBadge> : null}
{x.group === "pending" ? <StatusBadge tone="warning">{x.mediaEntityId ? "待定" : "尚未建档"}</StatusBadge> : null}
{x.group === "pending" ? (
<StatusBadge tone="warning">{x.mediaEntityId ? "待定" : "不在媒体库"}</StatusBadge>
) : null}
{x.upgrading ? <StatusBadge tone="warning">洗版中</StatusBadge> : null}
{x.awaitingNewSeason ? <StatusBadge tone="success">等待新季</StatusBadge> : null}
</>
@@ -72,7 +74,7 @@ export function ProgressBlock({ x, className }: { x: Pick<Item, "progress" | "mo
}
export function SlotBadges({ slots, max = 4 }: { slots: Item["slots"]; max?: number }) {
if (slots.length === 0) return <span className="text-meta text-muted-foreground">没有版本目标</span>;
if (slots.length === 0) return <span className="text-meta text-muted-foreground">没有收录版本</span>;
return (
<div className="flex min-w-0 flex-wrap gap-1">
{slots.slice(0, max).map((s) => {
@@ -4,7 +4,7 @@ import { Button } from "~/components/ui/button.tsx";
import { cn } from "~/lib/utils.ts";
import { moveItem } from "./model.ts";
// 版本目标排序:拖动(桌面)或上移/下移(键盘、手机)。第一个是主版本。
// 收录版本排序:拖动(桌面)或上移/下移(键盘、手机)。第一个是主版本。
export function SlotOrderList<T extends { id: string; name: string }>({
items,
@@ -16,7 +16,7 @@ import {
import { SlotOrderList } from "./slot-order.tsx";
import { AutoFillFields } from "./subscription-form.tsx";
// 对比视图的"版本目标"区(设计 §2.2):拖动排序(PUT slot-order)、行尾计数、补全此版本、自动补全开关与预算。
// 对比视图的"收录版本"区(设计 §2.2):拖动排序(PUT slot-order)、行尾计数、补全此版本、自动补全开关与预算。
type SlotRow = SubscriptionCoverage["rows"][number] & { id: string };
@@ -83,7 +83,7 @@ export function SlotPanel({
return (
<Panel
title="版本目标"
title="收录版本"
description="拖动调整顺序,第一个是主版本。主版本缺的照常追更;其他版本按下面的自动补全设置补齐。"
actions={
dirty ? (
@@ -101,7 +101,7 @@ export function SlotPanel({
>
{rows.length === 0 ? (
<p className="text-caption text-muted-foreground">
{sub.mediaEntityId ? "还没有版本目标。在下方规则区新建。" : "作品建档后才有版本目标。"}
{sub.mediaEntityId ? "还没有收录版本。在下方规则区新建。" : "作品加入媒体库后才有收录版本。"}
</p>
) : (
<SlotOrderList
@@ -173,7 +173,7 @@ function WorkPicker({ onPick, error }: { onPick: (t: Target) => void; error?: st
kind: "title",
titleQuery: c.year ? `${c.title} ${c.year}` : c.title,
mediaKind: c.entityType === "movie" ? "movie" : "series",
note: `TMDB ${c.externalId},建档失败,按标题追踪`,
note: `TMDB ${c.externalId},加入媒体库失败,按标题追踪`,
});
};
const row = (key: string, main: ReactNode, sub: string, onClick: () => void) => (
@@ -321,7 +321,7 @@ export function SubscriptionFormDialog({
const [totalTouched, setTotalTouched] = useState(false);
const [errors, setErrors] = useState<DraftErrors & { target?: string }>({});
const [dup, setDup] = useState<string | null>(null);
// 编辑:版本目标到了再回填升级策略
// 编辑:收录版本到了再回填升级策略
useEffect(() => {
if (sub && slotsQ.isSuccess && !upgradeTouched) setD((x) => ({ ...x, upgrade: initialUpgrade }));
}, [sub, slotsQ.isSuccess, initialUpgrade, upgradeTouched]);
@@ -454,7 +454,7 @@ export function SubscriptionFormDialog({
<DialogHeader>
<DialogTitle>{editing ? "编辑订阅" : "新建订阅"}</DialogTitle>
<DialogDescription>
{editing ? titleText : initialTarget ? titleText : "从媒体库或 TMDB 选作品;选 TMDB 条目会先建档。"}
{editing ? titleText : initialTarget ? titleText : "从媒体库或 TMDB 选作品;选 TMDB 条目会先加入媒体库。"}
</DialogDescription>
</DialogHeader>
{dup ? (
@@ -497,8 +497,8 @@ export function SubscriptionFormDialog({
{target.kind === "entity"
? "媒体库作品"
: target.kind === "tmdb"
? "TMDB 条目,保存时建档"
: (target.note ?? "按标题追踪,尚未建档")}
? "TMDB 条目,保存时加入媒体库"
: (target.note ?? "按标题追踪,不在媒体库")}
</p>
</div>
{initialTarget ? null : (
@@ -541,7 +541,9 @@ export function SubscriptionFormDialog({
<FormField
label="季"
htmlFor={f("season")}
hint={editing ? "季建好后不能改;要换季就新建订阅" : entityId ? undefined : "作品建档后可选季"}
hint={
editing ? "季建好后不能改;要换季就新建订阅" : entityId ? undefined : "作品加入媒体库后可选季"
}
>
<Pick
id={f("season")}
@@ -648,7 +650,7 @@ export function SubscriptionFormDialog({
options={(dls.data?.items ?? []).map((x) => ({ value: x.id, label: x.name }))}
/>
</FormField>
<FormField label="库根" htmlFor={f("root")}>
<FormField label="媒体库目录" htmlFor={f("root")}>
<Pick
id={f("root")}
value={d.libraryRootId}
@@ -668,12 +670,12 @@ export function SubscriptionFormDialog({
</FormField>
</Section>
<Section title="版本目标">
<Section title="收录版本">
{editing ? (
slotsQ.isPending ? (
<p className="text-caption text-muted-foreground">读取版本目标…</p>
<p className="text-caption text-muted-foreground">读取收录版本…</p>
) : subSlots.length === 0 ? (
<p className="text-caption text-muted-foreground">还没有版本目标。在对比视图里新建。</p>
<p className="text-caption text-muted-foreground">还没有收录版本。在对比视图里新建。</p>
) : (
<FormField label="顺序" htmlFor={f("order")} hint="第一个是主版本;其他版本按自动补全设置补齐">
<SlotOrderList
@@ -689,8 +691,8 @@ export function SubscriptionFormDialog({
htmlFor={f("tpl")}
hint={
target?.kind === "entity"
? "作品还没有版本目标时按模板顺序创建,第一个是主版本"
: "作品建档后按模板创建版本目标"
? "作品还没有收录版本时按模板顺序创建,第一个是主版本"
: "作品加入媒体库后按模板创建收录版本"
}
>
<Pick
@@ -708,7 +710,7 @@ export function SubscriptionFormDialog({
<FormField
label="升级策略"
htmlFor={f("upgrade")}
hint="作用于这个订阅的全部版本目标;已入库低于目标档位时继续找更好的"
hint="作用于这个订阅的全部收录版本;已入库低于目标档位时继续找更好的"
>
<Pick
id={f("upgrade")}
@@ -730,7 +732,7 @@ export function SubscriptionFormDialog({
<FormField
label="规则组"
htmlFor={f("groups")}
hint="按选择顺序串联;不选 = 订阅默认规则组。版本目标自己的规则组另算"
hint="按选择顺序串联;不选 = 订阅默认规则组。收录版本自己的规则组另算"
>
{groups.isPending ? (
<p className="text-caption text-muted-foreground">读取规则组…</p>
@@ -810,7 +812,7 @@ export function SubscriptionFormDialog({
</FormField>
</div>
<p className="text-meta text-muted-foreground">
分辨率、资源质量、特效写进这个订阅自己的版本目标;UHD 和杜比全景声按标题匹配,与「包含」同时生效。
分辨率、资源质量、特效写进这个订阅自己的收录版本;UHD 和杜比全景声按标题匹配,与「包含」同时生效。
</p>
<FormField
label="搜索关键词"
+1 -1
View File
@@ -9,7 +9,7 @@ describe("任务行状态", () => {
triggeredBy: null,
outcome,
});
it("引擎跑完但 Agent 会话超出预算 / 失败:显示业务结果,不显示「完成」", () => {
it("引擎跑完但智能体会话超出预算 / 失败:显示业务结果,不显示「完成」", () => {
expect(engineRunStatus({ status: "completed", business: agent("budget_exceeded") })).toEqual({
label: "超出预算",
tone: "warning",
+9 -9
View File
@@ -8,9 +8,9 @@ export const BUSINESS_KIND_LABEL: Record<EngineBusinessKind, string> = {
subscription: "订阅",
download: "下载",
organization: "整理",
agent: "Agent",
agent: "智能体",
schedule: "定时任务",
feed: "Feed",
feed: "RSS",
site: "站点",
decision: "待处理事项",
media: "作品",
@@ -23,7 +23,7 @@ export const SOURCE_LABEL: Record<TaskSource, string> = {
schedule: "定时",
event: "事件",
manual: "手动",
agent: "Agent",
agent: "智能体",
};
/** 来源 → 服务端 triggeredBy 筛选值(逗号分隔多值) */
export const SOURCE_TRIGGERED_BY: Record<TaskSource, string> = {
@@ -46,15 +46,15 @@ export const WORKFLOW_LABEL: Record<string, string> = {
submitDownload: "提交下载",
verifyAndImport: "核验入库",
refreshMetadata: "刷新元数据",
reconcileLibrary: "资产对账",
reconcileLibrary: "文件核对",
cleanupDownloadCache: "清理缓存",
resolveDecision: "处理待处理事项",
runAgent: "Agent 会话",
snapshotSite: "站点快照",
runAgent: "智能体会话",
snapshotSite: "站点抓取",
refreshSiteAccount: "刷新站点账号",
searchSubscription: "订阅搜索",
purgeTrash: "清理回收站",
refreshPiTokens: "刷新上游登录",
refreshPiTokens: "刷新模型服务登录",
};
export const workflowLabel = (name: string) => WORKFLOW_LABEL[name] ?? name;
@@ -115,11 +115,11 @@ export function businessLink(b: EngineRun["business"], input?: unknown): { to: s
case "organization":
return { to: `/console/organize/${id}`, label: "整理记录" };
case "agent":
return { to: `/console/agent/${id}`, label: "Agent 会话" };
return { to: `/console/agent/${id}`, label: "智能体会话" };
case "schedule":
return { to: `/console/schedules?job=${id}`, label: "定时任务" };
case "feed":
return { to: consolePath("sources"), label: "Feed" };
return { to: consolePath("sources"), label: "RSS" };
case "site":
return { to: `/console/sites/${id}`, label: "站点" };
case "decision":
@@ -19,7 +19,7 @@ import { errorMessage } from "~/lib/api.ts";
import { formatBytes } from "~/lib/utils.ts";
const STEPS = [
"生成下载计划,预留版本目标和预算;每个版本目标同时只有一个在途计划",
"生成下载计划,预留收录版本和预算;每个收录版本同时只有一个在途计划",
"已有相同资源或相同 hash 的任务时直接接管,不重复提交",
"缓存卷超过硬阈值或读不到磁盘状态时无法提交",
"提交前复核免费、做种等易变字段",
@@ -93,7 +93,7 @@ export function DownloadDialog({
{slots.length ? (
<div className="flex flex-wrap items-center gap-2">
<FilterSelect
label="版本目标"
label="收录版本"
value={slotId}
onChange={setSlotId}
allLabel="自动匹配"
@@ -103,7 +103,7 @@ export function DownloadDialog({
) : (
<p className="text-meta text-muted-foreground">
{release.entity
? "这个作品没有版本目标。先在订阅里新建。"
? "这个作品没有收录版本。先在订阅里新建。"
: "资源未关联本地作品。先在「身份与元数据」指定。"}
</p>
)}
@@ -143,7 +143,7 @@ export function DownloadDialog({
}}
>
<Bot />
委托 Agent
委托智能体
</Button>
<Button onClick={go} disabled={submit.isPending || !!done || !release.entity}>
<Download />
@@ -122,14 +122,14 @@ export function FeedFormDialog({
if (!parsed.success) {
const e = zodFieldErrors(parsed.error.issues);
if (e.pollIntervalSec) e.pollMin = "轮询间隔至少 1 分钟";
if (e.url) e.url = feed ? "URL 格式不对" : "填写完整的 Feed URL";
if (e.url) e.url = feed ? "URL 格式不对" : "填写完整的 RSS 地址";
setErrors(e);
return;
}
setErrors({});
const done = {
onSuccess: () => {
toast.success(feed ? "已保存" : "Feed 已创建");
toast.success(feed ? "已保存" : "RSS 已创建");
onOpenChange(false);
},
onError: (e: unknown) => toast.error(errorMessage(e)),
@@ -142,7 +142,7 @@ export function FeedFormDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>{feed ? "编辑 Feed" : "新建 Feed"}</DialogTitle>
<DialogTitle>{feed ? "编辑 RSS" : "新建 RSS"}</DialogTitle>
<DialogDescription>URL 加密保存,之后只显示脱敏地址。</DialogDescription>
</DialogHeader>
<form
@@ -208,8 +208,8 @@ export function FeedTable({
selectedKey={openId}
empty={{
icon: Rss,
title: filtered ? "没有符合筛选的 Feed" : "还没有 Feed",
description: filtered ? undefined : "新建 Feed 后按轮询间隔抓取,新资源进入识别和收录评估。",
title: filtered ? "没有符合筛选的 RSS" : "还没有 RSS",
description: filtered ? undefined : "新建 RSS 后按轮询间隔抓取,新资源进入识别和收录评估。",
}}
/>
@@ -67,7 +67,7 @@ function LocalInventory({ release, entry }: { release: WorkbenchRelease; entry:
) : (
<>
<p className="text-meta text-muted-foreground">
{slots.length ? `${slots.length} 个版本目标` : "没有版本目标"}
{slots.length ? `${slots.length} 个收录版本` : "没有收录版本"}
{entry ? (entry.subscribed ? " · 已订阅" : " · 未订阅") : ""}
</p>
{candidates.length ? (
@@ -79,7 +79,7 @@ function LocalInventory({ release, entry }: { release: WorkbenchRelease; entry:
<li key={c.slotId} className="rounded-lg border border-border/70 px-3 py-2">
<div className="flex flex-wrap items-center gap-2 text-caption">
<StatusBadge tone={d.tone}>{d.label}</StatusBadge>
<span className="text-foreground">{slot ? `目标 ${slot.name}` : "版本目标"}</span>
<span className="text-foreground">{slot ? `目标 ${slot.name}` : "收录版本"}</span>
<span className="text-muted-foreground">{relTime(c.evaluatedAt)}评估</span>
</div>
{c.reasons.length ? (
@@ -158,7 +158,8 @@ export function ReleaseDetailSheet({
})
}
>
<Bot />问 Agent
<Bot />
问智能体
</Button>
<Button size="sm" onClick={() => setDl(true)}>
<Download />
@@ -263,7 +264,7 @@ export function ReleaseDetailSheet({
</DetailSection>
{detail.data?.observations.length ? (
<DetailSection title="Feed 观察">
<DetailSection title="RSS 抓取记录">
<ul className="space-y-1 text-caption">
{detail.data.observations.map((o) => (
<li key={o.observationId} className="flex flex-wrap justify-between gap-2">
@@ -162,9 +162,9 @@ export function relTime(iso: string | null | undefined, now = Date.now()): strin
}
export const ORIGIN_LABEL: Record<WorkbenchRelease["origin"], string> = {
snapshot: "快照",
feed: "Feed",
live: "实时补查",
snapshot: "抓取",
feed: "RSS",
live: "去站点搜",
unknown: "来源未知",
};
@@ -66,7 +66,7 @@ export function SpecTags({ r }: { r: Pick<WorkbenchRelease, "parsed" | "title" |
/** 没有关联作品时的说明(明确写出,不猜) */
export function unresolvedLabel(release: Pick<WorkbenchRelease, "resolutionStatus">): string {
return release.resolutionStatus === "ambiguous"
? "识别有歧义"
? "没识别出作品"
: release.resolutionStatus === "unidentifiable"
? "无法识别"
: release.resolutionStatus === "rejected"
@@ -10,10 +10,10 @@ import { formatDateTime } from "~/lib/utils.ts";
import { relTime, SITE_HEALTH } from "./release-fields.ts";
const KIND: Record<SiteSnapshot["kind"], string> = {
periodic: "定期快照",
periodic: "定期抓取",
backfill: "缺口补漏",
live_search: "实时补查",
manual: "手动快照",
live_search: "去站点搜",
manual: "手动抓取",
};
const STATUS: Record<SiteSnapshot["status"], { label: string; tone: StatusTone }> = {
running: { label: "进行中", tone: "neutral" },
@@ -44,10 +44,10 @@ export function SnapshotPanel({ site }: { site: Site }) {
{!site.enabled ? <StatusBadge tone="warning">站点已停用</StatusBadge> : null}
{site.snapshotEnabled ? (
<StatusBadge tone={stale ? "warning" : "success"}>
{stale ? "快照过期" : "快照新鲜"} · {relTime(site.lastSnapshotAt)}
{stale ? "抓取已过期" : "已抓取"} · {relTime(site.lastSnapshotAt)}
</StatusBadge>
) : (
<StatusBadge>未开启定期快照</StatusBadge>
<StatusBadge>未开启定期抓取</StatusBadge>
)}
{site.snapshotIntervalSec ? (
<span className="text-meta text-muted-foreground">
@@ -64,14 +64,14 @@ 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)),
},
)
}
>
<Camera />
手动快照
手动抓取
</Button>
</div>
<div className="mt-3">
@@ -80,7 +80,7 @@ export function SnapshotPanel({ site }: { site: Site }) {
) : snaps.error ? (
<ErrorState plain error={snaps.error} onRetry={() => void snaps.refetch()} />
) : !snaps.data.items.length ? (
<p className="text-caption text-muted-foreground">还没有快照。</p>
<p className="text-caption text-muted-foreground">还没抓取过。</p>
) : (
<ul className="divide-y divide-border/60 text-caption">
{snaps.data.items.map((s) => (