perf(server): parallel media-server inventory, queued identify remote lookups, dev DB cleanup

- GET /inventory/media-servers: per-instance Promise.allSettled, per-instance
  concurrency cap (2) and timeout (8s); failed/timed-out instances are unknown,
  response carries partial=true (field not yet in contracts)
- S-7: POST /files/identify returns local matches plus remoteTaskId/remotePending
  and enqueues identifyRemote; the worker runs the TMDB lookups and publishes SSE
  identify.completed {taskId,status,items,truncated,excludedBySize,error};
  ?wait=true keeps the synchronous path, capped at 15s overall
- catalog.lookup / release.search honour the caller's abort signal
- scripts/dev-db-clean.ts: dry-run by default, --yes to drop leftover test
  schemas and w32-/w33-/fe4-/qa- prefixed entities; refuses non localhost:55433
- runbook: dev DB cleanup

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 19:47:41 +08:00
1 parent 5b8927d1dc
commit fd2f7bc33d
10 files changed
+683 -67

No files matched your search

@@ -16,7 +16,7 @@ import { liveSearch } from "./service/workbench.ts";
export function registerSourcesCapabilities(registry: CapabilityRegistry, rt: () => SourcesRuntime): void {
registerSiteCapabilities(registry, rt);
registry.register(capabilitySpec("catalog.lookup"), async (input) => {
registry.register(capabilitySpec("catalog.lookup"), async (input, ctx) => {
const { db } = rt().ctx;
const local: { entity: unknown; score: number; matchedAlias: string | null }[] = [];
if (input.externalId) {
@@ -57,10 +57,9 @@ export function registerSourcesCapabilities(registry: CapabilityRegistry, rt: ()
}[] = [];
const tmdb = await rt().tmdb();
if (input.includeRemote && input.query && tmdb) {
for (const m of (await tmdb.searchAll(input.query, input.year ? { year: input.year } : {})).slice(
0,
input.limit,
)) {
// 认调用方的 signal(HTTP 15s 超时 / 任务取消):中止后立即返回,不再等 TMDB(底层请求由客户端自身超时收尾)
const found = await abortable(tmdb.searchAll(input.query, input.year ? { year: input.year } : {}), ctx.signal);
for (const m of found.slice(0, input.limit)) {
remote.push({
provider: "tmdb",
entityType: m.entityType,
@@ -73,7 +72,7 @@ export function registerSourcesCapabilities(registry: CapabilityRegistry, rt: ()
return { local: local as never, remote };
});
registry.register(capabilitySpec("release.search"), async (input) => {
registry.register(capabilitySpec("release.search"), async (input, ctx) => {
const r = rt();
const { db } = r.ctx;
let q = db.selectFrom("source_release").selectAll();
@@ -93,7 +92,10 @@ export function registerSourcesCapabilities(registry: CapabilityRegistry, rt: ()
const coverage: { siteId: string; fromSnapshot: boolean; live: boolean; error: string | null }[] = [];
const needLive = input.allowLiveSearch && !!text && rows.length === 0;
for (const s of sites) {
if (needLive) {
if (needLive && ctx.signal.aborted) {
// 已超时/取消:剩余站点不再实时补查
coverage.push({ siteId: s.id, fromSnapshot: true, live: false, error: "aborted" });
} else if (needLive) {
const res = await liveSearch(r, s, text!);
coverage.push({ siteId: s.id, fromSnapshot: false, live: res.error === null, error: res.error });
} else coverage.push({ siteId: s.id, fromSnapshot: true, live: false, error: null });
@@ -188,3 +190,15 @@ async function identities(r: SourcesRuntime, entityId: string) {
.execute();
return toApiList(rows) as never[];
}
/** 调用方 signal 中止时立即 reject(原 Promise 结果丢弃) */
function abortable<T>(p: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
if (!signal) return p;
if (signal.aborted) return Promise.reject(signal.reason);
p.catch(() => {});
return new Promise<T>((resolve, reject) => {
const onAbort = () => reject(signal.reason);
signal.addEventListener("abort", onAbort, { once: true });
p.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
});
}