merge: copy/c1
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
commit
b7c28b5650
26 files changed
+121
-121
No files matched your search
@@ -86,7 +86,7 @@ export const BUDGET_LABEL: Record<string, string> = {
|
|||||||
const COMPACT_STATUS: Record<string, string> = {
|
const COMPACT_STATUS: Record<string, string> = {
|
||||||
completed: "已完成",
|
completed: "已完成",
|
||||||
declined: "没有压缩",
|
declined: "没有压缩",
|
||||||
aborted: "已中止",
|
aborted: "已停止",
|
||||||
failed: "失败",
|
failed: "失败",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ export function contextLabel(context: string | null): string | null {
|
|||||||
const first = context.split(" · ")[0] ?? "";
|
const first = context.split(" · ")[0] ?? "";
|
||||||
// 前缀里第一段是界面给的名称(「作品:沙丘2」);没有名称时只剩 key=ID,不上屏
|
// 前缀里第一段是界面给的名称(「作品:沙丘2」);没有名称时只剩 key=ID,不上屏
|
||||||
if (!first || first.includes("=")) return null;
|
if (!first || first.includes("=")) return null;
|
||||||
return first.replace(/^(作品|资源|下载|整理记录|站点|任务|引擎任务)[::]\s*/, "");
|
return first.replace(/^(作品|资源|下载|整理记录|站点|任务|引擎任务|后台任务)[::]\s*/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserMessage({
|
export function UserMessage({
|
||||||
@@ -289,7 +289,7 @@ export function StepSummary({
|
|||||||
const thinking = activity.some((a) => a.kind === "thinking");
|
const thinking = activity.some((a) => a.kind === "thinking");
|
||||||
const head = current
|
const head = current
|
||||||
? `${done ? `${done},` : ""}${runningText(current)}…`
|
? `${done ? `${done},` : ""}${runningText(current)}…`
|
||||||
: done || (running ? "思考中…" : thinking ? "想了一下" : "过程说明");
|
: done || (running ? "思考中…" : thinking ? "思考过程" : "过程说明");
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-w-0" data-slot="step-summary">
|
<div className="w-full min-w-0" data-slot="step-summary">
|
||||||
<button
|
<button
|
||||||
@@ -365,7 +365,7 @@ export function DeniedLines({ runId, calls }: { runId: string; calls: ToolCallVi
|
|||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSent((s) => new Set(s).add(c.capabilityId));
|
setSent((s) => new Set(s).add(c.capabilityId));
|
||||||
toast.success("已批准,智能体重试这一步");
|
toast.success("已批准,智能体会重试这一步");
|
||||||
},
|
},
|
||||||
onError: (e) => toast.error(errorMessage(e)),
|
onError: (e) => toast.error(errorMessage(e)),
|
||||||
},
|
},
|
||||||
@@ -399,7 +399,7 @@ function EndNotice({ item }: { item: Extract<StreamItem, { kind: "end" }> }) {
|
|||||||
<CircleSlash aria-hidden className="mt-0.5 size-3.5 shrink-0" />
|
<CircleSlash aria-hidden className="mt-0.5 size-3.5 shrink-0" />
|
||||||
<span className="min-w-0 break-words">
|
<span className="min-w-0 break-words">
|
||||||
{budget
|
{budget
|
||||||
? `超出预算(${BUDGET_LABEL[item.budget ?? ""] ?? "用量"}),已停止。已完成的动作不会撤销;调大预算后发消息可以继续。`
|
? `超出预算(${BUDGET_LABEL[item.budget ?? ""] ?? "用量"}),已停止。已完成的操作不会撤销,调大预算后发消息可继续`
|
||||||
: aborted
|
: aborted
|
||||||
? "已停止。"
|
? "已停止。"
|
||||||
: `运行失败${item.message ? `:${humanizeReason(item.message) ?? item.message}` : ""}`}
|
: `运行失败${item.message ? `:${humanizeReason(item.message) ?? item.message}` : ""}`}
|
||||||
@@ -523,7 +523,7 @@ export function AgentConversation({
|
|||||||
<UserMessage text={run.goal} tag={isProposeFirst(run) ? "先给方案" : undefined} />
|
<UserMessage text={run.goal} tag={isProposeFirst(run) ? "先给方案" : undefined} />
|
||||||
{blocks.map((b, bi) =>
|
{blocks.map((b, bi) =>
|
||||||
b.kind === "user" ? (
|
b.kind === "user" ? (
|
||||||
<UserMessage key={b.key} text={b.text} note={b.pending ? "已送达,等待智能体读取" : undefined} animateIn />
|
<UserMessage key={b.key} text={b.text} note={b.pending ? "已发送,等智能体读取" : undefined} animateIn />
|
||||||
) : (
|
) : (
|
||||||
<section key={b.key} aria-label="智能体回合" className="flex w-full min-w-0 flex-col items-start gap-2">
|
<section key={b.key} aria-label="智能体回合" className="flex w-full min-w-0 flex-col items-start gap-2">
|
||||||
{b.parts.map((p, pi) => {
|
{b.parts.map((p, pi) => {
|
||||||
@@ -587,7 +587,7 @@ export function AgentConversation({
|
|||||||
return (
|
return (
|
||||||
<MessageMarker key={p.key}>
|
<MessageMarker key={p.key}>
|
||||||
<Layers aria-hidden className="size-3.5 shrink-0" />
|
<Layers aria-hidden className="size-3.5 shrink-0" />
|
||||||
整理上下文 ·{" "}
|
压缩上下文 ·{" "}
|
||||||
{p.item.done ? (COMPACT_STATUS[p.item.status ?? "completed"] ?? p.item.status) : "进行中…"}
|
{p.item.done ? (COMPACT_STATUS[p.item.status ?? "completed"] ?? p.item.status) : "进行中…"}
|
||||||
</MessageMarker>
|
</MessageMarker>
|
||||||
);
|
);
|
||||||
@@ -626,7 +626,7 @@ function DraftCheckCard({ jobId, name }: { jobId: string; name: string }) {
|
|||||||
<CalendarCog aria-hidden className="size-4 shrink-0 text-muted-foreground" />
|
<CalendarCog aria-hidden className="size-4 shrink-0 text-muted-foreground" />
|
||||||
巡检草稿「{name}」<StatusBadge>未启用</StatusBadge>
|
巡检草稿「{name}」<StatusBadge>未启用</StatusBadge>
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-0.5 text-meta text-muted-foreground">到计划页检查目标、间隔和能力,确认后打开开关启用。</p>
|
<p className="mt-0.5 text-meta text-muted-foreground">在计划页检查目标、间隔和能力后启用。</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild size="sm" variant="outline">
|
<Button asChild size="sm" variant="outline">
|
||||||
<Link to={`${consolePath("schedules")}?job=${encodeURIComponent(jobId)}`}>检查并启用</Link>
|
<Link to={`${consolePath("schedules")}?job=${encodeURIComponent(jobId)}`}>检查并启用</Link>
|
||||||
@@ -672,8 +672,8 @@ export function resultFacts(report: GoalReport | null): ResultFact[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (r.imported && r.imported !== done.length) facts.push({ text: `已整理 ${r.imported} 个` });
|
if (r.imported && r.imported !== done.length) facts.push({ text: `已整理 ${r.imported} 个` });
|
||||||
if (r.inProgress) facts.push({ text: `${r.inProgress} 个还在进行` });
|
if (r.inProgress) facts.push({ text: `${r.inProgress} 个进行中` });
|
||||||
if (r.failed) facts.push({ text: `${r.failed} 个没有成功`, tone: "warning" });
|
if (r.failed) facts.push({ text: `${r.failed} 个失败`, tone: "warning" });
|
||||||
return facts.length ? facts : [{ text: "没有提交下载或整理" }];
|
return facts.length ? facts : [{ text: "没有提交下载或整理" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,7 +859,7 @@ export function RunAnnouncer({ status, finished }: { status: AgentRunStatus; fin
|
|||||||
const [msg, setMsg] = useState("");
|
const [msg, setMsg] = useState("");
|
||||||
const prev = useRef(status);
|
const prev = useRef(status);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prev.current !== status) setMsg(`运行${RUN_STATUS_LABEL[status]}`);
|
if (prev.current !== status) setMsg(`会话${RUN_STATUS_LABEL[status]}`);
|
||||||
prev.current = status;
|
prev.current = status;
|
||||||
}, [status]);
|
}, [status]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function useContextName(ctx: AgentContext | null): string | null {
|
|||||||
const task = useDownloadTask(path ? ctx?.downloadTaskId : undefined);
|
const task = useDownloadTask(path ? ctx?.downloadTaskId : undefined);
|
||||||
const record = useRecord(path ? ctx?.recordId : undefined);
|
const record = useRecord(path ? ctx?.recordId : undefined);
|
||||||
if (!ctx) return null;
|
if (!ctx) return null;
|
||||||
if (!path) return ctx.label ? ctx.label.replace(/^(作品|资源|下载|整理记录|站点|任务)[::]\s*/, "") : null;
|
if (!path) return ctx.label ? ctx.label.replace(/^(作品|资源|下载|整理记录|站点|任务|后台任务)[::]\s*/, "") : null;
|
||||||
if (ctx.entityId) return entity.data?.entity.title ?? "这部作品";
|
if (ctx.entityId) return entity.data?.entity.title ?? "这部作品";
|
||||||
if (ctx.siteId) return site.data?.name ?? ctx.siteId;
|
if (ctx.siteId) return site.data?.name ?? ctx.siteId;
|
||||||
if (ctx.downloadTaskId) return task.data?.name ?? "这个下载任务";
|
if (ctx.downloadTaskId) return task.data?.name ?? "这个下载任务";
|
||||||
@@ -162,8 +162,8 @@ export function AgentDrawer() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
aria-label="最小化(运行继续)"
|
aria-label="最小化,会话继续运行"
|
||||||
title="最小化(运行继续)"
|
title="最小化,会话继续运行"
|
||||||
onClick={minimizeAgentDrawer}
|
onClick={minimizeAgentDrawer}
|
||||||
>
|
>
|
||||||
<Minus />
|
<Minus />
|
||||||
@@ -256,8 +256,8 @@ function AgentMiniDock({ runId }: { runId: string }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
aria-label="关闭小窗(运行在后台继续)"
|
aria-label="关闭小窗,会话在后台继续"
|
||||||
title="关闭小窗(运行在后台继续)"
|
title="关闭小窗,会话在后台继续"
|
||||||
onClick={() => setAgentDrawerOpen(false)}
|
onClick={() => setAgentDrawerOpen(false)}
|
||||||
>
|
>
|
||||||
<X />
|
<X />
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export function OptionsCard({ runId, view, waiting }: { runId: string; view: Opt
|
|||||||
const risks = options.flatMap((o) => o.risks.map((r) => ({ key: `${o.id}:${r}`, option: o.title, text: r })));
|
const risks = options.flatMap((o) => o.risks.map((r) => ({ key: `${o.id}:${r}`, option: o.title, text: r })));
|
||||||
const submit = (optionId: string | null) => {
|
const submit = (optionId: string | null) => {
|
||||||
if (optionId === null && !reason.trim()) {
|
if (optionId === null && !reason.trim()) {
|
||||||
toast.error("写一句不要的原因,智能体按它调整");
|
toast.error("写一下为什么都不要");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPicking(optionId ?? "__none__");
|
setPicking(optionId ?? "__none__");
|
||||||
@@ -153,7 +153,7 @@ export function OptionsCard({ runId, view, waiting }: { runId: string; view: Opt
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSent({ optionId, reason: reason.trim() });
|
setSent({ optionId, reason: reason.trim() });
|
||||||
const o = view.options.find((x) => x.id === optionId);
|
const o = view.options.find((x) => x.id === optionId);
|
||||||
toast.success(optionId ? (o?.action?.approves ? "已选定并批准" : "已选定") : "已退回,智能体调整方案");
|
toast.success(optionId ? (o?.action?.approves ? "已选定并批准" : "已选定") : "已发送,智能体会重新给方案");
|
||||||
},
|
},
|
||||||
onError: (e) => toast.error(errorMessage(e)),
|
onError: (e) => toast.error(errorMessage(e)),
|
||||||
onSettled: () => setPicking(null),
|
onSettled: () => setPicking(null),
|
||||||
@@ -286,7 +286,7 @@ export function OptionsCard({ runId, view, waiting }: { runId: string; view: Opt
|
|||||||
disabled={!reason.trim() || choose.isPending}
|
disabled={!reason.trim() || choose.isPending}
|
||||||
onClick={() => submit(null)}
|
onClick={() => submit(null)}
|
||||||
>
|
>
|
||||||
退回
|
发送
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -336,7 +336,7 @@ export function InlineApproval({ view, stepTitle }: { view: ApprovalView; stepTi
|
|||||||
...(s === "rejected" && reason.trim() ? { reason: reason.trim() } : {}),
|
...(s === "rejected" && reason.trim() ? { reason: reason.trim() } : {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => toast.success(s === "approved" ? "已批准,智能体继续执行" : "已拒绝,智能体按理由调整"),
|
onSuccess: () => toast.success(s === "approved" ? "已批准,智能体继续执行" : "已拒绝,智能体会调整做法"),
|
||||||
onError: (e) => toast.error(errorMessage(e)),
|
onError: (e) => toast.error(errorMessage(e)),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -364,7 +364,7 @@ export function InlineApproval({ view, stepTitle }: { view: ApprovalView; stepTi
|
|||||||
value={reason}
|
value={reason}
|
||||||
maxLength={2000}
|
maxLength={2000}
|
||||||
aria-label="拒绝理由"
|
aria-label="拒绝理由"
|
||||||
placeholder="理由或建议(可选),智能体按它调整"
|
placeholder="理由或建议,可不填"
|
||||||
onChange={(e) => setReason(e.target.value)}
|
onChange={(e) => setReason(e.target.value)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function GoalComposer({
|
|||||||
maxRows={10}
|
maxRows={10}
|
||||||
maxLength={4000}
|
maxLength={4000}
|
||||||
aria-label="目标"
|
aria-label="目标"
|
||||||
placeholder="例如:把沙丘:第二部的 4K 收进来"
|
placeholder="例如:下载沙丘 2 的 4K 版本"
|
||||||
submitLabel={pending ? "提交中" : "开始"}
|
submitLabel={pending ? "提交中" : "开始"}
|
||||||
commands={NEW_RUN_COMMANDS}
|
commands={NEW_RUN_COMMANDS}
|
||||||
context={context}
|
context={context}
|
||||||
@@ -491,7 +491,7 @@ export function AgentRunPanel({
|
|||||||
maxRows={6}
|
maxRows={6}
|
||||||
maxLength={8000}
|
maxLength={8000}
|
||||||
aria-label="发消息"
|
aria-label="发消息"
|
||||||
placeholder={ended ? "接着说,输入 / 看指令" : "补充或纠正"}
|
placeholder={ended ? "继续对话,输入 / 查看指令" : "补充或纠正"}
|
||||||
commands={RUN_COMMANDS}
|
commands={RUN_COMMANDS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -499,13 +499,13 @@ export function AgentRunPanel({
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={confirmStop}
|
open={confirmStop}
|
||||||
onOpenChange={setConfirmStop}
|
onOpenChange={setConfirmStop}
|
||||||
title="停止这次运行?"
|
title="停止这个会话?"
|
||||||
description="后续步骤不再执行,已完成的动作(如已提交的下载)不会撤销。"
|
description="后续步骤不再执行。已完成的操作不会撤销,已提交的下载会继续。"
|
||||||
confirmLabel="停止运行"
|
confirmLabel="停止会话"
|
||||||
destructive
|
destructive
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
abort.mutateAsync(r.id).then(
|
abort.mutateAsync(r.id).then(
|
||||||
() => toast.success("已请求停止"),
|
() => toast.success("正在停止"),
|
||||||
(e) => {
|
(e) => {
|
||||||
toast.error(errorMessage(e));
|
toast.error(errorMessage(e));
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ export function AgentSessionList({
|
|||||||
? "没有等你处理的会话"
|
? "没有等你处理的会话"
|
||||||
: "没有符合条件的会话"
|
: "没有符合条件的会话"
|
||||||
: f.group === "auto"
|
: f.group === "auto"
|
||||||
? "还没有巡检会话,巡检到点后会出现在这里"
|
? "还没有巡检会话"
|
||||||
: "还没有会话,写一个目标开始"
|
: "还没有会话,写一个目标开始"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ describe("拒绝:缩成一行", () => {
|
|||||||
it("授权拒绝 → 需要你批准 + [批准并重试];白名单拦截没有按钮", () => {
|
it("授权拒绝 → 需要你批准 + [批准并重试];白名单拦截没有按钮", () => {
|
||||||
expect(deniedReason(denied)).toEqual({ text: "需要你批准", approvable: true });
|
expect(deniedReason(denied)).toEqual({ text: "需要你批准", approvable: true });
|
||||||
expect(deniedReason(call({ status: "denied", reason: "不在白名单" }))).toEqual({
|
expect(deniedReason(call({ status: "denied", reason: "不在白名单" }))).toEqual({
|
||||||
text: "本次没有开放",
|
text: "这次会话没开放这个能力",
|
||||||
approvable: false,
|
approvable: false,
|
||||||
});
|
});
|
||||||
const t = text(
|
const t = text(
|
||||||
@@ -529,7 +529,7 @@ describe("实际结果行(FIX-9):只列系统核实的事实", () => {
|
|||||||
const html = render(
|
const html = render(
|
||||||
createElement(ResultLine, { run: RUN, report: done, live: withText("提交了 2 个下载,整理了 1 个。") }),
|
createElement(ResultLine, { run: RUN, report: done, live: withText("提交了 2 个下载,整理了 1 个。") }),
|
||||||
);
|
);
|
||||||
expect(text(html).replace(/\s*、\s*/g, "、")).toContain("提交了 2 个下载、完成 1 次整理、1 个还在进行 · 41 秒");
|
expect(text(html).replace(/\s*、\s*/g, "、")).toContain("提交了 2 个下载、完成 1 次整理、1 个进行中 · 41 秒");
|
||||||
expect(html).toContain(`href="/console/organize/${ID(3)}"`);
|
expect(html).toContain(`href="/console/organize/${ID(3)}"`);
|
||||||
expect(html).toContain('href="/console/downloads"');
|
expect(html).toContain('href="/console/downloads"');
|
||||||
expect(html).not.toContain("与汇报不一致");
|
expect(html).not.toContain("与汇报不一致");
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export function contextFromPath(pathname: string): AgentContext | null {
|
|||||||
case "organize":
|
case "organize":
|
||||||
return { label: `整理记录 ${short}`, recordId: id, fromPath: true };
|
return { label: `整理记录 ${short}`, recordId: id, fromPath: true };
|
||||||
case "engine":
|
case "engine":
|
||||||
return { label: `引擎任务 ${short}`, taskId: id, fromPath: true };
|
return { label: `后台任务 ${short}`, taskId: id, fromPath: true };
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ function budgetText(b: Record<string, number | undefined>) {
|
|||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (b.maxToolCalls !== undefined) parts.push(`${b.maxToolCalls} 次调用`);
|
if (b.maxToolCalls !== undefined) parts.push(`${b.maxToolCalls} 次调用`);
|
||||||
if (b.maxTokens !== undefined) parts.push(`${tokens(b.maxTokens)} token`);
|
if (b.maxTokens !== undefined) parts.push(`${tokens(b.maxTokens)} token`);
|
||||||
if (b.maxDurationSec !== undefined) parts.push(`${b.maxDurationSec}s`);
|
if (b.maxDurationSec !== undefined) parts.push(`${b.maxDurationSec} 秒`);
|
||||||
if (b.maxCostUsd !== undefined) parts.push(`$${b.maxCostUsd}`);
|
if (b.maxCostUsd !== undefined) parts.push(`$${b.maxCostUsd}`);
|
||||||
return parts.length ? parts.join(" · ") : "沿用设置";
|
return parts.length ? parts.join(" · ") : "沿用默认";
|
||||||
}
|
}
|
||||||
|
|
||||||
function Node({ node, byParent, depth }: { node: AgentRunNode; byParent: Map<string, AgentRunNode[]>; depth: number }) {
|
function Node({ node, byParent, depth }: { node: AgentRunNode; byParent: Map<string, AgentRunNode[]>; depth: number }) {
|
||||||
@@ -119,7 +119,7 @@ export function SubagentTree({ snapshot, className }: { snapshot: RunSnapshot; c
|
|||||||
</p>
|
</p>
|
||||||
<RunLink id={parent.id}>{shortGoalTitle(parent.goal)}</RunLink>
|
<RunLink id={parent.id}>{shortGoalTitle(parent.goal)}</RunLink>
|
||||||
<p className="text-meta text-muted-foreground">
|
<p className="text-meta text-muted-foreground">
|
||||||
第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "}
|
第 {run.depth} 层子会话 · 权限不超过父会话 · 可用能力 {caps} 项 · 预算{" "}
|
||||||
{budgetText(run.budget as Record<string, number | undefined>)}
|
{budgetText(run.budget as Record<string, number | undefined>)}
|
||||||
{run.spawnReason ? ` · 理由:${run.spawnReason}` : ""}
|
{run.spawnReason ? ` · 理由:${run.spawnReason}` : ""}
|
||||||
</p>
|
</p>
|
||||||
@@ -129,7 +129,7 @@ export function SubagentTree({ snapshot, className }: { snapshot: RunSnapshot; c
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="flex flex-wrap items-center gap-1.5 text-meta text-muted-foreground">
|
<p className="flex flex-wrap items-center gap-1.5 text-meta text-muted-foreground">
|
||||||
<GitBranch aria-hidden className="size-3.5" />
|
<GitBranch aria-hidden className="size-3.5" />
|
||||||
子会话 {children.length} 个{familyUsage ? <FamilyUsage usage={familyUsage} /> : null}
|
{children.length} 个子会话{familyUsage ? <FamilyUsage usage={familyUsage} /> : null}
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-0.5">
|
<ul className="space-y-0.5">
|
||||||
{direct.map((c) => (
|
{direct.map((c) => (
|
||||||
@@ -143,5 +143,5 @@ export function SubagentTree({ snapshot, className }: { snapshot: RunSnapshot; c
|
|||||||
}
|
}
|
||||||
|
|
||||||
function FamilyUsage({ usage }: { usage: AgentFamilyUsage }) {
|
function FamilyUsage({ usage }: { usage: AgentFamilyUsage }) {
|
||||||
return <span className="ml-auto tabular-nums">会话树合计 {usageLine(usage)}</span>;
|
return <span className="ml-auto tabular-nums">合计 {usageLine(usage)}</span>;
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ export function authDenial(call: Pick<ToolCallView, "status" | "outputSummary">)
|
|||||||
/** 被服务端拒绝(白名单拦截或授权校验失败) */
|
/** 被服务端拒绝(白名单拦截或授权校验失败) */
|
||||||
export const isDenied = (call: ToolCallView) => call.status === "denied" || authDenial(call) !== null;
|
export const isDenied = (call: ToolCallView) => call.status === "denied" || authDenial(call) !== null;
|
||||||
|
|
||||||
/** 拒绝的一行说明:需要批准 / 本次没有开放 / 次数用完 */
|
/** 拒绝的一行说明:需要批准 / 这次会话没开放 / 次数用完 */
|
||||||
export function deniedReason(call: ToolCallView): { text: string; approvable: boolean } {
|
export function deniedReason(call: ToolCallView): { text: string; approvable: boolean } {
|
||||||
const auth = authDenial(call);
|
const auth = authDenial(call);
|
||||||
if (auth) {
|
if (auth) {
|
||||||
@@ -37,8 +37,8 @@ export function deniedReason(call: ToolCallView): { text: string; approvable: bo
|
|||||||
if (/自动化总开关|只读对账/.test(auth)) return { text: "自动执行已关闭", approvable: false };
|
if (/自动化总开关|只读对账/.test(auth)) return { text: "自动执行已关闭", approvable: false };
|
||||||
return { text: "需要你批准", approvable: true };
|
return { text: "需要你批准", approvable: true };
|
||||||
}
|
}
|
||||||
if (call.reason && /预算|次数/.test(call.reason)) return { text: "调用次数用完了", approvable: false };
|
if (call.reason && /预算|次数/.test(call.reason)) return { text: "工具调用次数已用完", approvable: false };
|
||||||
return { text: "本次没有开放", approvable: false };
|
return { text: "这次会话没开放这个能力", approvable: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 进度行的人话计数 ──
|
// ── 进度行的人话计数 ──
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const RULES: Rule[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /^\/console\/organize/,
|
match: /^\/console\/organize/,
|
||||||
goals: ["识别需要处理的整理记录里的作品", "这次整理为什么有文件失败", "把失败的文件重新整理一遍"],
|
goals: ["识别整理记录里没识别出的作品", "这次整理为什么有文件失败", "把失败的文件重新整理一遍"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /^\/console\/files/,
|
match: /^\/console\/files/,
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function ToolCallIoSheet({ call, onClose }: { call: ToolCallView; onClose: () =>
|
|||||||
<JsonView value={output.value ?? call.reason} maxHeight="18rem" tone={failed ? "destructive" : "neutral"} />
|
<JsonView value={output.value ?? call.reason} maxHeight="18rem" tone={failed ? "destructive" : "neutral"} />
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
{input.truncated || output.truncated ? (
|
{input.truncated || output.truncated ? (
|
||||||
<p className="text-caption text-muted-foreground">事件流只保存 500 字符以内的摘要,超出部分已截断。</p>
|
<p className="text-caption text-muted-foreground">只保存前 500 个字符,其余已截断。</p>
|
||||||
) : null}
|
) : null}
|
||||||
<DetailSection title="输入 schema">
|
<DetailSection title="输入 schema">
|
||||||
{tools.isPending ? (
|
{tools.isPending ? (
|
||||||
@@ -76,7 +76,7 @@ function ToolCallIoSheet({ call, onClose }: { call: ToolCallView; onClose: () =>
|
|||||||
) : tool ? (
|
) : tool ? (
|
||||||
<JsonView value={tool.inputJsonSchema} maxHeight="20rem" />
|
<JsonView value={tool.inputJsonSchema} maxHeight="20rem" />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-caption text-muted-foreground">这个能力没有注册,没有 schema。</p>
|
<p className="text-caption text-muted-foreground">这个能力未注册,没有 schema。</p>
|
||||||
)}
|
)}
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
{tool ? (
|
{tool ? (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export type ApprovalCardStatus =
|
|||||||
| "consumed";
|
| "consumed";
|
||||||
|
|
||||||
const LABEL: Record<ApprovalCardStatus, string> = {
|
const LABEL: Record<ApprovalCardStatus, string> = {
|
||||||
pending: "待批准",
|
pending: "等你批准",
|
||||||
submitting: "提交中",
|
submitting: "提交中",
|
||||||
approved: "已批准",
|
approved: "已批准",
|
||||||
rejected: "已拒绝",
|
rejected: "已拒绝",
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export function CodeBlock({
|
|||||||
) : (
|
) : (
|
||||||
<Check aria-hidden className="size-3" />
|
<Check aria-hidden className="size-3" />
|
||||||
)}
|
)}
|
||||||
{streaming ? "输出中" : "完整"}
|
{streaming ? "输出中" : "已完成"}
|
||||||
</span>
|
</span>
|
||||||
{copyable ? (
|
{copyable ? (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ export interface TodoItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LABEL: Record<TodoItemStatus, string> = {
|
const LABEL: Record<TodoItemStatus, string> = {
|
||||||
pending: "待办",
|
pending: "未开始",
|
||||||
"in-progress": "进行中",
|
"in-progress": "进行中",
|
||||||
completed: "已完成",
|
completed: "已完成",
|
||||||
cancelled: "被拒绝",
|
cancelled: "已取消",
|
||||||
error: "失败",
|
error: "失败",
|
||||||
skipped: "已跳过",
|
skipped: "已跳过",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface ToolApprovalParameter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COPY: Record<ToolApprovalStatus, string> = {
|
const COPY: Record<ToolApprovalStatus, string> = {
|
||||||
pending: "待授权",
|
pending: "等你批准",
|
||||||
approving: "提交中",
|
approving: "提交中",
|
||||||
approved: "已批准",
|
approved: "已批准",
|
||||||
denied: "已拒绝",
|
denied: "已拒绝",
|
||||||
@@ -68,7 +68,7 @@ export interface ToolApprovalProps {
|
|||||||
|
|
||||||
export function ToolApproval({
|
export function ToolApproval({
|
||||||
tool,
|
tool,
|
||||||
title = "允许执行这个工具?",
|
title = "允许调用这个工具?",
|
||||||
description,
|
description,
|
||||||
parameters = [],
|
parameters = [],
|
||||||
status = "pending",
|
status = "pending",
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ function RuntimeOverview({ downloaders }: { downloaders?: Dashboard["sources"]["
|
|||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState plain icon={Gauge} title="暂无运行状态" className="py-6" />
|
<EmptyState plain icon={Gauge} title="没有运行状态数据" className="py-6" />
|
||||||
)}
|
)}
|
||||||
</OverviewCard>
|
</OverviewCard>
|
||||||
);
|
);
|
||||||
@@ -192,18 +192,18 @@ function DownloaderOverview({ downloaders }: { downloaders?: Dashboard["sources"
|
|||||||
downloaders ??
|
downloaders ??
|
||||||
instances.map((d) => ({ id: d.instanceId, name: d.name, enabled: true, health: "unknown", lastSeenAt: null }));
|
instances.map((d) => ({ id: d.instanceId, name: d.name, enabled: true, health: "unknown", lastSeenAt: null }));
|
||||||
return (
|
return (
|
||||||
<OverviewCard title="下载器情况" icon={Download} to={consolePath("downloads")}>
|
<OverviewCard title="下载器" icon={Download} to={consolePath("downloads")}>
|
||||||
{q.isPending ? (
|
{q.isPending ? (
|
||||||
<LoadingState rows={2} />
|
<LoadingState rows={2} />
|
||||||
) : q.isError ? (
|
) : q.isError ? (
|
||||||
<ErrorState plain title="下载器状态加载失败" onRetry={() => q.refetch()} className="py-6" />
|
<ErrorState plain title="下载器状态加载失败" onRetry={() => q.refetch()} className="py-6" />
|
||||||
) : !sources.length ? (
|
) : !sources.length ? (
|
||||||
<EmptyState plain icon={Download} title={downloaders ? "尚未配置下载器" : "暂无启用下载器"} className="py-6" />
|
<EmptyState plain icon={Download} title={downloaders ? "还没有下载器" : "没有启用的下载器"} className="py-6" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="mb-3 text-title-sm font-semibold tabular-nums">
|
<p className="mb-3 text-title-sm font-semibold tabular-nums">
|
||||||
{board?.counts.active}
|
{board?.counts.active}
|
||||||
<span className="ml-1.5 text-caption font-normal text-muted-foreground">项进行中</span>
|
<span className="ml-1.5 text-caption font-normal text-muted-foreground">个下载进行中</span>
|
||||||
</p>
|
</p>
|
||||||
<ul className="divide-y divide-border/60">
|
<ul className="divide-y divide-border/60">
|
||||||
{sources.map((d) => {
|
{sources.map((d) => {
|
||||||
@@ -245,10 +245,10 @@ function DownloaderOverview({ downloaders }: { downloaders?: Dashboard["sources"
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mt-3 text-meta text-muted-foreground">
|
<p className="mt-3 text-meta text-muted-foreground">
|
||||||
进行中含等待计划、暂停和异常任务 · 速度含下载器全部任务
|
进行中包括还没提交、已暂停和出错的下载 · 速度是下载器里全部任务的合计
|
||||||
</p>
|
</p>
|
||||||
{board?.liveAt ? (
|
{board?.liveAt ? (
|
||||||
<p className="mt-1 text-meta text-muted-foreground">速度采样于 {formatRelative(board.liveAt)}</p>
|
<p className="mt-1 text-meta text-muted-foreground">速度更新于 {formatRelative(board.liveAt)}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const BUSINESS_KIND_LABEL: Record<EngineBusinessKind, string> = {
|
|||||||
schedule: "定时任务",
|
schedule: "定时任务",
|
||||||
feed: "RSS",
|
feed: "RSS",
|
||||||
site: "站点",
|
site: "站点",
|
||||||
decision: "待处理事项",
|
decision: "需要处理的事项",
|
||||||
media: "作品",
|
media: "作品",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ export function sourceOf(t: EngineTriggeredBy | null | undefined): TaskSource |
|
|||||||
|
|
||||||
/** 引擎工作流名 → 做什么 */
|
/** 引擎工作流名 → 做什么 */
|
||||||
export const WORKFLOW_LABEL: Record<string, string> = {
|
export const WORKFLOW_LABEL: Record<string, string> = {
|
||||||
ingestFeed: "抓取 Feed",
|
ingestFeed: "抓取 RSS",
|
||||||
resolveObservation: "识别资源",
|
resolveObservation: "识别资源",
|
||||||
evaluateAcquisition: "评估候选",
|
evaluateAcquisition: "评估资源",
|
||||||
submitDownload: "提交下载",
|
submitDownload: "提交下载",
|
||||||
verifyAndImport: "核验整理",
|
verifyAndImport: "核验整理",
|
||||||
scrapeMedia: "刮削",
|
scrapeMedia: "刮削",
|
||||||
@@ -42,7 +42,7 @@ export const WORKFLOW_LABEL: Record<string, string> = {
|
|||||||
prewarmMetadata: "预热元数据缓存",
|
prewarmMetadata: "预热元数据缓存",
|
||||||
reconcileLibrary: "文件核对",
|
reconcileLibrary: "文件核对",
|
||||||
cleanupDownloadCache: "清理缓存",
|
cleanupDownloadCache: "清理缓存",
|
||||||
resolveDecision: "处理待处理事项",
|
resolveDecision: "处理事项",
|
||||||
runAgent: "智能体会话",
|
runAgent: "智能体会话",
|
||||||
snapshotSite: "站点抓取",
|
snapshotSite: "站点抓取",
|
||||||
refreshSiteAccount: "刷新站点账号",
|
refreshSiteAccount: "刷新站点账号",
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export function RunActions({
|
|||||||
try {
|
try {
|
||||||
const res = await cancel.mutateAsync({ id: r.id, reason: "控制台手动取消" });
|
const res = await cancel.mutateAsync({ id: r.id, reason: "控制台手动取消" });
|
||||||
if (res.canceled) toast.success("已取消");
|
if (res.canceled) toast.success("已取消");
|
||||||
else toast.warning("任务已结束");
|
else toast.warning("任务已结束,不用取消");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(errorMessage(err, "取消失败"));
|
toast.error(errorMessage(err, "取消失败"));
|
||||||
throw err;
|
throw err;
|
||||||
@@ -243,7 +243,7 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun
|
|||||||
const hasError = r.error !== null && r.error !== undefined;
|
const hasError = r.error !== null && r.error !== undefined;
|
||||||
// 相关日志(日志页按对象筛选):本任务的操作留痕;关联对象的全部日志(任务、定时任务运行、审计)
|
// 相关日志(日志页按对象筛选):本任务的操作留痕;关联对象的全部日志(任务、定时任务运行、审计)
|
||||||
const audits = [
|
const audits = [
|
||||||
{ to: objectLogsPath("engine_run", r.id), label: "本任务的操作记录" },
|
{ to: objectLogsPath("engine_run", r.id), label: "这个任务的操作记录" },
|
||||||
...(b && BUSINESS_OBJECT_TYPE[b.kind]
|
...(b && BUSINESS_OBJECT_TYPE[b.kind]
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -275,11 +275,11 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun
|
|||||||
),
|
),
|
||||||
wide: true,
|
wide: true,
|
||||||
},
|
},
|
||||||
{ label: "做什么", value: workflowLabel(r.workflowName) },
|
{ label: "类型", value: workflowLabel(r.workflowName) },
|
||||||
{ label: "来源", value: src ? SOURCE_LABEL[src] : "未知" },
|
{ label: "来源", value: src ? SOURCE_LABEL[src] : "未知" },
|
||||||
{
|
{
|
||||||
label: "开始",
|
label: "开始",
|
||||||
value: r.startedAt ? `${formatDateTime(r.startedAt)}(${formatRelative(r.startedAt)})` : "未开始",
|
value: r.startedAt ? `${formatDateTime(r.startedAt)}(${formatRelative(r.startedAt)})` : "未开始",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ export function FeedFormDialog({
|
|||||||
const parsed = feed ? FeedSourceUpdate.safeParse(raw) : FeedSourceCreate.safeParse(raw);
|
const parsed = feed ? FeedSourceUpdate.safeParse(raw) : FeedSourceCreate.safeParse(raw);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const e = zodFieldErrors(parsed.error.issues);
|
const e = zodFieldErrors(parsed.error.issues);
|
||||||
if (e.pollIntervalSec) e.pollMin = "轮询间隔至少 1 分钟";
|
if (e.pollIntervalSec) e.pollMin = "抓取间隔至少 1 分钟";
|
||||||
if (e.url) e.url = feed ? "URL 格式不对" : "填写完整的 RSS 地址";
|
if (e.url) e.url = feed ? "RSS 地址格式不对" : "填写完整的 RSS 地址";
|
||||||
setErrors(e);
|
setErrors(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ export function FeedFormDialog({
|
|||||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{feed ? "编辑 RSS" : "新建 RSS"}</DialogTitle>
|
<DialogTitle>{feed ? "编辑 RSS" : "新建 RSS"}</DialogTitle>
|
||||||
<DialogDescription>URL 加密保存,之后只显示脱敏地址。</DialogDescription>
|
<DialogDescription>地址加密保存,之后只显示脱敏后的地址。</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form
|
<form
|
||||||
className="grid gap-1"
|
className="grid gap-1"
|
||||||
@@ -157,7 +157,7 @@ export function FeedFormDialog({
|
|||||||
label="名称"
|
label="名称"
|
||||||
htmlFor="feed-name"
|
htmlFor="feed-name"
|
||||||
error={errors.name}
|
error={errors.name}
|
||||||
hint={feed ? undefined : "可选;留空时用「站点名 RSS」或地址主机名"}
|
hint={feed ? undefined : "留空时用「站点名 RSS」或地址的主机名"}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
id="feed-name"
|
id="feed-name"
|
||||||
@@ -167,11 +167,11 @@ export function FeedFormDialog({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField
|
<FormField
|
||||||
label="URL"
|
label="RSS 地址"
|
||||||
htmlFor="feed-url"
|
htmlFor="feed-url"
|
||||||
required={!feed}
|
required={!feed}
|
||||||
error={errors.url}
|
error={errors.url}
|
||||||
hint={feed ? `当前:${feed.urlDisplay};留空表示不变` : "RSS / Atom 地址"}
|
hint={feed ? `当前:${feed.urlDisplay}。留空不修改` : "支持 RSS 和 Atom"}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
id="feed-url"
|
id="feed-url"
|
||||||
@@ -198,7 +198,7 @@ export function FeedFormDialog({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="轮询间隔(分钟)" htmlFor="feed-poll" error={errors.pollMin}>
|
<FormField label="抓取间隔(分钟)" htmlFor="feed-poll" error={errors.pollMin}>
|
||||||
<Input
|
<Input
|
||||||
id="feed-poll"
|
id="feed-poll"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -257,7 +257,7 @@ export function FeedFormDialog({
|
|||||||
<FormField
|
<FormField
|
||||||
label="站点账号"
|
label="站点账号"
|
||||||
htmlFor="feed-account"
|
htmlFor="feed-account"
|
||||||
hint={d.siteId && !siteAccounts.length ? "该站点没有账号" : undefined}
|
hint={d.siteId && !siteAccounts.length ? "这个站点没有账号" : undefined}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={d.siteAccountId || NONE}
|
value={d.siteAccountId || NONE}
|
||||||
@@ -295,7 +295,7 @@ export function FeedFormDialog({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="flex items-center justify-between gap-3 py-1 text-caption text-foreground">
|
<div className="flex items-center justify-between gap-3 py-1 text-caption text-foreground">
|
||||||
<label htmlFor="feed-enabled">启用(关闭后停止轮询)</label>
|
<label htmlFor="feed-enabled">启用,关闭后停止抓取</label>
|
||||||
<Switch id="feed-enabled" checked={d.enabled} onCheckedChange={(v) => set("enabled", v)} />
|
<Switch id="feed-enabled" checked={d.enabled} onCheckedChange={(v) => set("enabled", v)} />
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="mt-2 gap-2">
|
<DialogFooter className="mt-2 gap-2">
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const lastErrorText = (f: FeedSource) =>
|
|||||||
/** 游标窗口摘要:服务端 cursor.window 的条目数与上次抓取时间 */
|
/** 游标窗口摘要:服务端 cursor.window 的条目数与上次抓取时间 */
|
||||||
function cursorSummary(f: FeedSource): string {
|
function cursorSummary(f: FeedSource): string {
|
||||||
const w = f.cursor.window as { lastFetchedAt?: string | null; lastGuidSet?: unknown[] } | undefined;
|
const w = f.cursor.window as { lastFetchedAt?: string | null; lastGuidSet?: unknown[] } | undefined;
|
||||||
if (!w) return "尚无游标";
|
if (!w) return "还没有游标";
|
||||||
const n = Array.isArray(w.lastGuidSet) ? w.lastGuidSet.length : null;
|
const n = Array.isArray(w.lastGuidSet) ? w.lastGuidSet.length : null;
|
||||||
return `${n !== null ? `窗口 ${n} 条` : "窗口"}${w.lastFetchedAt ? ` · ${relTime(w.lastFetchedAt)}` : ""}`;
|
return `${n !== null ? `窗口 ${n} 条` : "窗口"}${w.lastFetchedAt ? ` · ${relTime(w.lastFetchedAt)}` : ""}`;
|
||||||
}
|
}
|
||||||
@@ -62,14 +62,14 @@ function FeedActions({ f }: { f: FeedSource }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
title="立即抓取(受站点限流)"
|
title="立即抓取,按站点限流排队"
|
||||||
aria-label="立即抓取"
|
aria-label="立即抓取"
|
||||||
disabled={!f.enabled || fetchNow.isPending}
|
disabled={!f.enabled || fetchNow.isPending}
|
||||||
onKeyDown={(e) => e.stopPropagation()}
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
fetchNow.mutate(f.id, {
|
fetchNow.mutate(f.id, {
|
||||||
onSuccess: () => toast.success("抓取已排队"),
|
onSuccess: () => toast.success("已加入抓取队列"),
|
||||||
onError: (err) => toast.error(errorMessage(err)),
|
onError: (err) => toast.error(errorMessage(err)),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -81,7 +81,7 @@ function FeedActions({ f }: { f: FeedSource }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OBS_STATUS: Record<Observation["status"], { label: string; tone: StatusTone } | null> = {
|
const OBS_STATUS: Record<Observation["status"], { label: string; tone: StatusTone } | null> = {
|
||||||
new: { label: "待识别", tone: "neutral" },
|
new: { label: "未处理", tone: "neutral" },
|
||||||
linked: null,
|
linked: null,
|
||||||
ignored: { label: "已忽略", tone: "neutral" },
|
ignored: { label: "已忽略", tone: "neutral" },
|
||||||
error: { label: "出错", tone: "warning" },
|
error: { label: "出错", tone: "warning" },
|
||||||
@@ -188,7 +188,7 @@ export function FeedTable({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "health",
|
key: "health",
|
||||||
header: "健康",
|
header: "状态",
|
||||||
cell: (f) => {
|
cell: (f) => {
|
||||||
const gap = feedGapHint(f);
|
const gap = feedGapHint(f);
|
||||||
return (
|
return (
|
||||||
@@ -230,19 +230,19 @@ export function FeedTable({
|
|||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
key: "poll",
|
key: "poll",
|
||||||
header: "轮询",
|
header: "抓取间隔",
|
||||||
hideBelow: "lg",
|
hideBelow: "lg",
|
||||||
cell: (f) => <span className="whitespace-nowrap">{Math.round(f.pollIntervalSec / 60)} 分钟</span>,
|
cell: (f) => <span className="whitespace-nowrap">{Math.round(f.pollIntervalSec / 60)} 分钟</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "success",
|
key: "success",
|
||||||
header: "最后成功",
|
header: "上次成功",
|
||||||
hideBelow: "sm",
|
hideBelow: "sm",
|
||||||
cell: (f) => (
|
cell: (f) => (
|
||||||
<div className="whitespace-nowrap">
|
<div className="whitespace-nowrap">
|
||||||
<p className="text-foreground">{relTime(f.lastSuccessAt)}</p>
|
<p className="text-foreground">{relTime(f.lastSuccessAt)}</p>
|
||||||
{f.consecutiveFailures ? (
|
{f.consecutiveFailures ? (
|
||||||
<p className="text-meta text-destructive">连续失败 {f.consecutiveFailures}</p>
|
<p className="text-meta text-destructive">连续失败 {f.consecutiveFailures} 次</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -264,7 +264,7 @@ export function FeedTable({
|
|||||||
empty={{
|
empty={{
|
||||||
icon: Rss,
|
icon: Rss,
|
||||||
title: filtered ? "没有符合筛选的 RSS" : "还没有 RSS",
|
title: filtered ? "没有符合筛选的 RSS" : "还没有 RSS",
|
||||||
description: filtered ? undefined : "新建 RSS 后按轮询间隔抓取,新资源进入识别和收录评估。",
|
description: filtered ? undefined : "新建 RSS 后按间隔抓取,抓到的新资源会自动识别,并按收录版本评估。",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -324,12 +324,12 @@ export function FeedTable({
|
|||||||
<DetailSection title="抓取状态">
|
<DetailSection title="抓取状态">
|
||||||
<KvList
|
<KvList
|
||||||
items={[
|
items={[
|
||||||
{ label: "最后成功", value: formatDateTime(open.lastSuccessAt) },
|
{ label: "上次成功", value: formatDateTime(open.lastSuccessAt) },
|
||||||
{ label: "最后抓取", value: formatDateTime(open.lastFetchAt) },
|
{ label: "上次抓取", value: formatDateTime(open.lastFetchAt) },
|
||||||
{ label: "下次抓取", value: formatDateTime(open.nextFetchAt) },
|
{ label: "下次抓取", value: formatDateTime(open.nextFetchAt) },
|
||||||
{ label: "退避至", value: formatDateTime(open.backoffUntil) },
|
{ label: "退避至", value: formatDateTime(open.backoffUntil) },
|
||||||
{ label: "连续失败", value: open.consecutiveFailures },
|
{ label: "连续失败", value: open.consecutiveFailures },
|
||||||
{ label: "轮询间隔", value: `${Math.round(open.pollIntervalSec / 60)} 分钟` },
|
{ label: "抓取间隔", value: `${Math.round(open.pollIntervalSec / 60)} 分钟` },
|
||||||
{ label: "游标", value: cursorSummary(open) },
|
{ label: "游标", value: cursorSummary(open) },
|
||||||
{ label: "解析器", value: open.parserVersion, mono: true },
|
{ label: "解析器", value: open.parserVersion, mono: true },
|
||||||
{ label: "ETag", value: open.etag, mono: true },
|
{ label: "ETag", value: open.etag, mono: true },
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ import { InventoryOverlay, ReleaseBadges } from "./release-list.tsx";
|
|||||||
import { ExternalIdTags, externalLinks, ReleasePageSections } from "./release-page-section.tsx";
|
import { ExternalIdTags, externalLinks, ReleasePageSections } from "./release-page-section.tsx";
|
||||||
|
|
||||||
const DECISION_LABEL = {
|
const DECISION_LABEL = {
|
||||||
accept: { label: "符合,可收录", tone: "success" },
|
accept: { label: "可下载", tone: "success" },
|
||||||
upgrade: { label: "可升级", tone: "success" },
|
upgrade: { label: "可升级", tone: "success" },
|
||||||
reject: { label: "不收录", tone: "destructive" },
|
reject: { label: "不收录", tone: "destructive" },
|
||||||
unknown_field: { label: "缺字段,待复核", tone: "warning" },
|
unknown_field: { label: "信息不全", tone: "warning" },
|
||||||
skipped: { label: "被跳过", tone: "neutral" },
|
skipped: { label: "已跳过", tone: "neutral" },
|
||||||
deferred: { label: "延后:已有计划在途", tone: "warning" },
|
deferred: { label: "延后:已有下载在进行", tone: "warning" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** 没关联作品时按识别状态说一句(原码不上屏) */
|
/** 没关联作品时按识别状态说一句(原码不上屏) */
|
||||||
const UNLINKED_TEXT: Record<string, string> = {
|
const UNLINKED_TEXT: Record<string, string> = {
|
||||||
unresolved: "还没识别作品。",
|
unresolved: "未识别。",
|
||||||
ambiguous: "有多个可能的作品,没有自动关联。",
|
ambiguous: "有多个可能的作品,没有自动关联。",
|
||||||
unidentifiable: "没识别出作品。",
|
unidentifiable: "无法识别作品。",
|
||||||
rejected: "识别结果已被排除。",
|
rejected: "识别结果已被排除。",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,9 +43,9 @@ function LocalInventory({ release, entry }: { release: WorkbenchRelease; entry:
|
|||||||
if (!release.entity) {
|
if (!release.entity) {
|
||||||
return (
|
return (
|
||||||
<p className="text-caption text-muted-foreground">
|
<p className="text-caption text-muted-foreground">
|
||||||
{UNLINKED_TEXT[release.resolutionStatus] ?? "未关联本地作品。"}
|
{UNLINKED_TEXT[release.resolutionStatus] ?? "未识别。"}
|
||||||
<Link to="/console/identity" className="ml-1 underline underline-offset-4">
|
<Link to="/console/identity" className="ml-1 underline underline-offset-4">
|
||||||
去指定作品
|
指定作品
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
@@ -87,7 +87,7 @@ function LocalInventory({ release, entry }: { release: WorkbenchRelease; entry:
|
|||||||
<li key={c.slotId} className="rounded-lg border border-border/70 px-3 py-2">
|
<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">
|
<div className="flex flex-wrap items-center gap-2 text-caption">
|
||||||
<StatusBadge tone={d.tone}>{d.label}</StatusBadge>
|
<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>
|
<span className="text-muted-foreground">{relTime(c.evaluatedAt)}评估</span>
|
||||||
</div>
|
</div>
|
||||||
{c.reasons.length ? (
|
{c.reasons.length ? (
|
||||||
@@ -158,7 +158,7 @@ export function ReleaseDetailSheet({
|
|||||||
{ siteId, rid: r.id },
|
{ siteId, rid: r.id },
|
||||||
{
|
{
|
||||||
onSuccess: (d) =>
|
onSuccess: (d) =>
|
||||||
d.refreshError ? toast.warning(`复核失败,显示上次数据:${d.refreshError}`) : toast.success("已复核"),
|
d.refreshError ? toast.warning(`刷新失败,显示的是上次的数据:${d.refreshError}`) : toast.success("已刷新"),
|
||||||
onError: (e) => toast.error(errorMessage(e)),
|
onError: (e) => toast.error(errorMessage(e)),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -224,7 +224,7 @@ export function ReleaseDetailSheet({
|
|||||||
</span>
|
</span>
|
||||||
<Button variant="outline" size="xs" className="ml-auto" disabled={refresh.isPending} onClick={doRefresh}>
|
<Button variant="outline" size="xs" className="ml-auto" disabled={refresh.isPending} onClick={doRefresh}>
|
||||||
<RefreshCw className={refresh.isPending ? "animate-spin" : undefined} />
|
<RefreshCw className={refresh.isPending ? "animate-spin" : undefined} />
|
||||||
复核详情
|
刷新详情
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{page && (externalLinks(page).length || page.tags.length || page.uploader) ? (
|
{page && (externalLinks(page).length || page.tags.length || page.uploader) ? (
|
||||||
@@ -240,7 +240,7 @@ export function ReleaseDetailSheet({
|
|||||||
) : null}
|
) : null}
|
||||||
{detail.data?.refreshError ? (
|
{detail.data?.refreshError ? (
|
||||||
<p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
|
<p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
|
||||||
最近一次复核失败,下面是之前的数据:{detail.data.refreshError}
|
上次刷新失败,下面是之前的数据:{detail.data.refreshError}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -254,7 +254,7 @@ export function ReleaseDetailSheet({
|
|||||||
) : detail.data && !detail.data.detailSupported ? (
|
) : detail.data && !detail.data.detailSupported ? (
|
||||||
<p className="text-caption text-muted-foreground">这个站点不支持抓详情页。</p>
|
<p className="text-caption text-muted-foreground">这个站点不支持抓详情页。</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-caption text-muted-foreground">还没有详情页内容。点「复核详情」抓取。</p>
|
<p className="text-caption text-muted-foreground">还没有详情页内容。点「刷新详情」抓取。</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!page?.fileTree.length && r.fileList?.length ? (
|
{!page?.fileTree.length && r.fileList?.length ? (
|
||||||
@@ -270,7 +270,7 @@ export function ReleaseDetailSheet({
|
|||||||
</DetailSection>
|
</DetailSection>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<DetailSection title="本地库存与候选理由">
|
<DetailSection title="本地版本与评估">
|
||||||
<LocalInventory release={r} entry={entry} />
|
<LocalInventory release={r} entry={entry} />
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
|
|
||||||
@@ -278,9 +278,9 @@ export function ReleaseDetailSheet({
|
|||||||
<DetailSection title="数据时效">
|
<DetailSection title="数据时效">
|
||||||
<KvList
|
<KvList
|
||||||
items={[
|
items={[
|
||||||
{ label: "促销观察于", value: formatDateTime(detail.data.volatile.promotionObservedAt) },
|
{ label: "促销更新于", value: formatDateTime(detail.data.volatile.promotionObservedAt) },
|
||||||
{ label: "促销截止", value: formatDateTime(detail.data.volatile.promotionExpiresAt) },
|
{ label: "促销截止", value: formatDateTime(detail.data.volatile.promotionExpiresAt) },
|
||||||
{ label: "做种人数观察于", value: formatDateTime(detail.data.volatile.peersObservedAt) },
|
{ label: "做种人数更新于", value: formatDateTime(detail.data.volatile.peersObservedAt) },
|
||||||
{ label: "详情抓取于", value: formatDateTime(detail.data.volatile.detailFetchedAt) },
|
{ label: "详情抓取于", value: formatDateTime(detail.data.volatile.detailFetchedAt) },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -294,8 +294,8 @@ export function ReleaseDetailSheet({
|
|||||||
{ label: "分类", value: categoryLabel(r) },
|
{ label: "分类", value: categoryLabel(r) },
|
||||||
{ label: "首次发现", value: formatDateTime(r.firstSeenAt) },
|
{ label: "首次发现", value: formatDateTime(r.firstSeenAt) },
|
||||||
{ label: "站点上传", value: formatDateTime(r.sourcePublishedAt) },
|
{ label: "站点上传", value: formatDateTime(r.sourcePublishedAt) },
|
||||||
{ label: "最近确认", value: `${formatDateTime(r.dataUpdatedAt)}(${ORIGIN_LABEL[r.origin]})` },
|
{ label: "数据更新于", value: `${formatDateTime(r.dataUpdatedAt)}(${ORIGIN_LABEL[r.origin]})` },
|
||||||
{ label: "Feed 观察次数", value: r.observedByFeeds },
|
{ label: "RSS 出现次数", value: r.observedByFeeds },
|
||||||
{ label: "站点 ID", value: r.siteReleaseId, mono: true },
|
{ label: "站点 ID", value: r.siteReleaseId, mono: true },
|
||||||
{ label: "infohash", value: r.infohashV1 ?? r.infohashV2, mono: true, wide: true },
|
{ label: "infohash", value: r.infohashV1 ?? r.infohashV2, mono: true, wide: true },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export const ORIGIN_LABEL: Record<WorkbenchRelease["origin"], string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SITE_HEALTH: Record<SiteHealth, { label: string; tone: StatusTone }> = {
|
export const SITE_HEALTH: Record<SiteHealth, { label: string; tone: StatusTone }> = {
|
||||||
unknown: { label: "健康未知", tone: "neutral" },
|
unknown: { label: "状态未知", tone: "neutral" },
|
||||||
healthy: { label: "健康", tone: "success" },
|
healthy: { label: "健康", tone: "success" },
|
||||||
degraded: { label: "降级", tone: "warning" },
|
degraded: { label: "降级", tone: "warning" },
|
||||||
failing: { label: "故障", tone: "destructive" },
|
failing: { label: "故障", tone: "destructive" },
|
||||||
@@ -196,6 +196,6 @@ export function accountSummary(accounts: SiteAccount[]): { label: string; tone:
|
|||||||
|
|
||||||
export function circuitLabel(site: Pick<Site, "circuitState" | "circuitOpenUntil">): string | null {
|
export function circuitLabel(site: Pick<Site, "circuitState" | "circuitOpenUntil">): string | null {
|
||||||
if (site.circuitState === "closed") return null;
|
if (site.circuitState === "closed") return null;
|
||||||
if (site.circuitState === "half_open") return "熔断半开(试探中)";
|
if (site.circuitState === "half_open") return "熔断中,正在试探恢复";
|
||||||
return `熔断中${site.circuitOpenUntil ? `,${relTime(site.circuitOpenUntil)}恢复` : ""}`;
|
return `熔断中${site.circuitOpenUntil ? `,${relTime(site.circuitOpenUntil)}恢复` : ""}`;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ export function unresolvedLabel(release: Pick<WorkbenchRelease, "resolutionStatu
|
|||||||
? "无法识别"
|
? "无法识别"
|
||||||
: release.resolutionStatus === "rejected"
|
: release.resolutionStatus === "rejected"
|
||||||
? "已排除"
|
? "已排除"
|
||||||
: "未识别到本地作品";
|
: "未识别";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 库存叠加:只来自本地身份/Slot/任务;未识别的资源明确写"未识别",不猜 */
|
/** 库存叠加:只来自本地身份/Slot/任务;未识别的资源明确写"未识别",不猜 */
|
||||||
@@ -92,7 +92,7 @@ export function InventoryOverlay({
|
|||||||
{release.entity.title}
|
{release.entity.title}
|
||||||
{release.entity.year ? ` (${release.entity.year})` : ""}
|
{release.entity.year ? ` (${release.entity.year})` : ""}
|
||||||
</span>
|
</span>
|
||||||
{loading ? <span className="text-meta text-muted-foreground">库存加载中…</span> : null}
|
{loading ? <span className="text-meta text-muted-foreground">本地版本加载中…</span> : null}
|
||||||
{!loading && entry && flags.length === 0 ? <StatusBadge>本地无版本</StatusBadge> : null}
|
{!loading && entry && flags.length === 0 ? <StatusBadge>本地无版本</StatusBadge> : null}
|
||||||
{flags.map((f) =>
|
{flags.map((f) =>
|
||||||
f.title ? (
|
f.title ? (
|
||||||
@@ -135,7 +135,7 @@ export function ReleaseRow({
|
|||||||
const snatched = snatchedOf(r);
|
const snatched = snatchedOf(r);
|
||||||
const timeTitle = [
|
const timeTitle = [
|
||||||
r.sourcePublishedAt ? `上传 ${absTime(r.sourcePublishedAt)}` : "上传时间未知",
|
r.sourcePublishedAt ? `上传 ${absTime(r.sourcePublishedAt)}` : "上传时间未知",
|
||||||
`${ORIGIN_LABEL[r.origin]}数据 ${relTime(r.dataUpdatedAt)}确认`,
|
`来源:${ORIGIN_LABEL[r.origin]} · ${relTime(r.dataUpdatedAt)}更新`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import { relTime, SITE_HEALTH } from "./release-fields.ts";
|
|||||||
|
|
||||||
const KIND: Record<SiteSnapshot["kind"], string> = {
|
const KIND: Record<SiteSnapshot["kind"], string> = {
|
||||||
periodic: "定期抓取",
|
periodic: "定期抓取",
|
||||||
backfill: "缺口补漏",
|
backfill: "补抓",
|
||||||
live_search: "去站点搜",
|
live_search: "去站点搜",
|
||||||
manual: "手动抓取",
|
manual: "手动抓取",
|
||||||
};
|
};
|
||||||
const STATUS: Record<SiteSnapshot["status"], { label: string; tone: StatusTone }> = {
|
const STATUS: Record<SiteSnapshot["status"], { label: string; tone: StatusTone }> = {
|
||||||
running: { label: "进行中", tone: "neutral" },
|
running: { label: "进行中", tone: "neutral" },
|
||||||
ok: { label: "完成", tone: "success" },
|
ok: { label: "已完成", tone: "success" },
|
||||||
partial: { label: "部分完成", tone: "warning" },
|
partial: { label: "部分完成", tone: "warning" },
|
||||||
failed: { label: "失败", tone: "destructive" },
|
failed: { label: "失败", tone: "destructive" },
|
||||||
};
|
};
|
||||||
@@ -44,7 +44,7 @@ export function SnapshotPanel({ site }: { site: Site }) {
|
|||||||
{!site.enabled ? <StatusBadge tone="warning">站点已停用</StatusBadge> : null}
|
{!site.enabled ? <StatusBadge tone="warning">站点已停用</StatusBadge> : null}
|
||||||
{site.snapshotEnabled ? (
|
{site.snapshotEnabled ? (
|
||||||
<StatusBadge tone={stale ? "warning" : "success"}>
|
<StatusBadge tone={stale ? "warning" : "success"}>
|
||||||
{stale ? "抓取已过期" : "已抓取"} · {relTime(site.lastSnapshotAt)}
|
{stale ? "抓取已过期" : "上次抓取"} · {relTime(site.lastSnapshotAt)}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
) : (
|
) : (
|
||||||
<StatusBadge>未开启定期抓取</StatusBadge>
|
<StatusBadge>未开启定期抓取</StatusBadge>
|
||||||
@@ -89,8 +89,8 @@ export function SnapshotPanel({ site }: { site: Site }) {
|
|||||||
<span className="text-foreground">{KIND[s.kind]}</span>
|
<span className="text-foreground">{KIND[s.kind]}</span>
|
||||||
{s.query ? <span className="min-w-0 truncate text-muted-foreground">“{s.query}”</span> : null}
|
{s.query ? <span className="min-w-0 truncate text-muted-foreground">“{s.query}”</span> : null}
|
||||||
<span className="text-muted-foreground tabular-nums">
|
<span className="text-muted-foreground tabular-nums">
|
||||||
{s.pages.length ? `第 ${s.pages.join(",")} 页 · ` : ""}
|
{s.pages.length ? `第 ${s.pages.join("、")} 页 · ` : ""}
|
||||||
{s.itemCount} 条(新 {s.newCount})
|
{s.itemCount} 条,新增 {s.newCount}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-auto text-meta text-muted-foreground" title={formatDateTime(s.startedAt)}>
|
<span className="ml-auto text-meta text-muted-foreground" title={formatDateTime(s.startedAt)}>
|
||||||
{relTime(s.startedAt)}
|
{relTime(s.startedAt)}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function AgentRunPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
aria-label="在侧边打开"
|
aria-label="在侧边打开"
|
||||||
title="在侧边打开(回到之前的页面)"
|
title="在侧边打开,回到之前的页面"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showAgentRunInDrawer(runId);
|
showAgentRunInDrawer(runId);
|
||||||
navigate(agentReturnPath() ?? "/console");
|
navigate(agentReturnPath() ?? "/console");
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function StatusLine({ d }: { d: Dashboard }) {
|
|||||||
await update.mutateAsync({ automation: { enabled } });
|
await update.mutateAsync({ automation: { enabled } });
|
||||||
toast.success(enabled ? "自动化已恢复" : "自动化已暂停");
|
toast.success(enabled ? "自动化已恢复" : "自动化已暂停");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(errorMessage(err, "切换失败"));
|
toast.error(errorMessage(err, enabled ? "恢复失败" : "暂停失败"));
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -217,11 +217,11 @@ function Busy({ w }: { w: Dashboard["currentWork"] }) {
|
|||||||
]);
|
]);
|
||||||
const empty = !downloads.length && !agents.length && !runs.length;
|
const empty = !downloads.length && !agents.length && !runs.length;
|
||||||
return (
|
return (
|
||||||
<Section title="在忙什么" icon={Activity} to="/console/logs?type=task&level=running">
|
<Section title="进行中" icon={Activity} to="/console/logs?type=task&level=running">
|
||||||
{running.isPending && !downloads.length ? (
|
{running.isPending && !downloads.length ? (
|
||||||
<LoadingState rows={3} />
|
<LoadingState rows={3} />
|
||||||
) : empty ? (
|
) : empty ? (
|
||||||
<EmptyState plain icon={CircleCheck} title="现在没有在跑的任务" className="py-6" />
|
<EmptyState plain icon={CircleCheck} title="没有进行中的任务" className="py-6" />
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-border/60">
|
<ul className="divide-y divide-border/60">
|
||||||
{downloads.map((t) => (
|
{downloads.map((t) => (
|
||||||
@@ -263,11 +263,11 @@ function NeedsMe() {
|
|||||||
const q = useAttention();
|
const q = useAttention();
|
||||||
const rows = attentionRows(q.data);
|
const rows = attentionRows(q.data);
|
||||||
return (
|
return (
|
||||||
<Section title="要我处理" icon={AlertTriangle}>
|
<Section title="需要你处理" icon={AlertTriangle}>
|
||||||
{q.isPending ? (
|
{q.isPending ? (
|
||||||
<LoadingState rows={2} />
|
<LoadingState rows={2} />
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<EmptyState plain icon={CircleCheck} title="没有要你处理的事" className="py-6" />
|
<EmptyState plain icon={CircleCheck} title="没有需要你处理的事" className="py-6" />
|
||||||
) : (
|
) : (
|
||||||
<AttentionList rows={rows} />
|
<AttentionList rows={rows} />
|
||||||
)}
|
)}
|
||||||
@@ -282,7 +282,7 @@ function RecentImports({ d }: { d: Dashboard }) {
|
|||||||
return (
|
return (
|
||||||
<Section title="最近整理" icon={LibraryIcon} to={consolePath("organize")}>
|
<Section title="最近整理" icon={LibraryIcon} to={consolePath("organize")}>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<EmptyState plain icon={LibraryIcon} title="最近没有新整理的作品" className="py-6" />
|
<EmptyState plain icon={LibraryIcon} title="最近没有整理完成的作品" className="py-6" />
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-border/60">
|
<ul className="divide-y divide-border/60">
|
||||||
{rows.map((r) => (
|
{rows.map((r) => (
|
||||||
@@ -294,7 +294,7 @@ function RecentImports({ d }: { d: Dashboard }) {
|
|||||||
<span className="block truncate">{r.title ?? r.targetPath?.split("/").pop() ?? "未关联作品"}</span>
|
<span className="block truncate">{r.title ?? r.targetPath?.split("/").pop() ?? "未关联作品"}</span>
|
||||||
{r.status === "partial" ? (
|
{r.status === "partial" ? (
|
||||||
<span className="block text-meta text-warning">
|
<span className="block text-meta text-warning">
|
||||||
{r.counts.succeeded}/{r.counts.total} 个文件已整理
|
已整理 {r.counts.succeeded}/{r.counts.total} 个文件
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -303,7 +303,7 @@ function RecentImports({ d }: { d: Dashboard }) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{today ? <p className="mt-auto pt-3 text-meta text-muted-foreground">今天整理 {today} 个文件</p> : null}
|
{today ? <p className="mt-auto pt-3 text-meta text-muted-foreground">今天已整理 {today} 个文件</p> : null}
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user