feat(identify-words): 识别词——全局识别词(MP 语法)+ 作品识别词(季集换算),规则页「识别词」页签

- 新表 identify_words(0094):全局一块 + 每部作品一块原文,带版本与审计;episode_mapping_rule 转成文本行后删表
- 全局识别词移植 MP WordsMatcher(屏蔽 / 替换 / 集数偏移 / 替换+偏移 / {[tmdbid=…]}),所有按标题 / 文件名识别的地方解析前套用,命中的行记在解析结果上;保存后后台按改动行重新识别
- 作品识别词:S02 >> S01 EP+24 / S01E13 >> S00E01 / S01 @组 >> 忽略,解析器 + 打印器 + 编译;发布组繁简视为同一个
- 去掉规则自动停用:算出来对不上季表只待确认,要停用就注释掉那一行
- 集数待确认的采用 / 忽略与智能体 apply_rule 改为往作品识别词追加一行(带来历注释)
- 编辑器可视化 / 文本两种模式改同一份原文,保存前试算;作品页「集数映射」用同一个编辑器

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-08 17:03:00 +08:00
1 parent 12f8fc59c3
commit 63e94b308e
90 files changed
+6366 -2131

No files matched your search

@@ -47,8 +47,13 @@ const decision = (over: Partial<Decision> = {}): Decision =>
},
},
options: [
{ key: "confirm_suggestion", label: "确认建议规则", description: "按 EP-30 换算到第 2 季" },
{ key: "ignore_rule", label: "忽略并建忽略规则" },
{
key: "confirm_suggestion",
label: "确认建议规则",
description: "把「S01 @Lilith-Raws >> S02 EP-30」写进作品识别词",
input: { line: "S01 @Lilith-Raws >> S02 EP-30" },
},
{ key: "ignore_rule", label: "忽略并写忽略规则", input: { line: "S01 @Lilith-Raws >> 忽略" } },
],
...over,
}) as Decision;
@@ -59,6 +64,7 @@ describe("待处理「集数待确认」", () => {
expect(v).toMatchObject({ entityId: ENTITY, season: 1, releaseGroup: "Lilith-Raws", count: 2 });
expect(v.releases.map((r) => r.source)).toEqual(["S01E40", "S01E41"]);
expect(v.suggestion?.expression).toBe("EP-30");
expect(v.suggestionLine).toBe("S01 @Lilith-Raws >> S02 EP-30");
expect(v.confirm?.key).toBe("confirm_suggestion");
expect(v.ignoreRule?.key).toBe("ignore_rule");
const none = episodeDecisionView(decision({ options: [], evidence: { mediaEntityId: ENTITY, suggestion: null } }));
@@ -71,20 +77,19 @@ describe("待处理「集数待确认」", () => {
);
expect(html).toContain("第 1 季 · Lilith-Raws");
expect(html).toContain("S01E40");
expect(html).toContain("EP-30");
expect(html).toContain("第 2 季");
expect(html).toContain("S01 @Lilith-Raws &gt;&gt; S02 EP-30");
expect(html).toContain("S02E10");
expect(html).toContain("套用后 2 条都对得上");
});
it("没有建议时提示到作品页手动建", () => {
it("没有建议时提示在作品识别词里手动写", () => {
const html = renderToStaticMarkup(
createElement(EpisodeDecisionBody, {
view: episodeDecisionView(decision({ evidence: { mediaEntityId: ENTITY, suggestion: null } })),
simulation: null,
}),
);
expect(html).toContain("到作品页手动建");
expect(html).toContain("在上面的作品识别词里手动写");
});
it("模拟结果一句话", () => {
@@ -96,7 +101,7 @@ describe("待处理「集数待确认」", () => {
});
const agentSuggestion = {
rule: { season: 1, releaseGroup: "Lilith-Raws", kind: "expression", expression: "EP-30", targetSeason: 2 },
line: "S01 @Lilith-Raws >> S02 EP-30",
reasons: ["40、41 的发布时间对上第 2 季第 10、11 集的播出日期"],
simulation: { samples: 2, resolved: 2, passed: 2, missing: 0, regressions: 0, changed: 2, scanned: 5, ok: true },
samples: [
@@ -124,7 +129,7 @@ describe("待处理「集数待确认」", () => {
it("智能体建议:取规则、理由、模拟结果与「确认智能体建议」", () => {
const v = episodeDecisionView(withAgent());
expect(v.agentSuggestion?.rule.expression).toBe("EP-30");
expect(v.agentSuggestion?.line).toBe("S01 @Lilith-Raws >> S02 EP-30");
expect(v.agentSuggestion?.reasons).toHaveLength(1);
expect(v.confirmAgent?.key).toBe("confirm_agent_suggestion");
expect(v.confirm?.key).toBe("confirm_suggestion");
@@ -132,13 +137,12 @@ describe("待处理「集数待确认」", () => {
expect(episodeDecisionView(decision())).toMatchObject({ agentSuggestion: null, confirmAgent: null });
});
it("渲染智能体建议:规则、作用域、理由、样本套用后的季集与模拟结果", () => {
it("渲染智能体建议:那一行作品识别词、理由、样本套用后的季集与模拟结果", () => {
const html = renderToStaticMarkup(
createElement(EpisodeDecisionBody, { view: episodeDecisionView(withAgent()), simulation: null }),
);
expect(html).toContain("智能体建议");
expect(html).toContain("偏移表达式");
expect(html).toContain("EP-30 → 第 2 季");
expect(html).toContain("S01 @Lilith-Raws &gt;&gt; S02 EP-30");
expect(html).toContain("对上第 2 季第 10、11 集的播出日期");
expect(html).toContain("S01E40 → S02E10");
expect(html).toContain("模拟:这 2 条都对得上,不影响已对上的资源");