feat(server): W4-A integration endpoints and contract gaps

- POST /acquisition-plans: manual submit via download.plan -> download.submit
  (same authorization/audit/idempotency path as the agent), slotId or entityId
- read-only /media-assets, /asset-locations, /asset-unit-coverage, /path-bindings,
  /reconcile-runs, /server-mappings, /artifacts, GET /title-aliases/{id};
  unimplemented write endpoints removed from api.ts; registry test now checks both directions
- candidate evaluation upsert per (slot, source_release) + migration 0022 dedupe/unique index
- organization records expose downloadTaskId (+ list filter); inventory adds
  subscribed/candidateReasons; feed sources expose derived possibleGap
- GET /events?after=&limit= JSON replay page for API key callers (same sse_event table)
- preference tag revision history from audit_event; subscriptions/identities/aliases
  lists carry mediaEntityTitle; subscription q/mediaKind search; slot subscriptionId
  filter + currentAsset summary; policy preview samples carry title/reasons
- DecisionSummary, IdentityRevisionList, ReleaseIdentityResult, PolicyPreviewSummary exported from contracts

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 21:10:02 +08:00
1 parent ea4c384664
commit d3ab54f2bd
34 files changed
+1083 -123

No files matched your search

@@ -26,6 +26,10 @@ export function SlotCard({
const hard = summarizeHard(slot.hardConditions, slot.fallback);
const soft = summarizeSoft(slot.softPreferences);
const upgrade = summarizeUpgrade(slot.upgradeThreshold);
// 服务端按 (Slot, 来源资源) 只保留最新一次评估;这里再按资源去重兜底(列表已按评估时间新→旧)
const latest = candidates.filter(
(c, i) => candidates.findIndex((x) => x.sourceReleaseId === c.sourceReleaseId) === i,
);
return (
<article className="min-w-0 rounded-xl border border-border/80 bg-foreground/[0.02] p-4">
<header className="flex flex-wrap items-center gap-2">
@@ -70,12 +74,12 @@ export function SlotCard({
<dd className="text-caption break-words text-foreground">{currentAsset ?? "无"}</dd>
</div>
</dl>
{candidates.length ? (
{latest.length ? (
<div className="mt-3 border-t border-border/60 pt-3">
<p className="mb-2 text-meta text-muted-foreground">最近候选 · 为什么选 / 不选</p>
<p className="mb-2 text-meta text-muted-foreground">最近候选 · 为什么选 / 不选(每个资源显示最近一次评估)</p>
<ul className="space-y-2">
{candidates.map((c) => (
<li key={`${c.sourceReleaseId}-${c.evaluatedAt}`} className="min-w-0">
{latest.map((c) => (
<li key={c.sourceReleaseId} className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<Tone v={CANDIDATE_DECISION[c.decision]} />
<span className="min-w-0 font-mono text-meta break-all text-foreground">{c.title}</span>
@@ -92,7 +96,7 @@ export function SlotCard({
{c.reasons.length ? (
<p className="mt-1 text-meta break-words text-muted-foreground">{c.reasons.join(";")}</p>
) : null}
<p className="text-micro text-muted-foreground tabular-nums">{formatDateTime(c.evaluatedAt)}</p>
<p className="text-micro text-muted-foreground tabular-nums">评估于 {formatDateTime(c.evaluatedAt)}</p>
</li>
))}
</ul>