import { Pause, Play, ShieldAlert } from "lucide-react"; import type { ReactNode } from "react"; import { toast } from "sonner"; import { useUpdateSettings } from "~/api/settings.ts"; import { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn } from "~/lib/utils.ts"; import { type FormValues, num } from "./resource-section.tsx"; import { SettingsBlock } from "./settings-block.tsx"; import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx"; // 系统分区:自动化总开关、只读对账、后台任务并发(WK-1)、存储预算与流水数据保留(CACHE-2)。 // 下载盘水位与保种在「下载器」,每日下载预算在「获取」,缓存有效期在「缓存」。 // CACHE-2:存储预算、流水保留期(settings.storage,docs/runbook/storage.md) const gbField = (k: string, label: string, hint?: string) => ({ name: `storage.budgets.${k}`, label, kind: "number" as const, hint, }); const dayField = (group: "retentionDays", k: string, label: string, hint?: string) => ({ name: `storage.${group}.${k}`, label, kind: "number" as const, hint, }); const pick = (v: FormValues, prefix: string, keys: readonly string[]) => Object.fromEntries(keys.map((k) => [k, num(v[`${prefix}.${k}`])])); // WK-1:io worker 三条道的并发(settings.automation.workerLanes,docs/runbook/dev.md「IO Worker 分道」) const LANE_KEYS = ["file", "network", "maintenance"] as const; export const WORKER_LANES_CARD: CardSpec = { anchor: "worker-lanes", title: "后台任务并发", description: "后台任务分三条道各自排队,互不占用。保存后几秒内生效,不用重启;服务器上设置了对应的环境变量时以环境变量为准。", fields: [ { name: "automation.workerLanes.file", label: "文件道", kind: "number", hint: "整理、硬链接、移动、校验;1–16" }, { name: "automation.workerLanes.network", label: "网络道", kind: "number", hint: "站点、RSS、TMDB、下载器、搜索;单站限流另计;1–16", }, { name: "automation.workerLanes.maintenance", label: "维护道", kind: "number", hint: "清理、回填、对账;前两道有活时让路;1–16", }, ], fromSettings: (s) => Object.fromEntries(LANE_KEYS.map((k) => [`automation.workerLanes.${k}`, n(s.automation.workerLanes[k])])), toPatch: (v) => ({ automation: { workerLanes: pick(v, "automation.workerLanes", LANE_KEYS) } }), }; const BUDGET_KEYS = ["pgDataGb", "pgDataWarnGb", "imagesGb", "backupsGb", "sessionsEvidenceGb"] as const; export const STORAGE_BUDGET_CARD: CardSpec = { anchor: "storage", title: "存储预算", description: "超过预算时运行状态页告警;图片缓存超出容量按最久未访问删除。单位 GB。", fields: [ gbField("pgDataGb", "数据库预算", "超过时标为需要处理"), gbField("pgDataWarnGb", "数据库告警线", "不高于数据库预算"), gbField("imagesGb", "图片缓存容量"), gbField("backupsGb", "备份目录预算"), 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) } }), }; const RETENTION_KEYS = [ "observation", "sseEvent", "heartbeatHours", "taskRun", "agentEvent", "audit", "capabilityStats", "identityPoolArchive", "engineRun", "outboxEvent", "resolutionAttempt", "identityResolved", "siteSnapshot", ] as const; export const STORAGE_RETENTION_CARD: CardSpec = { anchor: "storage-retention", title: "数据保留", description: "到期由定时清理任务分批删除。单位天,心跳为小时。", fields: [ dayField("retentionDays", "observation", "RSS 抓取记录"), dayField("retentionDays", "sseEvent", "实时事件"), dayField("retentionDays", "heartbeatHours", "进程心跳(小时)"), dayField("retentionDays", "taskRun", "任务运行记录"), dayField("retentionDays", "agentEvent", "智能体会话事件", "只删已结束的会话"), dayField("retentionDays", "audit", "审计日志", "能力调用审计除外"), dayField("retentionDays", "capabilityStats", "能力调用统计"), dayField( "retentionDays", "identityPoolArchive", "未识别资源归档", "未命中的条目这么久没有新证据就归档,仍可手动指定", ), dayField("retentionDays", "engineRun", "后台运行记录", "被任务记录引用的随任务一起保留"), dayField("retentionDays", "outboxEvent", "已投递的内部事件"), dayField("retentionDays", "resolutionAttempt", "识别尝试记录", "每个资源至少留最近 3 次"), dayField("retentionDays", "identityResolved", "未识别中已识别的条目", "忽略和归档的不删"), dayField("retentionDays", "siteSnapshot", "站点抓取结果", "每个站点保留最近一次成功的抓取"), ], fromSettings: (s) => Object.fromEntries(RETENTION_KEYS.map((k) => [`storage.retentionDays.${k}`, n(s.storage.retentionDays[k])])), toPatch: (v) => ({ storage: { retentionDays: pick(v, "storage.retentionDays", RETENTION_KEYS) } }), }; function ToggleRow({ id, icon, title, description, checked, onChange, disabled, tone, }: { id: string; icon: ReactNode; title: string; description: string; checked: boolean; onChange: (v: boolean) => void; disabled: boolean; tone: "success" | "warning"; }) { return (