阶段1-rpa-core建包: engine+31个纯Node能力迁入packages/rpa-core, 领域类型单一源化, createEngine工厂收拢模块级状态

- engine/(registry/types/runtime/expression)物理迁入包, 解耦shared/types(域类型归包, 宿主type-only re-export)
- 31个纯Node能力+27个测试迁入包; 宿主留守Electron/浏览器/AI依赖能力, index.ts经@rpa/core/capabilities拉起
- 新增createEngine()/defaultEngine: excel缓存/Excel句柄/db连接/FTP客户端/OCR实例/拟真设置挂引擎实例
- remaining-core子流程依赖改为host-bridge注入, 包内不再import宿主flows
- 宿主main/index.ts启动时注入子流程源; electron-vite/vitest/tsconfig配@rpa/core源码别名
- 验证: typecheck绿, 快照909个id diff为零, 332测试过(2个桌面冒烟因会话无交互SetForegroundWindow被拒, 环境阻塞)
This commit is contained in:
yu 2026-11-04 12:36:03 +08:00 committed by trae-solo-agent
parent 4139400dbd
commit caa8e19332
111 changed files with 581 additions and 337 deletions

View File

@ -6,9 +6,12 @@ export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
"@shared": resolve("src/shared")
}
alias: [
{ find: "@shared", replacement: resolve("src/shared") },
// rpa-core 包以源码形式打进主进程产物(不作为外部依赖运行时 require)
{ find: /^@rpa\/core$/, replacement: resolve("packages/rpa-core/src/index.ts") },
{ find: /^@rpa\/core\/capabilities$/, replacement: resolve("packages/rpa-core/src/capabilities/index.ts") }
]
},
build: {
rollupOptions: {

View File

@ -4,6 +4,9 @@
"private": true,
"description": "Canvas-based RPA desktop application",
"main": "./out/main/index.js",
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "electron-vite dev",
"dev:local": "node scripts/dev-safe.mjs",

View File

@ -0,0 +1,40 @@
{
"name": "@rpa/core",
"version": "0.1.0",
"private": true,
"description": "RPA 能力层:零 Electron 依赖的流程引擎、能力注册表与纯 Node 能力",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./capabilities": "./src/capabilities/index.ts"
},
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@gutenye/ocr-node": "^1.4.9",
"adm-zip": "^0.5.16",
"basic-ftp": "^6.2.3",
"exceljs": "^4.4.0",
"imapflow": "^2.10.0",
"mailparser": "^3.14.1",
"mssql": "^12.7.13",
"mysql2": "^3.24.6",
"nodemailer": "^6.9.16",
"pdf-lib": "^1.17.1",
"pg": "^8.23.1",
"pngjs": "^7.0.0",
"sm-crypto": "^0.5.8"
},
"devDependencies": {
"@types/adm-zip": "^0.5.7",
"@types/mssql": "^12.3.0",
"@types/nodemailer": "^6.4.17",
"@types/pg": "^8.23.1",
"@types/pngjs": "^6.0.5",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
}
}

View File

@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { getHandler } from "../engine/registry";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import "./data";
import "./desktop";

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { getHandler } from "../engine/registry";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import "./desktop";
import "./basic-encryption";

View File

@ -7,7 +7,7 @@ import { PDFDocument } from "pdf-lib";
import { PNG } from "pngjs";
import { getHandler } from "../engine/registry";
import type { RunContext } from "../engine/types";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import "./desktop";
import "./office";
import "./pdf";

View File

