feat: remove capability execution status markers
This commit is contained in:
1 parent
e956ec9055
commit
d22468e29f
16 files changed
+25
-73
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# RPA Studio
|
# RPA Studio
|
||||||
|
|
||||||
画布式 RPA 桌面端(Electron + React Flow)。把 1016 项能力目录拖到画布上,连线、配置、运行,生成可执行的 RPA 流程。
|
画布式 RPA 桌面端(Electron + React Flow)。把 894 项可编排能力拖到画布上,连线、配置、运行,生成可执行的 RPA 流程。
|
||||||
|
|
||||||
> 当前状态:任务优先的编排工作台。默认进入“编排”任务列表,编辑任务后进入上下流向画布;组件库按分类树展示,设置为独立入口。
|
> 当前状态:任务优先的编排工作台。默认进入“编排”任务列表,编辑任务后进入上下流向画布;组件库按分类树展示,设置为独立入口。
|
||||||
> 当前可编排能力:**894 / 894**(不接入项已分离备份;网页自动化、桌面自动化、数据、文件、办公、HTTP 等可直接编排)。
|
> 当前可编排能力:**894 / 894**(不接入项已分离备份;网页自动化、桌面自动化、数据、文件、办公、HTTP 等可直接编排)。
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function readBundle() {
|
|||||||
return { manifest, groups: manifest.groups.map(entry => ({ entry, flow: JSON.parse(fs.readFileSync(path.join(bundleRoot, entry.file), "utf8")) })) };
|
return { manifest, groups: manifest.groups.map(entry => ({ entry, flow: JSON.parse(fs.readFileSync(path.join(bundleRoot, entry.file), "utf8")) })) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 静态清单供离线校验;真实运行前再与 Electron 返回的 implemented 清单逐项比较。
|
// 静态清单供离线校验;真实运行前再与运行时注册清单逐项比较。
|
||||||
export function staticCatalog() {
|
export function staticCatalog() {
|
||||||
const registered = new Set();
|
const registered = new Set();
|
||||||
const directory = path.join(projectRoot, "src/main/capabilities");
|
const directory = path.join(projectRoot, "src/main/capabilities");
|
||||||
@@ -35,7 +35,7 @@ export function staticCatalog() {
|
|||||||
visit(source);
|
visit(source);
|
||||||
}
|
}
|
||||||
const catalog = JSON.parse(fs.readFileSync(path.join(projectRoot, "resources/catalog.json"), "utf8"));
|
const catalog = JSON.parse(fs.readFileSync(path.join(projectRoot, "resources/catalog.json"), "utf8"));
|
||||||
for (const c of catalog.components) c.implemented = containers.has(c.id) || registered.has(c.id) || registered.has(c.functionName);
|
for (const c of catalog.components) c.registered = containers.has(c.id) || registered.has(c.id) || registered.has(c.functionName);
|
||||||
return catalog;
|
return catalog;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +47,9 @@ export async function validator() {
|
|||||||
|
|
||||||
export async function verifyBundle(catalog = staticCatalog()) {
|
export async function verifyBundle(catalog = staticCatalog()) {
|
||||||
const { manifest, groups } = readBundle();
|
const { manifest, groups } = readBundle();
|
||||||
const expected = catalog.components.filter(c => c.group === "app" && c.implemented).map(c => c.id).sort();
|
const expected = catalog.components.filter(c => c.group === "app" && c.registered).map(c => c.id).sort();
|
||||||
const actual = groups.flatMap(g => g.entry.components).sort();
|
const actual = groups.flatMap(g => g.entry.components).sort();
|
||||||
if (groups.length !== 13 || actual.length !== 50 || new Set(actual).size !== 50 || JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error("流程包与当前 50 项可执行清单不一致,请重新生成并核对覆盖范围。");
|
if (groups.length !== 13 || actual.length !== 50 || new Set(actual).size !== 50 || JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error("流程包与当前 50 项验收清单不一致,请重新生成并核对覆盖范围。");
|
||||||
const validateFlow = await validator();
|
const validateFlow = await validator();
|
||||||
const validation = {};
|
const validation = {};
|
||||||
for (const { flow, entry } of groups) {
|
for (const { flow, entry } of groups) {
|
||||||
@@ -323,7 +323,7 @@ export function makeReport(evidence) {
|
|||||||
const counts = Object.fromEntries(priority.map(s => [s, rows.filter(r => r.status === s).length]));
|
const counts = Object.fromEntries(priority.map(s => [s, rows.filter(r => r.status === s).length]));
|
||||||
const escape = value => String(value ?? "无执行记录").replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ").slice(0, 1200);
|
const escape = value => String(value ?? "无执行记录").replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ").slice(0, 1200);
|
||||||
const recordedAt = evidence.generatedAt ? new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", dateStyle: "medium", timeStyle: "medium", hour12: false }).format(new Date(evidence.generatedAt)) + "(北京时间)" : "未执行";
|
const recordedAt = evidence.generatedAt ? new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", dateStyle: "medium", timeStyle: "medium", hour12: false }).format(new Date(evidence.generatedAt)) + "(北京时间)" : "未执行";
|
||||||
const lines = ["# 应用与系统组件测试报告", "", `记录时间:${recordedAt}。覆盖 50 项可执行组件,13 组任务。`, "",
|
const lines = ["# 应用与系统组件测试报告", "", `记录时间:${recordedAt}。覆盖 50 项验收组件,13 组任务。`, "",
|
||||||
`按组件统计:通过 ${counts.PASS},失败 ${counts.FAIL},阻塞 ${counts.BLOCKED},待人工确认 ${counts.MANUAL},未运行 ${counts.UNRUN}。`, "",
|
`按组件统计:通过 ${counts.PASS},失败 ${counts.FAIL},阻塞 ${counts.BLOCKED},待人工确认 ${counts.MANUAL},未运行 ${counts.UNRUN}。`, "",
|
||||||
"状态依据是断言、前置校验和人工证据,节点执行成功不直接代表功能通过。未执行手动系统操作。", "",
|
"状态依据是断言、前置校验和人工证据,节点执行成功不直接代表功能通过。未执行手动系统操作。", "",
|
||||||
"| 组 | 组件 ID | 名称 | 状态 |", "|---|---|---|---|"];
|
"| 组 | 组件 ID | 名称 | 状态 |", "|---|---|---|---|"];
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { validateFlow } from "../src/renderer/src/validation/flowValidation.ts";
|
|||||||
|
|
||||||
const containers = new Set(["logic_control_try_catch", "logic_control_if_else"]);
|
const containers = new Set(["logic_control_try_catch", "logic_control_if_else"]);
|
||||||
const catalog = JSON.parse(fs.readFileSync("resources/catalog.json", "utf8"));
|
const catalog = JSON.parse(fs.readFileSync("resources/catalog.json", "utf8"));
|
||||||
for (const c of catalog.components) c.implemented = containers.has(c.id) || hasCapability(c.id) || hasCapability(c.functionName);
|
const isRegistered = (component) => containers.has(component.id) || hasCapability(component.id) || hasCapability(component.functionName);
|
||||||
const componentMap = new Map(catalog.components.map(c => [c.id, c]));
|
const componentMap = new Map(catalog.components.map(c => [c.id, c]));
|
||||||
const temporary = [];
|
const temporary = [];
|
||||||
afterEach(() => { for (const dir of temporary.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); });
|
afterEach(() => { for (const dir of temporary.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); });
|
||||||
@@ -29,11 +29,11 @@ async function execute(number, change) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("应用与系统测试流程包", () => {
|
describe("应用与系统测试流程包", () => {
|
||||||
it("覆盖真实注册的全部 50 项,13 组无重复归属,不覆盖待接入组件", () => {
|
it("覆盖真实注册的全部 50 项,13 组无重复归属,不覆盖备份组件", () => {
|
||||||
const suite = buildSuite();
|
const suite = buildSuite();
|
||||||
expect(suite).toHaveLength(13);
|
expect(suite).toHaveLength(13);
|
||||||
const covered = suite.flatMap(group => group.entry.components).sort();
|
const covered = suite.flatMap(group => group.entry.components).sort();
|
||||||
const executable = catalog.components.filter(c => c.group === "app" && c.implemented).map(c => c.id).sort();
|
const executable = catalog.components.filter(c => c.group === "app" && isRegistered(c)).map(c => c.id).sort();
|
||||||
expect(covered).toHaveLength(50);
|
expect(covered).toHaveLength(50);
|
||||||
expect(new Set(covered).size).toBe(50);
|
expect(new Set(covered).size).toBe(50);
|
||||||
expect(covered).toEqual(executable);
|
expect(covered).toEqual(executable);
|
||||||
|
|||||||
+3
-15
@@ -2,21 +2,10 @@ import { app } from "electron";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { Catalog, CatalogComponent } from "../shared/types";
|
import type { Catalog, CatalogComponent } from "../shared/types";
|
||||||
import { hasCapability } from "./engine/registry";
|
|
||||||
|
|
||||||
let cached: Catalog | null = null;
|
let cached: Catalog | null = null;
|
||||||
let excludedIds: Set<string> | null = null;
|
let excludedIds: Set<string> | null = null;
|
||||||
|
|
||||||
const RUNTIME_CONTAINERS = new Set([
|
|
||||||
"logic_control_if_else",
|
|
||||||
"logic_control_count_for",
|
|
||||||
"logic_control_array_for_v3",
|
|
||||||
"logic_control_dictionary_for_v1",
|
|
||||||
"logic_control_while",
|
|
||||||
"infinite_loop",
|
|
||||||
"logic_control_try_catch"
|
|
||||||
]);
|
|
||||||
|
|
||||||
function resolveCatalogPath(): string {
|
function resolveCatalogPath(): string {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
process.env.RPA_CATALOG_PATH,
|
process.env.RPA_CATALOG_PATH,
|
||||||
@@ -64,10 +53,9 @@ export function loadCatalog(): Catalog {
|
|||||||
const raw = fs.readFileSync(catalogPath(), "utf8");
|
const raw = fs.readFileSync(catalogPath(), "utf8");
|
||||||
const catalog = JSON.parse(raw) as Catalog;
|
const catalog = JSON.parse(raw) as Catalog;
|
||||||
const excluded = loadExcludedIds();
|
const excluded = loadExcludedIds();
|
||||||
catalog.components = catalog.components.filter((component) => !excluded.has(component.id)).map((component: CatalogComponent) => ({
|
catalog.components = catalog.components
|
||||||
...component,
|
.filter((component) => !excluded.has(component.id))
|
||||||
implemented: RUNTIME_CONTAINERS.has(component.id) || hasCapability(component.id) || hasCapability(component.functionName)
|
.map((component: CatalogComponent) => component);
|
||||||
}));
|
|
||||||
catalog.counts.total = catalog.components.length;
|
catalog.counts.total = catalog.components.length;
|
||||||
catalog.counts.vendor = catalog.components.filter((component) => component.kind === "vendor").length;
|
catalog.counts.vendor = catalog.components.filter((component) => component.kind === "vendor").length;
|
||||||
catalog.counts.native = catalog.components.filter((component) => component.kind === "native").length;
|
catalog.counts.native = catalog.components.filter((component) => component.kind === "native").length;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ function component(id: string): CatalogComponent {
|
|||||||
const value = {
|
const value = {
|
||||||
id, name: id, kind: "vendor", group: "data", categories: [], categorySignature: "", description: "",
|
id, name: id, kind: "vendor", group: "data", categories: [], categorySignature: "", description: "",
|
||||||
messageTemplate: "", baseType: id, functionName: id, functionCalls: [], version: "", tags: [], platformTag: null,
|
messageTemplate: "", baseType: id, functionName: id, functionCalls: [], version: "", tags: [], platformTag: null,
|
||||||
icon: "data", risk: "low", fields: [], output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] }, slots: [], implemented: true
|
icon: "data", risk: "low", fields: [], output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] }, slots: []
|
||||||
} as CatalogComponent;
|
} as unknown as CatalogComponent;
|
||||||
components.set(id, value);
|
components.set(id, value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ function testComponent(id: string): CatalogComponent {
|
|||||||
fields: [],
|
fields: [],
|
||||||
output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] },
|
output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] },
|
||||||
slots: [],
|
slots: [],
|
||||||
implemented: true
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ async function executeNode(node: FlowNode, ctx: RunContext, states: Map<string,
|
|||||||
{
|
{
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
componentId: node.componentId,
|
componentId: node.componentId,
|
||||||
recommendation: "换用已接入组件;该能力已列入后续接入清单"
|
recommendation: "请检查组件配置或重新添加节点"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,12 +103,11 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="loading">
|
<div className="loading">
|
||||||
<Loader2 className="icon spin" style={{ width: 24, height: 24 }} />
|
<Loader2 className="icon spin" style={{ width: 24, height: 24 }} />
|
||||||
正在加载 1016 项组件目录…
|
正在加载组件目录…
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const executable = catalog.components.filter((item) => item.implemented).length;
|
|
||||||
|
|
||||||
const refreshFlows = async () => {
|
const refreshFlows = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -214,7 +213,7 @@ export function App() {
|
|||||||
本地执行 · 离线
|
本地执行 · 离线
|
||||||
</span>
|
</span>
|
||||||
<span className="engine-chip">
|
<span className="engine-chip">
|
||||||
{catalog.counts.total} 目录 · {executable} 可执行
|
{catalog.counts.total} 个可编排组件
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ function CanvasInner({ catalog, onAddAt }: { catalog: Catalog; onAddAt: (compone
|
|||||||
fields: [],
|
fields: [],
|
||||||
output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] },
|
output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] },
|
||||||
slots: [],
|
slots: [],
|
||||||
implemented: false
|
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
|
|||||||
@@ -67,12 +67,6 @@ export function SettingsPage({ catalog }: { catalog: Catalog }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const executable = catalog.components.filter((item) => item.implemented).length;
|
|
||||||
const groups = useMemo(() => catalog.groups.map((group) => {
|
|
||||||
const components = catalog.components.filter((component) => component.group === group.id);
|
|
||||||
const connected = components.filter((component) => component.implemented).length;
|
|
||||||
return { ...group, connected, pending: components.length - connected };
|
|
||||||
}), [catalog]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings">
|
<div className="settings">
|
||||||
@@ -96,10 +90,9 @@ export function SettingsPage({ catalog }: { catalog: Catalog }) {
|
|||||||
|
|
||||||
{tab === "catalog" && (
|
{tab === "catalog" && (
|
||||||
<>
|
<>
|
||||||
<SettingCard icon={Database} title="组件接入概览">
|
<SettingCard icon={Database} title="组件目录">
|
||||||
<dl className="kv"><dt>目录总数</dt><dd>{catalog.counts.total} 项</dd><dt>已接入执行器</dt><dd>{executable} 项({((executable / catalog.counts.total) * 100).toFixed(1)}%)</dd><dt>待接入</dt><dd>{catalog.counts.total - executable} 项</dd><dt>注册处理器 ID</dt><dd>{handlers?.count ?? "—"}</dd></dl>
|
<dl className="kv"><dt>可编排组件</dt><dd>{catalog.counts.total} 项</dd><dt>注册处理器 ID</dt><dd>{handlers?.count ?? "—"}</dd><dt>备份清单</dt><dd>resources/excluded.json</dd></dl>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
<div className="setting-card"><h3><Database className="icon" />分类覆盖</h3><div className="setting-group-table">{groups.map((group) => <div key={group.id}><strong>{group.label}</strong><span>{group.connected} 已接入</span><span>{group.pending} 待接入</span><progress max={group.count} value={group.connected} /></div>)}</div></div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ function PaletteRow({ component, onAdd }: { component: CatalogComponent; onAdd:
|
|||||||
>
|
>
|
||||||
<span className="palette-icon"><Icon className="icon" /></span>
|
<span className="palette-icon"><Icon className="icon" /></span>
|
||||||
<span className="palette-copy"><span className="palette-name">{component.name}</span><span className="palette-id">{component.id}</span></span>
|
<span className="palette-copy"><span className="palette-name">{component.name}</span><span className="palette-id">{component.id}</span></span>
|
||||||
<span className={`status-dot${component.implemented ? "" : " pending"}`} />
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -866,8 +866,6 @@ function RunTab({ node, component }: { node: FlowNode; component: Catalog["compo
|
|||||||
<strong>未运行</strong>
|
<strong>未运行</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>执行器</span>
|
|
||||||
<strong>已接入</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>超时</span>
|
<span>超时</span>
|
||||||
|
|||||||
@@ -546,11 +546,6 @@ textarea:focus-visible {
|
|||||||
box-shadow: 0 0 0 3px rgba(7, 148, 85, 0.08);
|
box-shadow: 0 0 0 3px rgba(7, 148, 85, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.pending {
|
|
||||||
background: #b9c2cd;
|
|
||||||
box-shadow: 0 0 0 3px rgba(120, 132, 148, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.palette-more {
|
.palette-more {
|
||||||
padding: 8px 14px 12px;
|
padding: 8px 14px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -911,19 +906,6 @@ textarea:focus-visible {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pending-badge {
|
|
||||||
margin-left: auto;
|
|
||||||
height: 16px;
|
|
||||||
padding: 0 5px;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #f0f3f7;
|
|
||||||
color: #8a96a6;
|
|
||||||
font-size: 9.5px;
|
|
||||||
font-weight: 650;
|
|
||||||
line-height: 16px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpa-node-title {
|
.rpa-node-title {
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { fieldIsActive, validateFlow } from "./flowValidation";
|
|||||||
const catalog = {
|
const catalog = {
|
||||||
schemaVersion: 1, generatedAt: "", counts: { total: 2, vendor: 1, native: 0, triggers: 1 }, groups: [],
|
schemaVersion: 1, generatedAt: "", counts: { total: 2, vendor: 1, native: 0, triggers: 1 }, groups: [],
|
||||||
components: [
|
components: [
|
||||||
{ id: "Trigger.Manual", name: "人工发起", kind: "trigger", group: "trigger", fields: [], slots: [], implemented: true },
|
{ id: "Trigger.Manual", name: "人工发起", kind: "trigger", group: "trigger", fields: [], slots: [] },
|
||||||
{ id: "Test.Action", name: "测试动作", kind: "vendor", group: "data", fields: [{ key: "value", label: "内容", required: true, widget: "text", defaultValue: "" }], slots: [], implemented: false }
|
{ id: "Test.Action", name: "测试动作", kind: "vendor", group: "data", fields: [{ key: "value", label: "内容", required: true, widget: "text", defaultValue: "" }], slots: [] }
|
||||||
]
|
]
|
||||||
} as unknown as Catalog;
|
} as unknown as Catalog;
|
||||||
|
|
||||||
@@ -38,10 +38,9 @@ describe("flow validation", () => {
|
|||||||
expect(fieldIsActive("System_ScreenSaverShowV1", "password", { set_password: "1" })).toBe(true);
|
expect(fieldIsActive("System_ScreenSaverShowV1", "password", { set_password: "1" })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reports pending components, required fields, and disconnected nodes", () => {
|
it("reports required fields and disconnected nodes", () => {
|
||||||
const issues = validateFlow(flow(), catalog);
|
const issues = validateFlow(flow(), catalog);
|
||||||
expect(issues).toEqual(expect.arrayContaining([
|
expect(issues).toEqual(expect.arrayContaining([
|
||||||
expect.objectContaining({ code: "not_implemented", nodeId: "action" }),
|
|
||||||
expect.objectContaining({ code: "required", nodeId: "action" }),
|
expect.objectContaining({ code: "required", nodeId: "action" }),
|
||||||
expect.objectContaining({ code: "disconnected", nodeId: "orphan", severity: "warning" })
|
expect.objectContaining({ code: "disconnected", nodeId: "orphan", severity: "warning" })
|
||||||
]));
|
]));
|
||||||
@@ -56,10 +55,10 @@ describe("flow validation", () => {
|
|||||||
it("does not require inactive designated browser fields or optional screenshot output", () => {
|
it("does not require inactive designated browser fields or optional screenshot output", () => {
|
||||||
const conditionalCatalog = structuredClone(catalog);
|
const conditionalCatalog = structuredClone(catalog);
|
||||||
conditionalCatalog.components.push({
|
conditionalCatalog.components.push({
|
||||||
id: "Element.Input", name: "输入", kind: "vendor", group: "web", implemented: true, slots: [],
|
id: "Element.Input", name: "输入", kind: "vendor", group: "web", slots: [],
|
||||||
fields: [{ key: "designated_obj", label: "指定对象", required: true, widget: "text", defaultValue: "" }]
|
fields: [{ key: "designated_obj", label: "指定对象", required: true, widget: "text", defaultValue: "" }]
|
||||||
} as never, {
|
} as never, {
|
||||||
id: "Image_Screenshot", name: "截图", kind: "vendor", group: "vision", implemented: true, slots: [],
|
id: "Image_Screenshot", name: "截图", kind: "vendor", group: "vision", slots: [],
|
||||||
fields: [{ key: "image_path", label: "输出路径", required: true, widget: "file", defaultValue: "" }]
|
fields: [{ key: "image_path", label: "输出路径", required: true, widget: "file", defaultValue: "" }]
|
||||||
} as never);
|
} as never);
|
||||||
const definition = flow();
|
const definition = flow();
|
||||||
@@ -91,7 +90,7 @@ describe("flow validation", () => {
|
|||||||
it("warns when a bare output variable name is used in an object field", () => {
|
it("warns when a bare output variable name is used in an object field", () => {
|
||||||
const objectCatalog = structuredClone(catalog);
|
const objectCatalog = structuredClone(catalog);
|
||||||
objectCatalog.components.push({
|
objectCatalog.components.push({
|
||||||
id: "Excel.Write", name: "写入", kind: "vendor", group: "office", implemented: true, slots: [],
|
id: "Excel.Write", name: "写入", kind: "vendor", group: "office", slots: [],
|
||||||
fields: [
|
fields: [
|
||||||
{ key: "workbook", label: "工作簿对象", required: false, widget: "text", type: "object", defaultValue: "" },
|
{ key: "workbook", label: "工作簿对象", required: false, widget: "text", type: "object", defaultValue: "" },
|
||||||
{ key: "value", label: "内容", required: true, widget: "text", defaultValue: "" }
|
{ key: "value", label: "内容", required: true, widget: "text", defaultValue: "" }
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ export function validateFlow(flow: FlowDefinition, catalog: Catalog): FlowValida
|
|||||||
issues.push({ severity: "error", code: "missing_component", nodeId: node.id, message: `组件不存在:${node.componentId}` });
|
issues.push({ severity: "error", code: "missing_component", nodeId: node.id, message: `组件不存在:${node.componentId}` });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!component.implemented) {
|
|
||||||
issues.push({ severity: "error", code: "not_implemented", nodeId: node.id, message: `「${node.label || component.name}」尚未接入执行器` });
|
|
||||||
}
|
|
||||||
for (const field of component.fields.filter((item) => item.required && item.widget !== "hidden" && item.widget !== "region")) {
|
for (const field of component.fields.filter((item) => item.required && item.widget !== "hidden" && item.widget !== "region")) {
|
||||||
if (!requiredInContext(component.id, field.key, node.values)) continue;
|
if (!requiredInContext(component.id, field.key, node.values)) continue;
|
||||||
const current = Object.prototype.hasOwnProperty.call(node.values, field.key) ? node.values[field.key] : field.defaultValue;
|
const current = Object.prototype.hasOwnProperty.call(node.values, field.key) ? node.values[field.key] : field.defaultValue;
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export interface CatalogComponent {
|
|||||||
output: CatalogOutput;
|
output: CatalogOutput;
|
||||||
slots: string[];
|
slots: string[];
|
||||||
nodeKind?: string;
|
nodeKind?: string;
|
||||||
implemented?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogGroup {
|
export interface CatalogGroup {
|
||||||
|
|||||||
Reference in new issue
Block a user