阶段4-tool契约: register支持CapabilityMeta(决策8 meta可选+新能力强制), 新增registerAll显式注册入口双入口过渡(决策9), RUNTIME_ERROR_CODES错误码固化+audit校验, 规范v1同步收紧

This commit is contained in:
yu 2026-11-04 13:41:24 +08:00 committed by trae-solo-agent [origin=hook_stop]
parent e691b9fa78
commit b54311d467
6 changed files with 197 additions and 4 deletions

View File

@ -45,10 +45,14 @@
禁止返回"为 UI 拼接的字符串"(渲染层需要展示时自行格式化)。
2. **必有超时**:涉及外部交互(进程/网络/窗口等待)的能力必须有 timeout 参数和默认值。
3. **错误契约**:失败抛 `RuntimeError(code, message, { recommendation })`;
code 用稳定英文枚举,message 中文,recommendation 给可执行建议。禁止裸 throw string。
code 用稳定英文枚举(必须登记在 `packages/rpa-core/src/engine/error-codes.ts`
的 `RUNTIME_ERROR_CODES`,任务 4.3 起机器可读),message 中文,recommendation 给可执行建议。禁止裸 throw string。
4. **平台标记**:Windows 专用能力(user32/UIA/COM)在 catalog 注册时带
`platforms: ["win32"]`;能用纯 Node 实现的能力不调 PowerShell。
5. **可测试**:每个能力有同名 `*.test.ts`(vitest),与实现同目录。
6. **结构化 meta(任务 4.1 起,新能力强制)**:`register(ids, handler, meta)` 第三参
必填 `CapabilityMeta`(summary / params / returns / timeoutMs / errorCodes /
needsScreenshot / platforms)。老能力逐步补齐,review 时放行不带 meta 的旧文件改动。
## 四、新能力验收 checklist

View File

@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { createEngine } from "../engine/engine";
import { capabilityCount, capabilityIds, getCapabilityMeta } from "../engine/registry";
import { isKnownErrorCode } from "../engine/error-codes";
import { capabilityDomains, registerAll } from "./register-all";
import "./index";
describe("registerAll 显式注册入口", () => {
it("副作用入口已注册全量包内能力,registerAll 全量幂等", async () => {
// 包内 capabilities/index 注册 719 个 id(909 快照 = 包内 + 宿主浏览器能力)
const before = capabilityCount();
expect(before).toBeGreaterThan(700);
const engine = await registerAll(createEngine());
expect(engine).toBeDefined();
expect(capabilityCount()).toBe(before);
});
it("按子集注册:只拉起指定域,未知域报错", async () => {
expect(capabilityDomains).toContain("excelCom");
await expect(registerAll(createEngine(), ["excelCom", "no-such-domain"])).rejects.toThrow("未知能力域");
await registerAll(createEngine(), ["files"]);
expect(getCapabilityMeta).toBeDefined();
});
it("meta(任务 4.1):老能力允许缺省;已声明 meta 的能力错误码必须在稳定清单内", () => {
// 全量注册后的每个 meta,其 errorCodes 都必须落在 RUNTIME_ERROR_CODES 清单内
for (const id of capabilityIds()) {
const meta = getCapabilityMeta(id);
if (!meta) continue;
for (const code of meta.errorCodes ?? []) {
expect(isKnownErrorCode(code), `${id} 的错误码 ${code} 未登记 error-codes.ts`).toBe(true);
}
}
});
});

View File

@ -0,0 +1,59 @@
import type { RpaEngine } from "../engine/engine";
/**
* 显式注册入口(任务 4.2,决策点 9:双入口过渡)。
*
* - 旧入口:`import "@rpa/core/capabilities"` 副作用注册全量(宿主/测试继续使用);
* - 新入口:`await registerAll(engine, groups?)` 按 subset 显式拉起,供
* Agent-Employee 等宿主按需注册。阶段 6 迁移时切显式入口、删副作用入口。
*
* 引擎当前持有全局注册表(阶段 1 决策 2),拉起模块即完成对全局表的注册,
* 返回 engine 以便链式使用。
*/
const DOMAINS: Record<string, () => Promise<unknown>> = {
control: () => import("./control"),
files: () => import("./files"),
http: () => import("./http"),
data: () => import("./data"),
office: () => import("./office"),
desktop: () => import("./desktop"),
desktopUia: () => import("./desktop-uia"),
time: () => import("./time-caps"),
windowsSystem: () => import("./windows-system"),
ftp: () => import("./ftp"),
ocr: () => import("./ocr-caps"),
pdf: () => import("./pdf"),
word: () => import("./word"),
wordExtended: () => import("./word-extended"),
message: () => import("./message"),
vision: () => import("./vision"),
cvVision: () => import("./cv-vision"),
extended: () => import("./extended"),
systemExtended: () => import("./system-extended"),
officeFileExtended: () => import("./office-file-extended"),
officeAliases: () => import("./office-aliases"),
excelCommon: () => import("./excel-common"),
excelCom: () => import("./excel-com"),
dataExtended: () => import("./data-extended"),
utilityExtended: () => import("./utility-extended"),
dataTableCommon: () => import("./data-table-common"),
basicEncryption: () => import("./basic-encryption"),
mailRemaining: () => import("./mail-remaining"),
remainingCore: () => import("./remaining-core"),
commonFinal: () => import("./common-final"),
humanDialog: () => import("./human-dialog"),
aiService: () => import("./ai-service"),
windowExtended: () => import("./window-extended")
};
export async function registerAll(engine: RpaEngine, groups?: string[]): Promise<RpaEngine> {
const wanted = groups && groups.length ? groups : Object.keys(DOMAINS);
for (const group of wanted) {
const load = DOMAINS[group];
if (!load) throw new Error(`未知能力域:${group}(可选:${Object.keys(DOMAINS).join(", ")})`);
await load();
}
return engine;
}
export const capabilityDomains = Object.keys(DOMAINS);

