feat: v0.3.8–v0.3.14 工作区汇总(依赖升级、整理来源、手动入库、原地入库等)

生产 v0.3.8~v0.3.13 均由 8013196 加工作区修改构建部署,此前未提交;本提交补齐源码,
并含未部署的 v0.3.14 原地入库。中间各版本源码状态未保存,无法逐版拆分,详见
docs/plan/17-full-upgrade.md 与 docs/plan/18-organize-release.md。

- v0.3.8 全面升级:Deno workspace 依赖、Router 8 / Vite 8、camofox 1.18.0 / Node 24、
  PostgreSQL 18.6、备份客户端 18.6、演示 qB 5.2.4
- v0.3.9 媒体整理:分类规则、整理来源与目录快照(0059)、文件管理整理入口
- v0.3.10 手动下载与入库:独立手动下载、详情页资源下载、TMDB / Local 元数据兜底
  (metadata.resolve_organize)、正常多版本 / 一次性洗版(0060)
- v0.3.11 搜索详情与桌面黑边修复
- v0.3.12 列表分页、搜索历史、首页概况
- v0.3.13 统一整理弹窗与队列
- v0.3.14 原地入库(0061,未部署)

同批审计修复:
- 刮削入队:确认原记录存在后才删重放产生的新记录;写回 run 关联失败时保持 queued,
  由处理器补关联(对齐 MP 先登记、只在入队失败时记失败)
- 识别升级链只在结果更确定时采用,不再用 no_match 顶替原有歧义候选
- bluray_dir 资产以原盘目录本身为刮削根(MP 同做法)
- metadata.resolve_organize 登记到入库排障 Skill 与能力地图
- db/src/types.ts 按迁移重新生成;contracts 整理动作枚举顺序与数据库对齐
- 测试:原地入库用例改为直接查记录表;domain 测试模板补 version
- 前端:通知铃铛弹层与用户菜单统一样式;站点卡片共用卡头、底栏贴底、分页间距

依赖升级证据只提交摘要文件,registry-snapshot.json 移至 .local/(SHA-256 记于文档)。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-02 15:47:17 +08:00
1 parent 8013196e3f
commit ceb66f8c77
265 files changed
+17751 -2483

No files matched your search

