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:
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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/,
|
||||
|
||||
@@ -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("人工接管 / 等待处理不看自动化时间", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
["订阅", "规则"],
|
||||
["下载面板", "整理记录", "文件管理", "媒体概览"],
|
||||
["设置", "审计", "运行状态"],
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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> = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="搜索关键词"
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("任务行状态", () => {
|
||||
triggeredBy: null,
|
||||
outcome,
|
||||
});
|
||||
it("引擎跑完但 Agent 会话超出预算 / 失败:显示业务结果,不显示「完成」", () => {
|
||||
it("引擎跑完但智能体会话超出预算 / 失败:显示业务结果,不显示「完成」", () => {
|
||||
expect(engineRunStatus({ status: "completed", business: agent("budget_exceeded") })).toEqual({
|
||||
label: "超出预算",
|
||||
tone: "warning",
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
Reference in new issue
Block a user