feat(web): W3-2 library, subscriptions and rules, preferences, decisions inbox, identity and metadata
- library list (kind/status/search, cursor paging) and detail: paths and roots, external identity mapping with lock, field-level metadata sources and locks, slot cards with rejection reasons, content units (provisional flag), downloads with verification; remap/lock/refresh/merge actions - subscriptions: list/create/update, slot rules editor (hard/soft/upgrade/completion), policy revision draft -> preview -> publish (grant warning) -> rollback - preferences: tag CRUD, disable/enable, suppression records and lift, history timeline - inbox: grouped by entity and kind, evidence/suggestion/impact/preview, resolve options, SSE refresh - identity: pool assign/dismiss/retry, external identity revisions and lock, title alias management - all responses validated against contracts zod schemas Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e5e42b1f0d
commit
ef73abe3df
25 files changed
+6670
-19
No files matched your search
@@ -0,0 +1,124 @@
|
||||
import type { ExternalIdentity, IdentityRevision } from "@umt/contracts";
|
||||
import { ExternalLink, Lock, LockOpen } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useIdentityRevisions, useUpdateIdentity } from "~/api/identity.ts";
|
||||
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { JsonView } from "~/components/console/json-view.tsx";
|
||||
import { KvList } from "~/components/console/kv-list.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { Timeline } from "~/components/console/timeline.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
import { EVIDENCE_LEVEL, IDENTITY_STATUS, VERIFY_STATUS } from "./labels.ts";
|
||||
import { Tone } from "./parts.tsx";
|
||||
|
||||
const ACTION: Record<IdentityRevision["action"], string> = {
|
||||
created: "创建",
|
||||
status_changed: "状态变更",
|
||||
relinked: "重新关联",
|
||||
evidence_updated: "证据更新",
|
||||
locked: "加人工锁",
|
||||
unlocked: "解除人工锁",
|
||||
};
|
||||
const ACTOR: Record<string, string> = { user: "用户", agent: "Agent", system: "系统", api_key: "API Key" };
|
||||
|
||||
export const identityRef = (i: Pick<ExternalIdentity, "provider" | "entityType" | "externalId">) =>
|
||||
`${i.provider}:${i.entityType}:${i.externalId}`;
|
||||
|
||||
function Revisions({ id }: { id: string }) {
|
||||
const q = useIdentityRevisions(id);
|
||||
if (q.isPending) return <LoadingState rows={2} />;
|
||||
if (q.isError) return <ErrorState plain error={q.error} onRetry={() => q.refetch()} />;
|
||||
if (q.data.length === 0) return <EmptyState plain title="没有修订记录" className="py-6" />;
|
||||
const items = [...q.data].sort((a, b) => b.revision - a.revision);
|
||||
return (
|
||||
<Timeline
|
||||
items={items.map((r) => ({
|
||||
id: r.id,
|
||||
at: r.createdAt,
|
||||
tone: r.toStatus ? IDENTITY_STATUS[r.toStatus].tone : "neutral",
|
||||
title: `r${r.revision} · ${ACTION[r.action]}${
|
||||
r.fromStatus || r.toStatus
|
||||
? `:${r.fromStatus ? IDENTITY_STATUS[r.fromStatus].label : "无"} → ${r.toStatus ? IDENTITY_STATUS[r.toStatus].label : "无"}`
|
||||
: ""
|
||||
}`,
|
||||
meta: `${ACTOR[r.actorType] ?? r.actorType}${r.actorId ? ` ${r.actorId}` : ""}`,
|
||||
description: r.reason ?? undefined,
|
||||
children: (
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<JsonView label="修订前" value={r.before} defaultOpen={false} maxHeight="12rem" />
|
||||
<JsonView label="修订后" value={r.after} defaultOpen={false} maxHeight="12rem" />
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExternalIdentitySheet({ item, onClose }: { item: ExternalIdentity | null; onClose: () => void }) {
|
||||
const update = useUpdateIdentity();
|
||||
if (!item) return null;
|
||||
const toggleLock = async () => {
|
||||
try {
|
||||
await update.mutateAsync({
|
||||
id: item.id,
|
||||
expectedRevision: item.revision,
|
||||
locked: !item.locked,
|
||||
reason: item.locked ? "控制台解除人工锁" : "控制台加人工锁",
|
||||
});
|
||||
toast.success(item.locked ? "已解除人工锁,自动匹配可再次修订" : "已加人工锁,自动匹配不会改动");
|
||||
onClose();
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err));
|
||||
}
|
||||
};
|
||||
const target = item.mediaEntityId;
|
||||
return (
|
||||
<DetailSheet
|
||||
open
|
||||
width="lg"
|
||||
onOpenChange={(o) => !o && onClose()}
|
||||
title={identityRef(item)}
|
||||
description="外部身份是辅助映射;本地 UUID 才是主身份"
|
||||
footer={
|
||||
<Button variant="outline" onClick={toggleLock} disabled={update.isPending || item.status === "retired"}>
|
||||
{item.locked ? <LockOpen /> : <Lock />}
|
||||
{item.locked ? "解除人工锁" : "加人工锁"}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<KvList
|
||||
items={[
|
||||
{ label: "状态", value: <Tone v={IDENTITY_STATUS[item.status]} /> },
|
||||
{ label: "证据等级", value: EVIDENCE_LEVEL[item.evidenceLevel] ?? item.evidenceLevel },
|
||||
{ label: "人工锁", value: item.locked ? "已锁定" : "未锁定" },
|
||||
{ label: "外部校验", value: <Tone v={VERIFY_STATUS[item.verifyStatus]} /> },
|
||||
{ label: "校验时间", value: formatDateTime(item.verifiedAt) },
|
||||
{ label: "修订号", value: `r${item.revision}` },
|
||||
{
|
||||
label: "关联",
|
||||
value: target ? (
|
||||
<Link
|
||||
to={consolePath("library", target)}
|
||||
className="inline-flex items-center gap-1 underline-offset-4 hover:underline"
|
||||
>
|
||||
查看作品
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Link>
|
||||
) : (
|
||||
`内容单元 ${item.contentUnitId ?? "—"}`
|
||||
),
|
||||
},
|
||||
{ label: "有效期", value: item.validUntil ? `至 ${formatDateTime(item.validUntil)}` : "长期" },
|
||||
]}
|
||||
/>
|
||||
<JsonView label="证据" value={Object.keys(item.evidence).length ? item.evidence : null} />
|
||||
<DetailSection title="修订历史">
|
||||
<Revisions id={item.id} />
|
||||
</DetailSection>
|
||||
</DetailSheet>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user