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:
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>
|
||||
|
||||
Reference in new issue
Block a user