feat(search): 资源搜索结果附带规则判定

关联到本地作品的资源按作品收录版本的评估口径(硬条件 + 规则组链 + 阶梯,同自动获取的
evaluateCandidate)给出 ruleVerdict:命中第几档 / 不满足哪些维度 / 与库里当前版本相比高几档。
只调用 acquisition-organize 导出的 loadRuleCatalog、slotSpecWithRules、parsedOf、contextOf、
currentFacts;现有版本 pri 按 rulePriOfAsset 同口径在 search 模块里组合。一页最多判定 100 条、
20 个作品,收录版本 / 订阅 / 资源行 / 规则目录批量查一次;没关联作品或没有规则时为 null。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 11:59:07 +08:00
1 parent fb6c76875c
commit a7d20ca370
4 files changed
+556 -1

No files matched your search

+15 -1
View File
@@ -16,6 +16,7 @@ import type { SourcesRuntime } from "../sources-identity/runtime.ts";
import { upsertSiteRelease } from "../sources-identity/service/snapshot.ts";
import { siteCan } from "../sources-identity/site-kinds.ts";
import { raceAbort } from "./media.ts";
import { ruleVerdicts } from "./rule-verdict.ts";
// 跨站资源搜索:选中的已启用站点逐站调适配器 search(每站共享限流器 + 单站超时 + 站点级并发上限);
// 按(站点, 查询词)并发合并、短缓存 —— 换站点选择时已查过的站点直接复用;结果 upsert 为资源
@@ -23,7 +24,7 @@ import { raceAbort } from "./media.ts";
// 作品关联与库存叠加在每次请求时现算(不进缓存),识别完成后刷新即可看到。
/** 缓存里的资源条目(不含作品关联与库存叠加) */
export type BaseRelease = Omit<SearchRelease, "mediaEntityId" | "entityMatch" | "entity" | "inventory">;
export type BaseRelease = Omit<SearchRelease, "mediaEntityId" | "entityMatch" | "entity" | "inventory" | "ruleVerdict">;
export interface Fanout {
items: BaseRelease[];
@@ -502,6 +503,18 @@ export async function decorate(
entities.set(e.id, e);
}
const inv = entityIds.length ? await loadInventory(entityIds) : new Map<string, InventoryEntry>();
// 规则判定:失败只记警告,不影响结果列表
const verdicts = await ruleVerdicts(
db,
items.map((it) => {
const link = links.get(it.key);
const e = link ? entities.get(link.id) : undefined;
return { key: it.key, sourceReleaseId: it.sourceReleaseId, entity: e ? { id: e.id, kind: e.kind } : null };
}),
).catch((err: Error) => {
rt.ctx.logger.warn({ err: { message: err.message } }, "搜索结果规则判定失败");
return new Map<string, never>();
});
return items.map((it) => {
const link = links.get(it.key);
const e = link ? entities.get(link.id) : undefined;
@@ -512,6 +525,7 @@ export async function decorate(
entityMatch: e ? link!.match : null,
entity: e ? { id: e.id, title: e.title, year: e.year, kind: e.kind as never } : null,
inventory: entry ? overlay(entry, it.sourceReleaseId) : null,
ruleVerdict: verdicts.get(it.key) ?? null,
};
});
}