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

+74 -32
View File
@@ -5,6 +5,7 @@ import * as auth from "./auth.ts";
import { CapabilityDescriptor, KnownCapabilityId } from "./capability.ts";
import { type ApiScope, CursorQuery, cursorPage, HealthStatus, Id, IdParam } from "./common.ts";
import * as content from "./content.ts";
import * as events from "./events.ts";
import * as execution from "./execution.ts";
import * as identity from "./identity.ts";
import * as instances from "./instances.ts";
@@ -111,10 +112,16 @@ const instanceTest = (path: string, summary: string) =>
ep({ method: "post", path, scope: "admin", summary, params: IdParam, response: instances.InstanceTestResult });
/** 入队后台任务的受理结果(taskId = 引擎 run id) */
const Accepted = z.object({ taskId: Id });
export const TaskAccepted = z.object({ taskId: Id });
export type TaskAccepted = z.infer<typeof TaskAccepted>;
/** 人工指定来源资源身份的结果 */
const ReleaseIdentityResult = z.object({ mediaEntityId: Id, contentUnitIds: z.array(Id) });
/** 人工指定来源资源身份的结果(待识别池 assign / source-releases identity) */
export const ReleaseIdentityResult = z.object({ mediaEntityId: Id, contentUnitIds: z.array(Id) });
export type ReleaseIdentityResult = z.infer<typeof ReleaseIdentityResult>;
/** 身份修订历史 */
export const IdentityRevisionList = z.object({ items: z.array(identity.IdentityRevision) });
export type IdentityRevisionList = z.infer<typeof IdentityRevisionList>;
/** 字段覆盖/取消覆盖的结果 */
const FieldOverrideResult = z.object({
@@ -128,7 +135,7 @@ const FieldParam = z.object({ id: Id, field: content.EntityMetadataField });
const EngineRunIdParam = z.object({ id: z.string().min(1).max(200) });
/** 待处理事项聚合分组 */
const DecisionSummaryGroup = z.object({
export const DecisionSummaryGroup = z.object({
kind: agent.DecisionKind,
mediaEntityId: Id.nullable(),
mediaEntityTitle: z.string().nullable(),
@@ -136,6 +143,9 @@ const DecisionSummaryGroup = z.object({
maxSeverity: z.enum(["info", "warning", "critical"]),
latestAt: z.string(),
});
export type DecisionSummaryGroup = z.infer<typeof DecisionSummaryGroup>;
export const DecisionSummary = z.object({ total: z.number().int(), groups: z.array(DecisionSummaryGroup) });
export type DecisionSummary = z.infer<typeof DecisionSummary>;
export const Api = {
// ── 内容 / 身份 ──
@@ -192,7 +202,7 @@ export const Api = {
scope: "organize.execute",
summary: "刷新元数据(字段级;用户锁优先,不动路径)",
params: IdParam,
response: Accepted,
response: TaskAccepted,
}),
setField: ep({
method: "put",
@@ -217,7 +227,6 @@ export const Api = {
"内容单元",
{
row: content.ContentUnit,
create: content.ContentUnitCreate,
update: content.ContentUnitUpdate,
filters: { mediaEntityId: Id.optional(), seasonNumber: z.coerce.number().int().optional() },
},
@@ -230,7 +239,6 @@ export const Api = {
{
row: content.TitleAlias,
create: content.TitleAliasCreate,
update: content.TitleAliasUpdate,
filters: { mediaEntityId: Id.optional(), q: z.string().max(200).optional() },
},
R,
@@ -259,17 +267,16 @@ export const Api = {
scope: "read",
summary: "身份修订历史",
params: IdParam,
response: z.object({ items: z.array(identity.IdentityRevision) }),
response: IdentityRevisionList,
}),
},
// ── 文件 ──
// ── 文件(只读;写入只经整理/对账流程,不开放直接改) ──
mediaAssets: crud(
"/media-assets",
"资产",
{
row: assets.MediaAsset,
update: assets.MediaAssetUpdate,
filters: { mediaEntityId: Id.optional(), status: assets.AssetStatus.optional() },
},
R,
@@ -279,8 +286,6 @@ export const Api = {
"资产覆盖",
{
row: assets.AssetUnitCoverage,
create: assets.AssetUnitCoverageCreate,
update: assets.AssetUnitCoverageUpdate,
filters: { mediaAssetId: Id.optional(), contentUnitId: Id.optional() },
},
R,
@@ -290,7 +295,6 @@ export const Api = {
"资产位置",
{
row: assets.AssetLocation,
update: assets.AssetLocationUpdate,
filters: { mediaAssetId: Id.optional(), status: assets.LocationStatus.optional() },
},
R,
@@ -300,8 +304,6 @@ export const Api = {
"路径绑定",
{
row: assets.PathBinding,
create: assets.PathBindingCreate,
update: assets.PathBindingUpdate,
filters: { mediaEntityId: Id.optional(), status: assets.BindingStatus.optional() },
},
R,
@@ -405,7 +407,7 @@ export const Api = {
summary: "立即同步站点快照(受站点限流)",
params: IdParam,
body: z.object({ maxPages: z.number().int().min(1).max(20).optional() }),
response: Accepted,
response: TaskAccepted,
}),
snapshots: ep({
method: "get",
@@ -544,7 +546,7 @@ export const Api = {
scope: "organize.execute",
summary: "解除来源资源的人工锁并重新识别",
params: IdParam,
response: Accepted,
response: TaskAccepted,
}),
},
observations: crud(
@@ -565,7 +567,13 @@ export const Api = {
row: policy.Subscription,
create: policy.SubscriptionCreate,
update: policy.SubscriptionUpdate,
filters: { status: policy.SubscriptionStatus.optional(), mediaEntityId: Id.optional() },
filters: {
status: policy.SubscriptionStatus.optional(),
mediaEntityId: Id.optional(),
mediaKind: policy.SubscriptionMediaKind.optional(),
/** 作品标题 / 原名 / 别名 / 未建档检索词,模糊匹配 */
q: z.string().max(200).optional(),
},
},
R,
),
@@ -614,7 +622,11 @@ export const Api = {
row: policy.AcquisitionSlot,
create: policy.AcquisitionSlotCreate,
update: policy.AcquisitionSlotUpdate,
filters: { mediaEntityId: Id.optional(), status: policy.SlotStatus.optional() },
filters: {
mediaEntityId: Id.optional(),
status: policy.SlotStatus.optional(),
subscriptionId: Id.optional(),
},
},
R,
),
@@ -632,6 +644,14 @@ export const Api = {
),
/** 删除 = 标记 deleted + 写抑制记录 */
remove: del("/preference-tags/{id}", "删除偏好标签(留抑制记录)"),
revisions: ep({
method: "get",
path: "/preference-tags/{id}/revisions",
scope: "read",
summary: "偏好标签修订历史(新→旧;取自审计事件)",
params: IdParam,
response: policy.PreferenceTagRevisionList,
}),
},
preferenceSuppressions: crud(
"/preference-suppressions",
@@ -686,15 +706,26 @@ export const Api = {
response: z.object({ items: z.array(execution.AssetOperation) }),
}),
},
acquisitionPlans: crud(
"/acquisition-plans",
"下载计划",
{
row: execution.AcquisitionPlan,
filters: { slotId: Id.optional(), status: execution.PlanStatus.optional() },
},
R,
),
acquisitionPlans: {
...crud(
"/acquisition-plans",
"下载计划",
{
row: execution.AcquisitionPlan,
filters: { slotId: Id.optional(), status: execution.PlanStatus.optional() },
},
R,
),
create: ep({
method: "post",
path: "/acquisition-plans",
scope: "organize.execute",
summary:
"手动提交下载:与自动收录同一流程(download.plan → download.submit,含硬条件/预算/缓存阈值/授权检查);未生成计划时返回拒绝原因",
body: execution.ManualAcquisitionRequest,
response: execution.ManualAcquisitionResult,
}),
},
downloadTasks: {
...crud(
"/download-tasks",
@@ -753,7 +784,7 @@ export const Api = {
scope: "read",
summary: "未关闭待处理事项按作品与问题类型聚合",
query: z.object({ status: agent.DecisionStatus.optional() }),
response: z.object({ total: z.number().int(), groups: z.array(DecisionSummaryGroup) }),
response: DecisionSummary,
}),
},
approvals: {
@@ -849,7 +880,6 @@ export const Api = {
"校验运行",
{
row: ops.ReconcileRun,
create: ops.ReconcileRunCreate,
filters: { kind: ops.ReconcileKind.optional(), status: ops.ReconcileStatus.optional() },
},
R,
@@ -859,7 +889,6 @@ export const Api = {
"库存映射",
{
row: ops.ServerMapping,
update: ops.ServerMappingUpdate,
filters: {
mediaServerInstanceId: Id.optional(),
mediaEntityId: Id.optional(),
@@ -1056,6 +1085,19 @@ export const Api = {
}),
},
// ── 事件 ──
events: {
list: ep({
method: "get",
path: "/events",
scope: "read",
summary:
"事件流:控制台用 SSE(text/event-stream,cookie 会话);API Key 调用者带 limit 取 JSON 补读页(after=上次 nextAfter)",
query: events.EventReplayQuery,
response: events.EventReplayPage,
}),
},
// ── 系统 ──
system: {
health: ep({
@@ -1137,7 +1179,7 @@ export const Api = {
scope: "organize.execute",
summary: "立即重新识别",
params: IdParam,
response: Accepted,
response: TaskAccepted,
}),
},