From a4c554f841f987164451532df7a8ea53a4158082 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 7 Oct 2026 02:10:24 +0800 Subject: [PATCH] =?UTF-8?q?chore(web):=20=E5=88=A0=E9=99=A4=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E5=BC=95=E7=94=A8=E7=9A=84=E6=95=B4=E7=90=86=E5=90=91?= =?UTF-8?q?=E5=AF=BC=E6=97=A7=E6=AD=A5=E9=AA=A4=E4=B8=8E=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit step-files / step-options / agentui/tool-approval 全仓无引用。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../app/components/agentui/tool-approval.tsx | 184 ------- .../components/organize-wizard/step-files.tsx | 519 ------------------ .../organize-wizard/step-options.tsx | 229 -------- 3 files changed, 932 deletions(-) delete mode 100644 apps/web/app/components/agentui/tool-approval.tsx delete mode 100644 apps/web/app/components/organize-wizard/step-files.tsx delete mode 100644 apps/web/app/components/organize-wizard/step-options.tsx diff --git a/apps/web/app/components/agentui/tool-approval.tsx b/apps/web/app/components/agentui/tool-approval.tsx deleted file mode 100644 index 3110db34..00000000 --- a/apps/web/app/components/agentui/tool-approval.tsx +++ /dev/null @@ -1,184 +0,0 @@ -// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/tool-approval.tsx -// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md -// 改造:状态徽章 amber/blue/emerald/rose → warning/中性/success/destructive;去掉 "Always allow"(本项目的持续授权 -// 只在设置页按策略版本发放,不在对话里一键放行);新增 `rejected` 醒目态:服务端拒绝了一次工具调用时, -// 整卡换 destructive 材质、role=alert、给出拒绝原因与授权说明;新增 `badges` 槽(scope/风险级别);参数区用 Disclosure。 -import { Check, ChevronDown, CircleAlert, LoaderCircle, ShieldAlert, ShieldCheck, X } from "lucide-react"; -import { type ReactNode, useId, useState } from "react"; -import { Button } from "~/components/ui/button.tsx"; -import { cn } from "~/lib/utils.ts"; -import { CHEVRON_CLASS, Disclosure } from "./disclosure.tsx"; - -export type ToolApprovalStatus = - | "pending" - | "approving" - | "approved" - | "denied" - | "rejected" - | "running" - | "complete" - | "error"; - -export interface ToolApprovalParameter { - id: string; - label: ReactNode; - value: ReactNode; -} - -const COPY: Record = { - pending: "等你批准", - approving: "提交中", - approved: "已批准", - denied: "已拒绝", - rejected: "服务端拒绝", - running: "执行中", - complete: "已完成", - error: "失败", -}; - -const BADGE: Record = { - pending: "border-warning/30 bg-warning/10 text-warning", - approving: "border-border bg-foreground/[0.04] text-muted-foreground", - running: "border-border bg-foreground/[0.04] text-muted-foreground", - approved: "border-success/30 bg-success/10 text-success", - complete: "border-success/30 bg-success/10 text-success", - denied: "border-destructive/30 bg-destructive/10 text-destructive", - rejected: "border-destructive/40 bg-destructive/15 text-destructive", - error: "border-destructive/30 bg-destructive/10 text-destructive", -}; - -export interface ToolApprovalProps { - tool: ReactNode; - title?: ReactNode; - description?: ReactNode; - parameters?: ToolApprovalParameter[]; - status?: ToolApprovalStatus; - badges?: ReactNode; - /** rejected 态:拒绝原因(服务端给出) */ - reason?: ReactNode; - /** rejected 态:为什么会被拒绝、怎样才能放行 */ - explanation?: ReactNode; - defaultOpen?: boolean; - onApprove?: () => void; - onDeny?: () => void; - /** 为什么按钮不可用(如非管理员会话) */ - actionsDisabledReason?: string; - className?: string; -} - -export function ToolApproval({ - tool, - title = "允许调用这个工具?", - description, - parameters = [], - status = "pending", - badges, - reason, - explanation, - defaultOpen = false, - onApprove, - onDeny, - actionsDisabledReason, - className, -}: ToolApprovalProps) { - const id = useId(); - const [open, setOpen] = useState(defaultOpen); - const busy = status === "approving" || status === "running"; - const rejected = status === "rejected"; - - return ( -
-
- - {busy ? ( - - ) : rejected ? ( - - ) : status === "error" ? ( - - ) : status === "denied" ? ( - - ) : status === "approved" || status === "complete" ? ( - - ) : ( - - )} - -
-
-
-
{title}
-
{tool}
-
- - {COPY[status]} - -
- {badges ?
{badges}
: null} - {rejected && reason ? ( -

原因:{reason}

- ) : null} - {description ?

{description}

: null} - {rejected && explanation ? ( -

{explanation}

- ) : null} - {parameters.length ? ( - - ) : null} -
-
- {parameters.length ? ( - -
- {parameters.map((p) => ( -
-
{p.label}
-
{p.value}
-
- ))} -
-
- ) : null} - {status === "pending" && (onApprove || onDeny) ? ( -
- - - {actionsDisabledReason ? ( - {actionsDisabledReason} - ) : null} -
- ) : null} -
- ); -} diff --git a/apps/web/app/components/organize-wizard/step-files.tsx b/apps/web/app/components/organize-wizard/step-files.tsx deleted file mode 100644 index c1c18e8d..00000000 --- a/apps/web/app/components/organize-wizard/step-files.tsx +++ /dev/null @@ -1,519 +0,0 @@ -import { ChevronDown, Undo2, X } from "lucide-react"; -import { useState } from "react"; -import { Link } from "react-router"; -import { filesMockEnabled } from "~/api/files.ts"; -import { useSlots } from "~/api/slots.ts"; -import { StatusBadge } from "~/components/console/status-badge.tsx"; -import { Checkbox } from "~/components/files/file-list.tsx"; -import { baseName, contentUnitLabel } from "~/components/files/model.ts"; -import { SLOT_STATUS } from "~/components/library/labels.ts"; -import { Button } from "~/components/ui/button.tsx"; -import { Input } from "~/components/ui/input.tsx"; -import { cn, formatBytes } from "~/lib/utils.ts"; -import { EntityPicker } from "./entity-picker.tsx"; -import { - groupRows, - isMovieKind, - MIB, - rowProblem, - type WizardEntity, - type WizardRow, - type WizardStats, -} from "./model.ts"; - -type RowPatch = Partial>; - -export type StepFilesProps = { - rows: WizardRow[]; - stats: WizardStats; - selected: Set; - onSelected: (s: Set) => void; - onEntity: (key: string, e: WizardEntity | null) => void; - onPatch: (key: string, p: RowPatch) => void; - onApplyGroup: (group: string) => void; - onBulkEntity: (e: WizardEntity) => void; - onBulkSeason: (s: string) => void; - onBulkOffset: (n: number) => void; - minSizeMb: string; - onMinSizeMb: (v: string) => void; - truncated: boolean; - mobile: boolean; - rootName: (rootId: string) => string; -}; - -/** 向导 ① 批量条(§6.1 WizardBulkBar):作品 / 季 / 集号偏移 / 最小体积 + 统计 */ -export function WizardBulkBar({ - selectedCount, - onBulkEntity, - onBulkSeason, - onBulkOffset, - minSizeMb, - onMinSizeMb, - stats, -}: { - selectedCount: number; - onBulkEntity: (e: WizardEntity) => void; - onBulkSeason: (s: string) => void; - onBulkOffset: (n: number) => void; - minSizeMb: string; - onMinSizeMb: (v: string) => void; - stats: WizardStats; -}) { - const [season, setSeason] = useState(""); - const [offset, setOffset] = useState(""); - const off = Number(offset); - const none = selectedCount === 0; - const hint = none ? "先勾选要改的行" : undefined; - return ( -
-
- 已选 {selectedCount} 项 -
- 作品 -
- e && onBulkEntity(e)} placeholder="应用到选中…" /> -
-
-
- 季 - setSeason(e.target.value)} - inputMode="numeric" - aria-label="批量季号" - className="h-8 w-14 text-center" - /> - -
-
- 集号偏移 - setOffset(e.target.value)} - placeholder="±N" - aria-label="集号偏移量(可为负)" - className="h-8 w-16 text-center" - /> - -
-
- 最小体积 - onMinSizeMb(e.target.value)} - inputMode="numeric" - aria-label="最小体积(MB)" - className="h-8 w-16 text-center" - /> - MB -
-
-
- {stats.videos} 个视频 - {stats.pending ? ( - {stats.pending} 项待定 - ) : ( - 没有待定项 - )} - {stats.excluded ? ( - - {stats.excluded} 项已排除{stats.bySize ? `(${stats.bySize} 项低于最小体积)` : ""} - - ) : null} -
-
- ); -} - -function RowMarks({ row }: { row: WizardRow }) { - const it = row.item; - return ( - - {it.existing.assetId ? ( - - 已整理 - - ) : null} - {it.records?.count ? ( - it.records.lastRecordId ? ( - - 有 {it.records.count} 条整理记录 - - ) : ( - 有 {it.records.count} 条整理记录 - ) - ) : null} - {row.hnrBlocked ? 做种中·H&R 未满足 : null} - {row.excluded === "below_min_size" ? 低于最小体积 : null} - {row.entity && !row.entity.id ? 将新建作品 : null} - - ); -} - -function parsedLine(row: WizardRow) { - const p = row.item.parsed; - return [ - p.title ?? "解析不出标题", - p.year ? String(p.year) : null, - p.season != null ? contentUnitLabel(p.season, p.episodes) : null, - p.resolution, - p.source, - p.releaseGroup, - row.item.size != null ? formatBytes(row.item.size, 1) : null, - ] - .filter(Boolean) - .join(" · "); -} - -function SlotSelect({ - entity, - value, - onChange, -}: { - entity: WizardEntity | null; - value: string; - onChange: (v: string) => void; -}) { - const mock = filesMockEnabled(); - const id = entity?.id ?? null; - const slots = useSlots({ mediaEntityId: id ?? undefined }, !!id && !mock); - const items = slots.data?.items ?? []; - return ( - - ); -} - -function ExcludeButton({ row, onPatch }: { row: WizardRow; onPatch: (p: RowPatch) => void }) { - return ( - - ); -} - -/** 向导 ①:按源目录分组的表格(桌面)/ 紧凑卡片(手机) */ -export function StepFiles(p: StepFilesProps) { - const groups = groupRows(p.rows); - const allKeys = p.rows.filter((r) => !r.excluded).map((r) => r.key); - const toggle = (k: string) => { - const n = new Set(p.selected); - if (n.has(k)) n.delete(k); - else n.add(k); - p.onSelected(n); - }; - const [bulkOpen, setBulkOpen] = useState(false); - const bulk = ( - - ); - - return ( -
- {p.mobile ? ( -
- - {bulkOpen ?
{bulk}
: null} -
- ) : ( - bulk - )} - {p.truncated ? ( -

- 目录里视频超过 200 个,只识别了前 200 个。进入子目录分批整理。 -

- ) : null} - - {p.mobile ? ( -
- {groups.map((g) => ( -
- p.onApplyGroup(g.group)} - canApply={g.rows.length > 1 && g.rows.some((r) => r.entity && !r.excluded)} - /> -
    - {g.rows.map((r) => { - const problem = rowProblem(r); - const movie = isMovieKind(r.entity?.kind); - return ( -
  • -
    - toggle(r.key)} - /> -

    - {baseName(r.path)} -

    - p.onPatch(r.key, x)} /> -
    - {!r.excluded ? ( -
    - p.onEntity(r.key, e)} - invalid={!r.entity} - /> - p.onPatch(r.key, { season: e.target.value })} - className="h-8 px-1 text-center" - /> - p.onPatch(r.key, { episodes: e.target.value })} - className="h-8 px-1 text-center" - /> -
    - ) : null} -
    - -
    - {problem ?

    {problem}

    : null} -
  • - ); - })} -
-
- ))} -
- ) : ( -
- - - - - - - - - - - - - {groups.map((g) => ( - - - - - {g.rows.map((r) => { - const problem = rowProblem(r); - const movie = isMovieKind(r.entity?.kind); - return ( - - - - - - - - - - ); - })} - - ))} -
- 0 && allKeys.every((k) => p.selected.has(k))} - partial={p.selected.size > 0 && !allKeys.every((k) => p.selected.has(k))} - label="全选" - onToggle={() => - p.onSelected(allKeys.every((k) => p.selected.has(k)) ? new Set() : new Set(allKeys)) - } - /> - 源文件作品季集收录版本 - 操作 -
- p.onApplyGroup(g.group)} - canApply={g.rows.length > 1 && g.rows.some((r) => r.entity && !r.excluded)} - /> -
- toggle(r.key)} - /> - -

