- GET /system/health splits processes into current instances (hosts with
a beat in the last 10 min, newest per host+kind, not stopped) and
retiredProcesses[]; 45s stale and level only consider current ones
- heartbeat writer marks stopped_at on graceful exit (migration 0043),
upsert clears it; pure api role now stops its writer on SIGTERM
- scheduled job system.heartbeat_cleanup (maintain, 1h) deletes rows
without a beat for 24h and samples older than 7 days
- status page: per-role alarms ("io worker 45 秒没有心跳"), collapsible
retired instances; glossary/runbook updated
- bump all members to 0.2.2, CHANGELOG [0.2.2]
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2436 lines
77 KiB
TypeScript
2436 lines
77 KiB
TypeScript
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 { CapabilityDescriptor, KnownCapabilityId } from "./capability.ts";
|
||
import { type ApiScope, CursorQuery, cursorPage, HealthStatus, Id, IdParam } from "./common.ts";
|
||
import * as content from "./content.ts";
|
||
import * as events from "./events.ts";
|
||
import * as execution from "./execution.ts";
|
||
import * as files from "./files.ts";
|
||
import * as identity from "./identity.ts";
|
||
import * as instances from "./instances.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 });
|
||
|
||
/** 引擎 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: "刷新元数据(字段级;用户锁优先,不动路径)",
|
||
params: IdParam,
|
||
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,
|
||
),
|
||
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),
|
||
}),
|
||
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,
|
||
),
|
||
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: "Agent 运行列表(新→旧)",
|
||
query: agent.AgentRunListQuery,
|
||
response: cursorPage(agent.AgentRun),
|
||
}),
|
||
get: ep({
|
||
method: "get",
|
||
path: "/agent/runs/{id}",
|
||
scope: "read",
|
||
summary: "Agent 运行详情:事件按 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/字幕/图片,≤20 MiB;流式;仅管理员控制台会话;审计)",
|
||
query: files.FilesDownloadQuery,
|
||
response: files.FilesDownloadResponse,
|
||
}),
|
||
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: "能力调用统计:每能力调用数、失败数、平均耗时、最近调用、调用来源(内置 Agent / 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,
|
||
}),
|
||
},
|
||
|
||
// ── 设置 ──
|
||
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: "Agent 上游列表(不含密钥)",
|
||
response: pi.PiUpstreamList,
|
||
}),
|
||
get: ep({
|
||
method: "get",
|
||
path: "/pi/upstreams/{id}",
|
||
scope: "admin",
|
||
summary: "Agent 上游详情",
|
||
params: IdParam,
|
||
response: pi.PiUpstream,
|
||
}),
|
||
create: ep({
|
||
method: "post",
|
||
path: "/pi/upstreams",
|
||
scope: "admin",
|
||
auth: "session",
|
||
summary: "新建 Agent 上游(apiKey 只写,存 secret 表)",
|
||
body: pi.PiUpstreamCreate,
|
||
response: pi.PiUpstream,
|
||
}),
|
||
update: ep({
|
||
method: "patch",
|
||
path: "/pi/upstreams/{id}",
|
||
scope: "admin",
|
||
auth: "session",
|
||
summary: "修改 Agent 上游(provider 不可改;makeDefault 设默认上游)",
|
||
params: IdParam,
|
||
body: pi.PiUpstreamUpdate,
|
||
response: pi.PiUpstream,
|
||
}),
|
||
remove: ep({
|
||
method: "delete",
|
||
path: "/pi/upstreams/{id}",
|
||
scope: "admin",
|
||
auth: "session",
|
||
summary: "删除 Agent 上游(连同凭据;是默认上游时清空默认)",
|
||
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: "上游可用模型(静态目录;Radius 先动态刷新)",
|
||
params: IdParam,
|
||
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,
|
||
}),
|
||
},
|
||
|
||
// ── 引擎面板(仅 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,
|
||
}),
|
||
},
|
||
|
||
// ── 媒体库 / 库存 / 首页 ──
|
||
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,
|
||
}),
|
||
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,
|
||
}),
|
||
},
|
||
|
||
// ── 浏览器(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,
|
||
}),
|
||
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,
|
||
}),
|
||
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: "交还控制权:Agent 恢复前必须重新读取页面,旧元素引用失效",
|
||
params: browser.BrowserSessionParams,
|
||
body: browser.BrowserControlRelease,
|
||
response: browser.BrowserControlResult,
|
||
}),
|
||
},
|
||
|
||
// ── 定时任务看板(W7-C):统一注册表 + Agent 巡检;全部只接受控制台会话 ──
|
||
schedules: {
|
||
list: ep({
|
||
method: "get",
|
||
path: "/schedules",
|
||
scope: "admin",
|
||
auth: "session",
|
||
summary: "定时任务看板:全部登记的周期任务与 Agent 巡检、统计、调度器是否在线",
|
||
response: schedules.ScheduleBoard,
|
||
}),
|
||
create: ep({
|
||
method: "post",
|
||
path: "/schedules",
|
||
scope: "admin",
|
||
auth: "session",
|
||
summary: "新建自定义 Agent 巡检(目标模板 + 能力白名单 + 预算 + 间隔)",
|
||
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: "删除自定义 Agent 巡检(内置任务不可删)",
|
||
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";
|