- Sites / accounts / feeds: registry view (/site-definitions), site CRUD with
enable/disable (history stays queryable, marked site_disabled), accounts and
feed URLs stored as secrets (only secret_ref / redacted urlDisplay exposed),
feed enable/disable/reset/fetch; one shared rate limiter + adapter per site.
- Tasks: ingestFeed (FeedFetchState/FeedCursor on feed_source, observation
feed+guid upsert with digest-change updates, source_release site+id
aggregation across feeds, auth failure -> account invalid + decision, no
retry, gap -> feed.gap_suspected -> budgeted backfill snapshot),
resolveObservation (parseRelease -> local recall via title_alias pg_trgm +
accepted identities -> TMDB -> resolveIdentity; auto binds entity /
provisional units / identities / aliases, otherwise identity_candidate pool
with evidence version and bounded review backoff; manual lock respected),
refreshMetadata (metadata_field + field_override, user locks win, paths
untouched), snapshotSite (site_snapshot coverage records).
- Events: feed.due, feed.gap_suspected, release.observed (self-consumed),
release.resolved, decision.created/updated; scheduler in worker start().
- Identity API: entity detail with redirects, manual mapping correction with
identity_revision, merge with identity_redirect, pool list/assign/dismiss/
retry, content unit numbering lock, aliases, external identities.
- Decisions: list/summary (by entity and kind)/detail/resolve; identity
options executed directly.
- Workbench: /sites/{id}/releases (snapshot first, coalesced live search with
backoff, origin + freshness) and /sites/{id}/releases/{rid} (volatile field
recheck times, refresh keeps data on failure).
- Capabilities: catalog.lookup, release.search, release.inspect,
identity.resolve.
- Migrations 0018-0020: site.adapter api, source_release.resolution_locked,
site_snapshot, identity_candidate, resolution_attempt, identity_redirect,
metadata_field, field_override.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
import { createRoute, type RouteConfig } from "@hono/zod-openapi";
|
|
import { type ApiScope, apiResponse } from "@umt/contracts";
|
|
import type { Context } from "hono";
|
|
import type { z } from "zod";
|
|
import { type AuthEnv, requireScope } from "../../http/auth.ts";
|
|
import type { ModuleApp } from "../types.ts";
|
|
import type { Actor } from "./service/common.ts";
|
|
|
|
// 本模块路由封装:createRoute(进 OpenAPI)+ requireScope + 入参校验 + 出参按合同 parse。
|
|
|
|
export interface RouteDef {
|
|
method: "get" | "post" | "patch" | "put" | "delete";
|
|
path: string;
|
|
scope: ApiScope;
|
|
summary: string;
|
|
tags: string[];
|
|
params?: z.ZodObject;
|
|
query?: z.ZodType;
|
|
body?: z.ZodType;
|
|
response: z.ZodType;
|
|
}
|
|
|
|
type Infer<T> = T extends z.ZodType ? z.infer<T> : Record<string, never>;
|
|
|
|
export interface Input<D extends RouteDef> {
|
|
params: Infer<D["params"]>;
|
|
query: Infer<D["query"]>;
|
|
body: Infer<D["body"]>;
|
|
actor: Actor;
|
|
c: Context<AuthEnv>;
|
|
}
|
|
|
|
/** 调用者(W2-A 的 c.var.actor:admin 会话 / API Key)→ 审计/修订用的操作者;管理员记为 user */
|
|
export function actorOf(c: Context<AuthEnv>): Actor {
|
|
const a = c.get("actor") as { type?: string; id?: string } | undefined;
|
|
if (!a) return { type: "system", id: null };
|
|
return { type: a.type === "api_key" ? "api_key" : "user", id: a.id ?? null };
|
|
}
|
|
|
|
export function route<D extends RouteDef>(app: ModuleApp, def: D, handler: (input: Input<D>) => Promise<unknown>) {
|
|
const request: RouteConfig["request"] = {};
|
|
if (def.params) request.params = def.params as never;
|
|
if (def.query) request.query = def.query as never;
|
|
if (def.body) request.body = { content: { "application/json": { schema: def.body as never } }, required: true };
|
|
const r = createRoute({
|
|
method: def.method,
|
|
path: def.path,
|
|
summary: def.summary,
|
|
tags: def.tags,
|
|
middleware: [requireScope(def.scope)] as never,
|
|
request,
|
|
responses: {
|
|
200: { description: def.summary, content: { "application/json": { schema: apiResponse(def.response) } } },
|
|
},
|
|
});
|
|
// biome-ignore lint/suspicious/noExplicitAny: 路由泛型在封装里擦除,入参已由 zod-openapi 校验
|
|
app.openapi(r, (async (c: any) => {
|
|
const input = {
|
|
params: def.params ? c.req.valid("param") : {},
|
|
query: def.query ? c.req.valid("query") : {},
|
|
body: def.body ? c.req.valid("json") : {},
|
|
actor: actorOf(c),
|
|
c,
|
|
} as Input<D>;
|
|
const data = def.response.parse(await handler(input));
|
|
return c.json({ ok: true, data }, 200);
|
|
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
|
}) as any);
|
|
}
|