PRA-only-demo/src/main/capabilities/human-dialog.ts

90 lines
5.4 KiB
TypeScript

import { BrowserWindow } from "electron";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
interface FormFieldSpec { key?: string; name?: string; label?: string; type?: string; value?: string; options?: unknown[] }
function parseFields(raw: unknown, nodeId: string): FormFieldSpec[] {
if (raw === undefined || raw === null || raw === "") {
throw new RuntimeError("validation_error", "表单配置 jsonlist 为空", { nodeId, componentId: nodeId, recommendation: "填写 JSON 数组,例如 [{\"key\":\"name\",\"label\":\"名称\",\"type\":\"text\"}]" });
}
if (Array.isArray(raw)) return raw as FormFieldSpec[];
const text = String(raw).trim();
if (text.startsWith("[")) {
try {
const parsed = JSON.parse(text) as unknown;
if (Array.isArray(parsed)) return parsed as FormFieldSpec[];
} catch { /* fall through */ }
}
throw new RuntimeError("validation_error", "表单配置 jsonlist 无效", { nodeId, componentId: nodeId, recommendation: "填写 JSON 数组,例如 [{\"key\":\"name\",\"label\":\"名称\",\"type\":\"text\"}]" });
}
function fieldKey(f: FormFieldSpec, index: number): string {
return String(f.key ?? f.name ?? f.label ?? "field" + (index + 1));
}
function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
async function showForm(options: { title: string; fields: FormFieldSpec[]; timeoutSec: number }): Promise<Record<string, string>> {
return new Promise((resolve) => {
const rows = options.fields.map((f, i) => {
const key = escapeHtml(fieldKey(f, i));
const label = escapeHtml(f.label ?? fieldKey(f, i));
const value = escapeHtml(String(f.value ?? ""));
const type = String(f.type ?? "text").toLowerCase();
if (type === "select" || (Array.isArray(f.options) && f.options.length)) {
const opts = (f.options ?? []).map(o => {
const ov = typeof o === "string" ? o : String((o as { value?: string }).value ?? (o as { label?: string }).label ?? "");
const ol = typeof o === "string" ? o : String((o as { label?: string }).label ?? ov);
return '<option value="' + escapeHtml(ov) + '"' + (ov === String(f.value ?? "") ? " selected" : "") + ">" + escapeHtml(ol) + "</option>";
}).join("");
return "<label>" + label + '<br/><select data-key="' + key + '">' + opts + "</select></label><br/>";
}
if (type === "textarea") {
return "<label>" + label + '<br/><textarea data-key="' + key + '" rows="3">' + value + "</textarea></label><br/>";
}
if (type === "checkbox") {
const checked = String(f.value ?? "") === "true" || String(f.value ?? "") === "1" ? " checked" : "";
return '<label><input type="checkbox" data-key="' + key + '"' + checked + " /> " + label + "</label><br/>";
}
const inputType = type === "password" ? "password" : "text";
return "<label>" + label + '<br/><input type="' + inputType + '" data-key="' + key + '" value="' + value + '" /></label><br/>';
}).join("\n");
const html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:Microsoft YaHei,sans-serif;margin:16px;font-size:14px;color:#1f2937}label{display:block;margin-bottom:10px}input,select,textarea{width:100%;box-sizing:border-box;padding:6px;margin-top:4px}#ok{margin-top:8px;padding:8px 24px;background:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer}</style></head><body><h3>' + escapeHtml(options.title) + "</h3>" + rows + '<button id="ok">确认</button><script>document.getElementById("ok").onclick=function(){var out={};document.querySelectorAll("[data-key]").forEach(function(el){out[el.dataset.key]=el.type==="checkbox"?el.checked:el.value});window.__formValues=out;};</script></body></html>';
const win = new BrowserWindow({
width: 480, height: 0, useContentSize: true, autoHideMenuBar: true,
title: options.title, alwaysOnTop: true,
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true }
});
let settled = false;
const timer = setInterval(async () => {
try {
const values = await win.webContents.executeJavaScript("window.__formValues || null", true);
if (values) { clearInterval(timer); win.destroy(); resolve(values as Record<string, string>); }
} catch { clearInterval(timer); resolve({}); }
}, 250);
const timeout = setTimeout(() => {
try { void win.webContents.executeJavaScript("document.getElementById(\"ok\").click()"); } catch {}
}, Math.max(5, options.timeoutSec) * 1000);
win.on("closed", () => { clearInterval(timer); clearTimeout(timeout); resolve({}); });
void win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html));
});
}
const DIALOG_IDS = [
"Dialog_GetConfig", "Dialog_GetConfigV1", "Dialog_GetConfigV2", "Dialog_GetConfigV3",
"Dialog_GetConfigV4", "Dialog_GetConfigV5", "Dialog_GetConfigV6", "Dialog_GetConfigV7", "Dialog_GetConfigV8"
];
register(DIALOG_IDS, async (ctx, node, args) => {
const fields = parseFields(args.jsonlist, node.id);
const title = String(args.title ?? args.configName ?? "请输入");
const timeoutSec = Number(args.wait_time ?? 30) || 30;
const values = await showForm({ title, fields, timeoutSec });
ctx.log("已获取人机交互表单输入:" + JSON.stringify(values), "info", node.id);
return values;
});