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
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: episode-mapping
|
||||
title: 集数映射
|
||||
description: 资源的集数对不上作品季表(绝对集号、连续编号、特别篇混在正片里)时,看清上下文、试一条结构化的映射规则,模拟全部通过才写入,写入后同类资源由规则自动处理。集数待确认事项的后台调查,或用户说「某部剧集数不对」「集数待确认」时用。
|
||||
triggers:
|
||||
- 集数(待确认|对不上|不对|错了?|映射|乱了)
|
||||
- (绝对|连续)(集号|编号)|集号(不对|对不上|错)
|
||||
- 映射规则
|
||||
- 认成.{0,8}第.{0,4}集|第.{0,4}集.{0,8}(认成|变成|对不上)
|
||||
capabilities:
|
||||
- episode_mapping.inspect
|
||||
- episode_mapping.simulate
|
||||
- episode_mapping.apply_rule
|
||||
- decision.list
|
||||
- catalog.lookup
|
||||
sessions: [chat, investigate, child]
|
||||
---
|
||||
|
||||
## 适用意图
|
||||
|
||||
- 后台调查「集数待确认」事项:规则、原号校验、绝对集号 / 播出日期换算都没对上,要判断这个发布组这一季该怎么映射。
|
||||
- 用户在对话里说「《药屋少女》第二季集数不对」「Lilith 的连续编号帮我对一下」「这部剧集数待确认怎么处理」。
|
||||
|
||||
## 步骤
|
||||
|
||||
1. 定位:后台调查直接用事项 ID;对话里先 decision.list 找这部作品的集数待确认事项,没有事项时用 catalog.lookup 拿作品 ID,再按作品 + 来源季 + 发布组看。
|
||||
2. 先 episode_mapping.inspect:看事项内资源的原标题、发布时间、原始季集,同组相邻资源已经怎么对上的,季表每季总集数与每集播出日期,现有规则(停用的看停用原因),以及确定性建议。
|
||||
3. 想规则,按优先级:
|
||||
- 首选 expression(偏移表达式):连续编号、整体差一个固定数,如 `EP-12` 换到第 2 季、`EP*2-1`;确定性建议给了就从它开始。
|
||||
- 其次 table(对照表):编号不规则、只有少数几集要对,逐行写 `来源集号 → 第几季第几集`。
|
||||
- 特别篇、总集篇、花絮、PV 用 special(特别篇 / 花絮 / 忽略),不要硬塞成某一集。
|
||||
- 作用域默认用事项的来源季和发布组;只有确信所有发布组都这么编号时才放宽到任意发布组。
|
||||
4. 再 episode_mapping.simulate(带事项 ID 和一两句理由):看事项内样本是否全部解决、有没有让已对上的资源变成待确认。没通过就按失败原因改规则再试,最多试三次。
|
||||
5. 通过且有把握时,episode_mapping.apply_rule 写一次(带理由:依据哪些播出日期 / 标题 / 相邻资源)。服务端会再模拟一次,不通过会拒绝。写入后同类资源由规则自动处理,不要逐集再调用。
|
||||
6. 拿不准(样本太少、几种规则都说得通、季表缺集)就不写:只读调查里模拟通过的规则会自动作为「智能体建议」留在事项上,让用户确认;说清楚还差什么证据。
|
||||
|
||||
## 需要批准
|
||||
|
||||
- episode_mapping.apply_rule(写映射规则)需要持续授权;没有授权时写一句「没做:写集数映射规则,需要你批准」,模拟通过的规则已留在待处理里可一键确认。
|
||||
- episode_mapping.inspect、episode_mapping.simulate 只读,不需要批准。
|
||||
|
||||
## 汇报模板
|
||||
|
||||
> 《药屋少女的呢喃》第 1 季 · Lilith-Raws 的 40、41 集是连续编号,已按「减 30,换到第 2 季」写入规则,两条资源分别对上第 2 季第 10、11 集,播出日期一致。
|
||||
|
||||
> 《药屋少女的呢喃》第 1 季 · Lilith-Raws 的集数还没定:模拟通过的规则是「减 30,换到第 2 季」,已作为智能体建议留在待处理里,确认后生效。
|
||||
|
||||
- 第一句写结论(写了什么规则 / 为什么没写),第二句写依据(播出日期、标题、相邻资源),需要用户做的放最后。
|
||||
|
||||
## 不要做
|
||||
|
||||
- 不要绕过模拟:没跑过 episode_mapping.simulate 或模拟没通过,不调用 episode_mapping.apply_rule。
|
||||
- 不要逐集处理,也不要为每条资源各写一条规则:一条规则覆盖这个发布组这一季。
|
||||
- 不要把特别篇、总集篇硬算成正片集号。
|
||||
- 不要改别的发布组、别的季已经对上的资源;模拟显示会让已对上的资源变成待确认就换规则。
|
||||
- 汇报里不写规则 ID、资源 ID 和能力名。
|
||||
@@ -4,7 +4,7 @@ import { CapabilityId } from "@umt/contracts";
|
||||
* 能力 ID ↔ 模型工具名 的唯一映射。
|
||||
* 上游(Anthropic/OpenAI)工具名只允许 ^[a-zA-Z0-9_-]{1,64}$,不能带点,
|
||||
* 所以 `download.submit` 对 Pi 暴露为 `download__submit`。
|
||||
* 域名部分不含下划线,反向映射按第一个 `__` 切分即可无歧义。
|
||||
* 域名部分最多含单个下划线分词(episode_mapping),不含连续下划线,反向映射按第一个 `__` 切分即可无歧义。
|
||||
*/
|
||||
const TOOL_NAME_SEPARATOR = "__";
|
||||
const TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
||||
|
||||
@@ -67,6 +67,17 @@ export const INTENT_ROUTES: readonly IntentRoute[] = [
|
||||
{ cap: "organize.execute", note: "执行整理" },
|
||||
],
|
||||
},
|
||||
{
|
||||
intent: "集数对不上、集数待确认(绝对集号、连续编号、特别篇)",
|
||||
steps: [
|
||||
{ cap: "episode_mapping.inspect", note: "事项内资源、同组相邻资源、季表、现有规则与确定性建议" },
|
||||
{ cap: "episode_mapping.simulate", note: "试草稿规则,看样本是否全部解决、有没有误伤已对上的资源" },
|
||||
{ cap: "episode_mapping.apply_rule", note: "模拟通过才写规则,写入后同类资源自动处理" },
|
||||
],
|
||||
tips: [
|
||||
"优先 expression(偏移表达式),其次 table(对照表),特别篇 / 花絮用 special;拿不准就不写,只把模拟通过的规则留作建议。",
|
||||
],
|
||||
},
|
||||
{
|
||||
intent: "问订阅、追更、缺集",
|
||||
steps: [
|
||||
@@ -171,7 +182,7 @@ export function renderRoutes(available: ReadonlySet<string>): string[] {
|
||||
const chain = steps.map((s) => `${s.cap}(${s.note}${needsApproval(s.cap) ? ",需要批准" : ""})`).join(" → ");
|
||||
out.push(`- ${r.intent}:${chain}`);
|
||||
for (const t of r.tips ?? []) {
|
||||
const caps = t.match(/\b[a-z]+\.[a-z_]+\b/g) ?? [];
|
||||
const caps = t.match(/\b[a-z][a-z_]*\.[a-z_]+\b/g) ?? [];
|
||||
if (caps.every((c) => !(c in CAPABILITY_CATALOG) || available.has(c))) out.push(` - ${t}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@ describe("能力 ID ↔ 工具名", () => {
|
||||
expect(capabilityIdToToolName("task.retry_later")).toBe("task__retry_later");
|
||||
expect(toolNameToCapabilityId("download__submit")).toBe("download.submit");
|
||||
expect(toolNameToCapabilityId("task__retry_later")).toBe("task.retry_later");
|
||||
for (const id of ["catalog.lookup", "policy.apply", "organize.execute"]) {
|
||||
// 域名可用单个下划线分词:按第一个 `__` 切分仍无歧义
|
||||
expect(capabilityIdToToolName("episode_mapping.apply_rule")).toBe("episode_mapping__apply_rule");
|
||||
expect(toolNameToCapabilityId("episode_mapping__apply_rule")).toBe("episode_mapping.apply_rule");
|
||||
expect(() => capabilityIdToToolName("episode__mapping.inspect")).toThrow();
|
||||
expect(() => capabilityIdToToolName("episode_.inspect")).toThrow();
|
||||
for (const id of ["catalog.lookup", "policy.apply", "organize.execute", "episode_mapping.inspect"]) {
|
||||
expect(toolNameToCapabilityId(capabilityIdToToolName(id))).toBe(id);
|
||||
}
|
||||
expect(toolNameToCapabilityId("bash")).toBeUndefined();
|
||||
|
||||
@@ -50,7 +50,7 @@ describe("加载(Pi harness loadSkills)", () => {
|
||||
|
||||
it("正文提到的能力都在 frontmatter 的 capabilities 里,且都是已知能力", () => {
|
||||
for (const s of skills) {
|
||||
const mentioned = new Set(s.content.match(/\b[a-z]+\.[a-z_]+\b/g) ?? []);
|
||||
const mentioned = new Set(s.content.match(/\b[a-z][a-z_]*\.[a-z_]+\b/g) ?? []);
|
||||
for (const m of mentioned) {
|
||||
if (!(CAPABILITY_IDS as readonly string[]).includes(m)) continue;
|
||||
expect(s.capabilities, `${s.name} 正文用了 ${m}`).toContain(m);
|
||||
@@ -99,6 +99,8 @@ const ROUTING: [string, string][] = [
|
||||
["下载进度怎么样了", "download-diagnose"],
|
||||
["帮我建个巡检,每天早上看看有没有失败的下载", "patrol-draft"],
|
||||
["每隔 6 小时检查一下站点登录", "patrol-draft"],
|
||||
["药屋少女第二季的集数对不上", "episode-mapping"],
|
||||
["Lilith 的连续编号帮我对一下", "episode-mapping"],
|
||||
];
|
||||
|
||||
describe("按意图选择", () => {
|
||||
@@ -107,6 +109,15 @@ describe("按意图选择", () => {
|
||||
expect(picked[0]?.name).toBe(name);
|
||||
});
|
||||
|
||||
it("集数待确认事项的后台调查载入「集数映射」", () => {
|
||||
const goal = "调查待处理事项 x(episode_mapping):集数待确认:药屋少女的呢喃 第 1 季 · Lilith-Raws。";
|
||||
const picked = selectSkills(skills, goal, {
|
||||
kind: "investigate",
|
||||
capabilities: ["episode_mapping.inspect", "episode_mapping.simulate", "decision.list"],
|
||||
});
|
||||
expect(picked[0]?.name).toBe("episode-mapping");
|
||||
});
|
||||
|
||||
it("闲聊不载入 Skill;最多载入 2 个", () => {
|
||||
expect(selectSkills(skills, "你好", { kind: "chat" })).toEqual([]);
|
||||
expect(selectSkills(skills, "巡检一下下载失败和订阅追更,再清理空间", { kind: "chat" }).length).toBe(2);
|
||||
|
||||
Reference in new issue
Block a user