- {baseName(r.path)} -

-

- {parsedLine(r)} -

-
- -
- {problem ?

{problem}

: null} - {(r.item.skipped ? [] : r.item.warnings).map((w) => ( -

- {w} -

- ))} -
- {r.excluded ? null : ( - p.onEntity(r.key, e)} - invalid={!r.entity} - /> - )} - - {r.excluded ? null : ( - p.onPatch(r.key, { season: e.target.value })} - className="h-8 px-1 text-center" - /> - )} - - {r.excluded ? null : ( - p.onPatch(r.key, { episodes: e.target.value })} - className="h-8 px-1 text-center" - /> - )} - - {r.excluded ? null : ( - p.onPatch(r.key, { slotId: v })} - /> - )} - - p.onPatch(r.key, x)} /> -
-
- )} -
- ); -} - -function GroupHead({ - dir, - rootName, - count, - onApply, - canApply, -}: { - dir: string; - rootName: string; - count: number; - onApply: () => void; - canApply: boolean; -}) { - return ( -
- - {dir ? dir.split("/").pop() : rootName} - - {count} 项 - {canApply ? ( - - ) : null} -
- ); -} - -export const mbToBytes = (v: string) => { - const n = Number(v.trim()); - return Number.isFinite(n) && n >= 0 ? Math.round(n * MIB) : null; -}; diff --git a/apps/web/app/components/organize-wizard/step-options.tsx b/apps/web/app/components/organize-wizard/step-options.tsx deleted file mode 100644 index a1e995fd..00000000 --- a/apps/web/app/components/organize-wizard/step-options.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import { Link } from "react-router"; -import type { FileRoot } from "~/api/files.ts"; -import { DirPicker } from "~/components/files/dir-picker.tsx"; -import { Switch } from "~/components/ui/switch.tsx"; -import { cn, formatBytes } from "~/lib/utils.ts"; -import type { WizardOptions } from "./model.ts"; - -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-

