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:
1 parent
ea4c384664
commit
d3ab54f2bd
34 files changed
+1083
-123
No files matched your search
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user