- migrations 0002-0012 split by domain (foundation/instances/content/identity/ sources/assets/policy/execution/agent/ops/organization): 41 tables, uuid pk, created_at/updated_at + touch trigger, text+CHECK status sets, pgcrypto/pg_trgm - key constraints: source_release(site_id, site_release_id), observation(feed_id, guid), external_identity accepted partial unique, download_task(instance_id, torrent_hash), one active acquisition_plan per slot, one active path_binding per entity/season, asset_location root+path uniqueness, organization_record change_seq/change_xid cursor - secret table (ciphertext/nonce/kdf) + secret_ref columns; outbox dispatch fields (ADR-002) - src/types.ts generated from the migrated schema (scripts/gen-types.ts), drift-checked in tests; int8 -> number, date -> string parsers; migrator accepts migrationTableSchema - contracts: Row/Create/Update schemas per domain, public organization-record API (changeSeq cursor), GlobalSettings, CapabilityDescriptor + 14 capability IO schemas, typed SSE events, Api endpoint table - tests: migration up/down round trip on real PG in an isolated schema, constraint checks, contracts<->db column/enum sync, positive/negative parse for every exported schema Co-Authored-By: Claude Fable 5.1 <[email protected]>
784 lines
21 KiB
TypeScript
784 lines
21 KiB
TypeScript
import { z } from "zod";
|
|
import * as agent from "./agent";
|
|
import * as assets from "./assets";
|
|
import { CapabilityDescriptor, KnownCapabilityId } from "./capability";
|
|
import { type ApiScope, CursorQuery, cursorPage, Id, IdParam } from "./common";
|
|
import * as content from "./content";
|
|
import * as execution from "./execution";
|
|
import * as identity from "./identity";
|
|
import * as instances from "./instances";
|
|
import * as ops from "./ops";
|
|
import * as organization from "./organization";
|
|
import * as policy from "./policy";
|
|
import { GlobalSettings, GlobalSettingsUpdate } from "./settings";
|
|
import * as sources from "./sources";
|
|
|
|
// 所有 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";
|
|
|
|
export interface EndpointDef {
|
|
method: HttpMethod;
|
|
path: string;
|
|
scope: ApiScope;
|
|
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 });
|
|
|
|
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,
|
|
}),
|
|
},
|
|
contentUnits: crud(
|
|
"/content-units",
|
|
"内容单元",
|
|
{
|
|
row: content.ContentUnit,
|
|
create: content.ContentUnitCreate,
|
|
update: content.ContentUnitUpdate,
|
|
filters: { mediaEntityId: Id.optional(), seasonNumber: z.coerce.number().int().optional() },
|
|
},
|
|
R,
|
|
),
|
|
titleAliases: {
|
|
...crud(
|
|
"/title-aliases",
|
|
"别名",
|
|
{
|
|
row: content.TitleAlias,
|
|
create: content.TitleAliasCreate,
|
|
update: content.TitleAliasUpdate,
|
|
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: z.object({ items: z.array(identity.IdentityRevision) }),
|
|
}),
|
|
},
|
|
|
|
// ── 文件 ──
|
|
mediaAssets: crud(
|
|
"/media-assets",
|
|
"资产",
|
|
{
|
|
row: assets.MediaAsset,
|
|
update: assets.MediaAssetUpdate,
|
|
filters: { mediaEntityId: Id.optional(), status: assets.AssetStatus.optional() },
|
|
},
|
|
R,
|
|
),
|
|
assetUnitCoverage: crud(
|
|
"/asset-unit-coverage",
|
|
"资产覆盖",
|
|
{
|
|
row: assets.AssetUnitCoverage,
|
|
create: assets.AssetUnitCoverageCreate,
|
|
update: assets.AssetUnitCoverageUpdate,
|
|
filters: { mediaAssetId: Id.optional(), contentUnitId: Id.optional() },
|
|
},
|
|
R,
|
|
),
|
|
assetLocations: crud(
|
|
"/asset-locations",
|
|
"资产位置",
|
|
{
|
|
row: assets.AssetLocation,
|
|
update: assets.AssetLocationUpdate,
|
|
filters: { mediaAssetId: Id.optional(), status: assets.LocationStatus.optional() },
|
|
},
|
|
R,
|
|
),
|
|
pathBindings: crud(
|
|
"/path-bindings",
|
|
"路径绑定",
|
|
{
|
|
row: assets.PathBinding,
|
|
create: assets.PathBindingCreate,
|
|
update: assets.PathBindingUpdate,
|
|
filters: { mediaEntityId: Id.optional(), status: assets.BindingStatus.optional() },
|
|
},
|
|
R,
|
|
),
|
|
|
|
// ── 实例 ──
|
|
libraryRoots: crud(
|
|
"/library-roots",
|
|
"媒体库根",
|
|
{
|
|
row: instances.LibraryRoot,
|
|
create: instances.LibraryRootCreate,
|
|
update: instances.LibraryRootUpdate,
|
|
},
|
|
R,
|
|
),
|
|
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() }),
|
|
}),
|
|
},
|
|
mediaServerInstances: crud(
|
|
"/media-server-instances",
|
|
"媒体服务器实例",
|
|
{
|
|
row: instances.MediaServerInstance,
|
|
create: instances.MediaServerInstanceCreate,
|
|
update: instances.MediaServerInstanceUpdate,
|
|
},
|
|
R,
|
|
),
|
|
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),
|
|
siteAccounts: crud(
|
|
"/site-accounts",
|
|
"站点账号",
|
|
{
|
|
row: sources.SiteAccount,
|
|
create: sources.SiteAccountCreate,
|
|
update: sources.SiteAccountUpdate,
|
|
filters: { siteId: Id.optional() },
|
|
},
|
|
R,
|
|
),
|
|
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 }),
|
|
}),
|
|
},
|
|
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,
|
|
),
|
|
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() },
|
|
},
|
|
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() },
|
|
},
|
|
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}", "删除偏好标签(留抑制记录)"),
|
|
},
|
|
preferenceSuppressions: crud(
|
|
"/preference-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,
|
|
),
|
|
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,
|
|
}),
|
|
},
|
|
approvals: crud(
|
|
"/approvals",
|
|
"审批",
|
|
{
|
|
row: agent.Approval,
|
|
update: agent.ApprovalUpdate,
|
|
filters: { status: agent.ApprovalStatus.optional(), planId: Id.optional() },
|
|
},
|
|
{ read: "read", write: "organize.execute" },
|
|
),
|
|
automationGrants: crud(
|
|
"/automation-grants",
|
|
"持续授权",
|
|
{
|
|
row: agent.AutomationGrant,
|
|
create: agent.AutomationGrantCreate,
|
|
update: agent.AutomationGrantUpdate,
|
|
filters: { status: agent.GrantStatus.optional() },
|
|
},
|
|
R,
|
|
),
|
|
agentRuns: {
|
|
...crud(
|
|
"/agent-runs",
|
|
"Agent 运行",
|
|
{
|
|
row: agent.AgentRun,
|
|
create: agent.AgentRunCreate,
|
|
filters: { status: agent.AgentRunStatus.optional(), taskId: Id.optional() },
|
|
},
|
|
{ read: "read", write: "organize.execute" },
|
|
),
|
|
cancel: ep({
|
|
method: "post",
|
|
path: "/agent-runs/{id}/cancel",
|
|
scope: "organize.execute",
|
|
summary: "取消 Agent 运行",
|
|
params: IdParam,
|
|
response: agent.AgentRun,
|
|
}),
|
|
message: ep({
|
|
method: "post",
|
|
path: "/agent-runs/{id}/messages",
|
|
scope: "organize.execute",
|
|
summary: "向运行中的会话追加用户消息(纠正/补充)",
|
|
params: IdParam,
|
|
body: z.object({ text: z.string().min(1).max(8000) }),
|
|
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,
|
|
create: ops.ReconcileRunCreate,
|
|
filters: { kind: ops.ReconcileKind.optional(), status: ops.ReconcileStatus.optional() },
|
|
},
|
|
R,
|
|
),
|
|
serverMappings: crud(
|
|
"/server-mappings",
|
|
"库存映射",
|
|
{
|
|
row: ops.ServerMapping,
|
|
update: ops.ServerMappingUpdate,
|
|
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,
|
|
}),
|
|
},
|
|
auditEvents: {
|
|
list: ep({
|
|
method: "get",
|
|
path: "/audit-events",
|
|
scope: "admin",
|
|
summary: "审计事件",
|
|
query: listQuery({
|
|
targetType: z.string().optional(),
|
|
targetId: z.string().optional(),
|
|
action: z.string().optional(),
|
|
}),
|
|
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,
|
|
}),
|
|
},
|
|
|
|
// ── 整理(公开合同) ──
|
|
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,
|
|
}),
|
|
},
|
|
|
|
// ── 能力 ──
|
|
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() }),
|
|
}),
|
|
},
|
|
|
|
// ── 设置 ──
|
|
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,
|
|
}),
|
|
},
|
|
} 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"]>;
|