+87 -62
View File
@@ -43,6 +43,7 @@ import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { PaginatedList } from "~/components/console/paginated-list.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { TechInfo } from "~/components/console/tech-info.tsx";
import { DROPPED_STATUS, droppedReason, PURPOSE_LABEL } from "~/components/downloads/plan-format.ts";
@@ -143,8 +144,8 @@ function PathsPanel({ detail }: { detail: LibraryDetail }) {
) : (
<div className="space-y-4">
{bindings.length ? (
<ul className="space-y-2">
{bindings.map((b) => {
<PaginatedList rows={bindings} as="ul" label="目录绑定" resetKey={detail.entity.id} className="space-y-2">
{(b) => {
const root = rootOf(b.libraryRootId);
return (
<li key={b.id} className="rounded-lg border border-border/70 px-3 py-2">
@@ -161,39 +162,41 @@ function PathsPanel({ detail }: { detail: LibraryDetail }) {
</p>
</li>
);
})}
</ul>
}}
</PaginatedList>
) : null}
{detail.assets.map((a) => (
<div key={a.asset.id} className="rounded-lg border border-border/70 px-3 py-2">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium text-foreground">{deliveryLine(a.asset) || "未知规格"}</span>
{a.asset.status !== "active" ? <StatusBadge tone="warning">{a.asset.status}</StatusBadge> : null}
{a.asset.verifiedAt ? null : <StatusBadge>未核验</StatusBadge>}
<PaginatedList rows={detail.assets} label="文件资产" resetKey={detail.entity.id} className="space-y-4">
{(a) => (
<div key={a.asset.id} className="rounded-lg border border-border/70 px-3 py-2">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium text-foreground">{deliveryLine(a.asset) || "未知规格"}</span>
{a.asset.status !== "active" ? <StatusBadge tone="warning">{a.asset.status}</StatusBadge> : null}
{a.asset.verifiedAt ? null : <StatusBadge>未核验</StatusBadge>}
</div>
<ul className="mt-2 space-y-1.5">
{a.locations.map((l) => (
<li key={l.id} className="min-w-0">
<div className="flex flex-wrap items-center gap-1.5">
{l.locationKind !== "library" ? (
<Chip>{LOCATION_KIND[l.locationKind] ?? l.locationKind}</Chip>
) : null}
{l.status !== "available" ? <Tone v={LOCATION_STATUS[l.status]} /> : null}
{l.libraryRootId ? (
<span className="text-meta text-muted-foreground">
{rootOf(l.libraryRootId)?.name ?? "媒体库目录"}
</span>
) : null}
</div>
<p className="mt-0.5 font-mono text-meta break-all text-muted-foreground">
{l.absolutePath ?? l.relativePath}
</p>
</li>
))}
{a.locations.length === 0 ? <li className="text-meta text-muted-foreground">没有登记位置</li> : null}
</ul>
</div>
<ul className="mt-2 space-y-1.5">
{a.locations.map((l) => (
<li key={l.id} className="min-w-0">
<div className="flex flex-wrap items-center gap-1.5">
{l.locationKind !== "library" ? (
<Chip>{LOCATION_KIND[l.locationKind] ?? l.locationKind}</Chip>
) : null}
{l.status !== "available" ? <Tone v={LOCATION_STATUS[l.status]} /> : null}
{l.libraryRootId ? (
<span className="text-meta text-muted-foreground">
{rootOf(l.libraryRootId)?.name ?? "媒体库目录"}
</span>
) : null}
</div>
<p className="mt-0.5 font-mono text-meta break-all text-muted-foreground">
{l.absolutePath ?? l.relativePath}
</p>
</li>
))}
{a.locations.length === 0 ? <li className="text-meta text-muted-foreground">没有登记位置</li> : null}
</ul>
</div>
))}
)}
</PaginatedList>
</div>
)}
</Panel>
@@ -226,11 +229,9 @@ function IdentityPanel({ detail }: { detail: LibraryDetail }) {
className="py-8"
/>
) : (
<ul className="space-y-2">
{list.map((i) => (
<IdentityRow key={i.id} i={i} />
))}
</ul>
<PaginatedList rows={list} as="ul" label="外部身份" resetKey={detail.entity.id} className="space-y-2">
{(i) => <IdentityRow key={i.id} i={i} />}
</PaginatedList>
)}
<IdentityDialog
entityId={detail.entity.id}
@@ -530,8 +531,13 @@ function SlotsPanel({ detail, units }: { detail: LibraryDetail; units: ContentUn
const assetOf = (id: string | null) => detail.assets.find((a) => a.asset.id === id);
return (
<Panel title="收录版本" description="每个目标单独判断硬条件、软偏好和升级">
<div className="grid gap-3 xl:grid-cols-2">
{detail.slots.map((s) => {
<PaginatedList
rows={detail.slots}
label="收录版本"
resetKey={detail.entity.id}
className="grid gap-3 xl:grid-cols-2"
>
{(s) => {
const cur = assetOf(s.currentAssetId);
const unit = units.find((u) => u.id === s.contentUnitId);
return (
@@ -543,8 +549,8 @@ function SlotsPanel({ detail, units }: { detail: LibraryDetail; units: ContentUn
candidates={detail.candidates.filter((c) => c.slotId === s.id).slice(0, 5)}
/>
);
})}
</div>
}}
</PaginatedList>
</Panel>
);
}
@@ -747,8 +753,14 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
}
>
{rows.length > 1 ? (
<fieldset aria-label="按收录版本看状态" className="m-0 mb-3 flex min-w-0 flex-wrap gap-1.5 border-0 p-0">
{rows.map((r, i) => (
<PaginatedList
rows={rows}
as="fieldset"
label="按收录版本看状态"
resetKey={e.id}
className="m-0 mb-3 flex min-w-0 flex-wrap gap-1.5 border-0 p-0"
>
{(r, i) => (
<button
key={r.slotId ?? "any"}
type="button"
@@ -762,8 +774,8 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
>
{r.name}
</button>
))}
</fieldset>
)}
</PaginatedList>
) : null}
{seasonNums.length === 0 ? (
<EmptyState
@@ -774,8 +786,8 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
className="py-8"
/>
) : (
<div className="space-y-2">
{seasonNums.map((n) => {
<PaginatedList rows={seasonNums} label="季列表" resetKey={`${e.id}:${rowIdx}`} className="space-y-2">
{(n) => {
const su = seasonUnitOf(n);
const eps = episodes.filter((u) => u.seasonNumber === n);
const total = su?.episodeTotal ?? (eps.length || null);
@@ -810,8 +822,15 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
{eps.length === 0 ? (
<p className="text-meta text-muted-foreground">这一季还没有集信息。</p>
) : (
<ul className="divide-y divide-border/60">
{eps.map((u) => {
<PaginatedList
rows={eps}
as="ul"
compact
label={`第 ${n} 季集列表`}
resetKey={`${e.id}:${n}:${rowIdx}`}
className="divide-y divide-border/60"
>
{(u) => {
const v = (f: string) => finalOf(u, f)?.value;
const str = (x: unknown) => (typeof x === "string" && x ? x : null);
const num = (x: unknown) => (typeof x === "number" ? x : null);
@@ -843,13 +862,13 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
}}
/>
);
})}
</ul>
}}
</PaginatedList>
)}
</SeasonSection>
);
})}
</div>
}}
</PaginatedList>
)}
<UnplacedVersions versions={versions.data?.unplaced ?? []} />
<UnitFieldDialog entityId={e.id} unit={editing} onOpenChange={(o) => !o && setEditing(null)} />
@@ -949,8 +968,14 @@ function DownloadsPanel({ detail }: { detail: LibraryDetail }) {
{dropped.length ? (
<div className={cn("px-4 py-3 sm:px-5", detail.downloads.length ? "border-t border-border/60" : "")}>
<p className="mb-2 text-meta text-muted-foreground">没提交到下载器</p>
<ul className="space-y-2">
{dropped.map(({ plan, releaseTitle }) => {
<PaginatedList
rows={dropped}
as="ul"
label="未提交下载计划"
resetKey={detail.entity.id}
className="space-y-2"
>
{({ plan, releaseTitle }) => {
const st = DROPPED_STATUS[plan.status as keyof typeof DROPPED_STATUS] ?? DROPPED_STATUS.failed;
const why = droppedReason(plan.statusReason);
return (
@@ -977,15 +1002,15 @@ function DownloadsPanel({ detail }: { detail: LibraryDetail }) {
/>
</li>
);
})}
</ul>
}}
</PaginatedList>
</div>
) : null}
{manual.length ? (
<div className="border-t border-border/60 px-4 py-3 sm:px-5">
<p className="mb-2 text-meta text-muted-foreground">其他整理记录</p>
<ul className="space-y-1.5">
{manual.map((r) => (
<PaginatedList rows={manual} as="ul" label="其他整理记录" resetKey={detail.entity.id} className="space-y-1.5">
{(r) => (
<li key={r.id} className="flex min-w-0 flex-wrap items-center gap-2">
<Tone v={ORG_STATUS[r.status]} />
<Link
@@ -1000,8 +1025,8 @@ function DownloadsPanel({ detail }: { detail: LibraryDetail }) {
{r.fileTotal} 个文件 · {formatDateTime(r.createdAt)}
</span>
</li>
))}
</ul>
)}
</PaginatedList>
</div>
) : null}
</Panel>