feat(agent): subscription.list, decision.list, download.inspect; grant-scoped requiresApproval
- three read-only summary capabilities (38 registered); task.inspect points download task ids to download.inspect - probe queries reject empty / single-letter / wildcard-only terms; externalId documented as string; every description says when to use it - download.plan requiresApproval uses coveringGrant (shared with authorize) Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3134e3a55e
commit
975a14ace7
7 files changed
+510
-94
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { DecisionKind, DecisionOption, DecisionStatus, Severity } from "./agent.ts";
|
||||
import { AgentOptionItem, AgentPlanStep, AgentPlanStepStatus } from "./agent-events.ts";
|
||||
import { MediaAsset } from "./assets.ts";
|
||||
import { BrowserController, BrowserDownloadVerification, BrowserSnapshot } from "./browser.ts";
|
||||
@@ -9,7 +10,14 @@ import { AcquisitionPlan, DownloadStatus, DownloadTask, Task, TaskStatus } from
|
||||
import { ExternalEntityType, ExternalIdentity } from "./identity.ts";
|
||||
import { ServerMapping } from "./ops.ts";
|
||||
import { OrganizationAction, OrganizationRecord } from "./organization.ts";
|
||||
import { AcquisitionSlot, PolicyRevision } from "./policy.ts";
|
||||
import {
|
||||
AcquisitionPriority,
|
||||
AcquisitionSlot,
|
||||
PolicyRevision,
|
||||
SubscriptionMediaKind,
|
||||
SubscriptionMode,
|
||||
SubscriptionStatus,
|
||||
} from "./policy.ts";
|
||||
import {
|
||||
SiteAccountError,
|
||||
SiteAccountOverall,
|
||||
@@ -30,14 +38,33 @@ import {
|
||||
|
||||
const Limit = z.number().int().min(1).max(100).default(20);
|
||||
|
||||
/**
|
||||
* 探查用检索词(AG-3):去掉首尾空白后不能为空,不能是单个英文字母/数字/符号(如 "a"、"*"),
|
||||
* 不能只有通配符与标点——这类查询只会拉回全表或噪声,浪费预算。中文单字片名(如「家」)允许。
|
||||
*/
|
||||
export const ProbeQuery = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(200)
|
||||
.refine((v) => !/^[ -~]$/.test(v), { message: "检索词太短:写作品名、别名或关键词,不要用单个字母或符号探查" })
|
||||
.refine((v) => !/^[\s*%?._\-+/\\|,;:!'"()[\]{}]+$/.test(v), { message: "检索词不能只有通配符或标点" })
|
||||
.describe("作品名、别名或关键词(至少一个完整词);不要用空串、单个字母或通配符探查");
|
||||
|
||||
/** 写能力的通用受理结果:返回 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(),
|
||||
query: ProbeQuery.optional(),
|
||||
externalId: z
|
||||
.object({
|
||||
provider: z.string().describe("如 tmdb、imdb、tvdb"),
|
||||
entityType: ExternalEntityType,
|
||||
externalId: z.string().describe('外部 ID 一律写成字符串,如 "693134"、"tt15239678"'),
|
||||
})
|
||||
.optional(),
|
||||
kind: MediaKind.optional(),
|
||||
year: z.number().int().optional(),
|
||||
includeRemote: z.boolean().default(false),
|
||||
@@ -62,7 +89,7 @@ export const CatalogLookupOutput = z.object({
|
||||
});
|
||||
|
||||
export const ReleaseSearchInput = z.object({
|
||||
query: z.string().min(1).max(200).optional(),
|
||||
query: ProbeQuery.optional(),
|
||||
mediaEntityId: Id.optional(),
|
||||
siteIds: z.array(Id).optional(),
|
||||
/** 本地快照不足时是否实时补查 */
|
||||
@@ -188,7 +215,8 @@ export const PolicyApplyOutput = z.object({ policy: PolicyRevision });
|
||||
|
||||
export const InventoryQueryInput = z.object({
|
||||
mediaEntityId: Id.optional(),
|
||||
query: z.string().max(200).optional(),
|
||||
/** 不给 query 也不给 mediaEntityId 时按最近更新列出 */
|
||||
query: ProbeQuery.optional(),
|
||||
includeServerInventory: z.boolean().default(false),
|
||||
limit: Limit,
|
||||
});
|
||||
@@ -205,6 +233,93 @@ export const InventoryQueryOutput = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
// ── AG-3 只读摘要 ──
|
||||
|
||||
const oneOrMany = <T extends z.ZodType>(t: T) =>
|
||||
z.union([t, z.array(t).min(1).max(10)]).transform((v) => (Array.isArray(v) ? v : [v]) as z.infer<T>[]);
|
||||
|
||||
export const SubscriptionListInput = z.object({
|
||||
/** 缺省列出全部状态 */
|
||||
status: oneOrMany(SubscriptionStatus).optional(),
|
||||
mediaKind: SubscriptionMediaKind.optional(),
|
||||
/** 按作品标题 / 检索词过滤 */
|
||||
query: ProbeQuery.optional(),
|
||||
groupBy: z.enum(["status", "mediaKind", "none"]).default("status"),
|
||||
limit: z.number().int().min(1).max(200).default(50),
|
||||
});
|
||||
export const SubscriptionBrief = z.object({
|
||||
id: Id,
|
||||
title: z.string().nullable(),
|
||||
mediaEntityId: Id.nullable(),
|
||||
mediaKind: SubscriptionMediaKind,
|
||||
mode: SubscriptionMode,
|
||||
status: SubscriptionStatus,
|
||||
priority: AcquisitionPriority,
|
||||
statusReason: z.string().nullable(),
|
||||
/** 版本目标:总数 / 已满足数 */
|
||||
slots: z.object({ total: NonNegInt, satisfied: NonNegInt }),
|
||||
lastSearchedAt: z.string().nullable(),
|
||||
nextSearchAt: z.string().nullable(),
|
||||
lastProgressAt: z.string().nullable(),
|
||||
});
|
||||
export const SubscriptionListOutput = z.object({
|
||||
/** 过滤后的总数(items 可能被 limit 截断) */
|
||||
total: NonNegInt,
|
||||
groups: z.array(z.object({ key: z.string(), count: NonNegInt })),
|
||||
items: z.array(SubscriptionBrief),
|
||||
});
|
||||
|
||||
export const DecisionListInput = z.object({
|
||||
/** 缺省只列未关闭的:open / investigating / awaiting_user */
|
||||
status: oneOrMany(DecisionStatus).optional(),
|
||||
kind: oneOrMany(DecisionKind).optional(),
|
||||
limit: z.number().int().min(1).max(100).default(20),
|
||||
});
|
||||
export const DecisionBrief = z.object({
|
||||
id: Id,
|
||||
kind: DecisionKind,
|
||||
status: DecisionStatus,
|
||||
severity: Severity,
|
||||
title: z.string(),
|
||||
summary: z.string().nullable(),
|
||||
subjectType: z.string().nullable(),
|
||||
subjectId: z.string().nullable(),
|
||||
attempts: NonNegInt,
|
||||
options: z.array(DecisionOption),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export const DecisionListOutput = z.object({
|
||||
total: NonNegInt,
|
||||
byKind: z.array(z.object({ kind: DecisionKind, count: NonNegInt })),
|
||||
items: z.array(DecisionBrief),
|
||||
});
|
||||
|
||||
export const DownloadInspectInput = z
|
||||
.object({
|
||||
downloadTaskId: Id.optional(),
|
||||
/** 种子 infohash(v1 40 位 / v2 64 位十六进制,大小写均可) */
|
||||
hash: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/)
|
||||
.optional(),
|
||||
})
|
||||
.refine((v) => !!v.downloadTaskId || !!v.hash, { message: "downloadTaskId 与 hash 至少给一个" });
|
||||
export const DownloadInspectOutput = z.object({
|
||||
downloadTask: DownloadTask,
|
||||
plan: z
|
||||
.object({ id: Id, status: z.string(), statusReason: z.string().nullable(), slotId: Id, taskId: Id.nullable() })
|
||||
.nullable(),
|
||||
release: z.object({ id: Id, title: z.string(), siteId: Id.nullable() }).nullable(),
|
||||
/** 关联业务任务(提交下载 / 核验入库):任务 ID 可交给 task.inspect / task.retry */
|
||||
tasks: z.array(z.object({ id: Id, kind: z.string(), status: TaskStatus, lastError: JsonObject.nullable() })),
|
||||
organization: z.array(z.object({ id: Id, status: z.string(), targetPath: z.string().nullable() })),
|
||||
decisions: z.array(z.object({ id: Id, kind: DecisionKind, status: DecisionStatus, title: z.string() })),
|
||||
/** 按 hash 查到多个下载器实例里的同一种子时,其余任务的 ID */
|
||||
otherMatches: z.array(Id),
|
||||
});
|
||||
|
||||
// ── 子代理(W7-B) ──
|
||||
|
||||
/** 子会话预算:缺省按父会话剩余预算的一半划拨;超过剩余预算直接拒绝 */
|
||||
@@ -220,7 +335,9 @@ export const AgentSpawnInput = z.object({
|
||||
reason: z.string().max(500).optional(),
|
||||
/** 必须是父会话能力白名单的子集;缺省继承父会话白名单 */
|
||||
capabilityAllowlist: z.array(z.string()).min(1).max(50).optional(),
|
||||
budget: SubagentBudget.optional(),
|
||||
budget: SubagentBudget.describe(
|
||||
"各项都不能超过本会话剩余预算(工具调用、token、费用、时长),超过直接拒绝;不给则按剩余的一半划拨",
|
||||
).optional(),
|
||||
/** 不得高于父会话:plan_only 的父会话只能派生 plan_only 子会话;缺省同父会话 */
|
||||
mode: z.enum(["execute", "plan_only"]).optional(),
|
||||
/** 覆盖子会话使用的 Agent 上游;缺省同父会话 */
|
||||
@@ -688,6 +805,9 @@ export const CAPABILITY_IO = {
|
||||
"policy.preview": { input: PolicyPreviewInput, output: PolicyPreviewOutput },
|
||||
"policy.apply": { input: PolicyApplyInput, output: PolicyApplyOutput },
|
||||
"inventory.query": { input: InventoryQueryInput, output: InventoryQueryOutput },
|
||||
"subscription.list": { input: SubscriptionListInput, output: SubscriptionListOutput },
|
||||
"decision.list": { input: DecisionListInput, output: DecisionListOutput },
|
||||
"download.inspect": { input: DownloadInspectInput, output: DownloadInspectOutput },
|
||||
"agent.spawn": { input: AgentSpawnInput, output: AgentSpawnOutput },
|
||||
"agent.await": { input: AgentAwaitInput, output: AgentAwaitOutput },
|
||||
"site.list": { input: SiteListInput, output: SiteListOutput },
|
||||
@@ -757,6 +877,12 @@ 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>;
|
||||
export type SubscriptionListInput = z.infer<typeof SubscriptionListInput>;
|
||||
export type SubscriptionListOutput = z.infer<typeof SubscriptionListOutput>;
|
||||
export type DecisionListInput = z.infer<typeof DecisionListInput>;
|
||||
export type DecisionListOutput = z.infer<typeof DecisionListOutput>;
|
||||
export type DownloadInspectInput = z.infer<typeof DownloadInspectInput>;
|
||||
export type DownloadInspectOutput = z.infer<typeof DownloadInspectOutput>;
|
||||
export type AgentSpawnInput = z.infer<typeof AgentSpawnInput>;
|
||||
export type AgentSpawnOutput = z.infer<typeof AgentSpawnOutput>;
|
||||
export type AgentAwaitInput = z.infer<typeof AgentAwaitInput>;
|
||||
|
||||
Reference in new issue
Block a user