fix(files): S-6 identify goes through the shared TMDB limiter and caps remote queries

- invoke-limits: sharedInvokeLimiter() (one semaphore set per process); agent
  capability routes default to it
- /files/identify: catalog.lookup with includeRemote runs under the shared
  TMDB semaphore + 15s timeout; at most 20 distinct remote queries per
  request, further items fall back to local recall with a warning

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 00:48:45 +08:00
1 parent ba1361a9b6
commit b4560be48b
4 files changed
+49 -8

No files matched your search

@@ -11,12 +11,16 @@ import { sql } from "@umt/db";
import { type IdentityCandidate, type ParsedRelease, parseReleasePath, queryTitles, rankCandidates } from "@umt/domain";
import { localIdentityCandidate } from "@umt/mapping";
import type { Principal } from "../../http/auth.ts";
import { EXTERNAL_INVOKE_TIMEOUT_MS, sharedInvokeLimiter } from "../agent/invoke-limits.ts";
import { actorOfP, errMessage, FilesError, type FmDeps, resolveIn } from "./common.ts";
import { kindOfName } from "./list.ts";
// 识别(只读,不落库):parseReleasePath(文件 + 父/祖父目录上下文)→ 本地召回(别名 pg_trgm + 外部身份)
// → 远端候选(catalog.lookup 能力,走 TMDB 缓存;失败只降级为提示)→ rankCandidates,每项前 5。
/** 单次 /files/identify 请求内去重后的远端(TMDB)查询上限 */
export const REMOTE_QUERY_MAX = 20;
const TRGM = 0.45;
const LEVEL: Record<string, FileIdentifyCandidate["level"]> = {
rule_confirmed: "high",
@@ -140,6 +144,7 @@ export async function identify(
return out;
};
let remoteQueries = 0;
const remoteFor = async (p: ParsedRelease) => {
const q = p.titleCn ?? p.title ?? p.titleEn;
if (!q) return { list: [], warning: null };
@@ -147,17 +152,32 @@ export async function identify(
const hit = remoteCache.get(key);
if (hit) return hit;
let res: { list: IdentityCandidate[]; warning: string | null };
// 单次请求内去重后的远端查询设上限,超出的项只做本地召回(验收 07 S-6)
if (remoteQueries >= REMOTE_QUERY_MAX) {
res = {
list: [],
warning: `本次识别的远端查询已达上限 ${REMOTE_QUERY_MAX} 个,该项仅本地召回;可缩小范围后再识别`,
};
remoteCache.set(key, res);
return res;
}
remoteQueries += 1;
try {
const out = (await d.ctx.capabilities.invoke(
"catalog.lookup",
{ query: q, includeRemote: true, limit: 10, ...(body.hints?.year ? { year: body.hints.year } : {}) },
{
const input = {
query: q,
includeRemote: true,
limit: 10,
...(body.hints?.year ? { year: body.hints.year } : {}),
};
// 经进程共享的 TMDB 信号量(并发 4)+ 15s 超时,与能力调用端点同一个限流器
const out = (await sharedInvokeLimiter().run("catalog.lookup", input, undefined, (signal) =>
d.ctx.capabilities.invoke("catalog.lookup", input, {
actorId: actorOfP(principal),
taskId: `files.identify:${randomUUID()}`,
scopeIds: [],
signal: AbortSignal.timeout(15_000),
signal: signal ?? AbortSignal.timeout(EXTERNAL_INVOKE_TIMEOUT_MS),
source: "web",
},
}),
)) as {
remote?: { provider: string; entityType: string; externalId: string; title: string; year: number | null }[];
};