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
@@ -2,6 +2,7 @@ import { CapabilityError, type CapabilityRegistry } from "@umt/agent";
|
|||||||
import { capabilitySpec } from "@umt/contracts";
|
import { capabilitySpec } from "@umt/contracts";
|
||||||
import { sql } from "@umt/db";
|
import { sql } from "@umt/db";
|
||||||
import { normalizeTitle, transition } from "@umt/domain";
|
import { normalizeTitle, transition } from "@umt/domain";
|
||||||
|
import { coveringGrant, type PlanBinding, planBinding } from "../agent/authorize.ts";
|
||||||
import { previewPolicy, publishRevision } from "./routes/rules.ts";
|
import { previewPolicy, publishRevision } from "./routes/rules.ts";
|
||||||
import { evaluateAcquisition, publishedAcquisitionPolicies } from "./services/acquisition.ts";
|
import { evaluateAcquisition, publishedAcquisitionPolicies } from "./services/acquisition.ts";
|
||||||
import { type AoDeps, enqueueTracked, ensureTaskRow, markTask } from "./services/common.ts";
|
import { type AoDeps, enqueueTracked, ensureTaskRow, markTask } from "./services/common.ts";
|
||||||
@@ -76,6 +77,178 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
|||||||
return { items } as never;
|
return { items } as never;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// AG-3:订阅摘要(分组计数 + 条目),版本目标只给总数 / 已满足数,细节用 inventory.query
|
||||||
|
registry.register(capabilitySpec("subscription.list"), async (input) => {
|
||||||
|
const db = deps().ctx.db;
|
||||||
|
let q = db
|
||||||
|
.selectFrom("subscription as s")
|
||||||
|
.leftJoin("media_entity as e", "e.id", "s.media_entity_id")
|
||||||
|
.where((eb) => eb.or([eb("e.status", "is", null), eb("e.status", "<>", "merged")]));
|
||||||
|
if (input.status?.length) q = q.where("s.status", "in", input.status);
|
||||||
|
if (input.mediaKind) q = q.where("s.media_kind", "=", input.mediaKind);
|
||||||
|
if (input.query) {
|
||||||
|
const like = `%${input.query.replace(/[\\%_]/g, (m) => `\\${m}`)}%`;
|
||||||
|
q = q.where((eb) =>
|
||||||
|
eb.or([
|
||||||
|
eb("e.title", "ilike", like),
|
||||||
|
eb("e.original_title", "ilike", like),
|
||||||
|
eb("s.title_query", "ilike", like),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const groupCol = input.groupBy === "mediaKind" ? "s.media_kind" : "s.status";
|
||||||
|
const [groups, rows] = await Promise.all([
|
||||||
|
input.groupBy === "none"
|
||||||
|
? q.select(sql<string>`'all'`.as("key")).select(sql<number>`count(*)::int`.as("count")).execute()
|
||||||
|
: q
|
||||||
|
.select(sql<string>`${sql.ref(groupCol)}::text`.as("key"))
|
||||||
|
.select(sql<number>`count(*)::int`.as("count"))
|
||||||
|
.groupBy(groupCol)
|
||||||
|
.orderBy("count", "desc")
|
||||||
|
.execute(),
|
||||||
|
q
|
||||||
|
.select([
|
||||||
|
"s.id",
|
||||||
|
"s.media_entity_id",
|
||||||
|
"s.title_query",
|
||||||
|
"s.media_kind",
|
||||||
|
"s.mode",
|
||||||
|
"s.status",
|
||||||
|
"s.priority",
|
||||||
|
"s.status_reason",
|
||||||
|
"s.last_searched_at",
|
||||||
|
"s.next_search_at",
|
||||||
|
"s.last_progress_at",
|
||||||
|
"e.title as entity_title",
|
||||||
|
])
|
||||||
|
.select((eb) => [
|
||||||
|
eb
|
||||||
|
.selectFrom("acquisition_slot as a")
|
||||||
|
.select(sql<number>`count(*)::int`.as("n"))
|
||||||
|
.whereRef("a.subscription_id", "=", "s.id")
|
||||||
|
.as("slot_total"),
|
||||||
|
eb
|
||||||
|
.selectFrom("acquisition_slot as a")
|
||||||
|
.select(sql<number>`count(*)::int`.as("n"))
|
||||||
|
.whereRef("a.subscription_id", "=", "s.id")
|
||||||
|
.where("a.status", "=", "satisfied")
|
||||||
|
.as("slot_satisfied"),
|
||||||
|
])
|
||||||
|
.orderBy("s.updated_at", "desc")
|
||||||
|
.limit(input.limit)
|
||||||
|
.execute(),
|
||||||
|
]);
|
||||||
|
const iso = (d: Date | null) => (d ? d.toISOString() : null);
|
||||||
|
return {
|
||||||
|
total: groups.reduce((n, g) => n + Number(g.count), 0),
|
||||||
|
groups: groups.map((g) => ({ key: g.key, count: Number(g.count) })),
|
||||||
|
items: rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
title: r.entity_title ?? r.title_query,
|
||||||
|
mediaEntityId: r.media_entity_id,
|
||||||
|
mediaKind: r.media_kind,
|
||||||
|
mode: r.mode,
|
||||||
|
status: r.status,
|
||||||
|
priority: r.priority,
|
||||||
|
statusReason: r.status_reason,
|
||||||
|
slots: { total: Number(r.slot_total ?? 0), satisfied: Number(r.slot_satisfied ?? 0) },
|
||||||
|
lastSearchedAt: iso(r.last_searched_at),
|
||||||
|
nextSearchAt: iso(r.next_search_at),
|
||||||
|
lastProgressAt: iso(r.last_progress_at),
|
||||||
|
})),
|
||||||
|
} as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
// AG-3:下载任务详情(按 id 或 hash);关联计划 / 资源 / 业务任务 / 整理记录 / 待处理事项
|
||||||
|
registry.register(capabilitySpec("download.inspect"), async (input) => {
|
||||||
|
const db = deps().ctx.db;
|
||||||
|
const hash = input.hash?.toLowerCase();
|
||||||
|
const found = await db
|
||||||
|
.selectFrom("download_task")
|
||||||
|
.selectAll()
|
||||||
|
.$if(!!input.downloadTaskId, (qb) => qb.where("id", "=", input.downloadTaskId!))
|
||||||
|
.$if(!input.downloadTaskId && !!hash, (qb) =>
|
||||||
|
qb.where((eb) =>
|
||||||
|
eb.or([eb("torrent_hash", "=", hash!), eb("infohash_v1", "=", hash!), eb("infohash_v2", "=", hash!)]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy("updated_at", "desc")
|
||||||
|
.limit(10)
|
||||||
|
.execute();
|
||||||
|
const dt = found[0];
|
||||||
|
if (!dt) {
|
||||||
|
if (input.downloadTaskId) {
|
||||||
|
const t = await db.selectFrom("task").select("id").where("id", "=", input.downloadTaskId).executeTakeFirst();
|
||||||
|
if (t) throw new CapabilityError("not_found", "这是业务任务 ID,不是下载任务 ID;业务任务用 task.inspect 查看");
|
||||||
|
}
|
||||||
|
throw new CapabilityError("not_found", "下载任务不存在");
|
||||||
|
}
|
||||||
|
const plan = dt.acquisition_plan_id
|
||||||
|
? await db
|
||||||
|
.selectFrom("acquisition_plan")
|
||||||
|
.select(["id", "status", "status_reason", "slot_id", "task_id"])
|
||||||
|
.where("id", "=", dt.acquisition_plan_id)
|
||||||
|
.executeTakeFirst()
|
||||||
|
: undefined;
|
||||||
|
const [release, tasks, organization, decisions] = await Promise.all([
|
||||||
|
dt.source_release_id
|
||||||
|
? db
|
||||||
|
.selectFrom("source_release")
|
||||||
|
.select(["id", "title", "site_id"])
|
||||||
|
.where("id", "=", dt.source_release_id)
|
||||||
|
.executeTakeFirst()
|
||||||
|
: Promise.resolve(undefined),
|
||||||
|
db
|
||||||
|
.selectFrom("task")
|
||||||
|
.select(["id", "kind", "status", "last_error"])
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([
|
||||||
|
eb.and([eb("subject_type", "=", "download_task"), eb("subject_id", "=", dt.id)]),
|
||||||
|
...(plan ? [eb.and([eb("subject_type", "=", "acquisition_plan"), eb("subject_id", "=", plan.id)])] : []),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.orderBy("created_at", "desc")
|
||||||
|
.limit(20)
|
||||||
|
.execute(),
|
||||||
|
db
|
||||||
|
.selectFrom("organization_record")
|
||||||
|
.select(["id", "status", "target_path"])
|
||||||
|
.where("download_task_id", "=", dt.id)
|
||||||
|
.orderBy("created_at", "desc")
|
||||||
|
.limit(20)
|
||||||
|
.execute(),
|
||||||
|
db
|
||||||
|
.selectFrom("decision")
|
||||||
|
.select(["id", "kind", "status", "title"])
|
||||||
|
.where("subject_id", "=", dt.id)
|
||||||
|
.orderBy("created_at", "desc")
|
||||||
|
.limit(20)
|
||||||
|
.execute(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
downloadTask: toApi(dt) as never,
|
||||||
|
plan: plan
|
||||||
|
? {
|
||||||
|
id: plan.id,
|
||||||
|
status: plan.status,
|
||||||
|
statusReason: plan.status_reason,
|
||||||
|
slotId: plan.slot_id,
|
||||||
|
taskId: plan.task_id,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
release: release ? { id: release.id, title: release.title, siteId: release.site_id } : null,
|
||||||
|
tasks: tasks.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
kind: t.kind,
|
||||||
|
status: t.status,
|
||||||
|
lastError: (t.last_error ?? null) as Record<string, unknown> | null,
|
||||||
|
})),
|
||||||
|
organization: organization.map((o) => ({ id: o.id, status: o.status, targetPath: o.target_path })),
|
||||||
|
decisions: decisions.map((x) => ({ id: x.id, kind: x.kind, status: x.status, title: x.title })),
|
||||||
|
otherMatches: found.slice(1).map((x) => x.id),
|
||||||
|
} as never;
|
||||||
|
});
|
||||||
|
|
||||||
registry.register(capabilitySpec("download.plan"), async (input) => {
|
registry.register(capabilitySpec("download.plan"), async (input) => {
|
||||||
const d = deps();
|
const d = deps();
|
||||||
const db = d.ctx.db;
|
const db = d.ctx.db;
|
||||||
@@ -119,17 +292,19 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
|||||||
.selectAll()
|
.selectAll()
|
||||||
.where("id", "=", input.slotId)
|
.where("id", "=", input.slotId)
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
const grant = await db
|
// 按授权范围判定(AG-3):有覆盖本计划(作品 / 类型 / 每日预算)的持续授权 → 不需审批;否则需要
|
||||||
.selectFrom("automation_grant")
|
const target = plan ? await planBinding(db, "acquisition_plan", plan.id) : null;
|
||||||
.select("id")
|
const covered = await coveringGrant(
|
||||||
.where("status", "=", "active")
|
db,
|
||||||
.where("action", "in", ["download.submit", "policy.acquisition", "*"])
|
"download.submit",
|
||||||
.executeTakeFirst();
|
target ?? (await slotBinding(db, fresh.media_entity_id)),
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
plan: plan ? (toApi(plan) as never) : null,
|
plan: plan ? (toApi(plan) as never) : null,
|
||||||
slot: toApi(fresh) as never,
|
slot: toApi(fresh) as never,
|
||||||
rejected,
|
rejected,
|
||||||
requiresApproval: !grant,
|
requiresApproval: !("grantId" in covered),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -412,3 +587,13 @@ function planHash(request: object, frozen: FrozenPlan): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isUuid = (s: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
const isUuid = (s: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
||||||
|
|
||||||
|
/** 没生成计划时按 Slot 的作品判定授权范围(字节预算按 0 计) */
|
||||||
|
async function slotBinding(db: AoDeps["ctx"]["db"], mediaEntityId: string): Promise<PlanBinding> {
|
||||||
|
const e = await db
|
||||||
|
.selectFrom("media_entity")
|
||||||
|
.select(["id", "kind"])
|
||||||
|
.where("id", "=", mediaEntityId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
return { payloadHash: "", revision: 0, mediaEntityId: e?.id ?? null, mediaKind: e?.kind ?? null, bytes: 0 };
|
||||||
|
}
|
||||||
@@ -102,6 +102,76 @@ export async function planBinding(db: Kysely<DB>, planType: string, planId: stri
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 持续授权是否覆盖本能力(不含单次审批与会话内审批):action 覆盖、active 未过期、绑定策略仍发布、
|
||||||
|
* 对象范围与每日预算。download.plan 用它判定 requiresApproval,authorize 用它放行。
|
||||||
|
*/
|
||||||
|
export async function coveringGrant(
|
||||||
|
db: Kysely<DB>,
|
||||||
|
capabilityId: string,
|
||||||
|
target: PlanBinding | null,
|
||||||
|
now: Date,
|
||||||
|
): Promise<{ grantId: string } | { reasons: string[] }> {
|
||||||
|
const d = { id: capabilityId };
|
||||||
|
const actions = [...(GRANT_ACTIONS[d.id] ?? [d.id]), "*"];
|
||||||
|
const grants = await db
|
||||||
|
.selectFrom("automation_grant as g")
|
||||||
|
.leftJoin("policy_revision as p", "p.id", "g.policy_revision_id")
|
||||||
|
.select(["g.id", "g.action", "g.scope", "g.budget", "g.policy_revision_id", "p.status as policy_status"])
|
||||||
|
.where("g.status", "=", "active")
|
||||||
|
.where("g.action", "in", actions)
|
||||||
|
.where((eb) => eb.or([eb("g.expires_at", "is", null), eb("g.expires_at", ">", now)]))
|
||||||
|
.orderBy("g.created_at", "desc")
|
||||||
|
.execute();
|
||||||
|
const reasons: string[] = [];
|
||||||
|
for (const g of grants) {
|
||||||
|
if (g.policy_revision_id && g.policy_status !== "published") {
|
||||||
|
reasons.push(`授权 ${g.id} 绑定的策略版本已不是发布状态`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const scope = (g.scope ?? {}) as { mediaEntityIds?: string[]; mediaKinds?: string[]; capabilities?: string[] };
|
||||||
|
if (scope.capabilities?.length && !scope.capabilities.includes(d.id)) {
|
||||||
|
reasons.push(`授权 ${g.id} 不含能力 ${d.id}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
scope.mediaEntityIds?.length &&
|
||||||
|
(!target?.mediaEntityId || !scope.mediaEntityIds.includes(target.mediaEntityId))
|
||||||
|
) {
|
||||||
|
reasons.push(`授权 ${g.id} 的对象范围不含目标作品`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (scope.mediaKinds?.length && target?.mediaKind && !kindCovered(scope.mediaKinds, target.mediaKind)) {
|
||||||
|
reasons.push(`授权 ${g.id} 的媒体类型范围不含 ${target.mediaKind}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const budget = (g.budget ?? {}) as { maxInvocationsPerDay?: number; maxBytesPerDay?: number };
|
||||||
|
if (budget.maxInvocationsPerDay !== undefined || budget.maxBytesPerDay !== undefined) {
|
||||||
|
const day = new Date(now);
|
||||||
|
day.setHours(0, 0, 0, 0);
|
||||||
|
const used = await sql<{ n: number }>`
|
||||||
|
select count(*)::int as n from capability_invocation
|
||||||
|
where grant_id = ${g.id} and status = 'succeeded' and created_at >= ${day}`.execute(db);
|
||||||
|
if (budget.maxInvocationsPerDay !== undefined && (used.rows[0]?.n ?? 0) >= budget.maxInvocationsPerDay) {
|
||||||
|
reasons.push(`授权 ${g.id} 今日次数预算已用尽`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (budget.maxBytesPerDay !== undefined && d.id === "download.submit") {
|
||||||
|
const bytes = await sql<{ b: string }>`
|
||||||
|
select coalesce(sum(p.reserved_bytes), 0)::bigint as b
|
||||||
|
from capability_invocation ci join acquisition_plan p on p.id::text = ci.input->>'planId'
|
||||||
|
where ci.grant_id = ${g.id} and ci.status = 'succeeded' and ci.created_at >= ${day}`.execute(db);
|
||||||
|
if (Number(bytes.rows[0]?.b ?? 0) + (target?.bytes ?? 0) > budget.maxBytesPerDay) {
|
||||||
|
reasons.push(`授权 ${g.id} 今日字节预算不足`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { grantId: g.id };
|
||||||
|
}
|
||||||
|
return { reasons };
|
||||||
|
}
|
||||||
|
|
||||||
export function createAuthorizer(deps: {
|
export function createAuthorizer(deps: {
|
||||||
db: Kysely<DB>;
|
db: Kysely<DB>;
|
||||||
settings: SettingsReader;
|
settings: SettingsReader;
|
||||||
@@ -168,62 +238,9 @@ export function createAuthorizer(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2) 持续授权
|
// 2) 持续授权
|
||||||
const actions = [...(GRANT_ACTIONS[d.id] ?? [d.id]), "*"];
|
const covered = await coveringGrant(db, d.id, target, now());
|
||||||
const grants = await db
|
if ("grantId" in covered) return { allowed: true, grantId: covered.grantId };
|
||||||
.selectFrom("automation_grant as g")
|
const reasons = covered.reasons;
|
||||||
.leftJoin("policy_revision as p", "p.id", "g.policy_revision_id")
|
|
||||||
.select(["g.id", "g.action", "g.scope", "g.budget", "g.policy_revision_id", "p.status as policy_status"])
|
|
||||||
.where("g.status", "=", "active")
|
|
||||||
.where("g.action", "in", actions)
|
|
||||||
.where((eb) => eb.or([eb("g.expires_at", "is", null), eb("g.expires_at", ">", now())]))
|
|
||||||
.orderBy("g.created_at", "desc")
|
|
||||||
.execute();
|
|
||||||
const reasons: string[] = [];
|
|
||||||
for (const g of grants) {
|
|
||||||
if (g.policy_revision_id && g.policy_status !== "published") {
|
|
||||||
reasons.push(`授权 ${g.id} 绑定的策略版本已不是发布状态`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const scope = (g.scope ?? {}) as { mediaEntityIds?: string[]; mediaKinds?: string[]; capabilities?: string[] };
|
|
||||||
if (scope.capabilities?.length && !scope.capabilities.includes(d.id)) {
|
|
||||||
reasons.push(`授权 ${g.id} 不含能力 ${d.id}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
scope.mediaEntityIds?.length &&
|
|
||||||
(!target?.mediaEntityId || !scope.mediaEntityIds.includes(target.mediaEntityId))
|
|
||||||
) {
|
|
||||||
reasons.push(`授权 ${g.id} 的对象范围不含目标作品`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (scope.mediaKinds?.length && target?.mediaKind && !kindCovered(scope.mediaKinds, target.mediaKind)) {
|
|
||||||
reasons.push(`授权 ${g.id} 的媒体类型范围不含 ${target.mediaKind}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const budget = (g.budget ?? {}) as { maxInvocationsPerDay?: number; maxBytesPerDay?: number };
|
|
||||||
if (budget.maxInvocationsPerDay !== undefined || budget.maxBytesPerDay !== undefined) {
|
|
||||||
const day = new Date(now());
|
|
||||||
day.setHours(0, 0, 0, 0);
|
|
||||||
const used = await sql<{ n: number }>`
|
|
||||||
select count(*)::int as n from capability_invocation
|
|
||||||
where grant_id = ${g.id} and status = 'succeeded' and created_at >= ${day}`.execute(db);
|
|
||||||
if (budget.maxInvocationsPerDay !== undefined && (used.rows[0]?.n ?? 0) >= budget.maxInvocationsPerDay) {
|
|
||||||
reasons.push(`授权 ${g.id} 今日次数预算已用尽`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (budget.maxBytesPerDay !== undefined && d.id === "download.submit") {
|
|
||||||
const bytes = await sql<{ b: string }>`
|
|
||||||
select coalesce(sum(p.reserved_bytes), 0)::bigint as b
|
|
||||||
from capability_invocation ci join acquisition_plan p on p.id::text = ci.input->>'planId'
|
|
||||||
where ci.grant_id = ${g.id} and ci.status = 'succeeded' and ci.created_at >= ${day}`.execute(db);
|
|
||||||
if (Number(bytes.rows[0]?.b ?? 0) + (target?.bytes ?? 0) > budget.maxBytesPerDay) {
|
|
||||||
reasons.push(`授权 ${g.id} 今日字节预算不足`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { allowed: true, grantId: g.id };
|
|
||||||
}
|
|
||||||
// 3) 会话内审批:控制台管理员会话里,没有持续授权的计划类动作在步骤处发起审批并挂起会话
|
// 3) 会话内审批:控制台管理员会话里,没有持续授权的计划类动作在步骤处发起审批并挂起会话
|
||||||
if (ap && target && !approvalId) {
|
if (ap && target && !approvalId) {
|
||||||
const planId = inp[ap.field] as string;
|
const planId = inp[ap.field] as string;
|
||||||
|
|||||||
@@ -188,7 +188,20 @@ export async function cancelTask(ctx: ServerContext, taskId: string, reason: str
|
|||||||
export function registerTaskCapabilities(registry: CapabilityRegistry, ctx: ServerContext): void {
|
export function registerTaskCapabilities(registry: CapabilityRegistry, ctx: ServerContext): void {
|
||||||
registry.register(capabilitySpec("task.inspect"), async (input) => {
|
registry.register(capabilitySpec("task.inspect"), async (input) => {
|
||||||
const t = await ctx.db.selectFrom("task").selectAll().where("id", "=", input.taskId).executeTakeFirst();
|
const t = await ctx.db.selectFrom("task").selectAll().where("id", "=", input.taskId).executeTakeFirst();
|
||||||
if (!t) throw new CapabilityError("not_found", "任务不存在");
|
if (!t) {
|
||||||
|
// 模型常把下载任务 ID 当业务任务 ID 传进来:明确指出该用哪个能力
|
||||||
|
const dt = await ctx.db
|
||||||
|
.selectFrom("download_task")
|
||||||
|
.select("id")
|
||||||
|
.where("id", "=", input.taskId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (dt)
|
||||||
|
throw new CapabilityError(
|
||||||
|
"invalid_input",
|
||||||
|
"这是下载任务 ID,不是业务任务 ID;下载任务用 download.inspect 查看(参数 downloadTaskId)",
|
||||||
|
);
|
||||||
|
throw new CapabilityError("not_found", "任务不存在");
|
||||||
|
}
|
||||||
const children = await ctx.db
|
const children = await ctx.db
|
||||||
.selectFrom("task")
|
.selectFrom("task")
|
||||||
.selectAll()
|
.selectAll()
|
||||||
@@ -203,6 +216,44 @@ export function registerTaskCapabilities(registry: CapabilityRegistry, ctx: Serv
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// AG-3:待处理事项摘要(含可选处理 options);缺省只列未关闭的
|
||||||
|
registry.register(capabilitySpec("decision.list"), async (input) => {
|
||||||
|
const status = input.status?.length ? input.status : (["open", "investigating", "awaiting_user"] as const);
|
||||||
|
let q = ctx.db.selectFrom("decision").where("status", "in", [...status]);
|
||||||
|
if (input.kind?.length) q = q.where("kind", "in", input.kind);
|
||||||
|
const [byKind, rows] = await Promise.all([
|
||||||
|
q
|
||||||
|
.select(["kind", sql<number>`count(*)::int`.as("count")])
|
||||||
|
.groupBy("kind")
|
||||||
|
.orderBy("count", "desc")
|
||||||
|
.execute(),
|
||||||
|
q
|
||||||
|
.selectAll()
|
||||||
|
.orderBy(sql`case severity when 'critical' then 0 when 'warning' then 1 else 2 end`)
|
||||||
|
.orderBy("updated_at", "desc")
|
||||||
|
.limit(input.limit)
|
||||||
|
.execute(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
total: byKind.reduce((n, k) => n + Number(k.count), 0),
|
||||||
|
byKind: byKind.map((k) => ({ kind: k.kind, count: Number(k.count) })),
|
||||||
|
items: rows.map((d) => ({
|
||||||
|
id: d.id,
|
||||||
|
kind: d.kind,
|
||||||
|
status: d.status,
|
||||||
|
severity: d.severity,
|
||||||
|
title: d.title,
|
||||||
|
summary: d.summary,
|
||||||
|
subjectType: d.subject_type,
|
||||||
|
subjectId: d.subject_id,
|
||||||
|
attempts: d.attempts,
|
||||||
|
options: (Array.isArray(d.options) ? d.options : []) as never,
|
||||||
|
createdAt: d.created_at.toISOString(),
|
||||||
|
updatedAt: d.updated_at.toISOString(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
registry.register(capabilitySpec("task.retry"), async (input, c) => {
|
registry.register(capabilitySpec("task.retry"), async (input, c) => {
|
||||||
const row = await retryTask(ctx, input.taskId, c.actorId, input.reason);
|
const row = await retryTask(ctx, input.taskId, c.actorId, input.reason);
|
||||||
return { taskId: row.id, status: row.status as TaskStatus };
|
return { taskId: row.id, status: row.status as TaskStatus };
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ describe.skipIf(!hasPg)("E2 Agent 委托闭环(fake 模型 + 真实能力/授权
|
|||||||
return r.body.data.id as string;
|
return r.body.data.id as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
it("启动断言:35 个能力全部注册,并在能力目录里可见", async () => {
|
it("启动断言:38 个能力全部注册,并在能力目录里可见", async () => {
|
||||||
expect(e.booted.ctx.capabilities.list()).toHaveLength(35);
|
expect(e.booted.ctx.capabilities.list()).toHaveLength(38);
|
||||||
const r = await e.api("GET", "/capabilities");
|
const r = await e.api("GET", "/capabilities");
|
||||||
expect(r.body.data.items.map((c: { id: string }) => c.id).sort()).toEqual(
|
expect(r.body.data.items.map((c: { id: string }) => c.id).sort()).toEqual(
|
||||||
e.booted.ctx.capabilities
|
e.booted.ctx.capabilities
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const TiB = 1024 ** 4;
|
|||||||
const SITE_IDS = ["site.list", "site.health_check", "site.refresh_login", "site.user_data"] as const;
|
const SITE_IDS = ["site.list", "site.health_check", "site.refresh_login", "site.user_data"] as const;
|
||||||
|
|
||||||
describe("站点能力规格", () => {
|
describe("站点能力规格", () => {
|
||||||
it("进入首批能力目录(17 + 4 站点 + 10 浏览器 + 2 会话交互 + 2 网页搜索 = 35);类别 / 风险 / 重放按约定", () => {
|
it("进入首批能力目录(17 + 3 只读摘要 + 4 站点 + 10 浏览器 + 2 会话交互 + 2 网页搜索 = 38);类别 / 风险 / 重放按约定", () => {
|
||||||
expect(CAPABILITY_IDS).toHaveLength(35);
|
expect(CAPABILITY_IDS).toHaveLength(38);
|
||||||
for (const id of SITE_IDS) expect(CAPABILITY_IDS).toContain(id);
|
for (const id of SITE_IDS) expect(CAPABILITY_IDS).toContain(id);
|
||||||
expect(capabilitySpec("site.list")).toMatchObject({ kind: "read", riskLevel: "low", replay: "safe" });
|
expect(capabilitySpec("site.list")).toMatchObject({ kind: "read", riskLevel: "low", replay: "safe" });
|
||||||
expect(capabilitySpec("site.user_data")).toMatchObject({ kind: "read", riskLevel: "low", replay: "safe" });
|
expect(capabilitySpec("site.user_data")).toMatchObject({ kind: "read", riskLevel: "low", replay: "safe" });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { DecisionKind, DecisionOption, DecisionStatus, Severity } from "./agent.ts";
|
||||||
import { AgentOptionItem, AgentPlanStep, AgentPlanStepStatus } from "./agent-events.ts";
|
import { AgentOptionItem, AgentPlanStep, AgentPlanStepStatus } from "./agent-events.ts";
|
||||||
import { MediaAsset } from "./assets.ts";
|
import { MediaAsset } from "./assets.ts";
|
||||||
import { BrowserController, BrowserDownloadVerification, BrowserSnapshot } from "./browser.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 { ExternalEntityType, ExternalIdentity } from "./identity.ts";
|
||||||
import { ServerMapping } from "./ops.ts";
|
import { ServerMapping } from "./ops.ts";
|
||||||
import { OrganizationAction, OrganizationRecord } from "./organization.ts";
|
import { OrganizationAction, OrganizationRecord } from "./organization.ts";
|
||||||
import { AcquisitionSlot, PolicyRevision } from "./policy.ts";
|
import {
|
||||||
|
AcquisitionPriority,
|
||||||
|
AcquisitionSlot,
|
||||||
|
PolicyRevision,
|
||||||
|
SubscriptionMediaKind,
|
||||||
|
SubscriptionMode,
|
||||||
|
SubscriptionStatus,
|
||||||
|
} from "./policy.ts";
|
||||||
import {
|
import {
|
||||||
SiteAccountError,
|
SiteAccountError,
|
||||||
SiteAccountOverall,
|
SiteAccountOverall,
|
||||||
@@ -30,14 +38,33 @@ import {
|
|||||||
|
|
||||||
const Limit = z.number().int().min(1).max(100).default(20);
|
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) */
|
/** 写能力的通用受理结果:返回 task/plan ID,由任务页追踪(REQ §12.3) */
|
||||||
export const Accepted = z.object({ taskId: Id, planId: Id.nullable() });
|
export const Accepted = z.object({ taskId: Id, planId: Id.nullable() });
|
||||||
export type Accepted = z.infer<typeof Accepted>;
|
export type Accepted = z.infer<typeof Accepted>;
|
||||||
|
|
||||||
export const CatalogLookupInput = z
|
export const CatalogLookupInput = z
|
||||||
.object({
|
.object({
|
||||||
query: z.string().min(1).max(200).optional(),
|
query: ProbeQuery.optional(),
|
||||||
externalId: z.object({ provider: z.string(), entityType: ExternalEntityType, externalId: z.string() }).optional(),
|
externalId: z
|
||||||
|
.object({
|
||||||
|
provider: z.string().describe("如 tmdb、imdb、tvdb"),
|
||||||
|
entityType: ExternalEntityType,
|
||||||
|
externalId: z.string().describe('外部 ID 一律写成字符串,如 "693134"、"tt15239678"'),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
kind: MediaKind.optional(),
|
kind: MediaKind.optional(),
|
||||||
year: z.number().int().optional(),
|
year: z.number().int().optional(),
|
||||||
includeRemote: z.boolean().default(false),
|
includeRemote: z.boolean().default(false),
|
||||||
@@ -62,7 +89,7 @@ export const CatalogLookupOutput = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const ReleaseSearchInput = z.object({
|
export const ReleaseSearchInput = z.object({
|
||||||
query: z.string().min(1).max(200).optional(),
|
query: ProbeQuery.optional(),
|
||||||
mediaEntityId: Id.optional(),
|
mediaEntityId: Id.optional(),
|
||||||
siteIds: z.array(Id).optional(),
|
siteIds: z.array(Id).optional(),
|
||||||
/** 本地快照不足时是否实时补查 */
|
/** 本地快照不足时是否实时补查 */
|
||||||
@@ -188,7 +215,8 @@ export const PolicyApplyOutput = z.object({ policy: PolicyRevision });
|
|||||||
|
|
||||||
export const InventoryQueryInput = z.object({
|
export const InventoryQueryInput = z.object({
|
||||||
mediaEntityId: Id.optional(),
|
mediaEntityId: Id.optional(),
|
||||||
query: z.string().max(200).optional(),
|
/** 不给 query 也不给 mediaEntityId 时按最近更新列出 */
|
||||||
|
query: ProbeQuery.optional(),
|
||||||
includeServerInventory: z.boolean().default(false),
|
includeServerInventory: z.boolean().default(false),
|
||||||
limit: Limit,
|
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) ──
|
// ── 子代理(W7-B) ──
|
||||||
|
|
||||||
/** 子会话预算:缺省按父会话剩余预算的一半划拨;超过剩余预算直接拒绝 */
|
/** 子会话预算:缺省按父会话剩余预算的一半划拨;超过剩余预算直接拒绝 */
|
||||||
@@ -220,7 +335,9 @@ export const AgentSpawnInput = z.object({
|
|||||||
reason: z.string().max(500).optional(),
|
reason: z.string().max(500).optional(),
|
||||||
/** 必须是父会话能力白名单的子集;缺省继承父会话白名单 */
|
/** 必须是父会话能力白名单的子集;缺省继承父会话白名单 */
|
||||||
capabilityAllowlist: z.array(z.string()).min(1).max(50).optional(),
|
capabilityAllowlist: z.array(z.string()).min(1).max(50).optional(),
|
||||||
budget: SubagentBudget.optional(),
|
budget: SubagentBudget.describe(
|
||||||
|
"各项都不能超过本会话剩余预算(工具调用、token、费用、时长),超过直接拒绝;不给则按剩余的一半划拨",
|
||||||
|
).optional(),
|
||||||
/** 不得高于父会话:plan_only 的父会话只能派生 plan_only 子会话;缺省同父会话 */
|
/** 不得高于父会话:plan_only 的父会话只能派生 plan_only 子会话;缺省同父会话 */
|
||||||
mode: z.enum(["execute", "plan_only"]).optional(),
|
mode: z.enum(["execute", "plan_only"]).optional(),
|
||||||
/** 覆盖子会话使用的 Agent 上游;缺省同父会话 */
|
/** 覆盖子会话使用的 Agent 上游;缺省同父会话 */
|
||||||
@@ -688,6 +805,9 @@ export const CAPABILITY_IO = {
|
|||||||
"policy.preview": { input: PolicyPreviewInput, output: PolicyPreviewOutput },
|
"policy.preview": { input: PolicyPreviewInput, output: PolicyPreviewOutput },
|
||||||
"policy.apply": { input: PolicyApplyInput, output: PolicyApplyOutput },
|
"policy.apply": { input: PolicyApplyInput, output: PolicyApplyOutput },
|
||||||
"inventory.query": { input: InventoryQueryInput, output: InventoryQueryOutput },
|
"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.spawn": { input: AgentSpawnInput, output: AgentSpawnOutput },
|
||||||
"agent.await": { input: AgentAwaitInput, output: AgentAwaitOutput },
|
"agent.await": { input: AgentAwaitInput, output: AgentAwaitOutput },
|
||||||
"site.list": { input: SiteListInput, output: SiteListOutput },
|
"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 PolicyApplyOutput = z.infer<typeof PolicyApplyOutput>;
|
||||||
export type InventoryQueryInput = z.infer<typeof InventoryQueryInput>;
|
export type InventoryQueryInput = z.infer<typeof InventoryQueryInput>;
|
||||||
export type InventoryQueryOutput = z.infer<typeof InventoryQueryOutput>;
|
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 AgentSpawnInput = z.infer<typeof AgentSpawnInput>;
|
||||||
export type AgentSpawnOutput = z.infer<typeof AgentSpawnOutput>;
|
export type AgentSpawnOutput = z.infer<typeof AgentSpawnOutput>;
|
||||||
export type AgentAwaitInput = z.infer<typeof AgentAwaitInput>;
|
export type AgentAwaitInput = z.infer<typeof AgentAwaitInput>;
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ export const CAPABILITY_IDS = [
|
|||||||
"policy.preview",
|
"policy.preview",
|
||||||
"policy.apply",
|
"policy.apply",
|
||||||
"inventory.query",
|
"inventory.query",
|
||||||
|
"subscription.list",
|
||||||
|
"decision.list",
|
||||||
|
"download.inspect",
|
||||||
"agent.spawn",
|
"agent.spawn",
|
||||||
"agent.await",
|
"agent.await",
|
||||||
"site.list",
|
"site.list",
|
||||||
@@ -129,7 +132,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"catalog.lookup": d(
|
"catalog.lookup": d(
|
||||||
"catalog.lookup",
|
"catalog.lookup",
|
||||||
"查作品",
|
"查作品",
|
||||||
"按标题/别名/外部 ID 查本地作品与元数据候选",
|
"按标题/别名/外部 ID 查本地作品与元数据候选。确认作品身份、拿作品 ID 时用;查库里已有文件与版本目标用 inventory.query",
|
||||||
"read",
|
"read",
|
||||||
"CatalogLookup",
|
"CatalogLookup",
|
||||||
"read",
|
"read",
|
||||||
@@ -140,7 +143,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"release.search": d(
|
"release.search": d(
|
||||||
"release.search",
|
"release.search",
|
||||||
"搜资源",
|
"搜资源",
|
||||||
"先查本地资源索引与站点快照,不足时对已开启站点实时补查",
|
"先查本地资源索引与站点快照,不足时对已开启站点实时补查。找可下载的资源时用,检索词写作品名或关键词;已知作品时优先传 mediaEntityId",
|
||||||
"read",
|
"read",
|
||||||
"ReleaseSearch",
|
"ReleaseSearch",
|
||||||
"read",
|
"read",
|
||||||
@@ -151,7 +154,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"release.inspect": d(
|
"release.inspect": d(
|
||||||
"release.inspect",
|
"release.inspect",
|
||||||
"看资源详情",
|
"看资源详情",
|
||||||
"读取资源详情、文件清单、促销与解析结果",
|
"读取资源详情、文件清单、促销与解析结果。看某条资源的文件清单、促销或解析细节时用;不要逐条遍历搜索结果",
|
||||||
"read",
|
"read",
|
||||||
"ReleaseInspect",
|
"ReleaseInspect",
|
||||||
"read",
|
"read",
|
||||||
@@ -162,7 +165,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"identity.resolve": d(
|
"identity.resolve": d(
|
||||||
"identity.resolve",
|
"identity.resolve",
|
||||||
"识别身份",
|
"识别身份",
|
||||||
"为观察/资源提出身份映射;证据充分时自动接受,有歧义时转为待处理事项",
|
"为观察/资源提出身份映射;证据充分时自动接受,有歧义时转为待处理事项。资源 / 观察还没对上作品时用;已识别的资源不要重复提交",
|
||||||
"propose",
|
"propose",
|
||||||
"IdentityResolve",
|
"IdentityResolve",
|
||||||
"organize.execute",
|
"organize.execute",
|
||||||
@@ -173,7 +176,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"download.plan": d(
|
"download.plan": d(
|
||||||
"download.plan",
|
"download.plan",
|
||||||
"生成下载计划",
|
"生成下载计划",
|
||||||
"评估候选,生成占用版本目标(slot)与预算预留的下载计划;不提交",
|
"评估候选,生成占用版本目标(slot)与预算预留的下载计划;不提交。选定资源后、提交前用;不会提交下载,也不预留版本目标",
|
||||||
"preview",
|
"preview",
|
||||||
"DownloadPlan",
|
"DownloadPlan",
|
||||||
"organize.execute",
|
"organize.execute",
|
||||||
@@ -184,7 +187,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"download.submit": d(
|
"download.submit": d(
|
||||||
"download.submit",
|
"download.submit",
|
||||||
"提交下载",
|
"提交下载",
|
||||||
"按已保存的计划提交到下载器;校验授权与硬阈值",
|
"按已保存的计划提交到下载器;校验授权与硬阈值。只提交 download.plan 生成的计划;结果未知时先用 download.inspect / task.inspect 核对,不要重复提交",
|
||||||
"execute",
|
"execute",
|
||||||
"DownloadSubmit",
|
"DownloadSubmit",
|
||||||
"organize.execute",
|
"organize.execute",
|
||||||
@@ -195,7 +198,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"download.recheck": d(
|
"download.recheck": d(
|
||||||
"download.recheck",
|
"download.recheck",
|
||||||
"重新校验/恢复下载",
|
"重新校验/恢复下载",
|
||||||
"对已纳管的下载任务强制重新校验(action=recheck),或恢复非阈值、非人工的暂停(action=resume);不换源、不删除、不改保存路径",
|
"对已纳管的下载任务强制重新校验(action=recheck),或恢复非阈值、非人工的暂停(action=resume);不换源、不删除、不改保存路径。下载失败或文件缺失时用;换源、删除不在此能力内",
|
||||||
"execute",
|
"execute",
|
||||||
"DownloadRecheck",
|
"DownloadRecheck",
|
||||||
"organize.execute",
|
"organize.execute",
|
||||||
@@ -206,7 +209,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"organize.plan": d(
|
"organize.plan": d(
|
||||||
"organize.plan",
|
"organize.plan",
|
||||||
"生成整理计划",
|
"生成整理计划",
|
||||||
"生成并保存整理计划:冻结源/目标路径、整理方式与前置条件;不执行",
|
"生成并保存整理计划:冻结源/目标路径、整理方式与前置条件;不执行。下载完成后整理入库前用;只生成计划不动文件",
|
||||||
"preview",
|
"preview",
|
||||||
"OrganizePlan",
|
"OrganizePlan",
|
||||||
"organize.read",
|
"organize.read",
|
||||||
@@ -217,7 +220,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"organize.execute": d(
|
"organize.execute": d(
|
||||||
"organize.execute",
|
"organize.execute",
|
||||||
"执行整理",
|
"执行整理",
|
||||||
"执行已保存的整理计划(复制/移动),生成整理记录",
|
"执行已保存的整理计划(复制/移动),生成整理记录。只执行 organize.plan 生成且指纹未变的计划;指纹不一致时重新生成计划",
|
||||||
"execute",
|
"execute",
|
||||||
"OrganizeExecute",
|
"OrganizeExecute",
|
||||||
"organize.execute",
|
"organize.execute",
|
||||||
@@ -228,7 +231,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"task.inspect": d(
|
"task.inspect": d(
|
||||||
"task.inspect",
|
"task.inspect",
|
||||||
"查看任务",
|
"查看任务",
|
||||||
"读取任务状态、业务时间线与错误",
|
"读取任务状态、业务时间线与错误。按业务任务 ID 查看;下载任务 ID 或种子 hash 用 download.inspect",
|
||||||
"read",
|
"read",
|
||||||
"TaskInspect",
|
"TaskInspect",
|
||||||
"read",
|
"read",
|
||||||
@@ -239,7 +242,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"task.retry": d(
|
"task.retry": d(
|
||||||
"task.retry",
|
"task.retry",
|
||||||
"重试任务",
|
"重试任务",
|
||||||
"对失败/死信任务重新排队;副作用经操作账本去重",
|
"对失败/死信任务重新排队;副作用经操作账本去重。只用于失败 / 死信的任务;同一任务不要反复重试",
|
||||||
"execute",
|
"execute",
|
||||||
"TaskRetry",
|
"TaskRetry",
|
||||||
"organize.execute",
|
"organize.execute",
|
||||||
@@ -250,7 +253,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"task.cancel": d(
|
"task.cancel": d(
|
||||||
"task.cancel",
|
"task.cancel",
|
||||||
"取消任务",
|
"取消任务",
|
||||||
"停止未来步骤,不撤销已完成的外部动作",
|
"停止未来步骤,不撤销已完成的外部动作。用户要求停止或任务明显无意义时用",
|
||||||
"execute",
|
"execute",
|
||||||
"TaskCancel",
|
"TaskCancel",
|
||||||
"organize.execute",
|
"organize.execute",
|
||||||
@@ -261,7 +264,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"policy.preview": d(
|
"policy.preview": d(
|
||||||
"policy.preview",
|
"policy.preview",
|
||||||
"预览策略",
|
"预览策略",
|
||||||
"用历史数据回放策略草案,展示新增命中与误排除",
|
"用历史数据回放策略草案,展示新增命中与误排除。发布策略前用,确认影响范围",
|
||||||
"preview",
|
"preview",
|
||||||
"PolicyPreview",
|
"PolicyPreview",
|
||||||
"read",
|
"read",
|
||||||
@@ -272,7 +275,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"policy.apply": d(
|
"policy.apply": d(
|
||||||
"policy.apply",
|
"policy.apply",
|
||||||
"发布策略",
|
"发布策略",
|
||||||
"发布策略版本(需授权);不直接执行 JavaScript",
|
"发布策略版本(需授权);不直接执行 JavaScript。只在用户明确要求发布策略时用",
|
||||||
"execute",
|
"execute",
|
||||||
"PolicyApply",
|
"PolicyApply",
|
||||||
"admin",
|
"admin",
|
||||||
@@ -283,7 +286,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"inventory.query": d(
|
"inventory.query": d(
|
||||||
"inventory.query",
|
"inventory.query",
|
||||||
"查库存",
|
"查库存",
|
||||||
"查询本地资产、版本目标满足情况与外部库存证据",
|
"查询本地资产、版本目标满足情况与外部库存证据。查某个作品有哪些文件与版本目标时用;想盘点全部就不给 query,不要用单字母探查",
|
||||||
"read",
|
"read",
|
||||||
"InventoryQuery",
|
"InventoryQuery",
|
||||||
"read",
|
"read",
|
||||||
@@ -291,6 +294,40 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
// AG-3 只读摘要:订阅 / 待处理事项 / 下载任务。输出是摘要(不含凭证与私有链接),供模型盘点与调查
|
||||||
|
"subscription.list": d(
|
||||||
|
"subscription.list",
|
||||||
|
"列出订阅",
|
||||||
|
"按状态或媒体类型分组列出订阅摘要(作品、模式、状态、版本目标满足数、上次/下次搜索)。盘点订阅、找缺版本时用;查单个作品的文件与版本目标用 inventory.query",
|
||||||
|
"read",
|
||||||
|
"SubscriptionList",
|
||||||
|
"read",
|
||||||
|
"low",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
"decision.list": d(
|
||||||
|
"decision.list",
|
||||||
|
"列出待处理事项",
|
||||||
|
"列出待处理事项摘要(种类、严重度、对象、尝试次数与可选处理 options),缺省只列未关闭的。巡检或汇总待处理时用;不要用它处理事项,处理由用户在待处理中心选择",
|
||||||
|
"read",
|
||||||
|
"DecisionList",
|
||||||
|
"read",
|
||||||
|
"low",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
"download.inspect": d(
|
||||||
|
"download.inspect",
|
||||||
|
"查看下载任务",
|
||||||
|
"按下载任务 ID 或种子 hash 读取下载任务状态、进度、错误、关联计划、资源、业务任务、整理记录与待处理事项。调查下载失败或无进展时用;业务任务 ID 用 task.inspect",
|
||||||
|
"read",
|
||||||
|
"DownloadInspect",
|
||||||
|
"read",
|
||||||
|
"low",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
// 子代理(设置 pi.subagents 开启后才提供给模型):类别 propose——只建有界子会话,不产生外部副作用,
|
// 子代理(设置 pi.subagents 开启后才提供给模型):类别 propose——只建有界子会话,不产生外部副作用,
|
||||||
// "先给方案"的会话也能派生(子会话同样只给方案);授权不高于父会话,由服务端校验
|
// "先给方案"的会话也能派生(子会话同样只给方案);授权不高于父会话,由服务端校验
|
||||||
"agent.spawn": d(
|
"agent.spawn": d(
|
||||||
@@ -307,7 +344,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"agent.await": d(
|
"agent.await": d(
|
||||||
"agent.await",
|
"agent.await",
|
||||||
"等待子会话",
|
"等待子会话",
|
||||||
"等待本会话派生的子会话结束(等待期间不调用模型),返回各自状态、用量与汇报摘要;超时仍未结束的标为 pending",
|
"等待本会话派生的子会话结束(等待期间不调用模型),返回各自状态、用量与汇报摘要;超时仍未结束的标为 pending。派生子会话后汇总前用;没有派生子会话时不要调用",
|
||||||
"read",
|
"read",
|
||||||
"AgentAwait",
|
"AgentAwait",
|
||||||
"read",
|
"read",
|
||||||
@@ -319,7 +356,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"site.list": d(
|
"site.list": d(
|
||||||
"site.list",
|
"site.list",
|
||||||
"列出站点",
|
"列出站点",
|
||||||
"列出已登记站点的健康、登录状态与账号摘要(不含凭证)",
|
"列出已登记站点的健康、登录状态与账号摘要(不含凭证)。排查站点问题或确认站点可用时用",
|
||||||
"read",
|
"read",
|
||||||
"SiteList",
|
"SiteList",
|
||||||
"read",
|
"read",
|
||||||
@@ -331,7 +368,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"site.health_check": d(
|
"site.health_check": d(
|
||||||
"site.health_check",
|
"site.health_check",
|
||||||
"检测站点",
|
"检测站点",
|
||||||
"检测站点首页连通与登录态,写回站点健康状态",
|
"检测站点首页连通与登录态,写回站点健康状态。站点连通或登录态可疑时用;不要频繁检测",
|
||||||
"execute",
|
"execute",
|
||||||
"SiteHealthCheck",
|
"SiteHealthCheck",
|
||||||
"admin",
|
"admin",
|
||||||
@@ -343,7 +380,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"site.refresh_login": d(
|
"site.refresh_login": d(
|
||||||
"site.refresh_login",
|
"site.refresh_login",
|
||||||
"刷新站点登录",
|
"刷新站点登录",
|
||||||
"对站点账号重新登录或校验 Cookie / API Key,成功后拉取用户数据;需持续授权或审批",
|
"对站点账号重新登录或校验 Cookie / API Key,成功后拉取用户数据;需持续授权或审批。站点登录失效时用;验证码等人工步骤无法自动完成",
|
||||||
"execute",
|
"execute",
|
||||||
"SiteLoginRefresh",
|
"SiteLoginRefresh",
|
||||||
"admin",
|
"admin",
|
||||||
@@ -354,7 +391,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
|||||||
"site.user_data": d(
|
"site.user_data": d(
|
||||||
"site.user_data",
|
"site.user_data",
|
||||||
"查站点用户数据",
|
"查站点用户数据",
|
||||||
"读取站点账号的最新用户数据(上传、下载、分享率、魔力、做种)与按天历史",
|
"读取站点账号的最新用户数据(上传、下载、分享率、魔力、做种)与按天历史。关心分享率、魔力或做种情况时用",
|
||||||
"read",
|
"read",
|
||||||
"SiteUserData",
|
"SiteUserData",
|
||||||
"read",
|
"read",
|
||||||
|
|||||||
Reference in new issue
Block a user