refactor(web): 下载盘、RSS、订阅、设置与待处理的剩余去噪项(DEAI-2)
- 下载面板:下载盘只显示「已用 x / 总量」与进度条,暂停线 / 在途 / 预留 / 可清理 / 做种保留收进详情; 阈值文案统一「暂停线 / 恢复线 / 下载盘满暂停」 - RSS 页去掉与站点页重复的站点表,节标题「RSS 地址」;用途只在非通用时显示 - 订阅:一条都没有时不铺筛选;表单里「空 = 24;0 = 关」等等式改成 placeholder + 一句说明 - 设置:浏览器服务、元数据识别、MCP 说明去掉组件名与实现细节;会话目录与 fake 脚本只在开发构建显示; API Key scope 前加中文名;命名模板变量去掉「MP 无」 - 待处理顶部计数卡改成一行筛选按钮 - 智能体悬浮按钮只在没有侧栏的窄屏出现,抽屉打开时不再显示「收起」胶囊 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
57ce40168d
commit
2c8c33c31c
15 files changed
+185
-237
No files matched your search
@@ -128,7 +128,7 @@ function useHideOnScroll(): boolean {
|
||||
/**
|
||||
* 右下角悬浮按钮 + 全局快捷键。挂在 console layout 里一次。
|
||||
* - Agent 会话页本身不显示(页面就是 Agent);最小化小窗在右下角时让位给小窗。
|
||||
* - 抽屉打开时变成"收起",停在抽屉左侧(窄屏抽屉占满全宽,用抽屉自带的关闭钮,FAB 隐藏)。
|
||||
* - 只在没有侧栏的窄屏显示(xl 起侧栏顶部就是入口);抽屉打开时隐藏,用抽屉自带的关闭钮。
|
||||
*/
|
||||
export function AgentLauncher() {
|
||||
useAgentHotkey();
|
||||
@@ -161,7 +161,8 @@ export function AgentLauncher() {
|
||||
"transition-[transform,bottom,right] duration-layout ease-spring active:scale-[0.96] focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none",
|
||||
"animate-in fade-in-0 zoom-in-95 duration-base motion-reduce:animate-none",
|
||||
"md:px-2 md:pr-5",
|
||||
open && "z-[60] hidden md:right-[calc(36rem+1rem)] md:flex",
|
||||
// 桌面(xl 起)侧栏已有入口,抽屉打开时用抽屉自己的关闭钮:这两种情况都不显示悬浮按钮
|
||||
open ? "hidden" : "xl:hidden",
|
||||
!open && scrolledAway && "translate-x-[calc(100%+2rem)] focus-visible:translate-x-0",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { consolePath, settingsPath } from "~/components/console/nav.ts";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { TechInfo } from "~/components/console/tech-info.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
@@ -37,10 +38,11 @@ function VolumeBar({ v }: { v: Volume }) {
|
||||
<span className="truncate">{volumeLabel(v)}</span>
|
||||
</span>
|
||||
<span className="text-meta text-muted-foreground tabular-nums">
|
||||
{pct === null ? "使用率未知" : `${pct.toFixed(1)}%`}
|
||||
{v.usedBytes !== null && v.totalBytes !== null
|
||||
? ` · ${formatBytes(v.usedBytes, 1)} / ${formatBytes(v.totalBytes, 1)}`
|
||||
: ""}
|
||||
? `已用 ${formatBytes(v.usedBytes, 0)} / ${formatBytes(v.totalBytes, 1)}`
|
||||
: pct === null
|
||||
? "用量未知"
|
||||
: `已用 ${Math.round(pct)}%`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative mt-1.5 h-1.5 rounded-full bg-foreground/[0.08]">
|
||||
@@ -57,28 +59,32 @@ function VolumeBar({ v }: { v: Volume }) {
|
||||
aria-hidden
|
||||
className="absolute -top-1 h-3.5 w-px bg-destructive"
|
||||
style={{ left: `${v.hardThresholdPercent}%` }}
|
||||
title={`硬阈值 ${v.hardThresholdPercent}%`}
|
||||
title={`暂停线 ${v.hardThresholdPercent}%`}
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute -top-1 h-3.5 w-px bg-muted-foreground"
|
||||
style={{ left: `${v.resumePercent}%` }}
|
||||
title={`恢复水位 ${v.resumePercent}%`}
|
||||
title={`恢复线 ${v.resumePercent}%`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-meta text-muted-foreground">
|
||||
<span>
|
||||
硬阈值 {v.hardThresholdPercent}% / 恢复 {v.resumePercent}%
|
||||
</span>
|
||||
<span>在途 {formatBytes(v.pendingBytes, 1)}</span>
|
||||
<span>预留 {formatBytes(v.reservedBytes, 1)}</span>
|
||||
<span>可清理 {formatBytes(v.cleanableBytes, 1)}</span>
|
||||
<span>保留锁定 {formatBytes(v.retentionLockedBytes, 1)}</span>
|
||||
</div>
|
||||
<TechInfo
|
||||
title="详情"
|
||||
className="mt-1"
|
||||
items={[
|
||||
{ label: "暂停线 / 恢复线", value: `${v.hardThresholdPercent}% / ${v.resumePercent}%`, mono: false },
|
||||
{ label: "下载中还要占用", value: formatBytes(v.pendingBytes, 1), mono: false },
|
||||
{ label: "已预留", value: formatBytes(v.reservedBytes, 1), mono: false },
|
||||
{ label: "可清理", value: formatBytes(v.cleanableBytes, 1), mono: false },
|
||||
{ label: "做种保留中", value: formatBytes(v.retentionLockedBytes, 1), mono: false },
|
||||
]}
|
||||
/>
|
||||
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
||||
{pct === null ? <StatusBadge tone="warning">读不到磁盘状态,已暂停新提交</StatusBadge> : null}
|
||||
{tripped ? <StatusBadge tone="destructive">已达硬阈值,停止新提交</StatusBadge> : null}
|
||||
{v.pausedByThreshold ? <StatusBadge tone="warning">阈值暂停 {v.pausedByThreshold} 个任务</StatusBadge> : null}
|
||||
{tripped ? <StatusBadge tone="destructive">超过暂停线,停止新下载</StatusBadge> : null}
|
||||
{v.pausedByThreshold ? (
|
||||
<StatusBadge tone="warning">因下载盘暂停 {v.pausedByThreshold} 个任务</StatusBadge>
|
||||
) : null}
|
||||
</div>
|
||||
{v.notes.map((n) => (
|
||||
<p key={n} className="mt-1 text-meta text-muted-foreground">
|
||||
@@ -97,7 +103,7 @@ export function CacheVolumes() {
|
||||
return (
|
||||
<section className="pg-slab mb-6 rounded-xl p-4">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">缓存水位</h2>
|
||||
<h2 className="text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">下载盘</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link to={settingsPath("automation", "cache")}>
|
||||
|
||||
@@ -50,7 +50,7 @@ export function DownloadStatusBadges({ t }: { t: DownloadTask }) {
|
||||
<span className="flex flex-wrap gap-1">
|
||||
<StatusBadge tone={DOWNLOAD_STATUS_TONE[t.status]}>{DOWNLOAD_STATUS_LABEL[t.status]}</StatusBadge>
|
||||
{t.pauseReason === "threshold" ? (
|
||||
<StatusBadge tone="warning">阈值暂停</StatusBadge>
|
||||
<StatusBadge tone="warning">下载盘满,已暂停</StatusBadge>
|
||||
) : t.pauseReason ? (
|
||||
<StatusBadge>{PAUSE_REASON_LABEL[t.pauseReason]}</StatusBadge>
|
||||
) : null}
|
||||
@@ -71,14 +71,14 @@ export function PauseResume({ t, size = "sm" }: { t: DownloadTask; size?: "sm" |
|
||||
variant="outline"
|
||||
size={size}
|
||||
disabled={act.isPending}
|
||||
title={threshold ? "阈值暂停,缓存回落到恢复水位后自动恢复" : undefined}
|
||||
title={threshold ? "下载盘超过暂停线,降到恢复线后自动恢复" : undefined}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
act.mutate(
|
||||
{ id: t.id, action: canResume ? "resume" : "pause" },
|
||||
{
|
||||
onSuccess: () => toast.success(canResume ? "已恢复" : "已暂停。阈值回落时不会自动恢复"),
|
||||
onSuccess: () => toast.success(canResume ? "已恢复" : "已暂停。下载盘回落时不会自动恢复"),
|
||||
onError: (err) => toast.error(errorMessage(err)),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -444,7 +444,6 @@ function VariablePalette({
|
||||
>
|
||||
<span className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="font-mono text-caption text-foreground">{v.name}</span>
|
||||
{v.ours ? <span className="text-micro text-muted-foreground">MP 无</span> : null}
|
||||
{v.example ? (
|
||||
<span className="ml-auto max-w-full truncate font-mono text-meta text-muted-foreground">
|
||||
{v.example}
|
||||
|
||||
@@ -86,7 +86,7 @@ function McpServerBlock() {
|
||||
section="agent"
|
||||
anchor="mcp"
|
||||
title="MCP 服务端"
|
||||
description="外部智能体用 API Key 经 MCP 调用同一套能力。授权、审计、幂等与控制台一致,有副作用的工具没有持续授权或单次审批时被拒绝。"
|
||||
description="外部程序用 API Key 经 MCP 调用和智能体同一套能力,授权与审计规则相同。"
|
||||
>
|
||||
{status.isPending ? (
|
||||
<LoadingState rows={3} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AgentToolsBlock } from "./agent-tools.tsx";
|
||||
import { WEBSEARCH_CARD } from "./agent-websearch.tsx";
|
||||
import { PiUpstreamsBlock } from "./pi-upstreams.tsx";
|
||||
import type { FieldValue } from "./resource-section.tsx";
|
||||
import type { FieldSpec, FieldValue } from "./resource-section.tsx";
|
||||
import { num, str } from "./resource-section.tsx";
|
||||
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
||||
|
||||
@@ -140,53 +140,58 @@ const INVESTIGATE_CARD: CardSpec = {
|
||||
}),
|
||||
};
|
||||
|
||||
const DEV_ONLY_FIELDS = new Set(["agent.sessionDir", "agent.fakeScript"]);
|
||||
|
||||
const ADVANCED_CARD: CardSpec = {
|
||||
anchor: "advanced",
|
||||
title: "高级",
|
||||
fields: [
|
||||
{ name: "agent.goalPollSec", label: "目标轮询间隔(秒)", kind: "number", hint: "等下载、入库结束时的检查间隔" },
|
||||
{
|
||||
name: "agent.goalWaitHours",
|
||||
label: "目标最长等待(小时)",
|
||||
kind: "number",
|
||||
hint: "超时后按「仍在等待」汇报结束",
|
||||
},
|
||||
{
|
||||
name: "agent.optionsTimeoutSec",
|
||||
label: "方案选择等待(秒)",
|
||||
kind: "number",
|
||||
hint: "智能体给出方案后等你选择的最长时间",
|
||||
},
|
||||
{
|
||||
name: "agent.optionsTimeoutAction",
|
||||
label: "超时未选",
|
||||
kind: "select",
|
||||
options: [
|
||||
{ value: "recommended", label: "自动选推荐项" },
|
||||
{ value: "stop", label: "不选,智能体收尾" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "agent.approvalTimeoutSec",
|
||||
label: "会话内审批有效期(秒)",
|
||||
kind: "number",
|
||||
hint: "步骤处的审批卡过期未批准,按拒绝处理",
|
||||
},
|
||||
{
|
||||
name: "agent.sessionDir",
|
||||
label: "会话目录",
|
||||
kind: "text",
|
||||
wide: true,
|
||||
placeholder: "留空用 DATA_DIR/agent-sessions",
|
||||
hint: "绝对路径,api 与 worker 都要可读写;有运行中的会话时不要改",
|
||||
},
|
||||
{
|
||||
name: "agent.fakeScript",
|
||||
label: "fake 脚本(JSON)",
|
||||
kind: "textarea",
|
||||
hint: "只对 fake 模型服务生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
|
||||
},
|
||||
],
|
||||
// 会话目录与 fake 脚本是开发调试项,生产构建不显示(DEAI-2)
|
||||
fields: (
|
||||
[
|
||||
{ name: "agent.goalPollSec", label: "目标轮询间隔(秒)", kind: "number", hint: "等下载、入库结束时的检查间隔" },
|
||||
{
|
||||
name: "agent.goalWaitHours",
|
||||
label: "目标最长等待(小时)",
|
||||
kind: "number",
|
||||
hint: "超时后按「仍在等待」汇报结束",
|
||||
},
|
||||
{
|
||||
name: "agent.optionsTimeoutSec",
|
||||
label: "方案选择等待(秒)",
|
||||
kind: "number",
|
||||
hint: "智能体给出方案后等你选择的最长时间",
|
||||
},
|
||||
{
|
||||
name: "agent.optionsTimeoutAction",
|
||||
label: "超时未选",
|
||||
kind: "select",
|
||||
options: [
|
||||
{ value: "recommended", label: "自动选推荐项" },
|
||||
{ value: "stop", label: "不选,智能体收尾" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "agent.approvalTimeoutSec",
|
||||
label: "会话内审批有效期(秒)",
|
||||
kind: "number",
|
||||
hint: "步骤处的审批卡过期未批准,按拒绝处理",
|
||||
},
|
||||
{
|
||||
name: "agent.sessionDir",
|
||||
label: "会话目录",
|
||||
kind: "text",
|
||||
wide: true,
|
||||
placeholder: "留空用 DATA_DIR/agent-sessions",
|
||||
hint: "绝对路径,api 与 worker 都要可读写;有运行中的会话时不要改",
|
||||
},
|
||||
{
|
||||
name: "agent.fakeScript",
|
||||
label: "fake 脚本(JSON)",
|
||||
kind: "textarea",
|
||||
hint: "只对 fake 模型服务生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
|
||||
},
|
||||
] as FieldSpec[]
|
||||
).filter((f) => import.meta.env.DEV || !DEV_ONLY_FIELDS.has(f.name)),
|
||||
fromSettings: (s) => ({
|
||||
"agent.goalPollSec": n(s.agent.goalPollSec),
|
||||
"agent.goalWaitHours": n(s.agent.goalWaitHours),
|
||||
|
||||
@@ -25,10 +25,10 @@ import { SettingsBlock } from "./settings-block.tsx";
|
||||
// ─── API Key ───
|
||||
|
||||
const SCOPES: { value: ApiScope; label: string; hint: string }[] = [
|
||||
{ value: "read", label: "read", hint: "只读查询全部数据" },
|
||||
{ value: "organize.read", label: "organize.read", hint: "只读整理记录,供外部增量同步" },
|
||||
{ value: "organize.execute", label: "organize.execute", hint: "发起整理" },
|
||||
{ value: "admin", label: "admin", hint: "全部管理权限,含改设置" },
|
||||
{ value: "read", label: "只读 · read", hint: "只读查询全部数据" },
|
||||
{ value: "organize.read", label: "读整理记录 · organize.read", hint: "只读整理记录,供外部增量同步" },
|
||||
{ value: "organize.execute", label: "执行整理 · organize.execute", hint: "发起整理" },
|
||||
{ value: "admin", label: "管理员 · admin", hint: "全部管理权限,含改设置" },
|
||||
];
|
||||
|
||||
function CreatedKeyDialog({ plaintext, onClose }: { plaintext: string | null; onClose: () => void }) {
|
||||
@@ -200,7 +200,7 @@ export function ApiKeysSection() {
|
||||
checked={on}
|
||||
onChange={() => setScopes((xs) => (on ? xs.filter((x) => x !== s.value) : [...xs, s.value]))}
|
||||
/>
|
||||
<code className="text-caption">{s.label}</code>
|
||||
<span className="text-caption">{s.label}</span>
|
||||
<span className="text-meta text-muted-foreground">{s.hint}</span>
|
||||
</label>
|
||||
);
|
||||
@@ -250,7 +250,7 @@ function ScopeLegend() {
|
||||
{SCOPES.map((s) => (
|
||||
<div key={s.value} className="contents">
|
||||
<dt>
|
||||
<code className="rounded-tight bg-foreground/[0.06] px-1.5 py-0.5">{s.label}</code>
|
||||
<span className="rounded-tight bg-foreground/[0.06] px-1.5 py-0.5">{s.label}</span>
|
||||
</dt>
|
||||
<dd className="text-muted-foreground">{s.hint}</dd>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +97,7 @@ export function BrowserProvidersBlock() {
|
||||
section="sites"
|
||||
anchor="browser"
|
||||
title="浏览器服务"
|
||||
description="原站视图和智能体打开站点网页用的 camofox-browser。部署自带的一台由环境变量登记(camofox-env)。"
|
||||
description="打开站点网页用(原站视图、智能体)。随部署提供的一台显示为内置。"
|
||||
plain
|
||||
>
|
||||
<ResourceSection<BrowserProviderView>
|
||||
|
||||
@@ -202,21 +202,20 @@ function ImageCacheStatus() {
|
||||
const IDENTITY_CARD: CardSpec = {
|
||||
anchor: "identity",
|
||||
title: "资源识别",
|
||||
description:
|
||||
"缺省只在资源命中订阅或本地作品别名、新作品收录规则前置条件通过、你手动识别或搜索时查 TMDB;其余进未识别资源,不进待处理。",
|
||||
description: "默认只在资源命中订阅、作品别名或新作品规则,或你手动识别和搜索时查 TMDB;其余放进未识别,不进待处理。",
|
||||
fields: [
|
||||
{
|
||||
name: "identity.eagerTmdbLookup",
|
||||
label: "每条新资源都查 TMDB",
|
||||
kind: "switch",
|
||||
hint: "旧行为。每条资源约 1–2 次搜索 + 命中时 1 次详情(识别缓存命中为 0);每小时 100 条的 RSS 约每天 2400–7200 次请求,站点独有的短剧 / 综艺多半查不到",
|
||||
hint: "每条新资源都查一次 TMDB。RSS 量大时每天可能上千次请求,站点独有的短剧和综艺多半查不到",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
name: "identity.autoEnrich",
|
||||
label: "TMDB 查不到时自动补全元数据",
|
||||
kind: "switch",
|
||||
hint: "资源命中订阅或新作品收录规则、TMDB 却没有结果时,自动搜索网页出一份补全建议(每条资源一次,不建作品)。每次约 1–2 次网页搜索、最多读 3 个网页、1 次 模型服务调用",
|
||||
hint: "资源命中订阅或新作品规则、TMDB 却查不到时,搜索网页出一份补全建议(每条资源一次,不建作品)。每次最多读 3 个网页,调用一次模型服务",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -591,11 +591,12 @@ export function SubscriptionFormDialog({
|
||||
label="总集数"
|
||||
htmlFor={f("total")}
|
||||
error={err("total")}
|
||||
hint={d.totalLocked ? "锁定后元数据刷新不再改写" : "空 = 按 TMDB;不锁定时只升不降"}
|
||||
hint={d.totalLocked ? "锁定后元数据刷新不再改写" : "留空按 TMDB"}
|
||||
>
|
||||
<Input
|
||||
id={f("total")}
|
||||
inputMode="numeric"
|
||||
placeholder="按 TMDB"
|
||||
value={d.total}
|
||||
aria-invalid={!!err("total")}
|
||||
onChange={(e) => {
|
||||
@@ -729,11 +730,7 @@ export function SubscriptionFormDialog({
|
||||
|
||||
<TabsContent value="advanced" className="mt-4 min-w-0 space-y-5">
|
||||
<Section title="过滤">
|
||||
<FormField
|
||||
label="规则组"
|
||||
htmlFor={f("groups")}
|
||||
hint="按选择顺序串联;不选 = 订阅默认规则组。收录版本自己的规则组另算"
|
||||
>
|
||||
<FormField label="规则组" htmlFor={f("groups")} hint="不选时用默认规则组">
|
||||
{groups.isPending ? (
|
||||
<p className="text-caption text-muted-foreground">读取规则组…</p>
|
||||
) : (groups.data?.items ?? []).length === 0 ? (
|
||||
@@ -835,11 +832,12 @@ export function SubscriptionFormDialog({
|
||||
label="主动搜索间隔(小时)"
|
||||
htmlFor={f("interval")}
|
||||
error={err("searchIntervalHours")}
|
||||
hint="空 = 24;0 = 关"
|
||||
hint="0 表示不主动搜索"
|
||||
>
|
||||
<Input
|
||||
id={f("interval")}
|
||||
inputMode="decimal"
|
||||
placeholder="24"
|
||||
value={d.searchIntervalHours}
|
||||
aria-invalid={!!err("searchIntervalHours")}
|
||||
onChange={(e) => patch({ searchIntervalHours: e.target.value })}
|
||||
|
||||
@@ -161,7 +161,12 @@ export function FeedTable({
|
||||
);
|
||||
},
|
||||
},
|
||||
{ key: "purpose", header: "用途", hideBelow: "md", cell: (f) => PURPOSE_LABEL[f.purpose] },
|
||||
{
|
||||
key: "purpose",
|
||||
header: "用途",
|
||||
hideBelow: "md",
|
||||
cell: (f) => (f.purpose === "generic" ? null : PURPOSE_LABEL[f.purpose]),
|
||||
},
|
||||
{
|
||||
key: "site",
|
||||
header: "站点 / 账号",
|
||||
|
||||
Reference in new issue
Block a user