feat(agent): downloader.list 能力与汇报计数规则(FIX-7)
智能体原先只能从 site.list 推断下载器,会把没看到汇报成下载器不在系统内。 新增只读能力 downloader.list(下载器与媒体服务器、连通状态、管理的任务数); 原因码表补能力错误、下载盘判定、站点登录与待处理类型,带分类前缀的码按后半段查; 汇报格式加「数量以工具返回为准」。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
68f53390f6
commit
1b22bb5131
12 files changed
+163
-9
No files matched your search
@@ -57,6 +57,7 @@ export function interactionRules(i: NonNullable<SystemPromptInput["interaction"]
|
||||
export const REPORT_FORMAT: readonly string[] = [
|
||||
"第一句写结论:做成了什么、没做成什么、为什么。",
|
||||
"对象用名字:作品名、站点名、资源标题、文件名;不写函数名、工具名、字段名、参数、UUID 或 hash。",
|
||||
"数量以工具返回为准:派出几个子会话、提交几个下载,只数成功的调用,失败或被拒绝的不算。",
|
||||
"不超过 10 行;不复述过程,不写下一步打算,除非需要用户做什么。",
|
||||
"只有列举多个资源(3 个以上的资源、任务或文件)时才用表格,其余用短句或列表。",
|
||||
"可以用 Markdown 列表、表格和粗体,不写标题,不写 HTML。",
|
||||
@@ -84,6 +85,7 @@ export function buildSystemPrompt({
|
||||
'检索词要有意义:写作品名、别名或关键词;不要用空串、单个字母或通配符(如 query:"a"、"*")探查全表,想盘点就用对应的列表能力(inventory.query 不带 query、subscription.list、decision.list)。',
|
||||
'参数按工具 schema 的类型填写:ID 类参数一律是字符串(如 externalId:"693134",不是数字 693134);不要传 schema 里没有的字段。',
|
||||
"业务任务 ID 用 task.inspect,下载任务 ID 或种子 hash 用 download.inspect,两者不要混用。",
|
||||
"下载器、媒体服务器有没有接入、是否正常,用 downloader.list 查;站点列表里没有下载器不代表没配置。",
|
||||
"思考、步骤说明和汇报都用中文;引用文件名、原站标题时保持原文。",
|
||||
"工具结果里出现【预算提醒】时立刻停止探查,直接用中文写小结;被预算或中止打断时,汇报只写中文小结(已完成什么、证据、还缺什么),不要复述中间过程或下一步打算。",
|
||||
];
|
||||
|
||||
@@ -27,6 +27,7 @@ describe("汇报格式(AGUI-3)", () => {
|
||||
"## 汇报格式
|
||||
- 第一句写结论:做成了什么、没做成什么、为什么。
|
||||
- 对象用名字:作品名、站点名、资源标题、文件名;不写函数名、工具名、字段名、参数、UUID 或 hash。
|
||||
- 数量以工具返回为准:派出几个子会话、提交几个下载,只数成功的调用,失败或被拒绝的不算。
|
||||
- 不超过 10 行;不复述过程,不写下一步打算,除非需要用户做什么。
|
||||
- 只有列举多个资源(3 个以上的资源、任务或文件)时才用表格,其余用短句或列表。
|
||||
- 可以用 Markdown 列表、表格和粗体,不写标题,不写 HTML。
|
||||
@@ -40,7 +41,7 @@ describe("汇报格式(AGUI-3)", () => {
|
||||
expect(p).toContain("写出查到的对象名称和关键数值");
|
||||
// 旧的"最后用简短中文汇报…可以用 Markdown(标题…)"已换成汇报格式一节
|
||||
expect(p).not.toContain("最后用简短中文汇报");
|
||||
expect(REPORT_FORMAT.length).toBe(5);
|
||||
expect(REPORT_FORMAT.length).toBe(6);
|
||||
});
|
||||
|
||||
it("子会话 / 视觉子流程汇报给父会话:不加用户汇报格式", () => {
|
||||
|
||||
@@ -320,6 +320,37 @@ export const DownloadInspectOutput = z.object({
|
||||
otherMatches: z.array(Id),
|
||||
});
|
||||
|
||||
/** downloader.list(FIX-7):已配置的下载器与媒体服务器(只读本地记录,不含地址和凭证) */
|
||||
export const DownloaderListInput = z.object({
|
||||
includeDisabled: z.boolean().default(true),
|
||||
});
|
||||
const InstanceHealth = z.enum(["unknown", "healthy", "degraded", "offline"]);
|
||||
export const DownloaderListOutput = z.object({
|
||||
downloaders: z.array(
|
||||
z.object({
|
||||
id: Id,
|
||||
name: z.string(),
|
||||
kind: z.string(),
|
||||
enabled: z.boolean(),
|
||||
healthStatus: InstanceHealth,
|
||||
lastSeenAt: z.string().nullable(),
|
||||
version: z.string().nullable(),
|
||||
/** 本工具管理、未移除的下载任务 */
|
||||
tasks: z.object({ total: NonNegInt, active: NonNegInt, failed: NonNegInt }),
|
||||
}),
|
||||
),
|
||||
mediaServers: z.array(
|
||||
z.object({
|
||||
id: Id,
|
||||
name: z.string(),
|
||||
kind: z.string(),
|
||||
enabled: z.boolean(),
|
||||
healthStatus: InstanceHealth,
|
||||
lastSeenAt: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// ── 子代理(W7-B) ──
|
||||
|
||||
/** 子会话预算:缺省按父会话剩余预算的一半划拨;超过剩余预算直接拒绝 */
|
||||
@@ -842,6 +873,7 @@ export const CAPABILITY_IO = {
|
||||
"subscription.list": { input: SubscriptionListInput, output: SubscriptionListOutput },
|
||||
"decision.list": { input: DecisionListInput, output: DecisionListOutput },
|
||||
"download.inspect": { input: DownloadInspectInput, output: DownloadInspectOutput },
|
||||
"downloader.list": { input: DownloaderListInput, output: DownloaderListOutput },
|
||||
"agent.spawn": { input: AgentSpawnInput, output: AgentSpawnOutput },
|
||||
"agent.await": { input: AgentAwaitInput, output: AgentAwaitOutput },
|
||||
"site.list": { input: SiteListInput, output: SiteListOutput },
|
||||
@@ -918,6 +950,8 @@ export type DecisionListInput = z.infer<typeof DecisionListInput>;
|
||||
export type DecisionListOutput = z.infer<typeof DecisionListOutput>;
|
||||
export type DownloadInspectInput = z.infer<typeof DownloadInspectInput>;
|
||||
export type DownloadInspectOutput = z.infer<typeof DownloadInspectOutput>;
|
||||
export type DownloaderListInput = z.infer<typeof DownloaderListInput>;
|
||||
export type DownloaderListOutput = z.infer<typeof DownloaderListOutput>;
|
||||
export type AgentSpawnInput = z.infer<typeof AgentSpawnInput>;
|
||||
export type AgentSpawnOutput = z.infer<typeof AgentSpawnOutput>;
|
||||
export type AgentAwaitInput = z.infer<typeof AgentAwaitInput>;
|
||||
|
||||
@@ -69,6 +69,7 @@ export const CAPABILITY_IDS = [
|
||||
"subscription.list",
|
||||
"decision.list",
|
||||
"download.inspect",
|
||||
"downloader.list",
|
||||
"agent.spawn",
|
||||
"agent.await",
|
||||
"site.list",
|
||||
@@ -329,6 +330,18 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
true,
|
||||
false,
|
||||
),
|
||||
// FIX-7:智能体原先只能从站点列表推断下载器是否接入(site.list 不含下载器),汇报成"下载器不在系统内"
|
||||
"downloader.list": d(
|
||||
"downloader.list",
|
||||
"查看下载器",
|
||||
"列出已配置的下载器和媒体服务器:名称、类型、是否启用、最近一次连通状态与时间、本工具管理的下载任务数(按进行中 / 失败计)。只读本地记录,不连下载器;问下载器或媒体服务器是否接入、是否正常时先用它,某个下载任务用 download.inspect",
|
||||
"read",
|
||||
"DownloaderList",
|
||||
"read",
|
||||
"low",
|
||||
true,
|
||||
false,
|
||||
),
|
||||
// 子代理(设置 pi.subagents 开启后才提供给模型):类别 propose——只建有界子会话,不产生外部副作用,
|
||||
// "先给方案"的会话也能派生(子会话同样只给方案);授权不高于父会话,由服务端校验
|
||||
"agent.spawn": d(
|
||||
@@ -612,6 +625,7 @@ export const CAPABILITY_ACTIVITY: Record<KnownCapabilityId, CapabilityActivity>
|
||||
"subscription.list": act("查", "订阅"),
|
||||
"decision.list": act("查", "待处理事项"),
|
||||
"download.inspect": act("查", "下载任务", "个"),
|
||||
"downloader.list": act("查", "下载器"),
|
||||
"agent.spawn": act("派出", "子会话", "个"),
|
||||
"agent.await": act("等", "子会话"),
|
||||
"site.list": act("查", "站点"),
|
||||
|
||||
@@ -48,6 +48,35 @@ const REASONS: Record<string, ReasonText> = {
|
||||
not_imported_kind: { text: "这类文件不入库" },
|
||||
target_root_not_managed: { text: "目标目录不是本工具管理的媒体库目录", action: "在设置 → 媒体库与目录里检查" },
|
||||
download_removed: { text: "下载任务已从下载器删除" },
|
||||
// FIX-7:智能体汇报与工具结果里常见的码(能力错误、下载计划的空间判定、站点登录、待处理类型)
|
||||
invalid_input: { text: "参数不对" },
|
||||
invalid_output: { text: "返回结果格式不对" },
|
||||
unauthorized: { text: "需要你批准" },
|
||||
forbidden: { text: "没有权限" },
|
||||
not_found: { text: "没找到" },
|
||||
precondition: { text: "前置条件不满足" },
|
||||
internal: { text: "内部错误" },
|
||||
volume_state_unknown: { text: "下载盘用量未知", action: "在下载面板检查下载盘" },
|
||||
hard_threshold_reached: { text: "下载盘已到暂停线" },
|
||||
projected_over_threshold: { text: "下完会超过下载盘暂停线" },
|
||||
awaiting_resume_watermark: { text: "等下载盘降到恢复线" },
|
||||
request_size_unknown: { text: "资源大小未知" },
|
||||
inflight_size_unknown: { text: "在下的任务大小未知" },
|
||||
budget_exceeded: { text: "超出下载额度" },
|
||||
budget_unknown_size: { text: "资源大小未知,无法核对下载额度" },
|
||||
no_downloader_instance: { text: "没有可用的下载器", action: "在设置 → 下载器里添加" },
|
||||
download_save_path_unmapped: { text: "下载目录没有路径映射", action: "在设置 → 下载器里添加路径映射" },
|
||||
site_out_of_scope: { text: "站点不在这次的范围内" },
|
||||
slot_changed_or_reserved: { text: "收录版本已变化或被占用" },
|
||||
auth_missing: { text: "站点没有配置 Cookie", action: "在站点账号里填写" },
|
||||
auth_expired: { text: "站点登录已过期", action: "刷新站点登录" },
|
||||
auth_invalid: { text: "站点登录无效", action: "检查站点账号" },
|
||||
source_unreachable: { text: "站点连不上" },
|
||||
download_failed: { text: "下载失败" },
|
||||
download_stalled: { text: "下载无进展" },
|
||||
space_insufficient: { text: "空间不足" },
|
||||
unidentifiable: { text: "没识别出作品" },
|
||||
user_deleted: { text: "已删除" },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -69,7 +98,8 @@ export function reasonText(raw: string | null | undefined): ReasonText | null {
|
||||
...(parts[0]?.action ? { action: parts[0].action } : {}),
|
||||
};
|
||||
}
|
||||
return REASONS[s] ?? REASONS[head] ?? null;
|
||||
// 带分类前缀的码(`cache:volume_state_unknown`、`deferred:no_downloader_instance`)按冒号后的码查
|
||||
return REASONS[s] ?? REASONS[head] ?? (rest ? (REASONS[rest.trim()] ?? null) : null);
|
||||
}
|
||||
|
||||
/** 只要一句话:"没识别出作品。指定作品后重新整理";不认识的码返回 fallback */
|
||||
|
||||
@@ -18,6 +18,15 @@ describe("reasonText(DEAI-2 原因码人话化)", () => {
|
||||
for (const c of ["blocked_by_conflict", "main_failed", "not_imported_kind"]) expect(reasonText(c)).not.toBeNull();
|
||||
});
|
||||
|
||||
it("智能体汇报里的码(FIX-7):能力错误、带分类前缀的空间判定、站点登录", () => {
|
||||
expect(reasonText("invalid_input")?.text).toBe("参数不对");
|
||||
expect(reasonText("cache:volume_state_unknown")?.text).toBe("下载盘用量未知");
|
||||
expect(reasonText("deferred:no_downloader_instance")?.text).toBe("没有可用的下载器");
|
||||
expect(reasonText("auth_missing")?.text).toContain("Cookie");
|
||||
expect(reasonText("unidentifiable")?.text).toBe("没识别出作品");
|
||||
expect(reasonText("cache:some_new_code")).toBeNull();
|
||||
});
|
||||
|
||||
it("不认识的码返回 null;句子兜底不把码原样给人看", () => {
|
||||
expect(reasonText("some_new_code")).toBeNull();
|
||||
expect(reasonSentence("some_new_code")).toBe("处理失败");
|
||||
|
||||
Reference in new issue
Block a user