feat(agent): 集数待确认的智能体兜底——查看上下文 / 模拟 / 写规则三个能力,写入前强制模拟,接后台调查与授权内恢复

能力 ID 的域名允许单个下划线分词(episode_mapping)。

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-03 20:08:20 +08:00
1 parent 45a114263b
commit 7793c7b3bc
27 files changed
+1747 -48

No files matched your search

@@ -1,5 +1,13 @@
import type { Decision, DecisionOption, EpisodeMappingPreview, EpisodeResolutionView } from "@umt/contracts";
import { BellOff, Check, PencilLine } from "lucide-react";
import type {
Decision,
DecisionOption,
EpisodeMappingPreview,
EpisodeMappingRuleSpec,
EpisodeResolutionView,
EpisodeResultBrief,
EpisodeSimulationSummary,
} from "@umt/contracts";
import { BellOff, Bot, Check, PencilLine } from "lucide-react";
import { useId, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
@@ -10,14 +18,24 @@ import { consolePath } from "~/components/console/nav.ts";
import { Button } from "~/components/ui/button.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { episodeCode, mappingHref, scopeText, sourceText } from "./model.ts";
import { canonicalText, episodeCode, mappingHref, RULE_KIND_LABEL, ruleText, scopeText, sourceText } from "./model.ts";
// 待处理中心里的「集数待确认」(episode_mapping):来源季集、原因、建议规则与它的模拟结果;
// 操作「确认」(建建议规则并重算)、「修改」(跳作品页集数映射并预填草稿)、「忽略」(可同时建忽略规则)。
// 智能体建议(批次 5):后台调查没有写规则的授权时,把模拟通过的规则和理由留在事项上,「确认智能体建议」一键建规则
// (服务端建规则前按当前资源再模拟一次)。
export const EPISODE_CONFIRM_KEY = "confirm_suggestion";
export const EPISODE_CONFIRM_AGENT_KEY = "confirm_agent_suggestion";
export const EPISODE_IGNORE_RULE_KEY = "ignore_rule";
export interface AgentSuggestion {
rule: EpisodeMappingRuleSpec;
reasons: string[];
simulation: EpisodeSimulationSummary;
samples: { title: string; source: EpisodeResolutionView["source"]; after: EpisodeResultBrief }[];
}
interface Suggestion {
expression: string;
targetSeason: number;
@@ -31,7 +49,9 @@ export interface EpisodeDecisionView {
releases: { id: string; title: string; source: string }[];
count: number;
suggestion: Suggestion | null;
agentSuggestion: AgentSuggestion | null;
confirm: DecisionOption | null;
confirmAgent: DecisionOption | null;
ignoreRule: DecisionOption | null;
}
@@ -51,6 +71,7 @@ export function episodeDecisionView(d: Decision): EpisodeDecisionView {
}))
: [];
const s = e.suggestion as Suggestion | null | undefined;
const a = e.agentSuggestion as AgentSuggestion | null | undefined;
return {
entityId: entity ?? (d.subjectType === "media_entity" ? d.subjectId : null),
season: typeof e.season === "number" ? e.season : null,
@@ -58,7 +79,16 @@ export function episodeDecisionView(d: Decision): EpisodeDecisionView {
releases,
count: typeof e.count === "number" ? e.count : releases.length,
suggestion: s && typeof s.expression === "string" ? s : null,
agentSuggestion:
a?.rule && a.simulation
? {
...a,
reasons: Array.isArray(a.reasons) ? a.reasons : [],
samples: Array.isArray(a.samples) ? a.samples : [],
}
: null,
confirm: d.options.find((o) => o.key === EPISODE_CONFIRM_KEY) ?? null,
confirmAgent: d.options.find((o) => o.key === EPISODE_CONFIRM_AGENT_KEY) ?? null,
ignoreRule: d.options.find((o) => o.key === EPISODE_IGNORE_RULE_KEY) ?? null,
};
}
@@ -72,6 +102,41 @@ export function simulationText(p: EpisodeMappingPreview | undefined): string | n
return fail ? `套用后 ${total - fail} 条对得上,${fail} 条仍对不上` : `套用后 ${total} 条都对得上`;
}
/** 智能体建议的模拟结果一句话 */
export function agentSimulationText(s: EpisodeSimulationSummary): string {
const head =
s.resolved === s.samples ? `模拟:这 ${s.samples} 条都对得上` : `模拟:${s.samples} 条里 ${s.resolved} 条对得上`;
return `${head}${s.regressions ? `,会影响 ${s.regressions} 条已对上的资源` : ",不影响已对上的资源"}`;
}
/** 智能体建议:规则、理由、模拟结果与样本套用后的季集 */
export function AgentSuggestionBody({ s }: { s: AgentSuggestion }) {
return (
<div className="space-y-1">
<div>
<span className="text-muted-foreground">{RULE_KIND_LABEL[s.rule.kind]}:</span>
<span className="font-mono">{ruleText(s.rule)}</span>
<span className="ml-2 text-meta text-muted-foreground">{scopeText(s.rule.season, s.rule.releaseGroup)}</span>
</div>
{s.reasons.length ? (
<ul className="list-disc space-y-0.5 pl-4 text-meta text-muted-foreground">
{s.reasons.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
) : null}
<ul className="space-y-0.5 text-meta text-muted-foreground">
{s.samples.slice(0, 3).map((x) => (
<li key={x.title} className="font-mono tabular-nums">
{sourceText({ source: x.source })} → {canonicalText({ ...x.after, special: x.after.special ?? undefined })}
</li>
))}
</ul>
<div className="text-meta text-muted-foreground">{agentSimulationText(s.simulation)}</div>
</div>
);
}
/** 展示部分(纯渲染,单测直接渲染) */
export function EpisodeDecisionBody({ view, simulation }: { view: EpisodeDecisionView; simulation: string | null }) {
const shown = view.releases.slice(-5);
@@ -93,6 +158,14 @@ export function EpisodeDecisionBody({ view, simulation }: { view: EpisodeDecisio
) : null}
</ul>
</dd>
{view.agentSuggestion ? (
<>
<dt className="text-muted-foreground">智能体建议</dt>
<dd className="min-w-0 text-foreground">
<AgentSuggestionBody s={view.agentSuggestion} />
</dd>
</>
) : null}
<dt className="text-muted-foreground">建议规则</dt>
<dd className="min-w-0 text-foreground">
{view.suggestion ? (
@@ -191,10 +264,32 @@ export function EpisodeDecision({ d, open }: { d: Decision; open: boolean }) {
<EpisodeDecisionBody view={view} simulation={sug ? simulationText(preview.data) : null} />
{open ? (
<div className="flex flex-wrap items-center gap-2">
{view.confirm ? (
{view.confirmAgent && view.agentSuggestion ? (
<ConfirmDialog
trigger={
<Button size="sm">
<Bot />
确认智能体建议
</Button>
}
title="按智能体建议的规则处理?"
description={`${view.confirmAgent.description ?? ""}。规则在作品页的集数映射里可以再改。`}
confirmLabel="确认"
onConfirm={() =>
resolve.mutateAsync({ id: d.id, optionKey: view.confirmAgent!.key }).then(
() => toast.success("已建规则并重算"),
(err) => {
toast.error(errorMessage(err, "处理失败"));
throw err;
},
)
}
/>
) : null}
{view.confirm ? (
<ConfirmDialog
trigger={
<Button size="sm" variant={view.confirmAgent && view.agentSuggestion ? "outline" : "default"}>
<Check />
确认
</Button>