- settings.agent.autoInvestigateMode investigate|recover, investigateMaxAttempts, recoverVerifySec - new capability download.recheck (execute, replay never): qB force recheck / resume, no source swap/delete - autoInvestigate moves to agent/investigate.ts: recover mode runs execute with a recovery allowlist only when an active grant (published policy if bound) covers the recovery actions; otherwise degrades to read-only investigation and leaves suggestion + recheck option on the decision - runAgent verifies recovery against business facts (download_task/task/source_release) before finalize; verified -> decision resolved by system:auto-recover, run summary '已恢复(系统核实)'; otherwise stays awaiting_user; per-root-cause attempt cap - e2e E6: missingFiles -> download_failed -> recheck -> verified; no grant degrades; swap rejected; dedupe; attempts exhausted; budget exceeded keeps attention Co-Authored-By: Claude Opus 5.5 <[email protected]>
255 lines
12 KiB
TypeScript
255 lines
12 KiB
TypeScript
import { z } from "zod";
|
|
import { MediaAsset } from "./assets.ts";
|
|
import { CAPABILITY_CATALOG, type CapabilitySpec, type KnownCapabilityId } from "./capability.ts";
|
|
import { Id, JsonObject, NonNegInt } from "./common.ts";
|
|
import { MediaEntity, MediaKind } from "./content.ts";
|
|
import { AcquisitionPlan, DownloadStatus, DownloadTask, Task, TaskStatus } from "./execution.ts";
|
|
import { ExternalEntityType, ExternalIdentity } from "./identity.ts";
|
|
import { ServerMapping } from "./ops.ts";
|
|
import { OrganizationAction, OrganizationRecord } from "./organization.ts";
|
|
import { AcquisitionSlot, PolicyRevision } from "./policy.ts";
|
|
import { SourceRelease } from "./sources.ts";
|
|
|
|
// 首批能力的输入/输出 schema;名字与 CAPABILITY_CATALOG 中的 inputSchema/outputSchema 一致。
|
|
// 写能力的输入只接受计划/对象 ID,不接受模型自拟的幂等键或任意路径。
|
|
|
|
const Limit = z.number().int().min(1).max(100).default(20);
|
|
|
|
/** 写能力的通用受理结果:返回 task/plan ID,由任务页追踪(REQ §12.3) */
|
|
export const Accepted = z.object({ taskId: Id, planId: Id.nullable() });
|
|
export type Accepted = z.infer<typeof Accepted>;
|
|
|
|
export const CatalogLookupInput = z
|
|
.object({
|
|
query: z.string().min(1).max(200).optional(),
|
|
externalId: z.object({ provider: z.string(), entityType: ExternalEntityType, externalId: z.string() }).optional(),
|
|
kind: MediaKind.optional(),
|
|
year: z.number().int().optional(),
|
|
includeRemote: z.boolean().default(false),
|
|
limit: Limit,
|
|
})
|
|
.refine((v) => !!v.query || !!v.externalId, { message: "query 与 externalId 至少给一个" });
|
|
export const CatalogLookupOutput = z.object({
|
|
local: z.array(z.object({ entity: MediaEntity, score: z.number(), matchedAlias: z.string().nullable() })),
|
|
remote: z.array(
|
|
z.object({
|
|
provider: z.string(),
|
|
entityType: ExternalEntityType,
|
|
externalId: z.string(),
|
|
title: z.string(),
|
|
year: z.number().int().nullable(),
|
|
}),
|
|
),
|
|
});
|
|
|
|
export const ReleaseSearchInput = z.object({
|
|
query: z.string().min(1).max(200).optional(),
|
|
mediaEntityId: Id.optional(),
|
|
siteIds: z.array(Id).optional(),
|
|
/** 本地快照不足时是否实时补查 */
|
|
allowLiveSearch: z.boolean().default(true),
|
|
limit: Limit,
|
|
});
|
|
export const ReleaseSearchOutput = z.object({
|
|
items: z.array(SourceRelease),
|
|
/** 各站点快照新鲜度与是否实时补查 */
|
|
coverage: z.array(
|
|
z.object({ siteId: Id, fromSnapshot: z.boolean(), live: z.boolean(), error: z.string().nullable() }),
|
|
),
|
|
});
|
|
|
|
export const ReleaseInspectInput = z.object({ sourceReleaseId: Id, refreshDetail: z.boolean().default(false) });
|
|
export const ReleaseInspectOutput = z.object({ release: SourceRelease, inventory: z.array(MediaAsset) });
|
|
|
|
export const IdentityResolveInput = z
|
|
.object({
|
|
observationId: Id.optional(),
|
|
sourceReleaseId: Id.optional(),
|
|
candidate: z
|
|
.object({
|
|
mediaEntityId: Id.optional(),
|
|
provider: z.string().optional(),
|
|
entityType: ExternalEntityType.optional(),
|
|
externalId: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
evidenceIds: z.array(Id).default([]),
|
|
})
|
|
.refine((v) => !!v.observationId || !!v.sourceReleaseId, { message: "需要 observationId 或 sourceReleaseId" });
|
|
export const IdentityResolveOutput = z.object({
|
|
status: z.enum(["resolved", "proposed", "ambiguous", "unidentifiable"]),
|
|
mediaEntityId: Id.nullable(),
|
|
identities: z.array(ExternalIdentity),
|
|
decisionId: Id.nullable(),
|
|
});
|
|
|
|
export const DownloadPlanInput = z.object({ slotId: Id, sourceReleaseId: Id });
|
|
export const DownloadPlanOutput = z.object({
|
|
plan: AcquisitionPlan.nullable(),
|
|
slot: AcquisitionSlot,
|
|
/** 硬条件不满足或预算/阈值阻止时的原因 */
|
|
rejected: z.array(z.object({ code: z.string(), message: z.string() })),
|
|
requiresApproval: z.boolean(),
|
|
});
|
|
|
|
export const DownloadSubmitInput = z.object({ planId: Id, approvalId: Id.optional() });
|
|
export const DownloadSubmitOutput = Accepted.extend({ downloadTask: DownloadTask.nullable() });
|
|
|
|
export const OrganizePlanInput = z
|
|
.object({
|
|
downloadTaskId: Id.optional(),
|
|
mediaAssetIds: z.array(Id).optional(),
|
|
action: OrganizationAction.default("copy"),
|
|
targetLibraryRootId: Id.optional(),
|
|
})
|
|
.refine((v) => !!v.downloadTaskId || !!v.mediaAssetIds?.length, { message: "需要 downloadTaskId 或 mediaAssetIds" });
|
|
export const OrganizePlanOutput = z.object({
|
|
planId: Id,
|
|
payloadHash: z.string(),
|
|
files: z.array(
|
|
z.object({
|
|
sourcePath: z.string(),
|
|
targetPath: z.string().nullable(),
|
|
action: OrganizationAction,
|
|
skipReason: z.string().nullable(),
|
|
}),
|
|
),
|
|
conflicts: z.array(z.object({ code: z.string(), message: z.string(), path: z.string().nullable() })),
|
|
requiresApproval: z.boolean(),
|
|
});
|
|
|
|
export const OrganizeExecuteInput = z.object({ planId: Id, payloadHash: z.string().min(1), approvalId: Id.optional() });
|
|
export const OrganizeExecuteOutput = Accepted.extend({ record: OrganizationRecord.nullable() });
|
|
|
|
/** 授权内恢复:强制重新校验(missingFiles / error 之后)或恢复暂停;换源、删除、清理不在此能力内 */
|
|
export const DownloadRecheckAction = z.enum(["recheck", "resume"]);
|
|
export const DownloadRecheckInput = z.object({
|
|
downloadTaskId: Id,
|
|
action: DownloadRecheckAction.default("recheck"),
|
|
reason: z.string().max(500).optional(),
|
|
});
|
|
export const DownloadRecheckOutput = z.object({
|
|
downloadTaskId: Id,
|
|
action: DownloadRecheckAction,
|
|
statusBefore: DownloadStatus,
|
|
rawStateBefore: z.string().nullable(),
|
|
/** 已向下载器发出请求;是否恢复以后续同步到的 download_task 状态为准 */
|
|
requested: z.boolean(),
|
|
});
|
|
|
|
export const TaskInspectInput = z.object({ taskId: Id });
|
|
export const TaskInspectOutput = z.object({
|
|
task: Task,
|
|
children: z.array(Task),
|
|
timeline: z.array(z.object({ at: z.string(), type: z.string(), summary: z.string(), data: JsonObject.optional() })),
|
|
});
|
|
|
|
export const TaskRetryInput = z.object({ taskId: Id, reason: z.string().max(500).optional() });
|
|
export const TaskRetryOutput = z.object({ taskId: Id, status: TaskStatus });
|
|
|
|
export const TaskCancelInput = z.object({ taskId: Id, reason: z.string().min(1).max(500) });
|
|
export const TaskCancelOutput = z.object({ taskId: Id, status: TaskStatus, cancelRequested: z.boolean() });
|
|
|
|
export const PolicyPreviewInput = z.object({
|
|
policyRevisionId: Id,
|
|
sampleLimit: z.number().int().min(1).max(1000).default(200),
|
|
});
|
|
export const PolicyPreviewOutput = z.object({
|
|
newlyMatched: NonNegInt,
|
|
newlyExcluded: NonNegInt,
|
|
samples: z.array(z.object({ sourceReleaseId: Id, before: z.string(), after: z.string(), reason: z.string() })),
|
|
});
|
|
|
|
export const PolicyApplyInput = z.object({
|
|
policyRevisionId: Id,
|
|
expectedRevision: z.number().int().min(1),
|
|
approvalId: Id.optional(),
|
|
});
|
|
export const PolicyApplyOutput = z.object({ policy: PolicyRevision });
|
|
|
|
export const InventoryQueryInput = z.object({
|
|
mediaEntityId: Id.optional(),
|
|
query: z.string().max(200).optional(),
|
|
includeServerInventory: z.boolean().default(false),
|
|
limit: Limit,
|
|
});
|
|
export const InventoryQueryOutput = z.object({
|
|
items: z.array(
|
|
z.object({
|
|
entity: MediaEntity,
|
|
assets: z.array(MediaAsset),
|
|
slots: z.array(AcquisitionSlot),
|
|
identities: z.array(ExternalIdentity.pick({ provider: true, entityType: true, externalId: true, status: true })),
|
|
/** 外部库存证据;"未知 ≠ 缺失" */
|
|
server: z.array(ServerMapping),
|
|
}),
|
|
),
|
|
});
|
|
|
|
/** 能力 ID → 输入/输出 schema(server 注册处理器、agent 生成工具定义时共用) */
|
|
export const CAPABILITY_IO = {
|
|
"catalog.lookup": { input: CatalogLookupInput, output: CatalogLookupOutput },
|
|
"release.search": { input: ReleaseSearchInput, output: ReleaseSearchOutput },
|
|
"release.inspect": { input: ReleaseInspectInput, output: ReleaseInspectOutput },
|
|
"identity.resolve": { input: IdentityResolveInput, output: IdentityResolveOutput },
|
|
"download.plan": { input: DownloadPlanInput, output: DownloadPlanOutput },
|
|
"download.submit": { input: DownloadSubmitInput, output: DownloadSubmitOutput },
|
|
"download.recheck": { input: DownloadRecheckInput, output: DownloadRecheckOutput },
|
|
"organize.plan": { input: OrganizePlanInput, output: OrganizePlanOutput },
|
|
"organize.execute": { input: OrganizeExecuteInput, output: OrganizeExecuteOutput },
|
|
"task.inspect": { input: TaskInspectInput, output: TaskInspectOutput },
|
|
"task.retry": { input: TaskRetryInput, output: TaskRetryOutput },
|
|
"task.cancel": { input: TaskCancelInput, output: TaskCancelOutput },
|
|
"policy.preview": { input: PolicyPreviewInput, output: PolicyPreviewOutput },
|
|
"policy.apply": { input: PolicyApplyInput, output: PolicyApplyOutput },
|
|
"inventory.query": { input: InventoryQueryInput, output: InventoryQueryOutput },
|
|
} as const satisfies Record<KnownCapabilityId, { input: z.ZodType; output: z.ZodType }>;
|
|
|
|
type CapabilityIo = typeof CAPABILITY_IO;
|
|
/** 首批能力 id 对应的运行时规格类型(input/output 为该能力的具体 schema) */
|
|
export type KnownCapabilitySpec<K extends KnownCapabilityId> = CapabilitySpec<
|
|
CapabilityIo[K]["input"],
|
|
CapabilityIo[K]["output"]
|
|
>;
|
|
|
|
/**
|
|
* 首批能力的运行时规格:CAPABILITY_CATALOG 的元数据 + CAPABILITY_IO 的 schema。
|
|
* server 各模块注册处理器时一律用它,不要手写描述:
|
|
* `registry.register(capabilitySpec("release.search"), handler)`
|
|
*/
|
|
export function capabilitySpec<K extends KnownCapabilityId>(id: K): KnownCapabilitySpec<K> {
|
|
const { inputSchema: _i, outputSchema: _o, ...meta } = CAPABILITY_CATALOG[id];
|
|
return { ...meta, inputSchema: CAPABILITY_IO[id].input, outputSchema: CAPABILITY_IO[id].output };
|
|
}
|
|
|
|
export type CatalogLookupInput = z.infer<typeof CatalogLookupInput>;
|
|
export type CatalogLookupOutput = z.infer<typeof CatalogLookupOutput>;
|
|
export type ReleaseSearchInput = z.infer<typeof ReleaseSearchInput>;
|
|
export type ReleaseSearchOutput = z.infer<typeof ReleaseSearchOutput>;
|
|
export type ReleaseInspectInput = z.infer<typeof ReleaseInspectInput>;
|
|
export type ReleaseInspectOutput = z.infer<typeof ReleaseInspectOutput>;
|
|
export type IdentityResolveInput = z.infer<typeof IdentityResolveInput>;
|
|
export type IdentityResolveOutput = z.infer<typeof IdentityResolveOutput>;
|
|
export type DownloadPlanInput = z.infer<typeof DownloadPlanInput>;
|
|
export type DownloadPlanOutput = z.infer<typeof DownloadPlanOutput>;
|
|
export type DownloadSubmitInput = z.infer<typeof DownloadSubmitInput>;
|
|
export type DownloadSubmitOutput = z.infer<typeof DownloadSubmitOutput>;
|
|
export type OrganizePlanInput = z.infer<typeof OrganizePlanInput>;
|
|
export type OrganizePlanOutput = z.infer<typeof OrganizePlanOutput>;
|
|
export type OrganizeExecuteInput = z.infer<typeof OrganizeExecuteInput>;
|
|
export type OrganizeExecuteOutput = z.infer<typeof OrganizeExecuteOutput>;
|
|
export type TaskInspectInput = z.infer<typeof TaskInspectInput>;
|
|
export type TaskInspectOutput = z.infer<typeof TaskInspectOutput>;
|
|
export type DownloadRecheckInput = z.infer<typeof DownloadRecheckInput>;
|
|
export type DownloadRecheckOutput = z.infer<typeof DownloadRecheckOutput>;
|
|
export type TaskRetryInput = z.infer<typeof TaskRetryInput>;
|
|
export type TaskRetryOutput = z.infer<typeof TaskRetryOutput>;
|
|
export type TaskCancelInput = z.infer<typeof TaskCancelInput>;
|
|
export type TaskCancelOutput = z.infer<typeof TaskCancelOutput>;
|
|
export type PolicyPreviewInput = z.infer<typeof PolicyPreviewInput>;
|
|
export type PolicyPreviewOutput = z.infer<typeof PolicyPreviewOutput>;
|
|
export type PolicyApplyInput = z.infer<typeof PolicyApplyInput>;
|
|
export type PolicyApplyOutput = z.infer<typeof PolicyApplyOutput>;
|
|
export type InventoryQueryInput = z.infer<typeof InventoryQueryInput>;
|
|
export type InventoryQueryOutput = z.infer<typeof InventoryQueryOutput>;
|