feat(server): W2-B sources, identity resolution, metadata and site workbench

- 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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 20:12:01 +08:00
1 parent 829ca4f7ac
commit a24bda952b
39 files changed
+6861 -15

No files matched your search

@@ -1,12 +1,69 @@
import type { ServerModule } from "../types.ts";
import type { ServerContext, ServerModule } from "../types.ts";
import { registerSourcesCapabilities } from "./capabilities.ts";
import { decisionRoutes } from "./routes/decisions.ts";
import { identityRoutes } from "./routes/identity.ts";
import { sourcesRoutes } from "./routes/sources.ts";
import { workbenchRoutes } from "./routes/workbench.ts";
import { type RuntimeOverrides, SourcesRuntime } from "./runtime.ts";
import { schedulerTick } from "./scheduler.ts";
import { registerSourcesTasks, sourcesOutboxRoutes } from "./tasks.ts";
/**
* W2-B 来源/身份/元数据:site、site_account、feed_source;ingestFeed / resolveObservation / refreshMetadata;
* W2-B 来源/身份/元数据:site、site_account、feed_source;ingestFeed / resolveObservation / refreshMetadata / snapshotSite;
* observation/source_release 写入与去重;身份解析、待识别池与 decision;站点工作台 API。
* 发出 release.resolved、decision.created(经 ctx.outbox.emit)。
* 发出 feed.due、feed.gap_suspected、release.observed(自消费)与 release.resolved、decision.created/updated。
*/
export const sourcesIdentityModule: ServerModule = {
name: "sources-identity",
registerRoutes() {},
registerTasks() {},
};
export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): ServerModule & {
runtime(ctx: ServerContext): SourcesRuntime;
} {
const runtimes = new WeakMap<ServerContext, SourcesRuntime>();
const runtime = (ctx: ServerContext) => {
let rt = runtimes.get(ctx);
if (!rt) {
rt = new SourcesRuntime(ctx, overrides);
runtimes.set(ctx, rt);
}
return rt;
};
let timer: ReturnType<typeof setInterval> | null = null;
let ticking: Promise<unknown> | null = null;
return {
name: "sources-identity",
runtime,
registerRoutes(app, ctx) {
const rt = () => runtime(ctx);
sourcesRoutes(app, rt);
workbenchRoutes(app, rt);
identityRoutes(app, rt);
decisionRoutes(app, rt);
},
registerTasks(registry, ctx) {
registerSourcesTasks(registry, () => runtime(ctx));
},
outboxRoutes: () => sourcesOutboxRoutes,
registerCapabilities(registry, ctx) {
registerSourcesCapabilities(registry, () => runtime(ctx));
},
start(ctx) {
const rt = runtime(ctx);
const tick = () => {
if (ticking) return;
ticking = schedulerTick(rt)
.catch((err) => ctx.logger.warn({ err }, "sources-identity 调度失败"))
.finally(() => {
ticking = null;
});
};
timer = setInterval(tick, overrides.schedulerIntervalMs ?? 30_000);
tick();
},
async stop() {
if (timer) clearInterval(timer);
timer = null;
await ticking;
},
};
}
export const sourcesIdentityModule = createSourcesIdentityModule();