@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { Catalog, CatalogComponent, FlowNode } from "../../shared/types";
import type { Catalog, CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./data-extended";

View File

@ -4,7 +4,7 @@ import path from "node:path";
import { afterAll, describe, expect, it } from "vitest";
import { capabilityIds, getHandler } from "../engine/registry";
import { RuntimeError } from "../engine/types";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import "./data";
import "./data-extended";

View File

@ -2,7 +2,7 @@ import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./data-table-common";

View File

@ -2,21 +2,12 @@ import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { register } from "../engine/registry";
import { RuntimeError, type CapabilityHandler } from "../engine/types";
import { type ManualOperationSettings, defaultEngine } from "../engine/engine";
const execFileAsync = promisify(execFile);
interface ManualOperationSettings {
enabled: boolean;
randomMove: boolean;
randomClick: boolean;
randomSleep: boolean;
minMs: number;
maxMs: number;
}
const manualOperation: ManualOperationSettings = {
enabled: false, randomMove: true, randomClick: true, randomSleep: true, minMs: 1000, maxMs: 3000
};
// 人为拟真设置本体挂在引擎实例上(任务 1.3),此处委托 defaultEngine。
const manualOperation: ManualOperationSettings = defaultEngine.manualOperation;
function jitter(value: number, amount = 3): number {
return Math.max(0, Math.round(value + (Math.random() * amount * 2 - amount)));

View File

@ -4,7 +4,7 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import ExcelJS from "exceljs";
import { getHandler } from "../engine/registry";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import "./excel-com";

View File

@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import ExcelJS from "exceljs";
import { afterEach, describe, expect, it } from "vitest";
import type { Catalog, CatalogComponent, FlowNode } from "../../shared/types";
import type { Catalog, CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./index";

View File

@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import ExcelJS from "exceljs";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./office";

View File

@ -1,10 +1,11 @@
import ExcelJS from "exceljs";
import path from "node:path";
import { defaultEngine } from "../engine/engine";
// Excel 工作簿共享缓存:同一文件路径在整条流程中复用同一个 ExcelJS 实例。
// 此前 office.ts / office-file-extended.ts / excel-common.ts / extended.ts 各自持有
// 工作簿实例并整簿写回磁盘,后保存者会把先保存者的修改整体覆盖掉。
const cache = new Map<string, ExcelJS.Workbook>();
// 缓存本体挂在引擎实例上(任务 1.3),此处委托 defaultEngine。
function keyOf(filePath: string): string {
return path.resolve(filePath);
@ -13,22 +14,22 @@ function keyOf(filePath: string): string {
/** 按路径取工作簿:命中缓存直接复用,未命中从磁盘读取后登记。 */
export async function acquireWorkbook(filePath: string): Promise<ExcelJS.Workbook> {
const key = keyOf(filePath);
let workbook = cache.get(key);
let workbook = defaultEngine.excelCache.get(key);
if (!workbook) {
workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(key);
cache.set(key, workbook);
defaultEngine.excelCache.set(key, workbook);
}
return workbook;
}
/** 把一个新建/另建的工作簿实例登记为该路径的共享实例。 */
export function publishWorkbook(filePath: string, workbook: ExcelJS.Workbook): ExcelJS.Workbook {
cache.set(keyOf(filePath), workbook);
defaultEngine.excelCache.set(keyOf(filePath), workbook);
return workbook;
}
/** 释放路径对应的共享实例(关闭工作簿时调用)。 */
export function releaseWorkbook(filePath: string): void {
cache.delete(keyOf(filePath));
defaultEngine.excelCache.delete(keyOf(filePath));
}

View File

@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import ExcelJS from "exceljs";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./extended";

View File

@ -3,7 +3,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./files";

View File

@ -3,11 +3,10 @@ import path from "node:path";
import fs from "node:fs";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
const flags = globalThis as { __rpaFtpClient?: Client };
import { defaultEngine } from "../engine/engine";
async function requireClient(nodeId: string): Promise<Client> {
if (flags.__rpaFtpClient) return flags.__rpaFtpClient;
if (defaultEngine.ftpClient) return defaultEngine.ftpClient;
throw new RuntimeError("validation_error", "尚未连接 FTP,请先使用 FTP_Connect", { nodeId, componentId: nodeId, recommendation: "先运行 FTP_Connect 建立连接" });
}
@ -27,14 +26,14 @@ register("FTP_Connect", async (ctx, node, args) => {
const secure = String(args.ftp_type ?? "").toLowerCase().includes("ftps");
const client = new Client(10_000);
await client.access({ host, port, secure, user, password });
flags.__rpaFtpClient = client;
defaultEngine.ftpClient = client;
ctx.log("已连接 FTP:" + host + ":" + port, "info", node.id);
return { connected: true, host, port };
});
register("FTP_Disconnect", async (ctx, node) => {
flags.__rpaFtpClient?.close();
flags.__rpaFtpClient = undefined;
defaultEngine.ftpClient?.close();
defaultEngine.ftpClient = undefined;
ctx.log("已断开 FTP 连接", "info", node.id);
return { disconnected: true };
});

View File

@ -0,0 +1,39 @@
/**
* rpa-core 纯 Node 能力注册入口。
*
* import 本模块即完成全部纯 Node 能力的副作用注册(与改造前
* src/main/capabilities/index.ts 的加载顺序保持一致,别名/重复 id 的
* last-wins 语义不变)。依赖 Electron 或宿主模块的能力(human-dialog /
* window-extended / ai-service / ai-caps / browser* / web-batch2 / web-crawl)
* 留在宿主 src/main/capabilities,阶段 2 通过 HostBridge 迁入。
*/
import "./control";
import "./files";
import "./http";
import "./data";
import "./office";
import "./desktop";
import "./desktop-uia";
import "./time-caps";
import "./windows-system";
import "./ftp";
import "./ocr-caps";
import "./pdf";
import "./word";
import "./word-extended";
import "./message";
import "./vision";
import "./cv-vision";
import "./extended";
import "./system-extended";
import "./office-file-extended";
import "./office-aliases";
import "./excel-common";
import "./excel-com";
import "./data-extended";
import "./utility-extended";
import "./data-table-common";
import "./basic-encryption";
import "./mail-remaining";
import "./remaining-core";
import "./common-final";

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import { getHandler } from "../engine/registry";
import type { RunContext } from "../engine/types";
import "./desktop";

View File

@ -1,6 +1,6 @@
import net from "node:net";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler, hasCapability } from "../engine/registry";
import "./message";

View File

@ -2,17 +2,16 @@ import path from "node:path";
import { promises as fs } from "node:fs";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
import { defaultEngine } from "../engine/engine";
interface OcrWord { text: string; x: number; y: number; width: number; height: number; confidence: number }
interface RapidOcrLine { text: string; mean: number; box?: number[][] }
let ocrInstance: unknown = null;
async function getOcr(): Promise<{ detect: (image: string) => Promise<{ texts: RapidOcrLine[] }> }> {
if (ocrInstance) return ocrInstance as { detect: (image: string) => Promise<{ texts: RapidOcrLine[] }> };
if (defaultEngine.ocrInstance) return defaultEngine.ocrInstance as { detect: (image: string) => Promise<{ texts: RapidOcrLine[] }> };
const mod = await import("@gutenye/ocr-node");
const instance = await mod.default.create();
ocrInstance = instance;
defaultEngine.ocrInstance = instance;
return instance;
}

View File

@ -1,7 +1,7 @@
import { existsSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { getHandler } from "../engine/registry";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import "./ocr-caps";
import "./remaining-core";

View File

@ -2,9 +2,8 @@ import { readFile, mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import ExcelJS from "exceljs";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import type { Catalog, CatalogComponent, FlowNode } from "../../shared/types";
import type { Catalog, CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./office";
@ -21,7 +20,8 @@ afterEach(async () => Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursiv
describe("common Excel aliases", () => {
it("registers only real catalog aliases", async () => {
const catalog = JSON.parse(await readFile(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../resources/catalog.json"), "utf8")) as Catalog;
// vitest 工作目录为仓库根,包内测试统一以 cwd 定位仓库资源
const catalog = JSON.parse(await readFile(path.resolve(process.cwd(), "resources/catalog.json"), "utf8")) as Catalog;
const ids = new Set(catalog.components.map((item) => item.id));
expect(EXCEL_COMMON_ALIASES.length).toBeGreaterThanOrEqual(25);
for (const id of EXCEL_COMMON_ALIASES) { expect(ids.has(id), id).toBe(true); expect(getHandler(id), id).toBeTruthy(); }

View File

@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path";
import { PDFDocument } from "pdf-lib";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import { getHandler } from "../engine/registry";
import type { RunContext } from "../engine/types";
import "./office-file-extended";

View File

@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
import path from "node:path";
import { register } from "../engine/registry";
import { RuntimeError, type CapabilityHandler } from "../engine/types";
import { defaultEngine } from "../engine/engine";
import { acquireWorkbook, publishWorkbook, releaseWorkbook } from "./excel-store";
interface ExcelHandle {
@ -10,7 +11,8 @@ interface ExcelHandle {
path: string;
}
const workbooks = new Map<string, ExcelJS.Workbook>();
// 句柄 id → 工作簿;本体挂在引擎实例上(任务 1.3),此处委托 defaultEngine。
const workbooks = defaultEngine.excelHandles;
function value(args: Record<string, unknown>, keys: string[], fallback = ""): unknown {
for (const key of keys) {

View File

@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
import { PDFDocument, StandardFonts } from "pdf-lib";
import { PNG } from "pngjs";
import { getHandler } from "../engine/registry";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import "./pdf";

View File

@ -1,7 +1,7 @@
import { existsSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { getHandler } from "../engine/registry";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import "./vision";
import "./remaining-core";

View File

@ -3,6 +3,8 @@ import path from "node:path";
import { getHandler, register } from "../engine/registry";
import { runFlow } from "../engine/runtime";
import { RuntimeError, type CapabilityHandler, type RunContext } from "../engine/types";
import { defaultEngine } from "../engine/engine";
import { getSubflowSource } from "../engine/host-bridge";
import { powershell, psString } from "./desktop";
import { findImage } from "./vision";
@ -24,7 +26,8 @@ function required(value: string, label: string, node: { id: string; componentId:
return value;
}
const dbConnections = new Map<string, any>();
// 连接 id → 数据库连接;本体挂在引擎实例上(任务 1.3)。
const dbConnections = defaultEngine.dbConnections as Map<string, any>;
function dbConfig(args: Record<string, unknown>): Record<string, unknown> {
const inline = record(args.DatabaseConfig || args.config || args.connectionID);
const merged = { ...inline, ...args };
@ -67,12 +70,14 @@ register("Database_CloseDBConnect", async (_ctx, _node, args) => {
dbConnections.delete(key); return { closed: true };
});
async function callSubflow(ctx: RunContext, node: { id: string; componentId: string }, args: Record<string, unknown>) {
const { listFlows, getFlow } = await import("../flows");
// 子流程仓库属宿主职责:经 host-bridge 注入(阶段 2 正式化),包内不 import "../flows"
const source = getSubflowSource();
if (!source) throw new RuntimeError("validation_error", "子流程源未注入", { nodeId: node.id, componentId: node.componentId, recommendation: "宿主需在启动时调用 setSubflowSource()" });
const wanted = text(args, ["module_name", "module", "flow"]);
const rows = await listFlows(); const found = rows.find((row) => row.id === wanted || row.name === wanted);
const rows = await source.listFlows(); const found = rows.find((row) => row.id === wanted || row.name === wanted);
if (!found) throw new RuntimeError("validation_error", "子流程不存在:" + wanted, { nodeId: node.id, componentId: node.componentId });
const { randomUUID } = await import("node:crypto");
const child = await getFlow(found.id); const inputs = record(args.args || args.input);
const child = await source.getFlow(found.id); const inputs = record(args.args || args.input);
return await runFlow(child, { runId: randomUUID(), evidenceDir: ctx.evidenceDir, signal: ctx.signal, emit: ctx.emit, log: ctx.log, componentOf: ctx.componentOf, browser: ctx.browser, variables: { ...ctx.vars, ...inputs } });
}
register("child_modules_v1", async (ctx, node, args) => callSubflow(ctx, node, args));

View File

@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./system-extended";

View File

@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { Catalog, CatalogComponent, FlowDefinition, FlowNode } from "../../shared/types";
import type { Catalog, CatalogComponent, FlowDefinition, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./utility-extended";

View File

@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { CatalogComponent, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./word";

View File

@ -0,0 +1,49 @@
import type { Client } from "basic-ftp";
import type { Workbook as ExcelJSWorkbook } from "exceljs";
/** 人为拟真操作的开关与随机参数(desktop 能力的进程级设置)。 */
export interface ManualOperationSettings {
enabled: boolean;
randomMove: boolean;
randomClick: boolean;
randomSleep: boolean;
minMs: number;
maxMs: number;
}
/**
* 引擎实例状态容器(任务 1.3,R4:模块级全局状态收拢)。
*
* 折中方案(决策记录 2026-11-04):能力文件的 register() 调用与全局注册表保持不变,
* 但 Excel 缓存、数据库连接、FTP 客户端等进程级可变状态统一挂到引擎实例上。
* 包内各能力模块通过 defaultEngine 访问;宿主/CLI 可用 createEngine() 产生隔离实例,
* 或拿到 defaultEngine 做测试间重置。
*/
export class RpaEngine {
/** excel-store:路径 → ExcelJS 工作簿共享缓存 */
readonly excelCache = new Map<string, ExcelJSWorkbook>();
/** office.ts:Excel 句柄 id → 工作簿 */
readonly excelHandles = new Map<string, ExcelJSWorkbook>();
/** remaining-core:连接 id → 数据库连接(mysql2/pg/mssql) */
readonly dbConnections = new Map<string, unknown>();
/** ftp.ts:当前 FTP 连接 */
ftpClient?: Client;
/** ocr-caps.ts:懒加载的 OCR 实例 */
ocrInstance?: unknown;
/** desktop.ts:人为拟真操作设置 */
manualOperation: ManualOperationSettings = {
enabled: false,
randomMove: true,
randomClick: true,
randomSleep: true,
minMs: 1000,
maxMs: 3000
};
}
export function createEngine(): RpaEngine {
return new RpaEngine();
}
/** 能力模块内部使用的默认引擎实例(与全局注册表配套的进程级单例)。 */
export const defaultEngine = createEngine();

View File

@ -0,0 +1,22 @@
import type { FlowDefinition } from "../types";
/**
* 宿主桥(HostBridge 候选,阶段 2 正式化接口)。
*
* 包内个别能力需要宿主数据(目前仅子流程仓库),通过显式 setter 注入,
* 包本身不得 import 宿主模块。宿主启动时调用 setSubflowSource()。
*/
export interface SubflowSource {
listFlows(): Promise<Array<{ id: string; name: string }>>;
getFlow(id: string): Promise<FlowDefinition>;
}
let subflowSource: SubflowSource | undefined;
export function setSubflowSource(source: SubflowSource): void {
subflowSource = source;
}
export function getSubflowSource(): SubflowSource | undefined {
return subflowSource;
}

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import type { CatalogComponent, FlowDefinition } from "../../shared/types";
import type { CatalogComponent, FlowDefinition } from "../types";
import { register } from "./registry";
import { runFlow } from "./runtime";

View File

@ -10,7 +10,7 @@ import type {
FlowEdge,
FlowNode,
RunEvent
} from "../../shared/types";
} from "../types";
import "../capabilities";
import { register } from "./registry";
import { runFlow } from "./runtime";

View File

@ -1,4 +1,4 @@
import type { FlowDefinition, FlowNode, RunError } from "../../shared/types";
import type { FlowDefinition, FlowNode, RunError } from "../types";
import { evaluate, evaluateCondition, interpolate, toNumber } from "./expression";
import { getHandler } from "./registry";
import {

View File

@ -1,4 +1,4 @@
import type { CatalogComponent, FlowDefinition, FlowNode, RunError, RunEvent } from "../../shared/types";
import type { CatalogComponent, FlowDefinition, FlowNode, RunError, RunEvent } from "../types";
export interface BrowserLike {
navigate(url: string): Promise<{ url: string; title: string }>;

View File

@ -0,0 +1,43 @@
/**
* @rpa/core 包入口。
*
* 只导出引擎 API 与类型,不做能力注册副作用;
* 纯 Node 能力通过 `@rpa/core/capabilities` 子路径按需拉起(保持与 demo 宿主
* src/main/capabilities/index.ts 相同的副作用注册模型,见任务 4.2 再显式化)。
*/
export { createEngine, defaultEngine, RpaEngine, type ManualOperationSettings } from "./engine/engine";
export { register, getHandler, hasCapability, capabilityIds, capabilityCount } from "./engine/registry";
export { runFlow } from "./engine/runtime";
export { RuntimeError, BreakSignal, ContinueSignal, StopSignal, asRuntimeError } from "./engine/types";
export { setSubflowSource, getSubflowSource, type SubflowSource } from "./engine/host-bridge";
// PowerShell 助手(阶段 3 将移入 driver/windows/):宿主留守能力(window-extended 等)经此引用
export { powershell, psString } from "./capabilities/desktop";
export type {
BrowserLike,
BrowserTabInfo,
BrowserElementReference,
BrowserElementInfo,
BrowserSelectOption,
BrowserSelectState,
BrowserSimilarElementOptions,
BrowserElementRegion,
RunContext,
CapabilityHandler,
NodeState,
RunResult
} from "./engine/types";
export type {
CatalogWidget,
CatalogField,
CatalogOutput,
CatalogComponent,
CatalogGroup,
Catalog,
FlowVariable,
FlowNodeOptions,
FlowNode,
FlowEdge,
FlowDefinition,
RunError,
RunEvent
} from "./types";

View File

@ -0,0 +1,187 @@
/**
* rpa-core 领域类型(任务 1.2,类型单一源)
*
* 从 src/shared/types 拆出:与流程/目录/运行相关、引擎必须依赖的模型归包所有。
* 宿主专属类型(AppInfo / AppSettings / FlowSummary / VisionLab* 等)留在 src/shared/types,
* 该文件通过 type-only re-export 引用本文件,保证全仓不出现两份定义。
*/
export type CatalogWidget =
| "text"
| "textarea"
| "number"
| "select"
| "radio"
| "boolean"
| "file"
| "folder"
| "element"
| "code"
| "secret"
| "region"
| "hidden";
export interface CatalogField {
key: string;
label: string;
widget: CatalogWidget;
type: "string" | "number" | "boolean" | "object";
displayType: string;
required: boolean;
defaultValue: string;
placeholder: string;
description: string;
advanced: boolean;
valueTypes: string[];
/** 下拉/单选选项:字符串(显示=存储值)或 { label, value }(显示与存储分离) */
options?: Array<string | { label: string; value: string }>;
unit?: string;
}
export interface CatalogOutput {
label: string;
key: string;
suggestedName: string;
valueTypes: string[];
fields: Array<{ key: string; label: string; defaultValue: string; valueTypes: string[] }>;
}
export interface CatalogComponent {
id: string;
name: string;
kind: "vendor" | "native" | "trigger";
group: string;
categories: string[];
categorySignature: string;
description: string;
messageTemplate: string;
baseType: string;
functionName: string;
functionCalls: Array<{ function: string; args: string[]; assignTo: string }>;
version: string;
tags: string[];
platformTag: string | null;
icon: string;
risk: "low" | "high";
fields: CatalogField[];
output: CatalogOutput;
slots: string[];
nodeKind?: string;
/** 同名旧版本 / 重复别名指向的保留能力 ID。 */
duplicateOf?: string;
}
export interface CatalogGroup {
id: string;
label: string;
icon: string;
count: number;
}
export interface Catalog {
schemaVersion: number;
generatedAt: string;
counts: {
total: number;
vendor: number;
native: number;
triggers: number;
fields: number;
};
groups: CatalogGroup[];
components: CatalogComponent[];
}
export interface FlowVariable {
name: string;
type: string;
defaultValue?: string;
description?: string;
scope?: "flow" | "local";
}
export interface FlowNodeOptions {
outputVar?: string;
delayBeforeMs?: number;
delayAfterMs?: number;
timeoutMs?: number;
retryCount?: number;
retryIntervalMs?: number;
skipOnError?: boolean;
logException?: boolean;
errorRouteEnabled?: boolean;
}
export interface FlowNode {
id: string;
componentId: string;
label?: string;
position: { x: number; y: number };
values: Record<string, unknown>;
options: FlowNodeOptions;
parentId?: string | null;
slot?: string;
enabled?: boolean;
width?: number;
height?: number;
}
export interface FlowEdge {
id: string;
source: string;
target: string;
sourceHandle?: string | null;
targetHandle?: string | null;
label?: string;
}
export interface FlowDefinition {
schemaVersion: 1;
id: string;
name: string;
description?: string;
enabled?: boolean;
trigger?: {
type: "manual" | "schedule" | "file";
label: string;
};
variables: FlowVariable[];
nodes: FlowNode[];
edges: FlowEdge[];
meta: {
createdAt: string;
updatedAt: string;
appVersion: string;
};
}
export interface RunError {
code: string;
reason: string;
recommendation?: string;
location?: { flowId?: string; nodeId?: string; componentId?: string; arg?: string };
detail?: string;
}
export interface RunEvent {
type:
| "run.started"
| "node.started"
| "node.log"
| "node.finished"
| "run.finished"
| "run.total";
runId: string;
nodeId?: string;
level?: "info" | "warn" | "error";
message?: string;
status?: string;
output?: unknown;
error?: RunError;
durationMs?: number;
evidence?: string[];
attempt?: number;
startedAt?: string;
nodeLabel?: string;
total?: number;
}

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}

View File

@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// 与根配置一致:桌面自动化冒烟测试共用同一物理桌面,串行执行防焦点竞争。
fileParallelism: false
}
});

View File

@ -1,5 +1,5 @@
{
"generatedAt": "2026-11-04T03:53:23.689Z",
"generatedAt": "2026-11-04T04:34:06.782Z",
"count": 909,
"ids": [
"App_GetPid",

View File

@ -5,8 +5,8 @@ import { afterEach, describe, expect, it } from "vitest";
import { buildSuite, bundleRoot } from "./app-system-test-flows.mjs";
import { installBundle, upgradeBundle, makeReport, caseResults } from "./app-system-tests.mjs";
import { applyCatalogOverrides } from "./catalog-overrides.mjs";
import { hasCapability } from "../src/main/capabilities/index.ts";
import { runFlow } from "../src/main/engine/runtime.ts";
import { hasCapability, runFlow } from "@rpa/core";
import "../src/main/capabilities/index.ts";
import { validateFlow } from "../src/renderer/src/validation/flowValidation.ts";
const containers = new Set(["logic_control_try_catch", "logic_control_if_else"]);
@ -58,7 +58,7 @@ describe("应用与系统测试流程包", () => {
const options = component.fields.find(field => field.key === "into_type").options;
expect(options.length).toBeGreaterThanOrEqual(4);
for (const option of options) {
const output = await import("../src/main/engine/registry.ts").then(({ getHandler }) => getHandler(id)({}, { id: "type-test", componentId: id }, { val: 42, into_type: option.value }, component));
const output = await import("@rpa/core").then(({ getHandler }) => getHandler(id)({}, { id: "type-test", componentId: id }, { val: 42, into_type: option.value }, component));
if (id === "Basic_JudgeDataType") expect(typeof output).toBe("boolean");
}
}

View File

@ -2,7 +2,7 @@ import { execFile, spawn, type ChildProcess } from "node:child_process";
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { RuntimeError } from "../engine/types";
import { RuntimeError } from "@rpa/core";
import { ExternalTransport } from "./transport";
/**

View File

@ -30,7 +30,7 @@ import {
type BrowserSelectState,
type BrowserSimilarElementOptions,
type BrowserTabInfo
} from "../engine/types";
} from "@rpa/core";
function assertUrl(raw: string): URL {
let url: URL;

View File

@ -1,6 +1,6 @@
import type { BrowserWindow } from "electron";
import WebSocket from "ws";
import { RuntimeError } from "../engine/types";
import { RuntimeError } from "@rpa/core";
/**
* CDP 传输层抽象。

View File

@ -1,5 +1,5 @@
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError } from "@rpa/core";
import { chatCompletion, requireAiConfig } from "./ai-service";
function inputText(args: Record<string, unknown>, nodeId: string): string {

View File

@ -1,5 +1,5 @@
import { loadSettings } from "../settings";
import { RuntimeError } from "../engine/types";
import { RuntimeError } from "@rpa/core";
export type ChatContentPart =
| { type: "text"; text: string }

View File

@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import { getHandler } from "../engine/registry";
import { getHandler } from "@rpa/core";
import {
RuntimeError,
type BrowserElementInfo,
@ -10,7 +10,7 @@ import {
type BrowserLike,
type BrowserSelectState,
type RunContext
} from "../engine/types";
} from "@rpa/core";
import { BROWSER_COMMON_IDS } from "./browser-common";
const component = {} as CatalogComponent;

View File

@ -1,6 +1,6 @@
import { browserSession } from "../browser/session";
import { register } from "../engine/registry";
import { RuntimeError, type BrowserElementInfo, type BrowserLike, type RunContext } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError, type BrowserElementInfo, type BrowserLike, type RunContext } from "@rpa/core";
const GET_ELEMENT_IDS = ["Element_GetElement", "Element_GetElementV1", "Element_GetElementV2", "Element_GetElementNew"];
const GET_INFO_IDS = ["Element_GetElementInfo", "Element_GetElementInfo_V1"];

View File

@ -2,8 +2,8 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { BrowserLike, RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import type { BrowserLike, RunContext } from "@rpa/core";
import { getHandler } from "@rpa/core";
import { BROWSER_CONTROL_IDS } from "./browser-control";
const component = {} as CatalogComponent; const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} });

View File

@ -1,8 +1,8 @@
import path from "node:path";
import { promises as fs } from "node:fs";
import { browserSession } from "../browser/session";
import { register } from "../engine/registry";
import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError, type BrowserLike, type RunContext } from "@rpa/core";
async function session(ctx: RunContext): Promise<BrowserLike> { if (!ctx.browser) ctx.browser = await browserSession(); return ctx.browser; }
function selector(args: Record<string, unknown>): unknown { const value = args.uiSelector ?? args.download_selector ?? args.selector; if (value === undefined || value === null || value === "") throw new RuntimeError("validation_error", "元素选择器为空"); return value; }

View File

@ -2,8 +2,8 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import { getHandler } from "../engine/registry";
import { RuntimeError, type BrowserElementRegion, type BrowserLike, type RunContext } from "../engine/types";
import { getHandler } from "@rpa/core";
import { RuntimeError, type BrowserElementRegion, type BrowserLike, type RunContext } from "@rpa/core";
import { BROWSER_DOM_IDS } from "./browser-dom";
const component = {} as CatalogComponent;

View File

@ -1,7 +1,7 @@
import path from "node:path";
import { browserSession } from "../browser/session";
import { register } from "../engine/registry";
import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError, type BrowserLike, type RunContext } from "@rpa/core";
export const BROWSER_DOM_IDS = [
"Element_GetAttribute", "Element_GetAttribute_v1", "Element_GetAttribute_v2", "Element_GetAttribute_v3",

View File

@ -2,8 +2,8 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import { getHandler } from "../engine/registry";
import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types";
import { getHandler } from "@rpa/core";
import { RuntimeError, type BrowserLike, type RunContext } from "@rpa/core";
import "./browser-extended";
const EXTENDED_IDS = [

View File

@ -1,6 +1,6 @@
import { browserSession } from "../browser/session";
import { register } from "../engine/registry";
import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError, type BrowserLike, type RunContext } from "@rpa/core";
const CLICK_IDS = ["Element_Click_V1", "Element_Click_V2", "Element_Click_V4", "Element_Click_V5"];
const TEXT_INPUT_IDS = ["Element_SetValue_V1", "Element_SetValue_V2", "Element_SetValue_V3", "Element_SetValue_V5"];

View File

@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import { getHandler } from "../engine/registry";
import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types";
import { getHandler } from "@rpa/core";
import { RuntimeError, type BrowserLike, type RunContext } from "@rpa/core";
import "./browser";
const component = {} as CatalogComponent;

View File

@ -1,7 +1,7 @@
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError } from "@rpa/core";
import { browserSession, externalBrowserSession } from "../browser/session";
import type { BrowserLike, RunContext } from "../engine/types";
import type { BrowserLike, RunContext } from "@rpa/core";
async function session(ctx: RunContext): Promise<BrowserLike> {
if (!ctx.browser) ctx.browser = await browserSession();

View File

@ -1,7 +1,7 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import type { Catalog } from "../../shared/types";
import { hasCapability } from "../engine/registry";
import { hasCapability } from "@rpa/core";
import "./index";
const planned = [

View File

@ -1,7 +1,7 @@
import type { Catalog, CatalogComponent, CatalogField } from "../../shared/types";
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { hasCapability } from "../engine/registry";
import { hasCapability } from "@rpa/core";
import "./index";
const widgets = new Set(["text","textarea","number","select","radio","boolean","file","folder","element","code","secret","region","hidden"]);

View File

@ -5,9 +5,9 @@ import path from "node:path";
import { PDFDocument } from "pdf-lib";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./desktop"; import "./pdf"; import "./system-extended"; import "./window-extended"; import "./common-final";
import type { RunContext } from "@rpa/core";
import { getHandler } from "@rpa/core";
import "@rpa/core/capabilities"; import "./window-extended";
const dirs: string[] = []; const context = { vars: {} } as RunContext; const component = {} as CatalogComponent;
const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} });

View File

@ -1,6 +1,6 @@
import { BrowserWindow } from "electron";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError } from "@rpa/core";
interface FormFieldSpec { key?: string; name?: string; label?: string; type?: string; value?: string; options?: unknown[] }

View File

@ -1,44 +1,21 @@
import "./control";
import "./files";
import "./http";
import "./data";
/**
* 宿主能力注册入口。
*
* 纯 Node 能力已迁入 @rpa/core(packages/rpa-core),第一行 import 拉起包内全部注册;
* 此处只保留依赖 Electron / 宿主模块的能力,阶段 2 通过 HostBridge 逐步迁入。
* 导出顺序整体保持改造前 index.ts 的注册顺序(别名/重复 id last-wins 语义不变)。
*/
import "@rpa/core/capabilities";
import "./browser";
import "./browser-extended";
import "./browser-dom";
import "./browser-common";
import "./browser-control";
import "./office";
import "./desktop";
import "./desktop-uia";
import "./web-batch2";
import "./web-crawl";
import "./human-dialog";
import "./time-caps";
import "./windows-system";
import "./ftp";
import "./ai-service";
import "./ai-caps";
import "./ocr-caps";
import "./pdf";
import "./word";
import "./word-extended";
import "./message";
import "./vision";
import "./cv-vision";
import "./extended";
import "./system-extended";
import "./office-file-extended";
import "./office-aliases";
import "./excel-common";
import "./excel-com";
import "./window-extended";
import "./data-extended";
import "./utility-extended";
import "./data-table-common";
import "./basic-encryption";
import "./mail-remaining";
import "./remaining-core";
import "./common-final";
export { capabilityCount, capabilityIds, getHandler, hasCapability, register } from "../engine/registry";
export { capabilityCount, capabilityIds, getHandler, hasCapability, register } from "@rpa/core";

View File

@ -2,8 +2,8 @@ import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import { getHandler } from "../engine/registry";
import type { RunContext } from "../engine/types";
import { getHandler } from "@rpa/core";
import type { RunContext } from "@rpa/core";
import "./window-extended";
const run = promisify(execFile);

View File

@ -1,12 +1,10 @@
import { createServer, type IncomingMessage, type ServerResponse, type Server } from "node:http";
import net from "node:net";
import { describe, expect, it } from "vitest";
import { getHandler } from "../engine/registry";
import { getHandler } from "@rpa/core";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { RunContext } from "../engine/types";
import "./http";
import "./message";
import "./common-final";
import type { RunContext } from "@rpa/core";
import "@rpa/core/capabilities";
import "./ai-caps";
const ctx = {

View File

@ -1,6 +1,6 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { capabilityCount, hasCapability } from "../engine/registry";
import { capabilityCount, hasCapability } from "@rpa/core";
import "./index";
const selected = [

View File

@ -1,6 +1,6 @@
import { browserSession } from "../browser/session";
import { register } from "../engine/registry";
import { RuntimeError, type BrowserLike, type BrowserTabInfo, type RunContext } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError, type BrowserLike, type BrowserTabInfo, type RunContext } from "@rpa/core";
/** 优先使用流程中「打开网页」已建立的浏览器会话(含外部 Edge/Chrome),否则退回内置 */
async function currentSession(ctx: RunContext): Promise<BrowserLike> {

View File

@ -1,6 +1,6 @@
import { browserSession, externalBrowserSession } from "../browser/session";
import { register } from "../engine/registry";
import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types";
import { register } from "@rpa/core";
import { RuntimeError, type BrowserLike, type RunContext } from "@rpa/core";
/**
* 采集/监听组件的浏览器会话解析:

Some files were not shown because too many files have changed in this diff Show More