Files
UHD_Media_Tool/packages/contracts/src/api.ts
T
root 8315ece4d1 Merge branch 'dl2'
Co-Authored-By: Claude Fable 5.1 <[email protected]>

# Conflicts:
#	apps/web/app/routes/console/downloads.tsx
#	docs/plan/00-first-version-plan.md
2026-09-29 21:45:15 +08:00

2602 lines
83 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
import * as agent from "./agent.ts";
import * as assets from "./assets.ts";
import * as auth from "./auth.ts";
import * as browser from "./browser.ts";
import * as cache from "./cache.ts";
import { MetadataEnrichOutput, WebSearchTestInput, WebSearchTestResult } from "./capabilities-io.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 board from "./download-board.ts";
import * as events from "./events.ts";
import * as execution from "./execution.ts";
import * as files from "./files.ts";
import * as identity from "./identity.ts";
import * as instances from "./instances.ts";
import * as libraryVersions from "./library-versions.ts";
import * as mcp from "./mcp.ts";
import * as ops from "./ops.ts";
import * as organization from "./organization.ts";
import * as pi from "./pi.ts";
import * as policy from "./policy.ts";
import * as schedules from "./schedules.ts";
import * as search from "./search.ts";
import * as settingsNs from "./settings.ts";
import { GlobalSettings, GlobalSettingsUpdate } from "./settings.ts";
import * as sources from "./sources.ts";
export * from "./search.ts";
// 所有 REST 端点的路径常量与请求/响应 schema 汇总。
// path 相对 API_PREFIX,参数用 OpenAPI 风格 `{id}`(server 的 createRoute 直接使用);
// web 用 buildPath() 填参数。响应体统一套 apiResponse() 信封,这里的 response 是 data 部分。
export const API_PREFIX = "/api/v1";
export type HttpMethod = "get" | "post" | "patch" | "put" | "delete";
/**
* 鉴权方式(不写即按 scope 校验,cookie 会话与 API Key 同样适用):
* - public:免鉴权(必须与服务端 http/auth.ts 的 PUBLIC_AUTH_PATHS 一致,测试断言);
* - session:只接受控制台管理员 cookie 会话,API Key(含 admin scope)一律 403。
*/
export type EndpointAuth = "public" | "session";
export interface EndpointDef {
method: HttpMethod;
path: string;
scope: ApiScope;
auth?: EndpointAuth;
summary: string;
params?: z.ZodObject;
query?: z.ZodType;
body?: z.ZodType;
response: z.ZodType;
}
const ep = <E extends EndpointDef>(e: E): E => e;
/** 列表查询:游标分页 + 资源自有过滤 */
const listQuery = <S extends z.ZodRawShape>(filters: S) => CursorQuery.extend(filters);
/** 删除/无内容动作的响应 */
export const OkResult = z.object({ ok: z.literal(true) });
interface CrudSchemas {
row: z.ZodType;
create?: z.ZodType;
update?: z.ZodType;
filters?: z.ZodRawShape;
}
/** 标准资源端点:list / get / create / update / remove(按给出的 schema 决定是否生成) */
function crud<const B extends string>(
base: B,
name: string,
s: CrudSchemas,
scopes: { read: ApiScope; write: ApiScope },
) {
const item = `${base}/{id}` as const;
return {
list: ep({
method: "get",
path: base,
scope: scopes.read,
summary: `${name}列表`,
query: listQuery(s.filters ?? {}),
response: cursorPage(s.row),
}),
get: ep({
method: "get",
path: item,
scope: scopes.read,
summary: `${name}详情`,
params: IdParam,
response: s.row,
}),
...(s.create
? {
create: ep({
method: "post",
path: base,
scope: scopes.write,
summary: `新建${name}`,
body: s.create,
response: s.row,
}),
}
: {}),
...(s.update
? {
update: ep({
method: "patch",
path: item,
scope: scopes.write,
summary: `修改${name}`,
params: IdParam,
body: s.update,
response: s.row,
}),
}
: {}),
};
}
const R = { read: "read", write: "admin" } as const;
const del = (path: string, summary: string) =>
ep({ method: "delete", path, scope: "admin", summary, params: IdParam, response: OkResult });
/** 实例连通性测试 */
const instanceTest = (path: string, summary: string) =>
ep({ method: "post", path, scope: "admin", summary, params: IdParam, response: instances.InstanceTestResult });
/** 入队后台任务的受理结果(taskId = 引擎 run id) */
export const TaskAccepted = z.object({ taskId: Id });
export type TaskAccepted = z.infer<typeof TaskAccepted>;
/** 人工指定来源资源身份的结果(待识别池 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({
mediaEntityId: Id,
changedFields: z.array(z.string()),
lockedFields: z.array(z.string()),
});
const FieldParam = z.object({ id: Id, field: content.EntityMetadataField });
const UnitFieldParam = z.object({ id: Id, field: content.UnitMetadataField });
const UnitFieldOverrideResult = z.object({
contentUnitId: Id,
changedFields: z.array(z.string()),
lockedFields: z.array(z.string()),
});
/** 引擎 run id 不是 uuid(openworkflow 自有 id) */
const EngineRunIdParam = z.object({ id: z.string().min(1).max(200) });
/** 待处理事项聚合分组 */
export const DecisionSummaryGroup = z.object({
kind: agent.DecisionKind,
mediaEntityId: Id.nullable(),
mediaEntityTitle: z.string().nullable(),
count: z.number().int(),
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 = {
// ── 内容 / 身份 ──
mediaEntities: {
...crud(
"/media-entities",
"作品",
{
row: content.MediaEntity,
create: content.MediaEntityCreate,
update: content.MediaEntityUpdate,
filters: {
kind: content.MediaKind.optional(),
status: content.MediaEntityStatus.optional(),
q: z.string().max(200).optional(),
},
},
R,
),
merge: ep({
method: "post",
path: "/media-entities/{id}/merge",
scope: "admin",
summary: "合并重复作品(保留旧 ID 跳转)",
params: IdParam,
body: z.object({ intoId: Id, reason: z.string().max(2000) }),
response: content.MediaEntity,
}),
detail: ep({
method: "get",
path: "/media-entities/{id}/detail",
scope: "read",
summary: "作品详情:外部身份、别名、内容单元、字段来源与覆盖(旧 ID 自动跳转)",
params: IdParam,
response: content.MediaEntityDetail,
}),
setIdentity: ep({
method: "post",
path: "/media-entities/{id}/identities",
scope: "organize.execute",
summary: "人工纠错外部映射:旧映射退役(留修订),新映射 accepted + 人工锁",
params: IdParam,
body: identity.EntityIdentitySet,
response: z.object({
mediaEntityId: Id,
changed: z.boolean(),
identityId: Id.optional(),
retired: z.array(Id).optional(),
}),
}),
refresh: ep({
method: "post",
path: "/media-entities/{id}/refresh",
scope: "organize.execute",
summary:
"刷新元数据(字段级;用户锁优先,不动路径)。标题 / 简介按设置 metadata.languages 回退;full=1 拉取全部季的每集信息",
params: IdParam,
query: z.object({ full: z.enum(["0", "1", "true", "false"]).optional() }),
response: TaskAccepted,
}),
setField: ep({
method: "put",
path: "/media-entities/{id}/fields/{field}",
scope: "organize.execute",
summary: "覆盖作品字段并锁定(自动刷新不再改写)",
params: FieldParam,
body: content.FieldOverrideInput,
response: FieldOverrideResult,
}),
clearField: ep({
method: "delete",
path: "/media-entities/{id}/fields/{field}",
scope: "organize.execute",
summary: "取消字段覆盖与锁定",
params: FieldParam,
response: FieldOverrideResult,
}),
},
contentUnits: {
...crud(
"/content-units",
"内容单元",
{
row: content.ContentUnit,
update: content.ContentUnitUpdate,
filters: { mediaEntityId: Id.optional(), seasonNumber: z.coerce.number().int().optional() },
},
R,
),
setField: ep({
method: "put",
path: "/content-units/{id}/fields/{field}",
scope: "organize.execute",
summary: "覆盖季 / 集字段并锁定(元数据刷新不再改写)",
params: UnitFieldParam,
body: content.FieldOverrideInput,
response: UnitFieldOverrideResult,
}),
clearField: ep({
method: "delete",
path: "/content-units/{id}/fields/{field}",
scope: "organize.execute",
summary: "取消季 / 集字段覆盖与锁定",
params: UnitFieldParam,
response: UnitFieldOverrideResult,
}),
},
titleAliases: {
...crud(
"/title-aliases",
"别名",
{
row: content.TitleAlias,
create: content.TitleAliasCreate,
filters: { mediaEntityId: Id.optional(), q: z.string().max(200).optional() },
},
R,
),
remove: del("/title-aliases/{id}", "删除别名"),
},
externalIdentities: {
...crud(
"/external-identities",
"外部身份",
{
row: identity.ExternalIdentity,
create: identity.ExternalIdentityCreate,
update: identity.ExternalIdentityUpdate,
filters: {
mediaEntityId: Id.optional(),
provider: z.string().optional(),
status: identity.IdentityStatus.optional(),
},
},
R,
),
revisions: ep({
method: "get",
path: "/external-identities/{id}/revisions",
scope: "read",
summary: "身份修订历史",
params: IdParam,
response: IdentityRevisionList,
}),
},
// ── 文件(只读;写入只经整理/对账流程,不开放直接改) ──
mediaAssets: crud(
"/media-assets",
"资产",
{
row: assets.MediaAsset,
filters: { mediaEntityId: Id.optional(), status: assets.AssetStatus.optional() },
},
R,
),
assetUnitCoverage: crud(
"/asset-unit-coverage",
"资产覆盖",
{
row: assets.AssetUnitCoverage,
filters: { mediaAssetId: Id.optional(), contentUnitId: Id.optional() },
},
R,
),
assetLocations: crud(
"/asset-locations",
"资产位置",
{
row: assets.AssetLocation,
filters: { mediaAssetId: Id.optional(), status: assets.LocationStatus.optional() },
},
R,
),
pathBindings: crud(
"/path-bindings",
"路径绑定",
{
row: assets.PathBinding,
filters: { mediaEntityId: Id.optional(), status: assets.BindingStatus.optional() },
},
R,
),
// ── 实例 ──
libraryRoots: {
...crud(
"/library-roots",
"库根",
{
row: instances.LibraryRoot,
create: instances.LibraryRootCreate,
update: instances.LibraryRootUpdate,
},
R,
),
test: instanceTest("/library-roots/{id}/test", "检查库根在线(目录 + 哨兵文件)"),
remove: del("/library-roots/{id}", "删除库根"),
},
downloaderInstances: {
...crud(
"/downloader-instances",
"下载器实例",
{
row: instances.DownloaderInstance,
create: instances.DownloaderInstanceCreate,
update: instances.DownloaderInstanceUpdate,
},
R,
),
test: ep({
method: "post",
path: "/downloader-instances/{id}/test",
scope: "admin",
summary: "测试下载器连接",
params: IdParam,
response: z.object({ ok: z.boolean(), version: z.string().nullable(), message: z.string().nullable() }),
}),
remove: del("/downloader-instances/{id}", "删除下载器实例"),
},
mediaServerInstances: {
...crud(
"/media-server-instances",
"媒体服务器实例",
{
row: instances.MediaServerInstance,
create: instances.MediaServerInstanceCreate,
update: instances.MediaServerInstanceUpdate,
},
R,
),
test: instanceTest("/media-server-instances/{id}/test", "测试媒体服务器连接"),
remove: del("/media-server-instances/{id}", "删除媒体服务器实例"),
},
pathMappings: {
...crud(
"/path-mappings",
"路径映射",
{
row: instances.PathMapping,
create: instances.PathMappingCreate,
update: instances.PathMappingUpdate,
},
R,
),
verify: ep({
method: "post",
path: "/path-mappings/{id}/verify",
scope: "admin",
summary: "校验路径映射可读写与挂载健康",
params: IdParam,
response: instances.PathMapping,
}),
remove: del("/path-mappings/{id}", "删除路径映射"),
},
// ── 来源 ──
sites: {
...crud("/sites", "站点", { row: sources.Site, create: sources.SiteCreate, update: sources.SiteUpdate }, R),
list: ep({
method: "get",
path: "/sites",
scope: "read",
summary: "站点列表(include=accounts 附带账号与整体账号状态)",
query: sources.SiteListQuery,
response: cursorPage(sources.SiteListItem),
}),
remove: del("/sites/{id}", "删除站点:停用后删账号与凭证;已抓到的资源和下载任务保留(标站点已关闭)"),
healthCheck: ep({
method: "post",
path: "/sites/{id}/health-check",
scope: "admin",
summary: "检测健康:首页连通 + 登录态,同步返回并写站点健康列",
params: IdParam,
response: sources.SiteHealthResult,
}),
refreshLogin: ep({
method: "post",
path: "/sites/{id}/refresh-login",
scope: "admin",
summary: "刷新登录:账密重新登录换 Cookie;Cookie / API Key 校验;成功后拉用户数据",
params: IdParam,
body: sources.SiteRefreshLoginInput,
response: sources.SiteRefreshLoginAccepted,
}),
userData: ep({
method: "get",
path: "/sites/{id}/user-data",
scope: "read",
summary: "站点账号用户数据按天历史",
params: IdParam,
query: sources.SiteUserDataQuery,
response: z.object({ items: z.array(sources.SiteUserDataPoint) }),
}),
enable: ep({
method: "post",
path: "/sites/{id}/enable",
scope: "admin",
summary: "开启站点",
params: IdParam,
response: sources.Site,
}),
disable: ep({
method: "post",
path: "/sites/{id}/disable",
scope: "admin",
summary: "关闭站点(停止网络访问与新候选,历史资源仍可查)",
params: IdParam,
response: sources.Site,
}),
snapshot: ep({
method: "post",
path: "/sites/{id}/snapshot",
scope: "admin",
summary: "立即同步站点快照(受站点限流)",
params: IdParam,
body: z.object({ maxPages: z.number().int().min(1).max(20).optional() }),
response: TaskAccepted,
}),
snapshots: ep({
method: "get",
path: "/sites/{id}/snapshots",
scope: "read",
summary: "站点快照/补查记录(覆盖范围与新鲜度)",
params: IdParam,
query: CursorQuery,
response: cursorPage(sources.SiteSnapshot),
}),
requestStats: ep({
method: "get",
path: "/sites/{id}/request-stats",
scope: "read",
summary: "站点请求统计:最近 24 小时对该站发出的请求次数(按小时)与生效的访问默认",
params: IdParam,
response: sources.SiteRequestStats,
}),
credentials: ep({
method: "put",
path: "/sites/{id}/credentials",
scope: "admin",
summary: "设置站点账号凭证(按 kind:password / cookie / api_key / passkey;存为 secret,不回显)",
params: IdParam,
body: instances.SiteCredentialsPut,
response: instances.SiteCredentialsResult,
}),
},
siteDefinitions: {
list: ep({
method: "get",
path: "/site-definitions",
scope: "read",
summary: "随产品发布的站点适配器定义(含是否已登记)",
response: z.object({ items: z.array(sources.SiteDefinitionView) }),
}),
},
/** 站点工作台(整理视图):库存叠加走 inventory.overlay */
siteWorkbench: {
releases: ep({
method: "get",
path: "/sites/{id}/releases",
scope: "read",
summary: "资源列表/搜索(快照优先,必要时实时补查;标注来源与更新时间)",
params: IdParam,
query: sources.WorkbenchReleaseQuery,
response: sources.WorkbenchReleasePage,
}),
release: ep({
method: "get",
path: "/sites/{id}/releases/{rid}",
scope: "read",
summary: "资源详情(refresh=true 时实时复核详情;失败保留既有数据)",
params: z.object({ id: Id, rid: Id }),
query: z.object({ refresh: z.enum(["true", "false"]).optional() }),
response: sources.WorkbenchReleaseDetail,
}),
},
siteAccounts: {
...crud(
"/site-accounts",
"站点账号",
{
row: sources.SiteAccount,
create: sources.SiteAccountCreate,
update: sources.SiteAccountUpdate,
filters: { siteId: Id.optional() },
},
R,
),
remove: del("/site-accounts/{id}", "删除站点账号与其凭证"),
},
feedSources: {
...crud(
"/feed-sources",
"Feed",
{
row: sources.FeedSource,
create: sources.FeedSourceCreate,
update: sources.FeedSourceUpdate,
filters: { siteId: Id.optional(), health: sources.FeedHealth.optional() },
},
R,
),
fetchNow: ep({
method: "post",
path: "/feed-sources/{id}/fetch",
scope: "admin",
summary: "立即抓取(仍受站点限流)",
params: IdParam,
response: z.object({ taskId: Id }),
}),
enable: ep({
method: "post",
path: "/feed-sources/{id}/enable",
scope: "admin",
summary: "启用 Feed",
params: IdParam,
response: sources.FeedSource,
}),
disable: ep({
method: "post",
path: "/feed-sources/{id}/disable",
scope: "admin",
summary: "停用 Feed",
params: IdParam,
response: sources.FeedSource,
}),
reset: ep({
method: "post",
path: "/feed-sources/{id}/reset",
scope: "admin",
summary: "手动恢复:关闭熔断、清空退避(凭证已更新后使用)",
params: IdParam,
response: sources.FeedSource,
}),
},
sourceReleases: {
...crud(
"/source-releases",
"资源",
{
row: sources.SourceRelease,
filters: {
siteId: Id.optional(),
mediaEntityId: Id.optional(),
resolutionStatus: sources.ResolutionStatus.optional(),
q: z.string().max(200).optional(),
},
},
R,
),
assignIdentity: ep({
method: "post",
path: "/source-releases/{id}/identity",
scope: "organize.execute",
summary: "人工指定资源的作品/内容单元(加人工锁,自动识别不再改写)",
params: IdParam,
body: identity.ReleaseIdentityAssign,
response: ReleaseIdentityResult,
}),
unlock: ep({
method: "post",
path: "/source-releases/{id}/unlock",
scope: "organize.execute",
summary: "解除资源的人工锁并重新识别",
params: IdParam,
response: TaskAccepted,
}),
},
observations: crud(
"/observations",
"观察",
{
row: sources.Observation,
filters: { feedId: Id.optional(), status: sources.ObservationStatus.optional() },
},
R,
),
// ── 规则 ──
subscriptions: {
...crud(
"/subscriptions",
"订阅",
{
row: policy.Subscription,
create: policy.SubscriptionCreate,
update: policy.SubscriptionUpdate,
filters: {
status: policy.SubscriptionStatus.optional(),
mediaEntityId: Id.optional(),
mediaKind: policy.SubscriptionMediaKind.optional(),
/** 作品标题 / 原名 / 别名 / 未建档检索词,模糊匹配 */
q: z.string().max(200).optional(),
},
},
R,
),
/** 日历(静态路径,须先于 /subscriptions/{id} 注册) */
calendar: ep({
method: "get",
path: "/subscriptions/calendar",
scope: "read",
summary:
"订阅日历:追踪中订阅范围内集的播出日期(同日同季聚合)与入库状态,电影取上映日期;没有播出日期的列在 unknown。from/to 为 YYYY-MM-DD,跨度不超过 62 天",
query: z.object({
from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
mediaKind: policy.SubscriptionMediaKind.optional(),
}),
response: policy.SubscriptionCalendar,
}),
reset: ep({
method: "post",
path: "/subscriptions/{id}/reset",
scope: "admin",
summary: "重置进度:取消已预留未提交、下载任务已不在的计划,清延后记录,立即重评;仍在下载的计划与已入库资产保留",
params: IdParam,
response: policy.SubscriptionResetResult,
}),
/** 总览:在 crud 列表上加分组/排序/筛选,条目带卡片派生字段,另给各分组计数(docs/design/subscriptions.md §1) */
list: ep({
method: "get",
path: "/subscriptions",
scope: "read",
summary:
"订阅总览(分组、进度、各版本摘要、上次更新、下次搜索、站点、规则组;cancelled 只在显式 status=cancelled 时返回)",
query: listQuery({
status: policy.SubscriptionStatus.optional(),
mediaEntityId: Id.optional(),
mediaKind: policy.SubscriptionMediaKind.optional(),
/** 作品标题 / 原名 / 别名 / 未建档检索词,模糊匹配 */
q: z.string().max(200).optional(),
group: policy.SubscriptionGroup.optional(),
sort: policy.SubscriptionSort.optional(),
order: z.enum(["asc", "desc"]).optional(),
siteId: Id.optional(),
ruleGroupId: Id.optional(),
origin: policy.SubscriptionOrigin.optional(),
upgrading: z.enum(["true", "false"]).optional(),
}),
response: policy.SubscriptionOverviewPage,
}),
coverage: ep({
method: "get",
path: "/subscriptions/{id}/coverage",
scope: "read",
summary:
"覆盖矩阵:季 → 集 × 收录版本(电影:内容版本 × 收录版本),每格 have/inflight/missing/skipped;缓存 30s,refresh=true 强制重算",
params: IdParam,
query: z.object({ refresh: z.enum(["true", "false"]).optional() }),
response: policy.SubscriptionCoverage,
}),
pause: ep({
method: "post",
path: "/subscriptions/{id}/pause",
scope: "admin",
summary: "暂停订阅(不评估、不搜索)",
params: IdParam,
response: policy.Subscription,
}),
resume: ep({
method: "post",
path: "/subscriptions/{id}/resume",
scope: "admin",
summary: "恢复已暂停的订阅(TMDB 未就绪回到 pending),立即重评并排上搜索",
params: IdParam,
response: policy.Subscription,
}),
resubscribe: ep({
method: "post",
path: "/subscriptions/{id}/resubscribe",
scope: "admin",
summary: "重新订阅已完成/已取消的订阅(历史保留),立即重评并搜索",
params: IdParam,
response: policy.Subscription,
}),
batch: ep({
method: "post",
path: "/subscriptions/batch",
scope: "admin",
summary: "批量:立即搜索 / 暂停 / 恢复 / 删除(删除 = 取消,写审计)",
body: policy.SubscriptionBatch,
response: policy.SubscriptionBatchResult,
}),
fillSlot: ep({
method: "post",
path: "/subscriptions/{id}/slots/{slotId}/fill",
scope: "admin",
summary: "手动补全某收录版本(不受自动补全开关与补全预算限制)",
params: z.object({ id: Id, slotId: Id }),
body: policy.SlotFillRequest,
response: z.object({ runId: z.string(), deduplicated: z.boolean() }),
}),
slotOrder: ep({
method: "put",
path: "/subscriptions/{id}/slot-order",
scope: "admin",
summary: "收录版本排序(第一个为主版本)",
params: IdParam,
body: policy.SlotOrderSet,
response: z.array(policy.AcquisitionSlot),
}),
search: ep({
method: "post",
path: "/subscriptions/{id}/search",
scope: "admin",
summary: "手动主动搜索(按标题/别名在已开启站点搜索,结果走同一识别评估链)",
params: IdParam,
response: z.object({ runId: z.string(), deduplicated: z.boolean() }),
}),
progress: ep({
method: "get",
path: "/subscriptions/{id}/progress",
scope: "read",
summary: "订阅进度:季总集数账本、每集状态(已覆盖/在途/缺失/疑似缺集)、收录版本在途与延后",
params: IdParam,
response: policy.SubscriptionProgress,
}),
setEpisodeTotal: ep({
method: "post",
path: "/subscriptions/{id}/episode-total",
scope: "admin",
summary: "手动设置季总集数(只升;locked 锁定后刷新不再改写)",
params: IdParam,
body: policy.EpisodeTotalSet,
response: policy.SubscriptionProgress,
}),
},
subscriptionHistory: crud(
"/subscription-history",
"订阅完成历史",
{
row: policy.SubscriptionHistory,
filters: { mediaEntityId: Id.optional(), subscriptionId: Id.optional() },
},
R,
),
policyRevisions: {
...crud(
"/policy-revisions",
"策略版本",
{
row: policy.PolicyRevision,
create: policy.PolicyRevisionCreate,
update: policy.PolicyRevisionUpdate,
filters: { policyKey: z.string().optional(), status: policy.PolicyStatus.optional() },
},
R,
),
preview: ep({
method: "post",
path: "/policy-revisions/{id}/preview",
scope: "read",
summary: "历史回放预览",
params: IdParam,
response: policy.PolicyRevision,
}),
publish: ep({
method: "post",
path: "/policy-revisions/{id}/publish",
scope: "admin",
summary: "发布策略版本",
params: IdParam,
body: z.object({ expectedRevision: z.number().int().min(1) }),
response: policy.PolicyRevision,
}),
rollback: ep({
method: "post",
path: "/policy-revisions/{id}/rollback",
scope: "admin",
summary: "回滚到该版本",
params: IdParam,
response: policy.PolicyRevision,
}),
},
acquisitionSlots: crud(
"/acquisition-slots",
"收录版本",
{
row: policy.AcquisitionSlot,
create: policy.AcquisitionSlotCreate,
update: policy.AcquisitionSlotUpdate,
filters: {
mediaEntityId: Id.optional(),
status: policy.SlotStatus.optional(),
subscriptionId: Id.optional(),
},
},
R,
),
preferenceTags: {
...crud(
"/preference-tags",
"偏好标签",
{
row: policy.PreferenceTag,
create: policy.PreferenceTagCreate,
update: policy.PreferenceTagUpdate,
filters: { status: policy.PreferenceStatus.optional(), kind: policy.PreferenceKind.optional() },
},
R,
),
/** 删除 = 标记 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",
"偏好抑制",
{
row: policy.PreferenceSuppression,
create: policy.PreferenceSuppressionCreate,
update: policy.PreferenceSuppressionUpdate,
filters: { active: z.enum(["true", "false"]).optional() },
},
R,
),
// ── 规则(W9-D):规则组 = 收录过滤条件 + 偏好标签合一。静态路径先于 /rules/{id} 注册 ──
rules: {
keywords: ep({
method: "get",
path: "/rules/keywords",
scope: "read",
summary: "关键字表:内置(对齐 MP,含判定依据与上游正则)+ 自定义条件",
response: policy.RuleKeywordList,
}),
parse: ep({
method: "post",
path: "/rules/parse",
scope: "read",
summary: "规则串 → 结构化层(`>` 分层,层内!& | 与括号);错误逐条给位置与相近名",
body: policy.RuleParseRequest,
response: policy.RuleParseResult,
}),
serialize: ep({
method: "post",
path: "/rules/serialize",
scope: "read",
summary: "结构化层 → 规范化规则串",
body: policy.RuleSerializeRequest,
response: policy.RuleSerializeResult,
}),
defaults: ep({
method: "get",
path: "/rules/defaults",
scope: "read",
summary: "三套默认规则组链:订阅 / 洗版 / 搜索",
response: policy.RuleDefaults,
}),
setDefaults: ep({
method: "put",
path: "/rules/defaults",
scope: "admin",
summary: "设置默认规则组链(按数组顺序串联,pri 取最后一个带层的组)",
body: policy.RuleDefaults,
response: policy.RuleDefaults,
}),
...crud(
"/rules",
"规则组",
{
row: policy.RuleGroup,
create: policy.RuleGroupCreate,
update: policy.RuleGroupUpdate,
filters: {
kind: policy.RuleGroupKind.optional(),
source: policy.RuleGroupSource.optional(),
enabled: z.enum(["true", "false"]).optional(),
includeDeleted: z.enum(["true", "false"]).optional(),
q: z.string().max(80).optional(),
},
},
R,
),
/** 删除 = 标记 deleted_at + 写抑制记录 */
remove: del("/rules/{id}", "删除规则组(留抑制记录;被引用时 409)"),
revisions: ep({
method: "get",
path: "/rules/{id}/revisions",
scope: "read",
summary: "规则组修订历史(新→旧;取自审计事件)",
params: IdParam,
response: policy.RuleGroupRevisionList,
}),
preview: ep({
method: "post",
path: "/rules/{id}/preview",
scope: "read",
summary: "试算:对最近 N 条已识别资源(或指定资源/标题)跑本组,给 pri、命中层与拒绝原因",
params: IdParam,
body: policy.RulePreviewRequest,
response: policy.RulePreviewResult,
}),
},
ruleAtoms: {
...crud(
"/rules/atoms",
"自定义条件",
{ row: policy.RuleAtom, create: policy.RuleAtomCreate, update: policy.RuleAtomUpdate },
R,
),
remove: del("/rules/atoms/{id}", "删除自定义条件(被规则组引用时 409)"),
},
/** /policy-revisions 的新路径(旧路径保留为别名,见 docs/runbook/rules.md) */
rulePolicies: {
list: ep({
method: "get",
path: "/rules/policies",
scope: "read",
summary: "收录策略版本列表",
query: listQuery({ policyKey: z.string().optional(), status: policy.PolicyStatus.optional() }),
response: cursorPage(policy.PolicyRevision),
}),
get: ep({
method: "get",
path: "/rules/policies/{id}",
scope: "read",
summary: "收录策略版本",
params: IdParam,
response: policy.PolicyRevision,
}),
create: ep({
method: "post",
path: "/rules/policies",
scope: "admin",
summary: "新建策略草案",
body: policy.PolicyRevisionCreate,
response: policy.PolicyRevision,
}),
update: ep({
method: "patch",
path: "/rules/policies/{id}",
scope: "admin",
summary: "修改策略草案",
params: IdParam,
body: policy.PolicyRevisionUpdate,
response: policy.PolicyRevision,
}),
preview: ep({
method: "post",
path: "/rules/policies/{id}/preview",
scope: "read",
summary: "预览策略草案",
params: IdParam,
response: policy.PolicyRevision,
}),
publish: ep({
method: "post",
path: "/rules/policies/{id}/publish",
scope: "admin",
summary: "发布策略版本",
params: IdParam,
body: z.object({ expectedRevision: z.number().int().min(1) }),
response: policy.PolicyRevision,
}),
rollback: ep({
method: "post",
path: "/rules/policies/{id}/rollback",
scope: "admin",
summary: "回滚到该策略版本",
params: IdParam,
response: policy.PolicyRevision,
}),
},
/** /preference-suppressions 的新路径(旧路径保留为别名) */
ruleSuppressions: crud(
"/rules/suppressions",
"规则抑制记录",
{
row: policy.PreferenceSuppression,
create: policy.PreferenceSuppressionCreate,
update: policy.PreferenceSuppressionUpdate,
filters: { active: z.enum(["true", "false"]).optional() },
},
R,
),
// ── 执行 ──
tasks: {
...crud(
"/tasks",
"任务",
{
row: execution.Task,
filters: {
status: execution.TaskStatus.optional(),
kind: execution.TaskKind.optional(),
subjectId: Id.optional(),
},
},
R,
),
cancel: ep({
method: "post",
path: "/tasks/{id}/cancel",
scope: "organize.execute",
summary: "取消任务(停止未来步骤)",
params: IdParam,
body: execution.TaskCancelRequest,
response: execution.Task,
}),
retry: ep({
method: "post",
path: "/tasks/{id}/retry",
scope: "organize.execute",
summary: "重试失败任务",
params: IdParam,
response: execution.Task,
}),
operations: ep({
method: "get",
path: "/tasks/{id}/operations",
scope: "read",
summary: "任务的操作账本(业务时间线)",
params: IdParam,
response: z.object({ items: z.array(execution.AssetOperation) }),
}),
},
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",
"下载任务",
{
row: execution.DownloadTask,
filters: {
instanceId: Id.optional(),
status: execution.DownloadStatus.optional(),
managed: z.enum(["true", "false"]).optional(),
},
},
R,
),
board: ep({
method: "get",
path: "/download-tasks/board",
scope: "read",
summary:
"下载面板:按进行中 / 已完成分组列出下载任务,附每个种子的实时速度、剩余时间、连接数与下载器总速度(实时数据短时缓存)",
query: board.DownloadBoardQuery,
response: board.DownloadBoard,
}),
pause: ep({
method: "post",
path: "/download-tasks/{id}/pause",
scope: "organize.execute",
summary: "暂停(记为用户暂停,阈值恢复不会自动恢复)",
params: IdParam,
response: execution.DownloadTask,
}),
resume: ep({
method: "post",
path: "/download-tasks/{id}/resume",
scope: "organize.execute",
summary: "恢复下载",
params: IdParam,
response: execution.DownloadTask,
}),
},
// ── 人工 / AI ──
decisions: {
...crud(
"/decisions",
"待处理事项",
{
row: agent.Decision,
filters: { status: agent.DecisionStatus.optional(), kind: agent.DecisionKind.optional() },
},
R,
),
resolve: ep({
method: "post",
path: "/decisions/{id}/resolve",
scope: "organize.execute",
summary: "处理待处理事项",
params: IdParam,
body: agent.DecisionResolveInput,
response: agent.Decision,
}),
summary: ep({
method: "get",
path: "/decisions/summary",
scope: "read",
summary: "未关闭待处理事项按作品与问题类型聚合",
query: z.object({ status: agent.DecisionStatus.optional() }),
response: DecisionSummary,
}),
},
approvals: {
...crud(
"/approvals",
"审批",
{
row: agent.Approval,
filters: { status: agent.ApprovalStatus.optional(), planId: Id.optional() },
},
R,
),
// 审批的申请与决定只在控制台管理员会话里做:API Key(哪怕持 organize.execute)不能给自己的计划开审批(验收 S1)
update: ep({
method: "patch",
path: "/approvals/{id}",
scope: "admin",
auth: "session",
summary: "批准/拒绝/撤销单次审批(只接受控制台管理员会话)",
params: IdParam,
body: agent.ApprovalUpdate,
response: agent.Approval,
}),
create: ep({
method: "post",
path: "/approvals",
scope: "admin",
auth: "session",
summary: "为计划申请单次审批(绑定 plan id + payload hash + revision + 有效期;只接受控制台管理员会话)",
body: agent.ApprovalRequest,
response: agent.Approval,
}),
},
automationGrants: crud(
"/automation-grants",
"持续授权",
{
row: agent.AutomationGrant,
create: agent.AutomationGrantCreate,
update: agent.AutomationGrantUpdate,
filters: { status: agent.GrantStatus.optional() },
},
R,
),
/** Agent 会话(W2-D):建 run 只入队持久任务 runAgent;增量经全局 SSE(agent.*),断线按 runSeq 用 afterSeq 补读 */
agentRuns: {
list: ep({
method: "get",
path: "/agent/runs",
scope: "read",
summary: "智能体运行列表(新→旧)",
query: agent.AgentRunListQuery,
response: cursorPage(agent.AgentRun),
}),
get: ep({
method: "get",
path: "/agent/runs/{id}",
scope: "read",
summary: "智能体运行详情:事件按 seq 分页、追加消息、服务端核查的目标汇报",
params: IdParam,
query: agent.AgentRunDetailQuery,
response: agent.AgentRunDetail,
}),
create: ep({
method: "post",
path: "/agent/runs",
scope: "organize.execute",
summary: "委托目标:建 agent_run 并入队 runAgent(proposeFirst = 先看方案再执行;mode 已弃用)",
body: agent.AgentRunStartInput,
response: agent.AgentRun,
}),
message: ep({
method: "post",
path: "/agent/runs/{id}/messages",
scope: "organize.execute",
summary: "追加用户消息(纠正/补充);运行已结束时唤醒续跑",
params: IdParam,
body: agent.AgentRunMessageInput,
response: agent.AgentRun,
}),
// 回答 agent.propose_options(会话挂起等待中):选一个方案或都不要,会话随即继续
choose: ep({
method: "post",
path: "/agent/runs/{id}/choice",
scope: "organize.execute",
summary: "回答方案选择:选一个方案或都不要(说明原因),挂起的会话随即继续",
params: IdParam,
body: agent.AgentRunChoiceInput,
response: agent.AgentRun,
}),
abort: ep({
method: "post",
path: "/agent/runs/{id}/abort",
scope: "organize.execute",
summary: "中止运行(停止未来步骤,不撤销已完成的外部动作)",
params: IdParam,
body: agent.AgentRunAbortInput,
response: agent.AgentRun,
}),
},
artifacts: crud(
"/artifacts",
"产物",
{
row: agent.Artifact,
filters: { agentRunId: Id.optional(), taskId: Id.optional(), kind: agent.ArtifactKind.optional() },
},
R,
),
// ── 运维 ──
reconcileRuns: crud(
"/reconcile-runs",
"校验运行",
{
row: ops.ReconcileRun,
filters: { kind: ops.ReconcileKind.optional(), status: ops.ReconcileStatus.optional() },
},
R,
),
serverMappings: crud(
"/server-mappings",
"库存映射",
{
row: ops.ServerMapping,
filters: {
mediaServerInstanceId: Id.optional(),
mediaEntityId: Id.optional(),
matchStatus: ops.ServerMatchStatus.optional(),
},
},
R,
),
apiKeys: {
list: ep({
method: "get",
path: "/api-keys",
scope: "admin",
summary: "API Key 列表",
query: CursorQuery,
response: cursorPage(ops.ApiKey),
}),
create: ep({
method: "post",
path: "/api-keys",
scope: "admin",
summary: "新建 API Key(明文只返回一次)",
body: ops.ApiKeyCreate,
response: ops.ApiKeyCreated,
}),
update: ep({
method: "patch",
path: "/api-keys/{id}",
scope: "admin",
summary: "修改/撤销 API Key",
params: IdParam,
body: ops.ApiKeyUpdate,
response: ops.ApiKey,
}),
revoke: del("/api-keys/{id}", "撤销 API Key"),
},
auditEvents: {
list: ep({
method: "get",
path: "/audit-events",
scope: "admin",
summary: "审计事件(按 actor/action/target 过滤,新→旧)",
query: ops.AuditEventQuery,
response: cursorPage(ops.AuditEvent),
}),
},
secrets: {
list: ep({
method: "get",
path: "/secrets",
scope: "admin",
summary: "凭证元数据列表",
query: CursorQuery,
response: cursorPage(ops.Secret),
}),
create: ep({
method: "post",
path: "/secrets",
scope: "admin",
summary: "新建凭证(只写)",
body: ops.SecretCreate,
response: ops.Secret,
}),
update: ep({
method: "patch",
path: "/secrets/{id}",
scope: "admin",
summary: "轮换凭证",
params: IdParam,
body: ops.SecretUpdate,
response: ops.Secret,
}),
remove: del("/secrets/{id}", "删除凭证(被引用时 409)"),
},
// ── 整理(公开合同) ──
organization: {
list: ep({
method: "get",
path: "/organization-records",
scope: "organize.read",
summary: "整理记录列表",
query: organization.OrganizationListQuery,
response: cursorPage(organization.PublicOrganizationRecord),
}),
get: ep({
method: "get",
path: "/organization-records/{id}",
scope: "organize.read",
summary: "整理记录详情(含逐文件结果)",
params: IdParam,
response: organization.PublicOrganizationRecord,
}),
changes: ep({
method: "get",
path: "/organization-records/changes",
scope: "organize.read",
summary: "按 changeSeq 增量读取(含既有记录状态更新)",
query: organization.OrganizationChangesQuery,
response: organization.OrganizationChangesResponse,
}),
create: ep({
method: "post",
path: "/organization-records",
scope: "organize.execute",
summary: "手动发起整理",
body: organization.OrganizeRequest,
response: organization.OrganizeAccepted,
}),
},
// ── 文件管理(docs/plan/06-file-manager-spec.md;只浏览已登记根,写操作两段式 dryRun → planId) ──
files: {
roots: ep({
method: "get",
path: "/files/roots",
scope: "organize.read",
summary: "可浏览的根(库根 + 下载根),含实时健康与空间",
response: files.FileRootList,
}),
list: ep({
method: "get",
path: "/files",
scope: "organize.read",
summary: "列目录(分页/排序/过滤/截断,行内业务状态);根离线 503",
query: files.FilesListQuery,
response: files.FilesListResponse,
}),
stat: ep({
method: "get",
path: "/files/stat",
scope: "organize.read",
summary: "单项详情:realpath/inode/nlink/sidecars/路径绑定",
query: files.FileStatQuery,
response: files.FileStat,
}),
mkdir: ep({
method: "post",
path: "/files/mkdir",
scope: "organize.execute",
summary: "新建目录(非递归,已存在 409,仅 writable 根)",
body: files.FilesMkdirRequest,
response: files.FilesMkdirResult,
}),
rename: ep({
method: "post",
path: "/files/rename",
scope: "organize.execute",
summary: "同目录改名(不覆盖;同步 asset_location 与 path_binding 修订;做种中 409)",
body: files.FilesRenameRequest,
response: files.FilesRenameResult,
}),
move: ep({
method: "post",
path: "/files/move",
scope: "organize.execute",
summary: "移动/复制到目录:dryRun 返回 planId,执行回传 planId(源变更 409 plan_stale)",
body: files.FilesMoveRequest,
response: files.FilesTransferResult,
}),
copy: ep({
method: "post",
path: "/files/copy",
scope: "organize.execute",
summary: "复制到目录(同 /files/move mode=copy)",
body: files.FilesCopyRequest,
response: files.FilesTransferResult,
}),
organize: ep({
method: "post",
path: "/files/organize",
scope: "organize.execute",
summary: "按作品分组整理入库(每组走 planImport,与 /organization-records 同实现)",
body: files.FilesOrganizeRequest,
response: files.FilesOrganizeResult,
}),
delete: ep({
method: "post",
path: "/files/delete",
scope: "organize.execute",
summary: "删除(默认进回收站;引用检查:做种/资产/绑定;permanent 仅 admin 会话)",
body: files.FilesDeleteRequest,
response: files.FilesDeleteResult,
}),
trash: ep({
method: "get",
path: "/files/trash",
scope: "organize.read",
summary: "回收站列表",
query: files.FilesTrashQuery,
response: files.FilesTrashList,
}),
trashRestore: ep({
method: "post",
path: "/files/trash/restore",
scope: "organize.execute",
summary: "从回收站还原(原位被占的项失败 conflict)",
body: files.FilesTrashRestoreRequest,
response: files.FilesTrashRestoreResult,
}),
trashPurge: ep({
method: "post",
path: "/files/trash/purge",
scope: "admin",
auth: "session",
summary: "清空回收站(缺省只清过期项;仅管理员控制台会话,API Key 与 DEV key 一律 403)",
body: files.FilesTrashPurgeRequest,
response: files.FilesTrashPurgeResult,
}),
identify: ep({
method: "post",
path: "/files/identify",
scope: "organize.read",
summary: "识别(只读):解析 + 本地/TMDB 召回前 5 候选,不落库",
body: files.FilesIdentifyRequest,
response: files.FilesIdentifyResult,
}),
scrape: ep({
method: "post",
path: "/files/scrape",
scope: "organize.execute",
summary: "刮削已登记作品(字段级刷新,遵守字段锁;NFO 投影 W5)",
body: files.FilesScrapeRequest,
response: files.FilesScrapeResult,
}),
probe: ep({
method: "post",
path: "/files/probe",
scope: "organize.execute",
summary: "ffprobe 摘要(≤20 项;已登记资产写回 media_info,所以要 organize.execute)",
body: files.FilesProbeRequest,
response: files.FilesProbeResult,
}),
download: ep({
method: "get",
path: "/files/download",
scope: "admin",
auth: "session",
summary: "下载小文件(nfo/字幕/图片/文本/.torrent,≤20 MiB;流式;仅管理员控制台会话;审计)",
query: files.FilesDownloadQuery,
response: files.FilesDownloadResponse,
}),
preview: ep({
method: "get",
path: "/files/preview",
scope: "admin",
auth: "session",
summary:
"结构化预览(文本/NFO 前 1 MiB 带编码探测、字幕前 200 条、.torrent 文件列表、图片尺寸;仅管理员控制台会话)",
query: files.FilesPreviewQuery,
response: files.FilePreview,
}),
records: ep({
method: "get",
path: "/files/records",
scope: "organize.read",
summary: "按整理记录 id 或 planId 回查整理记录(整理页预填)",
query: files.FilesRecordsQuery,
response: files.FilesRecordsResult,
}),
// W9-H 文件管理 v2 后端缺口
entityFromCandidate: ep({
method: "post",
path: "/files/entity-from-candidate",
scope: "organize.execute",
summary: "按 TMDB 候选就地建本地作品 + 外部映射(accepted/manual)+ 季集单元;幂等",
body: files.FilesEntityFromCandidateRequest,
response: files.FilesEntityFromCandidateResult,
}),
scrapeBatch: ep({
method: "post",
path: "/files/scrape-batch",
scope: "organize.execute",
summary: "批量刷新元数据:按作品去重入队 refreshMetadata(锁定字段不变)",
body: files.FilesScrapeBatchRequest,
response: files.FilesScrapeBatchResult,
}),
tree: ep({
method: "get",
path: "/files/tree",
scope: "organize.read",
summary: "目录摘要:上一级、子目录(是否有下级)、视频/已登记计数(树与上一级用)",
query: files.FilesTreeQuery,
response: files.FilesTreeResult,
}),
recordSources: ep({
method: "get",
path: "/files/record-sources",
scope: "organize.read",
summary: "整理记录的源路径换算成登记目录 FileRef(重新整理用)",
query: files.FilesRecordSourcesQuery,
response: files.FilesRecordSourcesResult,
}),
suggestName: ep({
method: "post",
path: "/files/suggest-name",
scope: "organize.read",
summary: "按命名模板(作品 > 库根 > 全局)生成推荐名;识别不可信时 name=null + 原因",
body: files.FilesSuggestNameRequest,
response: files.FilesSuggestNameResult,
}),
},
// ── 能力 ──
capabilities: {
list: ep({
method: "get",
path: "/capabilities",
scope: "read",
summary: "能力注册中心描述",
response: z.object({ items: z.array(CapabilityDescriptor) }),
}),
invoke: ep({
method: "post",
path: "/capabilities/{capabilityId}/invoke",
scope: "read",
summary: "调用能力(实际所需 scope 见描述符;服务端按能力再次鉴权)",
params: z.object({ capabilityId: KnownCapabilityId }),
body: z.object({ input: z.unknown(), preview: z.boolean().default(false) }),
response: z.object({ output: z.unknown(), taskId: Id.nullable(), planId: Id.nullable() }),
}),
stats: ep({
method: "get",
path: "/capabilities/stats",
scope: "read",
summary: "能力调用统计:每能力调用数、失败数、平均耗时、最近调用、调用来源(内置智能体 / MCP / REST)",
query: mcp.CapabilityStatsQuery,
response: mcp.CapabilityStatsResponse,
}),
tools: ep({
method: "get",
path: "/capabilities/tools",
scope: "read",
summary: "能力清单:描述符、工具名、输入输出 JSON Schema、当前授权状态",
response: mcp.CapabilityToolsResponse,
}),
},
// ── 对外 MCP 服务端(Streamable HTTP 在 /mcp,不进 OpenAPI;这里是状态与配置)──
mcp: {
status: ep({
method: "get",
path: "/mcp/status",
scope: "admin",
summary: "MCP 服务端状态:启用、允许的 API Key、当前会话、最近调用、限速",
response: mcp.McpServerStatus,
}),
update: ep({
method: "patch",
path: "/mcp/config",
scope: "admin",
summary: "修改 MCP 服务端配置(启用开关、允许的 API Key)",
body: mcp.McpServerConfigUpdate,
response: mcp.McpServerConfig,
}),
},
// ── 缓存(C2-A,docs/runbook/cache.md) ──
cache: {
tmdb: ep({
method: "get",
path: "/cache/tmdb",
scope: "admin",
summary: "TMDB 缓存条目(请求响应与识别结果:键、过期时间、大小)与上限 / TTL",
query: cache.TmdbCacheQuery,
response: cache.TmdbCacheList,
}),
tmdbClear: ep({
method: "post",
path: "/cache/tmdb/clear",
scope: "admin",
summary: "清空 TMDB 缓存(请求响应与识别结果)",
response: cache.CacheClearResult,
}),
images: ep({
method: "get",
path: "/cache/images",
scope: "admin",
summary: "图片代理缓存用量",
response: cache.ImageCacheStatus,
}),
imagesClear: ep({
method: "post",
path: "/cache/images/clear",
scope: "admin",
summary: "清空图片代理缓存",
response: cache.CacheClearResult,
}),
},
// ── 网页搜索(WEB-1,docs/runbook/websearch.md) ──
websearch: {
test: ep({
method: "post",
path: "/websearch/test",
scope: "admin",
summary: "用已保存的搜索服务 Key 实际查询一次(不走缓存,计入每分钟上限)",
body: WebSearchTestInput,
response: WebSearchTestResult,
}),
},
// ── 设置 ──
settings: {
get: ep({ method: "get", path: "/settings", scope: "read", summary: "全局设置", response: GlobalSettings }),
update: ep({
method: "patch",
path: "/settings",
scope: "admin",
summary: "修改全局设置",
body: GlobalSettingsUpdate,
response: GlobalSettings,
}),
},
// ── 整理命名模板(W9-C,docs/design/naming-template.md):保存前服务端编译校验 ──
naming: {
variables: ep({
method: "get",
path: "/naming/variables",
scope: "read",
summary: "命名变量表(名称对齐 MP,含说明/示例值/我们特有标记)、过滤器、预览内置样本",
response: settingsNs.NamingVariablesResponse,
}),
preview: ep({
method: "post",
path: "/naming/preview",
scope: "read",
summary:
"预览:模板 + 样本(文件名 / 资源 / 资产 / 内置)→ 相对路径、段角色、错误(行列号)、提示;已有路径绑定时目录用绑定",
body: settingsNs.NamingPreviewInput,
response: settingsNs.NamingPreviewResult,
}),
templates: ep({
method: "get",
path: "/naming/templates",
scope: "read",
summary: "全局 / 库根 / 作品三级模板与按类型的生效结果(作品 > 库根 > 全局)",
query: settingsNs.NamingTemplatesQuery,
response: settingsNs.NamingTemplatesView,
}),
saveTemplates: ep({
method: "put",
path: "/naming/templates",
scope: "admin",
summary: "保存全局命名模板(逐条编译校验,有错 422 不保存)",
body: settingsNs.NamingSettingsPut,
response: settingsNs.NamingSettings,
}),
libraryRoot: ep({
method: "put",
path: "/library-roots/{id}/naming",
scope: "admin",
summary: "库根级命名覆盖(按类型;null 清除)",
params: IdParam,
body: settingsNs.LibraryRootNamingPut,
response: settingsNs.LibraryRootNamingPut,
}),
entity: ep({
method: "put",
path: "/media-entities/{id}/naming-override",
scope: "admin",
summary: "作品级命名覆盖(按类型;null 清除)。已有路径绑定的目录不受影响,只影响之后的文件名与新季目录",
params: IdParam,
body: settingsNs.EntityNamingOverridePut,
response: settingsNs.EntityNamingOverridePut,
}),
},
// ── Pi 模型上游(W7-A):多上游共存 + 订阅登录;写操作与登录只接受控制台会话 ──
pi: {
providers: ep({
method: "get",
path: "/pi/providers",
scope: "admin",
summary: "Pi provider 能力表(认证方式、订阅登录方式与条款提示、baseUrl 覆盖、env 字段、静态模型表)",
query: pi.PiProvidersQuery,
response: pi.PiProvidersResponse,
}),
list: ep({
method: "get",
path: "/pi/upstreams",
scope: "admin",
summary: "模型服务列表(不含密钥)",
response: pi.PiUpstreamList,
}),
get: ep({
method: "get",
path: "/pi/upstreams/{id}",
scope: "admin",
summary: "模型服务详情",
params: IdParam,
response: pi.PiUpstream,
}),
create: ep({
method: "post",
path: "/pi/upstreams",
scope: "admin",
auth: "session",
summary: "新建模型服务(apiKey 只写,存 secret 表)",
body: pi.PiUpstreamCreate,
response: pi.PiUpstream,
}),
update: ep({
method: "patch",
path: "/pi/upstreams/{id}",
scope: "admin",
auth: "session",
summary: "修改模型服务(provider 不可改;makeDefault 设默认模型服务)",
params: IdParam,
body: pi.PiUpstreamUpdate,
response: pi.PiUpstream,
}),
remove: ep({
method: "delete",
path: "/pi/upstreams/{id}",
scope: "admin",
auth: "session",
summary: "删除模型服务(连同凭据;是默认模型服务时清空默认)",
params: IdParam,
response: OkResult,
}),
test: ep({
method: "post",
path: "/pi/upstreams/{id}/test",
scope: "admin",
auth: "session",
summary: "测试模型服务:默认只解析凭据与校验模型(不产生费用);mode=completion 发一次最小补全",
params: IdParam,
body: pi.PiUpstreamTestRequest,
response: pi.PiUpstreamTestResult,
}),
models: ep({
method: "get",
path: "/pi/upstreams/{id}/models",
scope: "admin",
summary: "模型列表:已获取过远程列表时返回远程与静态目录的合并(远程优先),否则静态目录",
params: IdParam,
response: pi.PiUpstreamModels,
}),
modelsRefresh: ep({
method: "post",
path: "/pi/upstreams/{id}/models/refresh",
scope: "admin",
auth: "session",
summary: "获取模型列表:用该模型服务的凭证调 provider 的模型列表接口并存库;失败或不支持时回静态目录并给出原因",
params: IdParam,
response: pi.PiUpstreamModels,
}),
modelsProbe: ep({
method: "post",
path: "/pi/models/probe",
scope: "admin",
auth: "session",
summary: "新建模型服务前试拉模型列表(凭证只用于这一次请求,不落库)",
body: pi.PiModelsProbeRequest,
response: pi.PiUpstreamModels,
}),
oauthStart: ep({
method: "post",
path: "/pi/upstreams/{id}/oauth/start",
scope: "admin",
auth: "session",
summary: "开始订阅登录:返回授权链接(browser)或验证码(device)",
params: IdParam,
body: pi.PiOAuthStartRequest,
response: pi.PiOAuthStartResult,
}),
oauthComplete: ep({
method: "post",
path: "/pi/upstreams/{id}/oauth/complete",
scope: "admin",
auth: "session",
summary: "完成订阅登录:browser 粘回地址栏 URL;device 服务端轮询(本次最多等 waitMs,未完成返回 pending)",
params: IdParam,
body: pi.PiOAuthCompleteRequest,
response: pi.PiOAuthCompleteResult,
}),
oauthLogout: ep({
method: "post",
path: "/pi/upstreams/{id}/oauth/logout",
scope: "admin",
auth: "session",
summary: "退出订阅登录:删除本地 token(不会撤销提供方侧授权)",
params: IdParam,
response: pi.PiUpstream,
}),
},
// ── 登录门禁 ──
/** /auth/setup、/auth/login、/auth/session 免鉴权;改密与 TOTP 只接受控制台 cookie 会话 */
auth: {
setup: ep({
method: "post",
path: "/auth/setup",
scope: "read",
auth: "public",
summary: "首次初始化管理员",
body: auth.AdminSetupRequest,
response: auth.SessionInfo,
}),
login: ep({
method: "post",
path: "/auth/login",
scope: "read",
auth: "public",
summary: "管理员登录",
body: auth.LoginRequest,
response: auth.LoginResult,
}),
logout: ep({ method: "post", path: "/auth/logout", scope: "read", summary: "注销当前会话", response: OkResult }),
session: ep({
method: "get",
path: "/auth/session",
scope: "read",
auth: "public",
summary: "当前登录态(免鉴权)",
response: auth.SessionInfo,
}),
password: ep({
method: "post",
path: "/auth/password",
scope: "admin",
auth: "session",
summary: "修改密码(吊销其它会话)",
body: auth.PasswordChangeRequest,
response: auth.PasswordChangeResult,
}),
totpStatus: ep({
method: "get",
path: "/auth/totp",
scope: "admin",
auth: "session",
summary: "TOTP 状态",
response: auth.TotpStatus,
}),
totp: ep({
method: "post",
path: "/auth/totp",
scope: "admin",
auth: "session",
summary: "TOTP 绑定/启用/停用/重新生成恢复码",
body: auth.TotpAction,
response: auth.TotpActionResult,
}),
totpSetup: ep({
method: "get",
path: "/auth/totp/setup",
scope: "admin",
auth: "session",
summary: "待确认的 TOTP 密钥与 otpauth URL(前端渲染二维码)",
response: auth.TotpSetup,
}),
passkeys: ep({
method: "get",
path: "/auth/passkeys",
scope: "admin",
auth: "session",
summary: "已登记的 Passkey",
response: auth.PasskeyList,
}),
passkeyRegisterOptions: ep({
method: "post",
path: "/auth/passkeys/register/options",
scope: "admin",
auth: "session",
summary: "Passkey 注册挑战",
response: auth.PasskeyChallenge,
}),
passkeyRegister: ep({
method: "post",
path: "/auth/passkeys/register/verify",
scope: "admin",
auth: "session",
summary: "校验并登记 Passkey",
body: auth.PasskeyRegisterRequest,
response: auth.PasskeyInfo,
}),
passkeyRename: ep({
method: "patch",
path: "/auth/passkeys/{id}",
scope: "admin",
auth: "session",
summary: "重命名 Passkey",
params: IdParam,
body: auth.PasskeyRenameRequest,
response: auth.PasskeyInfo,
}),
passkeyDelete: ep({
method: "delete",
path: "/auth/passkeys/{id}",
scope: "admin",
auth: "session",
summary: "删除 Passkey",
params: IdParam,
response: OkResult,
}),
passkeyLoginOptions: ep({
method: "post",
path: "/auth/passkeys/login/options",
scope: "read",
auth: "public",
summary: "Passkey 登录挑战(免鉴权)",
response: auth.PasskeyChallenge,
}),
passkeyLogin: ep({
method: "post",
path: "/auth/passkeys/login/verify",
scope: "read",
auth: "public",
summary: "Passkey 登录(免鉴权;成功即下发会话)",
body: auth.PasskeyLoginRequest,
response: auth.LoginResult,
}),
},
// ── 事件 ──
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({
method: "get",
path: "/health",
scope: "read",
summary: "服务健康状态(免鉴权)",
response: HealthStatus,
}),
healthDetail: ep({
method: "get",
path: "/system/health",
scope: "admin",
summary: "进程与健康:进程心跳 / 事件循环延迟 / SSE 连接 / DB / 磁盘 / 挂载根 / 引擎 / 定时任务",
response: ops.SystemHealth,
}),
healthHistory: ep({
method: "get",
path: "/system/health/history",
scope: "admin",
summary: "心跳采样时间序列(事件循环延迟、运行中 run、SSE 连接;按桶聚合)",
query: ops.SystemHealthHistoryQuery,
response: ops.SystemHealthHistory,
}),
storage: ep({
method: "get",
path: "/system/storage",
scope: "admin",
summary: "存储:表体积前 15、库总体积、各缓存用量与预算、备份占用、DATA_DIR 分目录用量、清理任务状态",
response: ops.SystemStorage,
}),
storageCleanup: ep({
method: "post",
path: "/system/storage/cleanup/{target}",
scope: "admin",
summary: "立即执行某个存储清理任务(排入对应定时任务,已在排队或运行中则合并)",
params: ops.StorageCleanupParam,
response: ops.StorageCleanupResult,
}),
},
// ── 引擎面板(仅 admin) ──
engine: {
runs: ep({
method: "get",
path: "/engine/runs",
scope: "admin",
summary: "引擎 runs(状态/名称/时间筛选,新→旧)",
query: ops.EngineRunQuery,
response: cursorPage(ops.EngineRun),
}),
run: ep({
method: "get",
path: "/engine/runs/{id}",
scope: "admin",
summary: "run 详情(step attempts + signals)",
params: EngineRunIdParam,
response: ops.EngineRunDetail,
}),
stats: ep({
method: "get",
path: "/engine/stats",
scope: "admin",
summary: "按状态计数 / 最老待处理 / 活跃 worker 快照",
response: ops.EngineStats,
}),
cancel: ep({
method: "post",
path: "/engine/runs/{id}/cancel",
scope: "admin",
summary: "取消 run",
params: EngineRunIdParam,
body: ops.EngineCancelRequest,
response: ops.EngineCancelResult,
}),
retry: ep({
method: "post",
path: "/engine/runs/{id}/retry",
scope: "admin",
summary: "按原任务名与输入重新入队(新幂等键 retry:<id>:<序号>)",
params: EngineRunIdParam,
response: z.object({ runId: z.string(), retryOf: z.string(), attempt: z.number().int().positive() }),
}),
},
// ── 待识别池 ──
identityPool: {
list: ep({
method: "get",
path: "/identity-pool",
scope: "read",
summary: "未识别资源(可按原始标题检索;候选不建正式作品)",
query: identity.IdentityPoolQuery,
response: cursorPage(identity.IdentityPoolItem),
}),
assign: ep({
method: "post",
path: "/identity-pool/{id}/assign",
scope: "organize.execute",
summary: "为未识别资源手动指定候选 / 本地作品 / 外部身份",
params: IdParam,
body: identity.ReleaseIdentityAssign,
response: ReleaseIdentityResult,
}),
dismiss: ep({
method: "post",
path: "/identity-pool/{id}/dismiss",
scope: "organize.execute",
summary: "忽略未识别资源(不再自动复查)",
params: IdParam,
body: z.object({ note: z.string().max(2000).optional() }),
response: identity.IdentityCandidate,
}),
retry: ep({
method: "post",
path: "/identity-pool/{id}/retry",
scope: "organize.execute",
summary: "立即重新识别",
params: IdParam,
response: TaskAccepted,
}),
// META-1 元数据补全:先看建议(metadata.enrich,不改作品),再在控制台应用
enrich: ep({
method: "post",
path: "/identity-pool/{id}/enrich",
scope: "organize.execute",
summary: "元数据补全建议(网页搜索 + 白名单网页 + 模型抽取;不改作品;同一标题 7 天内返回上次结果)",
params: IdParam,
body: identity.EnrichRequest,
response: MetadataEnrichOutput,
}),
enrichApply: ep({
method: "post",
path: "/identity-pool/{id}/enrich/apply",
scope: "admin",
auth: "session",
summary: "应用元数据补全建议:建本地临时作品(或并入同名作品)并关联未识别资源(只接受控制台管理员会话)",
params: IdParam,
body: identity.EnrichApplyInput,
response: identity.EnrichApplyResult,
}),
},
// ── 媒体库 / 库存 / 首页 ──
library: {
list: ep({
method: "get",
path: "/library",
scope: "read",
summary: "媒体库列表(已登记资产或正在追踪的作品)",
query: organization.LibraryListQuery,
response: cursorPage(organization.LibraryItem),
}),
get: ep({
method: "get",
path: "/library/{id}",
scope: "read",
summary: "作品详情:实际路径、身份映射、元数据来源、各收录版本、候选拒绝理由、下载历史与校验结果",
params: IdParam,
response: organization.LibraryDetail,
}),
versions: ep({
method: "get",
path: "/library/{id}/versions",
scope: "read",
summary: "作品的入库版本:每季每集的本地文件版本与规格、季 / 作品汇总、与版本目标的比对",
params: IdParam,
query: libraryVersions.LibraryVersionsQuery,
response: libraryVersions.LibraryVersions,
}),
reconcile: ep({
method: "post",
path: "/library/reconcile",
scope: "admin",
summary: "立即做一轮资产对账(库根哨兵 + 增量 stat;不删记录、不补下载)",
body: z.object({ libraryRootId: z.uuid().optional() }),
response: z.object({ runId: z.uuid(), stats: z.record(z.string(), z.unknown()) }),
}),
},
inventory: {
overlay: ep({
method: "get",
path: "/inventory",
scope: "read",
summary: "库存叠加(站点工作台):按作品返回已有版本 / 收录版本缺口 / 下载中",
query: organization.InventoryQuery,
response: organization.InventoryResponse,
}),
mediaServers: ep({
method: "get",
path: "/inventory/media-servers",
scope: "read",
summary: "Emby 兼容库存查询(多实例;接口失败/截断 = unknown,不等于缺失)",
query: organization.MediaServerInventoryQuery,
response: organization.MediaServerInventory,
}),
},
dashboard: {
get: ep({
method: "get",
path: "/dashboard",
scope: "read",
summary: "首页聚合:自动化概览/当前工作/需要我处理/缓存与保留/来源健康/最近产出",
response: organization.Dashboard,
}),
},
downloadCache: {
cleanup: ep({
method: "post",
path: "/download-cache/cleanup",
scope: "admin",
summary: "立即按阈值清理下载缓存(只清已验证入库且满足保留/H&R 的纳管任务)",
body: z.object({ downloaderInstanceId: z.uuid().optional() }),
response: z.object({ report: z.record(z.string(), z.unknown()) }),
}),
},
// ── 全局搜索(W9-G):顶栏搜索 / 命令面板 / 资源结果页 ──
search: {
all: ep({
method: "get",
path: "/search",
scope: "read",
summary:
"全局搜索聚合:本地作品 / TMDB 候选 / 跨站资源(15s 超时、并发合并、30s 缓存、库存叠加)/ 页面与操作;每组带 tookMs",
query: search.SearchQuery,
response: search.SearchResponse,
}),
releases: ep({
method: "get",
path: "/search/releases",
scope: "read",
summary: "跨站资源结果页:站点/分辨率/来源/大小/免费/H&R 筛选、排序、游标;与 /search 共用缓存",
query: search.ReleaseSearchQuery,
response: search.ReleaseSearchPage,
}),
plan: ep({
method: "post",
path: "/search/releases/{siteId}/{siteReleaseId}/plan",
scope: "organize.execute",
summary: "从搜索结果直接下载:定位资源后走 POST /acquisition-plans 同一流程",
params: search.SearchReleaseParams,
body: search.SearchReleasePlanRequest,
response: execution.ManualAcquisitionResult,
}),
tmdbDetail: ep({
method: "get",
path: "/media/tmdb/{type}/{id}",
scope: "read",
summary:
"仅 TMDB 作品的详情:TMDB 信息与季列表、本地库存对比、是否已建档、搜资源的查询词;include=mediaServers 同时查 Emby",
params: search.TmdbMediaParams,
query: search.TmdbMediaQuery,
response: search.TmdbMediaDetail,
}),
tmdbSeason: ep({
method: "get",
path: "/media/tmdb/tv/{id}/season/{season}",
scope: "read",
summary: "仅 TMDB 剧集的季详情与每集信息(标题 / 简介按语言回退链;走 TMDB 请求缓存)",
params: search.TmdbSeasonParams,
response: search.TmdbSeasonDetailView,
}),
},
// ── 浏览器(C1-E):站点浏览器会话、原站视图、人工接管;写动作需人工持有控制权(接管) ──
browser: {
providers: ep({
method: "get",
path: "/browser/providers",
scope: "admin",
summary: "浏览器服务列表(地址、凭证引用、启用、能力 supported/verified);probe=true 现场探测并写回",
query: browser.BrowserProvidersQuery,
response: z.object({ items: z.array(browser.BrowserProviderView) }),
}),
createProvider: ep({
method: "post",
path: "/browser/providers",
scope: "admin",
summary: "登记浏览器服务(camofox 地址、访问凭据存凭证库、启用)",
body: browser.BrowserProviderCreate,
response: browser.BrowserProviderView,
}),
updateProvider: ep({
method: "patch",
path: "/browser/providers/{id}",
scope: "admin",
summary: "修改浏览器服务地址、访问凭据或启用(环境变量登记的只能改启用)",
params: browser.BrowserProviderParams,
body: browser.BrowserProviderUpdate,
response: browser.BrowserProviderView,
}),
deleteProvider: ep({
method: "delete",
path: "/browser/providers/{id}",
scope: "admin",
summary: "删除浏览器服务(有未关闭会话或由环境变量登记时拒绝)",
params: browser.BrowserProviderParams,
response: z.object({ id: z.string() }),
}),
probeProvider: ep({
method: "post",
path: "/browser/providers/{id}/probe",
scope: "admin",
summary: "测试连通:探测该浏览器服务的 /health 与 /openapi.json 并写回能力",
params: browser.BrowserProviderParams,
response: browser.BrowserProviderView,
}),
sessions: ep({
method: "get",
path: "/browser/sessions",
scope: "admin",
summary: "浏览器会话列表(以数据库为准;默认只列未关闭的)",
query: browser.BrowserSessionsQuery,
response: z.object({ items: z.array(browser.BrowserSessionView) }),
}),
open: ep({
method: "post",
path: "/browser/sessions",
scope: "admin",
summary: "打开站点浏览器会话(同一站点/账号已有未关闭会话时直接返回它)",
body: browser.BrowserSessionOpen,
response: browser.BrowserSessionView,
}),
get: ep({
method: "get",
path: "/browser/sessions/{id}",
scope: "admin",
summary: "浏览器会话详情(控制者、标签页、租约、是否需重新读取页面)",
params: browser.BrowserSessionParams,
response: browser.BrowserSessionView,
}),
close: ep({
method: "delete",
path: "/browser/sessions/{id}",
scope: "admin",
summary: "关闭浏览器会话(关闭上游标签页,撤销租约)",
params: browser.BrowserSessionParams,
response: browser.BrowserSessionView,
}),
openTab: ep({
method: "post",
path: "/browser/sessions/{id}/tabs",
scope: "admin",
summary: "新建标签页并设为当前(需持有控制权)",
params: browser.BrowserSessionParams,
body: browser.BrowserTabOpen,
response: browser.BrowserSessionView,
}),
closeTab: ep({
method: "delete",
path: "/browser/sessions/{id}/tabs/{tabId}",
scope: "admin",
summary: "关闭标签页(需持有控制权)",
params: browser.BrowserTabParams,
response: browser.BrowserSessionView,
}),
navigate: ep({
method: "post",
path: "/browser/sessions/{id}/navigate",
scope: "admin",
summary: "地址栏导航或后退/前进/刷新(需持有控制权;限站点域名)",
params: browser.BrowserSessionParams,
body: browser.BrowserNavigate,
response: browser.BrowserNavigation,
}),
snapshot: ep({
method: "get",
path: "/browser/sessions/{id}/snapshot",
scope: "admin",
summary: "结构化页面快照(凭证已遮蔽)",
params: browser.BrowserSessionParams,
query: browser.BrowserSnapshotQuery,
response: browser.BrowserSnapshot,
}),
screenshot: ep({
method: "get",
path: "/browser/sessions/{id}/screenshot",
scope: "admin",
summary: "当前画面截图(base64,只给原站视图,不进模型)",
params: browser.BrowserSessionParams,
query: browser.BrowserScreenshotQuery,
response: browser.BrowserScreenshotView,
}),
click: ep({
method: "post",
path: "/browser/sessions/{id}/click",
scope: "admin",
summary: "点击(快照引用 / 选择器 / 截图坐标;需持有控制权)",
params: browser.BrowserSessionParams,
body: browser.BrowserClick,
response: browser.BrowserActionResult,
}),
type: ep({
method: "post",
path: "/browser/sessions/{id}/type",
scope: "admin",
summary: "输入文本 / 站点账号凭证 / 按键(需持有控制权;凭证由服务端注入)",
params: browser.BrowserSessionParams,
body: browser.BrowserType,
response: browser.BrowserActionResult,
}),
scroll: ep({
method: "post",
path: "/browser/sessions/{id}/scroll",
scope: "admin",
summary: "滚动页面(需持有控制权)",
params: browser.BrowserSessionParams,
body: browser.BrowserScroll,
response: browser.BrowserActionResult,
}),
select: ep({
method: "post",
path: "/browser/sessions/{id}/select",
scope: "admin",
summary: "下拉框选择(需持有控制权)",
params: browser.BrowserSessionParams,
body: browser.BrowserSelect,
response: browser.BrowserActionResult,
}),
extract: ep({
method: "post",
path: "/browser/sessions/{id}/extract",
scope: "admin",
summary: "按 JSON Schema(x-ref)提取页面字段",
params: browser.BrowserSessionParams,
body: browser.BrowserExtract,
response: browser.BrowserExtractResult,
}),
download: ep({
method: "post",
path: "/browser/sessions/{id}/download",
scope: "organize.execute",
summary:
"捕获浏览器下载 → 后端暂存并核验 infohash、匹配本地资源;给出 submit 时经 POST /acquisition-plans 同一流程提交",
params: browser.BrowserSessionParams,
body: browser.BrowserDownload,
response: browser.BrowserDownloadResult,
}),
reconnect: ep({
method: "post",
path: "/browser/sessions/{id}/reconnect",
scope: "admin",
summary:
"浏览器服务重启后重新连接(幂等):上游上下文还在就什么都不做;否则注入账号 Cookie、新开标签页回到原地址,控制权不变;失败 503 browser_reconnect_failed",
params: browser.BrowserSessionParams,
response: browser.BrowserSessionView,
}),
acquire: ep({
method: "post",
path: "/browser/sessions/{id}/control/acquire",
scope: "admin",
summary: "人工接管:先停止该会话新的自动化输入并等进行中的操作收尾,再发放人工租约",
params: browser.BrowserSessionParams,
body: browser.BrowserControlAcquire,
response: browser.BrowserControlResult,
}),
release: ep({
method: "post",
path: "/browser/sessions/{id}/control/release",
scope: "admin",
summary: "交还控制权:智能体恢复前必须重新读取页面,旧元素引用失效",
params: browser.BrowserSessionParams,
body: browser.BrowserControlRelease,
response: browser.BrowserControlResult,
}),
},
// ── 定时任务看板(W7-C):统一注册表 + Agent 巡检;全部只接受控制台会话 ──
schedules: {
list: ep({
method: "get",
path: "/schedules",
scope: "admin",
auth: "session",
summary: "定时任务看板:全部登记的周期任务与智能体巡检、统计、调度器是否在线",
response: schedules.ScheduleBoard,
}),
create: ep({
method: "post",
path: "/schedules",
scope: "admin",
auth: "session",
summary: "新建自定义智能体巡检(目标模板 + 能力白名单 + 预算 + 间隔)",
body: schedules.AgentCheckCreate,
response: schedules.ScheduledJob,
}),
update: ep({
method: "patch",
path: "/schedules/{id}",
scope: "admin",
auth: "session",
summary: "修改开关 / 间隔或 cron / 参数;reset 恢复默认开关与调度",
params: schedules.ScheduleIdParam,
body: schedules.ScheduleUpdate,
response: schedules.ScheduledJob,
}),
remove: ep({
method: "delete",
path: "/schedules/{id}",
scope: "admin",
auth: "session",
summary: "删除自定义智能体巡检(内置任务不可删)",
params: schedules.ScheduleIdParam,
response: OkResult,
}),
runNow: ep({
method: "post",
path: "/schedules/{id}/run-now",
scope: "admin",
auth: "session",
summary: "立即执行一次(不改下次运行时间;已在排队或运行中则合并)",
params: schedules.ScheduleIdParam,
response: schedules.ScheduleRunNowResult,
}),
skip: ep({
method: "post",
path: "/schedules/{id}/skip",
scope: "admin",
auth: "session",
summary: "跳过下一次运行(下次运行时间顺延一个周期)",
params: schedules.ScheduleIdParam,
response: schedules.ScheduledJob,
}),
runs: ep({
method: "get",
path: "/schedules/{id}/runs",
scope: "admin",
auth: "session",
summary: "最近运行历史(新→旧)",
params: schedules.ScheduleIdParam,
query: schedules.ScheduleRunsQuery,
response: z.object({ items: z.array(schedules.ScheduledJobRun) }),
}),
},
} as const;
/** 用参数填充 `{name}` 路径占位,返回带 API_PREFIX 的完整路径 */
export function buildPath(path: string, params: Record<string, string> = {}): string {
const filled = path.replace(/\{(\w+)\}/g, (_, k: string) => {
const v = params[k];
if (v === undefined) throw new Error(`缺少路径参数 ${k}`);
return encodeURIComponent(v);
});
return `${API_PREFIX}${filled}`;
}
/** 扁平列出全部端点(生成 OpenAPI、权限表与测试用) */
export function listEndpoints(): EndpointDef[] {
return Object.values(Api).flatMap((group) => Object.values(group) as EndpointDef[]);
}
export type EndpointBody<E extends EndpointDef> = E["body"] extends z.ZodType ? z.input<E["body"]> : never;
export type EndpointResponse<E extends EndpointDef> = z.infer<E["response"]>;
export * from "./files.ts";
export * from "./mcp.ts";
export * from "./schedules.ts";