diff --git a/docs/dev-conventions.md b/docs/dev-conventions.md index fce9bfd..0c9d743 100644 --- a/docs/dev-conventions.md +++ b/docs/dev-conventions.md @@ -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 diff --git a/packages/rpa-core/src/capabilities/register-all.test.ts b/packages/rpa-core/src/capabilities/register-all.test.ts new file mode 100644 index 0000000..bac2343 --- /dev/null +++ b/packages/rpa-core/src/capabilities/register-all.test.ts @@ -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); + } + } + }); +}); diff --git a/packages/rpa-core/src/capabilities/register-all.ts b/packages/rpa-core/src/capabilities/register-all.ts new file mode 100644 index 0000000..5b85b36 --- /dev/null +++ b/packages/rpa-core/src/capabilities/register-all.ts @@ -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 Promise> = { + 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 { + 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); diff --git a/packages/rpa-core/src/engine/error-codes.ts b/packages/rpa-core/src/engine/error-codes.ts new file mode 100644 index 0000000..b71ee2c --- /dev/null +++ b/packages/rpa-core/src/engine/error-codes.ts @@ -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); +} diff --git a/packages/rpa-core/src/engine/registry.ts b/packages/rpa-core/src/engine/registry.ts index d36526b..9b26b97 100644 --- a/packages/rpa-core/src/engine/registry.ts +++ b/packages/rpa-core/src/engine/registry.ts @@ -1,16 +1,54 @@ import type { CapabilityHandler } from "./types"; -const handlers = new Map(); +/** + * 能力结构化元数据(任务 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(); +const metas = new Map(); + +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); } diff --git a/packages/rpa-core/src/index.ts b/packages/rpa-core/src/index.ts index 739b5e2..01827d0 100644 --- a/packages/rpa-core/src/index.ts +++ b/packages/rpa-core/src/index.ts @@ -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,