feat(engine): createEngine 注入参数 + 消除模块级 driver 捕获(迁移阶段1-A1)

- createEngine(options) 支持 driver 注入,新增 engine.setDriver;
  缺省保持按平台自动选择的行为
- desktop/common-final/system-extended/window-extended 四文件模块加载时
  捕获 defaultEngine.driver 改为惰性 getDriver():非 win32 import 不再抛错
  (此前 darwin 上 import 即炸),调用时才触发 platform_unsupported
- engine-driver.test 补注入/隔离/后置替换 4 例
This commit is contained in:
yu committed 2026-11-06 10:20:29 +08:00
1 parent 9b76dc6987
commit 1da00827ac
7 files changed
+111 -48

No files matched your search

@@ -8,7 +8,7 @@ import { powershell, psString } from "../driver/windows/powershell";
import { defaultEngine } from "../engine/engine"; import { defaultEngine } from "../engine/engine";
// 窗口/屏幕查询统一走驱动(任务 3.3) // 窗口/屏幕查询统一走驱动(任务 3.3)
const driver = defaultEngine.driver; const getDriver = () => defaultEngine.driver;
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string { function text(args: Record<string, unknown>, keys: string[], fallback = ""): string {
for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); } for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); }
@@ -24,21 +24,21 @@ register("Window_GetRect", delegate("Window_GetWindowRect"));
register("Window_MaximumSize", (ctx, node, args, component) => delegate("Window_SetWindowState")(ctx, node, { ...args, WindowType: "maximize" }, component)); register("Window_MaximumSize", (ctx, node, args, component) => delegate("Window_SetWindowState")(ctx, node, { ...args, WindowType: "maximize" }, component));
register(["Keyboard_DriveSend", "Keyboard_DriveSend_V1", "Keyboard_DriveSend_V2"], delegate("Keyboard_Send_V1")); register(["Keyboard_DriveSend", "Keyboard_DriveSend_V1", "Keyboard_DriveSend_V2"], delegate("Keyboard_Send_V1"));
register("Window_GetScreenSize", async () => driver.screen.size()); register("Window_GetScreenSize", async () => getDriver().screen.size());
register("Window_WindowList", async (_ctx, _node, args) => { register("Window_WindowList", async (_ctx, _node, args) => {
const title = text(args, ["match_title", "select_title"]); const regex = String(args.is_regex ?? "0") === "1"; const title = text(args, ["match_title", "select_title"]); const regex = String(args.is_regex ?? "0") === "1";
return driver.window.list({ title, regex }); return getDriver().window.list({ title, regex });
}); });
register("Window_SetWindowShow", async (_ctx, node, args) => { register("Window_SetWindowShow", async (_ctx, node, args) => {
const title = text(args, ["窗口对象", "window_obj", "title"]); if (!title || title === "窗口对象") throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); const title = text(args, ["窗口对象", "window_obj", "title"]); if (!title || title === "窗口对象") throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId });
const show = !text(args, ["WindowType"], "show").toLowerCase().includes("hide"); const show = !text(args, ["WindowType"], "show").toLowerCase().includes("hide");
await driver.window.setState(title, show ? 5 : 0); await getDriver().window.setState(title, show ? 5 : 0);
return show; return show;
}); });
register("Window_WaitWindow", async (_ctx, node, args) => { register("Window_WaitWindow", async (_ctx, node, args) => {
const title = text(args, ["match_title", "select_title", "window_obj", "target_window"]); if (!title) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); const title = text(args, ["match_title", "select_title", "window_obj", "target_window"]); if (!title) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId });
const expected = text(args, ["switchType"], "0") !== "1"; const deadline = Date.now() + Math.max(0, Number(args.second ?? 5)) * 1000; const expected = text(args, ["switchType"], "0") !== "1"; const deadline = Date.now() + Math.max(0, Number(args.second ?? 5)) * 1000;
do { const exists = await driver.window.exists(title); if (exists === expected) return true; await new Promise((resolve) => setTimeout(resolve, 200)); } while (Date.now() <= deadline); do { const exists = await getDriver().window.exists(title); if (exists === expected) return true; await new Promise((resolve) => setTimeout(resolve, 200)); } while (Date.now() <= deadline);
return false; return false;
}); });
+12 -10
View File
@@ -6,7 +6,9 @@ import { powershell, psString } from "../driver/windows/powershell";
// 人为拟真设置本体挂在引擎实例上(任务 1.3),此处委托 defaultEngine。 // 人为拟真设置本体挂在引擎实例上(任务 1.3),此处委托 defaultEngine。
const manualOperation: ManualOperationSettings = defaultEngine.manualOperation; const manualOperation: ManualOperationSettings = defaultEngine.manualOperation;
// 桌面自动化驱动(任务 3.3):user32 注入原语统一走 WindowsDriver // 桌面自动化驱动(任务 3.3):user32 注入原语统一走 WindowsDriver
const driver = defaultEngine.driver; // 惰性获取驱动(迁移阶段1-A1):非 win32 上 import 本模块不再抛错,
// 调用桌面能力时才触发 platform_unsupported;也使注入 driver 的引擎可被测试/宿主使用
const getDriver = () => defaultEngine.driver;
function jitter(value: number, amount = 3): number { function jitter(value: number, amount = 3): number {
return Math.max(0, Math.round(value + (Math.random() * amount * 2 - amount))); return Math.max(0, Math.round(value + (Math.random() * amount * 2 - amount)));
@@ -63,7 +65,7 @@ const keyboardSend: CapabilityHandler = async (_ctx, node, args) => {
const isTextSend = node.componentId === "Keyboard_Send" || node.componentId === "Keyboard_Send_V1"; const isTextSend = node.componentId === "Keyboard_Send" || node.componentId === "Keyboard_Send_V1";
// 指定目标窗口时先激活再输入,内容才会进到目标窗口而不是运行时的焦点窗口 // 指定目标窗口时先激活再输入,内容才会进到目标窗口而不是运行时的焦点窗口
const targetTitle = text(args, ["window_title", "target_window", "window_obj", "match_title", "select_title"]); const targetTitle = text(args, ["window_title", "target_window", "window_obj", "match_title", "select_title"]);
await driver.keyboard.send({ keys, intervalMs, mode: mode === "2" ? "clipboard" : mode === "1" ? "manual" : "api", textMode: isTextSend, activateTitle: targetTitle || undefined }); await getDriver().keyboard.send({ keys, intervalMs, mode: mode === "2" ? "clipboard" : mode === "1" ? "manual" : "api", textMode: isTextSend, activateTitle: targetTitle || undefined });
await manualDelay(); await manualDelay();
return keys; return keys;
}; };
@@ -88,47 +90,47 @@ const disableManualOperation: CapabilityHandler = async () => {
const mouseClick: CapabilityHandler = async (_ctx, node, args) => { const mouseClick: CapabilityHandler = async (_ctx, node, args) => {
const [x, y] = manualJitter(point(args.position ?? args.point ?? args.location ?? "[0,0]"), false, true); const [x, y] = manualJitter(point(args.position ?? args.point ?? args.location ?? "[0,0]"), false, true);
const button = text(args, ["side", "button"], "0").toLowerCase(); const button = text(args, ["side", "button"], "0").toLowerCase();
await driver.mouse.click({ x, y }, { button: button.includes("right") || button === "1" ? "right" : "left" }); await getDriver().mouse.click({ x, y }, { button: button.includes("right") || button === "1" ? "right" : "left" });
return { x, y }; return { x, y };
}; };
const mouseMove: CapabilityHandler = async (_ctx, node, args) => { const mouseMove: CapabilityHandler = async (_ctx, node, args) => {
const [x, y] = manualJitter(point(args.position ?? args.point ?? args.location ?? "[0,0]"), true, false); const [x, y] = manualJitter(point(args.position ?? args.point ?? args.location ?? "[0,0]"), true, false);
await driver.mouse.moveTo({ x, y }); await getDriver().mouse.moveTo({ x, y });
return { x, y }; return { x, y };
}; };
const mouseDrag: CapabilityHandler = async (_ctx, node, args) => { const mouseDrag: CapabilityHandler = async (_ctx, node, args) => {
const [sx, sy] = point(args.Start ?? args.start ?? "[0,0]"); const [sx, sy] = point(args.Start ?? args.start ?? "[0,0]");
const [ex, ey] = point(args.End ?? args.end ?? "[0,0]"); const [ex, ey] = point(args.End ?? args.end ?? "[0,0]");
await driver.mouse.drag({ x: sx, y: sy }, { x: ex, y: ey }, { holdMs: 80, releaseDelayMs: 80 }); await getDriver().mouse.drag({ x: sx, y: sy }, { x: ex, y: ey }, { holdMs: 80, releaseDelayMs: 80 });
return { start: [sx, sy], end: [ex, ey] }; return { start: [sx, sy], end: [ex, ey] };
}; };
const mouseRoll: CapabilityHandler = async (_ctx, node, args) => { const mouseRoll: CapabilityHandler = async (_ctx, node, args) => {
const amount = number(args, ["RollNum", "rollNum", "amount"], 1) * 120; const amount = number(args, ["RollNum", "rollNum", "amount"], 1) * 120;
await driver.mouse.scroll(amount); await getDriver().mouse.scroll(amount);
return amount; return amount;
}; };
const setActiveWindow: CapabilityHandler = async (_ctx, node, args) => { const setActiveWindow: CapabilityHandler = async (_ctx, node, args) => {
const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]); const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]);
if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId });
return driver.window.focus(selector); return getDriver().window.focus(selector);
}; };
const getActiveWindow: CapabilityHandler = async () => driver.window.foregroundTitle(); const getActiveWindow: CapabilityHandler = async () => getDriver().window.foregroundTitle();
const windowExists: CapabilityHandler = async (_ctx, node, args) => { const windowExists: CapabilityHandler = async (_ctx, node, args) => {
const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]); const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]);
if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId });
return driver.window.exists(selector); return getDriver().window.exists(selector);
}; };
const closeWindow: CapabilityHandler = async (_ctx, node, args) => { const closeWindow: CapabilityHandler = async (_ctx, node, args) => {
const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]); const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]);
if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId });
return driver.window.close(selector); return getDriver().window.close(selector);
}; };
const runApp: CapabilityHandler = async (_ctx, node, args) => { const runApp: CapabilityHandler = async (_ctx, node, args) => {
@@ -33,7 +33,7 @@ function selector(args: Record<string, unknown>): string {
} }
// 窗口矩形/状态操作统一走驱动(任务 3.3) // 窗口矩形/状态操作统一走驱动(任务 3.3)
const driver = defaultEngine.driver; const getDriver = () => defaultEngine.driver;
register("System_GetEnv", (_ctx, node, args) => { register("System_GetEnv", (_ctx, node, args) => {
const name = required(args, ["name"], "环境变量名称", node.id, node.componentId); const name = required(args, ["name"], "环境变量名称", node.id, node.componentId);
@@ -104,14 +104,14 @@ register("Clipboard_ClearClipboard", async () => {
register("Window_GetWindowRect", async (_ctx, node, args) => { register("Window_GetWindowRect", async (_ctx, node, args) => {
const title = required({ title: selector(args) }, ["title"], "窗口标题", node.id, node.componentId); const title = required({ title: selector(args) }, ["title"], "窗口标题", node.id, node.componentId);
return driver.window.findByTitle(title); return getDriver().window.findByTitle(title);
}); });
register(["Window_SetWindowState", "Window_SetWindowState_v1"], async (_ctx, node, args) => { register(["Window_SetWindowState", "Window_SetWindowState_v1"], async (_ctx, node, args) => {
const title = required({ title: selector(args) }, ["title"], "窗口标题", node.id, node.componentId); const title = required({ title: selector(args) }, ["title"], "窗口标题", node.id, node.componentId);
const state = value(args, ["WindowType", "switchType"], "maximize").toLowerCase(); const state = value(args, ["WindowType", "switchType"], "maximize").toLowerCase();
const command = state.includes("min") ? 6 : state.includes("max") ? 3 : 9; const command = state.includes("min") ? 6 : state.includes("max") ? 3 : 9;
await driver.window.setState(title, command); await getDriver().window.setState(title, command);
return state; return state;
}); });
@@ -124,6 +124,6 @@ register("Window_SizeWindow_v1", async (_ctx, node, args) => {
} }
const [width, height] = Array.isArray(size) ? size.map(Number) : [0, 0]; const [width, height] = Array.isArray(size) ? size.map(Number) : [0, 0];
if (!(width > 0 && height > 0)) throw new RuntimeError("validation_error", "窗口尺寸无效", { nodeId: node.id, componentId: node.componentId }); if (!(width > 0 && height > 0)) throw new RuntimeError("validation_error", "窗口尺寸无效", { nodeId: node.id, componentId: node.componentId });
await driver.window.setSize(title, width, height); await getDriver().window.setSize(title, width, height);
return { width, height }; return { width, height };
}); });
@@ -4,7 +4,7 @@ import { getHostBridge } from "../engine/host-bridge";
import { defaultEngine } from "../engine/engine"; import { defaultEngine } from "../engine/engine";
// 桌面自动化驱动(任务 3.3):窗口矩形/鼠标注入原语统一走 WindowsDriver // 桌面自动化驱动(任务 3.3):窗口矩形/鼠标注入原语统一走 WindowsDriver
const driver = defaultEngine.driver; const getDriver = () => defaultEngine.driver;
function value(args: Record<string, unknown>, keys: string[], fallback = ""): string { function value(args: Record<string, unknown>, keys: string[], fallback = ""): string {
for (const key of keys) { for (const key of keys) {
@@ -77,17 +77,17 @@ function physicalToDip(x: number, y: number): [number, number] {
return [Math.round(x), Math.round(y)]; return [Math.round(x), Math.round(y)];
} }
const getWindow: CapabilityHandler = async (_ctx, node, args) => driver.window.findByTitle(requiredTitle(args, node.id, node.componentId)); const getWindow: CapabilityHandler = async (_ctx, node, args) => getDriver().window.findByTitle(requiredTitle(args, node.id, node.componentId));
register(["Window_Get", "Window_GetWindow", "Window_GetWindow_v1"], getWindow); register(["Window_Get", "Window_GetWindow", "Window_GetWindow_v1"], getWindow);
register("Window_ActiveWindow_v1", async (_ctx, node, args) => { register("Window_ActiveWindow_v1", async (_ctx, node, args) => {
const title = requiredTitle(args, node.id, node.componentId); const title = requiredTitle(args, node.id, node.componentId);
const maximize = [true, 1, "1", "true", "True"].includes(args.isMax as never); const maximize = [true, 1, "1", "true", "True"].includes(args.isMax as never);
return driver.window.activate(title, { maximize }); return getDriver().window.activate(title, { maximize });
}); });
register(["Window_GetWindowMessage", "Window_GetWindowMessage_v1"], async (_ctx, node, args) => { register(["Window_GetWindowMessage", "Window_GetWindowMessage_v1"], async (_ctx, node, args) => {
const object = await driver.window.findByTitle(requiredTitle(args, node.id, node.componentId)); const object = await getDriver().window.findByTitle(requiredTitle(args, node.id, node.componentId));
const type = value(args, ["switchType"], "all").toLowerCase(); const type = value(args, ["switchType"], "all").toLowerCase();
if (type === "title") return object.title; if (type === "title") return object.title;
if (type === "pid") return object.pid; if (type === "pid") return object.pid;
@@ -98,7 +98,7 @@ register(["Window_GetWindowMessage", "Window_GetWindowMessage_v1"], async (_ctx,
register(["Window_SetPos", "Window_MoveWindow", "Window_MoveWindow_v1", "Window_SizeWindow"], async (_ctx, node, args) => { register(["Window_SetPos", "Window_MoveWindow", "Window_MoveWindow_v1", "Window_SizeWindow"], async (_ctx, node, args) => {
const title = requiredTitle(args, node.id, node.componentId); const title = requiredTitle(args, node.id, node.componentId);
const current = await driver.window.findByTitle(title); const current = await getDriver().window.findByTitle(title);
const moveOnly = node.componentId.includes("MoveWindow"); const moveOnly = node.componentId.includes("MoveWindow");
const sizeOnly = node.componentId === "Window_SizeWindow"; const sizeOnly = node.componentId === "Window_SizeWindow";
const position = args.location !== undefined ? parsePoint(args.location, "窗口位置", node.id, node.componentId) : [Number(args.x ?? current.x), Number(args.y ?? current.y)] as [number, number]; const position = args.location !== undefined ? parsePoint(args.location, "窗口位置", node.id, node.componentId) : [Number(args.x ?? current.x), Number(args.y ?? current.y)] as [number, number];
@@ -109,7 +109,7 @@ register(["Window_SetPos", "Window_MoveWindow", "Window_MoveWindow_v1", "Window_
const width = moveOnly ? Number(current.width) : size[0]; const width = moveOnly ? Number(current.width) : size[0];
const height = moveOnly ? Number(current.height) : size[1]; const height = moveOnly ? Number(current.height) : size[1];
if (!(width > 0 && height > 0)) throw new RuntimeError("validation_error", "窗口尺寸必须大于 0", { nodeId: node.id, componentId: node.componentId }); if (!(width > 0 && height > 0)) throw new RuntimeError("validation_error", "窗口尺寸必须大于 0", { nodeId: node.id, componentId: node.componentId });
return driver.window.setPos(title, { x, y, width, height }); return getDriver().window.setPos(title, { x, y, width, height });
}); });
const moveMouse: CapabilityHandler = async (_ctx, node, args) => { const moveMouse: CapabilityHandler = async (_ctx, node, args) => {
@@ -122,7 +122,7 @@ const moveMouse: CapabilityHandler = async (_ctx, node, args) => {
let dipTarget: [number, number]; let dipTarget: [number, number];
if (relative) { if (relative) {
// 相对激活窗口左上角:窗口矩形是物理像素,换回 DIP 再叠加偏移,最后统一转物理 // 相对激活窗口左上角:窗口矩形是物理像素,换回 DIP 再叠加偏移,最后统一转物理
const { x: wx, y: wy } = await driver.window.foregroundPosition(); const { x: wx, y: wy } = await getDriver().window.foregroundPosition();
if (!Number.isFinite(wx) || !Number.isFinite(wy)) throw new RuntimeError("browser_error", "无法获取激活窗口位置", { nodeId: node.id, componentId: node.componentId }); if (!Number.isFinite(wx) || !Number.isFinite(wy)) throw new RuntimeError("browser_error", "无法获取激活窗口位置", { nodeId: node.id, componentId: node.componentId });
const [dipWx, dipWy] = physicalToDip(wx, wy); const [dipWx, dipWy] = physicalToDip(wx, wy);
dipTarget = [dipWx + rawX, dipWy + rawY]; dipTarget = [dipWx + rawX, dipWy + rawY];
@@ -131,10 +131,10 @@ const moveMouse: CapabilityHandler = async (_ctx, node, args) => {
} }
const [tx, ty] = dipToPhysical(dipTarget[0], dipTarget[1]); const [tx, ty] = dipToPhysical(dipTarget[0], dipTarget[1]);
if (moveType === "2") { if (moveType === "2") {
await driver.mouse.moveTo({ x: tx, y: ty }); await getDriver().mouse.moveTo({ x: tx, y: ty });
} else { } else {
// 平滑移动:从当前光标位置出发,按速度插值(路径全程物理像素,与目标同一坐标系) // 平滑移动:从当前光标位置出发,按速度插值(路径全程物理像素,与目标同一坐标系)
await driver.mouse.moveSmooth({ x: tx, y: ty }, { ease: moveType === "0", speed }); await getDriver().mouse.moveSmooth({ x: tx, y: ty }, { ease: moveType === "0", speed });
} }
return { x: Math.round(dipTarget[0]), y: Math.round(dipTarget[1]) }; return { x: Math.round(dipTarget[0]), y: Math.round(dipTarget[1]) };
}; };
@@ -147,7 +147,7 @@ const dragMouse: CapabilityHandler = async (_ctx, node, args) => {
const [epx, epy] = dipToPhysical(ex, ey); const [epx, epy] = dipToPhysical(ex, ey);
const right = ["1", "right"].includes(value(args, ["side"], "0").toLowerCase()); const right = ["1", "right"].includes(value(args, ["side"], "0").toLowerCase());
// 旧实现按下后停 80ms、拖到终点立即松开(与桌面版 Window_Mouse 不同,松开前不停顿) // 旧实现按下后停 80ms、拖到终点立即松开(与桌面版 Window_Mouse 不同,松开前不停顿)
await driver.mouse.drag({ x: spx, y: spy }, { x: epx, y: epy }, { button: right ? "right" : "left", holdMs: 80, releaseDelayMs: 0 }); await getDriver().mouse.drag({ x: spx, y: spy }, { x: epx, y: epy }, { button: right ? "right" : "left", holdMs: 80, releaseDelayMs: 0 });
return { start: [Math.round(sx), Math.round(sy)], end: [Math.round(ex), Math.round(ey)] }; return { start: [Math.round(sx), Math.round(sy)], end: [Math.round(ex), Math.round(ey)] };
}; };
register(["Mouse_DragToV1", "Mouse_DragToV2", "Mouse_DragToNew"], dragMouse); register(["Mouse_DragToV1", "Mouse_DragToV2", "Mouse_DragToNew"], dragMouse);
@@ -159,6 +159,6 @@ register(["Mouse_ClickCombine", "Mouse_ClickCombine_v1", "Mouse_ClickCombine_v2"
const right = value(args, ["click_key"], "left").toLowerCase() === "right"; const right = value(args, ["click_key"], "left").toLowerCase() === "right";
const doubleClick = value(args, ["click_time"], "click").toLowerCase().includes("double"); const doubleClick = value(args, ["click_time"], "click").toLowerCase().includes("double");
// 旧实现每次 down-up 之后停 80ms // 旧实现每次 down-up 之后停 80ms
await driver.mouse.click({ x: px, y: py }, { button: right ? "right" : "left", double: doubleClick, settleMs: 80 }); await getDriver().mouse.click({ x: px, y: py }, { button: right ? "right" : "left", double: doubleClick, settleMs: 80 });
return { x: Math.round(x), y: Math.round(y), button: right ? "right" : "left", clicks: doubleClick ? 2 : 1 }; return { x: Math.round(x), y: Math.round(y), button: right ? "right" : "left", clicks: doubleClick ? 2 : 1 };
}); });
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { AutomationDriver } from "../driver/types";
import { createEngine } from "./engine"; import { createEngine } from "./engine";
import { WindowsDriver } from "../driver/windows/windows-driver"; import { WindowsDriver } from "../driver/windows/windows-driver";
@@ -20,3 +21,50 @@ describe("driver 平台自动选择(迁移指南五件小事 #2)", () => {
} }
}); });
}); });
describe("createEngine 注入(迁移阶段1-A1)", () => {
const fakeDriver = (): AutomationDriver =>
({
mouse: {},
keyboard: {},
window: {},
screen: { size: async () => ({ x: 0, y: 0, width: 800, height: 600 }) },
element: {}
}) as unknown as AutomationDriver;
it("createEngine 可注入自定义 driver", () => {
const driver = fakeDriver();
const engine = createEngine({ driver });
expect(engine.driver).toBe(driver);
});
it("注入后不触发平台自动选择(darwin 上也可用)", () => {
const driver = fakeDriver();
const engine = createEngine({ driver });
const original = process.platform;
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
try {
expect(engine.driver).toBe(driver);
expect(engine.driver.screen.size).toBeTruthy();
} finally {
Object.defineProperty(process, "platform", { value: original, configurable: true });
}
});
it("两个引擎注入不同 driver 互不影响", () => {
const driverA = fakeDriver();
const driverB = fakeDriver();
const engineA = createEngine({ driver: driverA });
const engineB = createEngine({ driver: driverB });
expect(engineA.driver).toBe(driverA);
expect(engineB.driver).toBe(driverB);
expect(engineA.driver).not.toBe(engineB.driver);
});
it("setDriver 可后置替换", () => {
const engine = createEngine();
const driver = fakeDriver();
engine.setDriver(driver);
expect(engine.driver).toBe(driver);
});
});
+13 -2
View File
@@ -59,10 +59,21 @@ export class RpaEngine {
} }
return this._driver; return this._driver;
} }
/** 注入驱动实现(测试替身或宿主适配器);注入后不再走平台自动选择 */
setDriver(driver: AutomationDriver): void {
this._driver = driver;
}
} }
export function createEngine(): RpaEngine { export interface EngineOptions {
return new RpaEngine(); /** 注入桌面自动化驱动(测试替身或宿主适配器);缺省按 process.platform 自动选择 */
driver?: AutomationDriver;
}
export function createEngine(options?: EngineOptions): RpaEngine {
const engine = new RpaEngine();
if (options?.driver) engine.setDriver(options.driver);
return engine;
} }
/** 能力模块内部使用的默认引擎实例(与全局注册表配套的进程级单例)。 */ /** 能力模块内部使用的默认引擎实例(与全局注册表配套的进程级单例)。 */
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { CatalogComponent, FlowDefinition, FlowNode } from "../../types"; import type { CatalogComponent, FlowDefinition, FlowNode } from "../types";
import { validateFlowGraph, hasBlockingIssues } from "./graph-validator"; import { validateFlowGraph, hasBlockingIssues } from "./graph-validator";
import { register } from "./registry"; import { register } from "./registry";
import { RuntimeError } from "./types"; import { RuntimeError } from "./types";
@@ -14,8 +14,9 @@ function makeFlow(partial: Partial<FlowDefinition> & { nodes: FlowNode[] }): Flo
name: "测试流程", name: "测试流程",
variables: [], variables: [],
nodes: partial.nodes, nodes: partial.nodes,
edges: partial.edges || [] edges: partial.edges || [],
} as FlowDefinition; meta: { createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", appVersion: "test" }
};
} }
function node(id: string, componentId: string, overrides: Partial<FlowNode> = {}): FlowNode { function node(id: string, componentId: string, overrides: Partial<FlowNode> = {}): FlowNode {
@@ -29,19 +30,20 @@ function node(id: string, componentId: string, overrides: Partial<FlowNode> = {}
}; };
} }
const fakeComponent = (id: string): CatalogComponent => ({ const fakeComponent = (id: string): CatalogComponent =>
id, ({
name: id, id,
version: "1.0.0", name: id,
tags: [], version: "1.0.0",
platformTag: null, tags: [],
icon: "", platformTag: null,
risk: "low", icon: "",
fields: [], risk: "low",
output: { type: "any", description: "" }, fields: [],
slots: [], output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] },
functionCalls: [] slots: [],
}); functionCalls: []
}) as unknown as CatalogComponent;
const baseOptions = (signal: AbortSignal): RunFlowOptions => ({ const baseOptions = (signal: AbortSignal): RunFlowOptions => ({
runId: "run_test", runId: "run_test",