{title}

- {children} -
- ); -} - -function Choice({ - checked, - disabled, - onSelect, - title, - hint, -}: { - checked: boolean; - disabled?: string | null; - onSelect: () => void; - title: string; - hint?: React.ReactNode; -}) { - return ( - // biome-ignore lint/a11y/useSemanticElements: 带说明文字的整卡单选,原生 radio 无法套卡片样式;键盘可达(button) - - ); -} - -/** 向导 ②:MP 手动整理字段的映射(§6.2) */ -export function StepOptions({ - options, - onChange, - roots, - hnrCount, - kinds, - profileLabel, -}: { - options: WizardOptions; - onChange: (o: WizardOptions) => void; - roots: FileRoot[]; - /** 做种中且 H&R 未满足的行数:大于 0 时不能移动 */ - hnrCount: number; - /** 本次涉及的作品类型(过滤库根) */ - kinds: Set; - profileLabel?: string; -}) { - const set = (p: Partial) => onChange({ ...options, ...p }); - const libRoots = roots.filter((r) => r.kind === "library" && r.writable && r.health !== "offline"); - /** 库根类型与本次作品类型不符时的原因 */ - const rootMismatch = (r: FileRoot): string | null => { - if (r.mediaKind === "mixed" || r.mediaKind === "other" || !r.mediaKind) return null; - const hasMovie = [...kinds].some((k) => k === "movie"); - const hasShow = [...kinds].some((k) => k !== "movie"); - if (r.mediaKind === "movie" && hasShow) return "电影库,本次含剧集"; - if (r.mediaKind !== "movie" && hasMovie) return "剧集库,本次含电影"; - return null; - }; - const t = options.target; - return ( -
-
-
-
- set({ action: "copy" })} - title="复制" - hint="保留源文件,不影响做种" - /> - set({ action: "move" })} - disabled={hnrCount ? `${hnrCount} 项做种中且 H&R 未满足,不能移动` : null} - title="移动" - hint="整理后删除源文件;跨卷移动会先复制、核验,再删除源文件" - /> -
-
- -
-
- set({ onExisting: "conflict" })} - title="标为需要处理" - hint="这组不执行,在整理记录里标为需要处理" - /> - set({ onExisting: "skip" })} - title="跳过" - hint="只跳过已存在的文件,其余照常整理" - /> - set({ onExisting: "replace_with_verify" })} - title="替换" - hint="新文件核验通过后,旧文件移入回收站" - /> -
-
-
- -
-
-
- set({ target: { mode: "auto" } })} - title="自动" - hint={ - profileLabel - ? `按来源「${profileLabel}」的目标目录与分类规则整理` - : "已绑定的作品沿用原目录,否则按作品类型选媒体库目录" - } - /> - {libRoots.map((r) => ( - set({ target: { mode: "root", rootId: r.id } })} - title={r.name} - hint={ - - {r.hostPath} - {r.usage ? ` · 剩余 ${formatBytes(r.usage.freeBytes, 1)}` : ""} - - } - /> - ))} - set({ target: { mode: "dir", ref: t.mode === "dir" ? t.ref : { root: "", path: "" } } })} - title="指定目录" - hint="文件按模板命名后直接放进这个目录,不建路径绑定" - /> - {t.mode === "dir" ? ( - set({ target: { mode: "dir", ref } })} - className="max-h-64" - /> - ) : null} -
-
- -
-
- set({ naming: "template" })} - title="按命名模板" - hint={ - <> - 模板按 作品覆盖 → 媒体库目录 → 全局 取第一个,预览里显示实际来源。 - - 去设置 - - - } - /> - set({ naming: "keep" })} - title="保留原文件名" - hint="目录仍按模板或已有绑定" - /> -
-
- -
-
- - 刷新元数据 - 按字段刷新,锁定的字段不变 - - set({ refreshMetadata: v })} - aria-label="整理后刷新元数据" - /> -
-

- 同目录同名字幕和外挂音轨会随视频整理,预览里列出。现有 NFO、图片不搬入媒体库,可在整理后刮削生成。 -

-
-
-
- ); -}