View File

@ -0,0 +1,52 @@
/**
* 稳定错误码枚举(任务 4.3,R9)。
*
* 契约:RuntimeError 的 code 必须取自本清单;新增能力需要新错误码时先在此登记。
* message 保持中文(给人看),机器消费走 code 与 options 里的结构化字段
* (recommendation / nodeId / componentId / arg / detail)。
* 宿主侧另有自己的扩展码(browser 会话 / 拾取 / vision-lab 等),不进本清单。
*/
export const RUNTIME_ERROR_CODES = [
// 通用
"validation_error",
"timeout",
"cancelled",
"capability_not_implemented",
"expression_error",
"user_exception",
"not_found",
"network_error",
"http_error",
"external_service_failed",
// 桌面驱动
"desktop_command_failed",
"desktop_element_not_found",
"desktop_error",
// 浏览器
"browser_error",
"browser_disconnected",
"tab_unavailable",
// AI 服务
"ai_not_configured",
"ai_request_timeout",
"ai_rate_limited",
"ai_request_failed",
// 视觉
"cv_not_found",
// Office
"excel_com_failed",
"word_table_not_found",
"word_text_not_found",
"outlook_unavailable",
// 其他
"decryption_failed",
"host_unavailable"
] as const;
export type ErrorCode = (typeof RUNTIME_ERROR_CODES)[number];
/** 运行期校验(audit 测试用):code 必须在清单内 */
export function isKnownErrorCode(code: string): boolean {
return (RUNTIME_ERROR_CODES as readonly string[]).includes(code);
}

View File

@ -1,16 +1,54 @@
import type { CapabilityHandler } from "./types";
const handlers = new Map<string, CapabilityHandler>();
/**
* 能力结构化元数据(任务 4.1,决策点 8:meta 可选 + 新能力强制)。
* 老能力逐步补齐;规范 v1 验收 checklist 对新能力要求必填。
*/
export interface CapabilityParamMeta {
key: string;
label?: string;
type: "string" | "number" | "boolean" | "object" | "array";
required?: boolean;
defaultValue?: string | number | boolean | null;
description?: string;
}
export function register(ids: string | string[], handler: CapabilityHandler): void {
export interface CapabilityMeta {
/** 一句话能力摘要(中文) */
summary?: string;
/** 参数 schema(按字段名列出) */
params?: CapabilityParamMeta[];
/** 返回值说明 */
returns?: string;
/** 默认超时毫秒(无外部交互语义的能力省略) */
timeoutMs?: number;
/** 可能抛出的稳定错误码(见 engine/error-codes.ts) */
errorCodes?: string[];
/** 执行中是否需要回传截图(Agent 侧视觉判断用) */
needsScreenshot?: boolean;
/** 平台限制;省略表示跨平台 */
platforms?: Array<"win32">;
}
const handlers = new Map<string, CapabilityHandler>();
const metas = new Map<string, CapabilityMeta>();
export function register(ids: string | string[], handler: CapabilityHandler, meta?: CapabilityMeta): void {
const list = Array.isArray(ids) ? ids : [ids];
for (const id of list) handlers.set(id, handler);
for (const id of list) {
handlers.set(id, handler);
if (meta) metas.set(id, meta);
}
}
export function getHandler(id: string): CapabilityHandler | undefined {
return handlers.get(id);
}
export function getCapabilityMeta(id: string): CapabilityMeta | undefined {
return metas.get(id);
}
export function hasCapability(id: string): boolean {
return handlers.has(id);
}

View File

@ -10,6 +10,11 @@ export { register, getHandler, hasCapability, capabilityIds, capabilityCount } f
export { runFlow } from "./engine/runtime";
export { RuntimeError, BreakSignal, ContinueSignal, StopSignal, asRuntimeError } from "./engine/types";
export { setSubflowSource, getSubflowSource, type SubflowSource } from "./engine/host-bridge";
// 错误契约(任务 4.3):稳定错误码枚举 + 机器可读结构(RuntimeError.code / recommendation / detail 等)
export { RUNTIME_ERROR_CODES, isKnownErrorCode, type ErrorCode } from "./engine/error-codes";
// 能力元数据(任务 4.1)与显式注册入口(任务 4.2,双入口过渡)
export { getCapabilityMeta, type CapabilityMeta, type CapabilityParamMeta } from "./engine/registry";
export { registerAll, capabilityDomains } from "./capabilities/register-all";
export {
setHostBridge,
getHostBridge,