Files
Ant-Browser/frontend/src/modules/browser/automationScripts.ts
T

474 lines
14 KiB
TypeScript

import {
AUTOMATION_SCRIPT_MANIFEST_VERSION,
AUTOMATION_SCRIPT_PACKAGE_FORMAT,
DUAL_INSTANCE_RUNTIME_SCRIPT_ID,
type AutomationScriptRecord,
type AutomationScriptType,
} from "./automationScripts/definitions";
import {
createAutomationScriptPublicAPIConfig,
prepareAutomationScriptPublicAPIConfigForSave,
} from "./automationScripts/publicApi";
import {
normalizeAutomationScriptTargetConfig,
} from "./automationScripts/targets";
import {
buildDefaultAutomationScripts,
buildNotesTemplate,
buildParamsTemplate,
buildScriptTemplate,
buildSelectorTemplate,
createDualInstanceRuntimeScriptDraft,
normalizeDualInstanceRuntimeParamsText,
} from "./automationScripts/builtins";
import {
normalizeAutomationScriptSource,
} from "./automationScripts/metadata";
export * from "./automationScripts/definitions";
export {
AUTOMATION_SCRIPT_PUBLIC_API_METHOD_OPTIONS,
AUTOMATION_SCRIPT_PUBLIC_API_REQUEST_MODE_OPTIONS,
AUTOMATION_SCRIPT_PUBLIC_API_RESPONSE_MODE_OPTIONS,
applyAutomationScriptPublicAPIVariables,
buildAutomationScriptPublicAPIPath,
buildAutomationScriptPublicAPIRequestExample,
buildAutomationScriptPublicAPIResponseExample,
collectAutomationScriptPublicAPIVariableValues,
createAutomationScriptPublicAPIConfig,
getAutomationScriptPublicAPIRequestModeLabel,
getAutomationScriptPublicAPIResponseModeLabel,
isAutomationScriptPublicAPIVariableName,
normalizeAutomationScriptPublicAPIConfig,
prepareAutomationScriptPublicAPIConfigForSave,
resolveAutomationScriptPublicAPIConfig,
suggestAutomationScriptPublicAPIPath,
} from "./automationScripts/publicApi";
export {
canRefreshAutomationScriptSource,
getAutomationScriptRefreshLabel,
getAutomationScriptSourceLabel,
getAutomationScriptStatusLabel,
getAutomationScriptTypeLabel,
normalizeAutomationScriptSource,
} from "./automationScripts/metadata";
export {
createAutomationScriptTargetSelector,
describeAutomationScriptTargetConfig,
findAutomationTargetProfile,
formatAutomationTargetIdentity,
getAutomationScriptTargetModeLabel,
normalizeAutomationScriptTargetConfig,
normalizeAutomationScriptTargetSelector,
} from "./automationScripts/targets";
const AUTOMATION_SCRIPTS_STORAGE_KEY = "automation_scripts_v1";
function nowIso(): string {
return new Date().toISOString();
}
function createScriptId(): string {
if (
typeof crypto !== "undefined" &&
typeof crypto.randomUUID === "function"
) {
return crypto.randomUUID();
}
return `script-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
function normalizeTags(tags: unknown): string[] {
if (!Array.isArray(tags)) {
return [];
}
const deduped = new Set<string>();
for (const item of tags) {
const normalized = String(item || "").trim();
if (normalized) {
deduped.add(normalized);
}
}
return Array.from(deduped);
}
function normalizeScriptRecord(raw: unknown): AutomationScriptRecord | null {
if (!raw || typeof raw !== "object") {
return null;
}
const source = raw as Partial<AutomationScriptRecord>;
const type = source.type === "launch-api" ? "launch-api" : "playwright-cdp";
const status =
source.status === "ready" || source.status === "disabled"
? source.status
: "draft";
const createdAt =
typeof source.createdAt === "string" && source.createdAt.trim()
? source.createdAt
: nowIso();
const updatedAt =
typeof source.updatedAt === "string" && source.updatedAt.trim()
? source.updatedAt
: createdAt;
const normalizedSource = normalizeAutomationScriptSource(source.source);
const normalizedTargetConfig = normalizeAutomationScriptTargetConfig(
source.targetConfig,
);
const record: AutomationScriptRecord = {
packageFormat:
typeof source.packageFormat === "string" && source.packageFormat.trim()
? source.packageFormat.trim()
: AUTOMATION_SCRIPT_PACKAGE_FORMAT,
manifestVersion:
typeof source.manifestVersion === "number" && source.manifestVersion > 0
? source.manifestVersion
: AUTOMATION_SCRIPT_MANIFEST_VERSION,
id:
typeof source.id === "string" && source.id.trim()
? source.id
: createScriptId(),
name:
typeof source.name === "string" && source.name.trim()
? source.name.trim()
: "未命名脚本",
description:
typeof source.description === "string" ? source.description.trim() : "",
type,
status,
entryFile:
typeof source.entryFile === "string" && source.entryFile.trim()
? source.entryFile.trim()
: "index.cjs",
tags: normalizeTags(source.tags),
selectorText:
typeof source.selectorText === "string" && source.selectorText.trim()
? source.selectorText
: buildSelectorTemplate(type),
paramsText:
typeof source.paramsText === "string" && source.paramsText.trim()
? source.paramsText
: buildParamsTemplate(type),
scriptText:
typeof source.scriptText === "string" && source.scriptText.trim()
? source.scriptText
: buildScriptTemplate(type),
notes:
typeof source.notes === "string" && source.notes.trim()
? source.notes
: buildNotesTemplate(type),
targetConfig: normalizedTargetConfig,
publicAPI: createAutomationScriptPublicAPIConfig(),
source: normalizedSource,
createdAt,
updatedAt,
};
record.publicAPI = prepareAutomationScriptPublicAPIConfigForSave({
id: record.id,
name: record.name,
selectorText: record.selectorText,
paramsText: record.paramsText,
publicAPI: source.publicAPI ?? null,
});
if (record.id === DUAL_INSTANCE_RUNTIME_SCRIPT_ID) {
const dualInstanceDraft = createDualInstanceRuntimeScriptDraft();
const usesLegacyDualInstanceScript =
record.scriptText.includes("params.primaryCode") ||
record.scriptText.includes("params.secondaryCode");
return {
...record,
selectorText: "",
paramsText: normalizeDualInstanceRuntimeParamsText(record.paramsText),
targetConfig: normalizeAutomationScriptTargetConfig(null),
scriptText: usesLegacyDualInstanceScript
? dualInstanceDraft.scriptText
: record.scriptText,
};
}
return record;
}
export function normalizeAutomationScriptRecordPayload(
raw: unknown,
): AutomationScriptRecord | null {
return normalizeScriptRecord(raw);
}
export function createAutomationScriptDraft(
type: AutomationScriptType = "playwright-cdp",
): AutomationScriptRecord {
const createdAt = nowIso();
const name =
type === "launch-api" ? "新建 Launch API 脚本" : "百度搜索示例";
const description =
type === "launch-api"
? ""
: "启动示例实例,打开百度并搜索关键词,用来验证 Launch API + Playwright CDP 链路。";
return {
packageFormat: AUTOMATION_SCRIPT_PACKAGE_FORMAT,
manifestVersion: AUTOMATION_SCRIPT_MANIFEST_VERSION,
id: createScriptId(),
name,
description,
type,
status: type === "playwright-cdp" ? "ready" : "draft",
entryFile: "index.cjs",
tags: type === "launch-api" ? ["HTTP"] : ["Playwright", "示例"],
selectorText: buildSelectorTemplate(type),
paramsText: buildParamsTemplate(type),
scriptText: buildScriptTemplate(type),
notes: buildNotesTemplate(type),
targetConfig: normalizeAutomationScriptTargetConfig(null),
publicAPI: createAutomationScriptPublicAPIConfig(),
source: {
type: "manual",
uri: "",
ref: "",
path: "",
importedAt: "",
},
createdAt,
updatedAt: createdAt,
};
}
export function duplicateAutomationScript(
script: AutomationScriptRecord,
): AutomationScriptRecord {
const createdAt = nowIso();
return {
...script,
id: createScriptId(),
name: `${script.name} - 副本`,
status: "draft",
publicAPI: {
...createAutomationScriptPublicAPIConfig(),
requestBodyText: script.publicAPI.requestBodyText,
responseBodyText: script.publicAPI.responseBodyText,
variables: script.publicAPI.variables.map((variable) => ({ ...variable })),
},
createdAt,
updatedAt: createdAt,
};
}
function stringifyJsonBlock(value: unknown, fallback: string): string {
if (typeof value === "string" && value.trim()) {
return value;
}
if (value && typeof value === "object") {
try {
return JSON.stringify(value, null, 2);
} catch {
return fallback;
}
}
return fallback;
}
export function importAutomationScript(text: string): AutomationScriptRecord {
const normalized = text.trim();
if (!normalized) {
throw new Error("导入内容不能为空");
}
let parsed: any;
try {
parsed = JSON.parse(normalized);
} catch {
throw new Error("导入内容不是合法 JSON");
}
const manifest =
parsed?.manifest && typeof parsed.manifest === "object"
? parsed.manifest
: parsed;
const type: AutomationScriptType =
manifest?.type === "launch-api" ? "launch-api" : "playwright-cdp";
const timestamp = nowIso();
const imported = normalizeScriptRecord({
packageFormat:
typeof parsed?.packageFormat === "string"
? parsed.packageFormat
: typeof parsed?.format === "string"
? parsed.format
: AUTOMATION_SCRIPT_PACKAGE_FORMAT,
manifestVersion:
typeof parsed?.manifestVersion === "number"
? parsed.manifestVersion
: AUTOMATION_SCRIPT_MANIFEST_VERSION,
id: createScriptId(),
name: typeof manifest?.name === "string" ? manifest.name : undefined,
description:
typeof manifest?.description === "string" ? manifest.description : "",
type,
status: "draft",
entryFile:
typeof manifest?.entryFile === "string"
? manifest.entryFile
: "index.cjs",
tags: Array.isArray(manifest?.tags) ? manifest.tags : [],
selectorText: stringifyJsonBlock(
parsed?.selector ?? parsed?.selectorText,
buildSelectorTemplate(type),
),
paramsText: stringifyJsonBlock(
parsed?.params ?? parsed?.paramsText,
buildParamsTemplate(type),
),
scriptText:
typeof parsed?.script === "string"
? parsed.script
: typeof parsed?.scriptText === "string"
? parsed.scriptText
: buildScriptTemplate(type),
notes:
typeof parsed?.notes === "string"
? parsed.notes
: typeof parsed?.manifest?.notes === "string"
? parsed.manifest.notes
: buildNotesTemplate(type),
targetConfig:
parsed?.targetConfig && typeof parsed.targetConfig === "object"
? parsed.targetConfig
: parsed?.manifest?.targetConfig &&
typeof parsed.manifest.targetConfig === "object"
? parsed.manifest.targetConfig
: null,
publicAPI:
parsed?.publicAPI && typeof parsed.publicAPI === "object"
? parsed.publicAPI
: parsed?.manifest?.publicAPI &&
typeof parsed.manifest.publicAPI === "object"
? parsed.manifest.publicAPI
: null,
source:
parsed?.source && typeof parsed.source === "object"
? parsed.source
: {
type: "text",
uri: "",
ref: "",
path: "",
importedAt: timestamp,
},
createdAt: timestamp,
updatedAt: timestamp,
});
if (!imported) {
throw new Error("导入内容无法识别为脚本");
}
return imported;
}
function buildDefaultScripts(): AutomationScriptRecord[] {
return buildDefaultAutomationScripts();
}
function sortScripts(
items: AutomationScriptRecord[],
): AutomationScriptRecord[] {
return [...items].sort((left, right) => {
return (
new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
);
});
}
export function loadAutomationScripts(): AutomationScriptRecord[] {
if (typeof window === "undefined" || !window.localStorage) {
return buildDefaultScripts();
}
try {
const raw = window.localStorage.getItem(AUTOMATION_SCRIPTS_STORAGE_KEY);
if (!raw) {
return buildDefaultScripts();
}
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
return buildDefaultScripts();
}
const scripts = parsed
.map((item) => normalizeScriptRecord(item))
.filter((item): item is AutomationScriptRecord => item !== null);
if (scripts.length === 0) {
return buildDefaultScripts();
}
return sortScripts(scripts);
} catch {
return buildDefaultScripts();
}
}
export function saveAutomationScripts(scripts: AutomationScriptRecord[]) {
if (typeof window === "undefined" || !window.localStorage) {
return;
}
try {
window.localStorage.setItem(
AUTOMATION_SCRIPTS_STORAGE_KEY,
JSON.stringify(sortScripts(scripts)),
);
} catch {
// Ignore storage failures and keep the editor usable.
}
}
export function exportAutomationScript(script: AutomationScriptRecord): string {
return JSON.stringify(
{
manifest: {
packageFormat: script.packageFormat,
manifestVersion: script.manifestVersion,
id: script.id,
name: script.name,
description: script.description,
type: script.type,
status: script.status,
entryFile: script.entryFile,
tags: script.tags,
notes: script.notes,
targetConfig: script.targetConfig,
publicAPI: script.publicAPI,
source: script.source,
createdAt: script.createdAt,
updatedAt: script.updatedAt,
},
format: script.packageFormat,
manifestVersion: script.manifestVersion,
selector: safeParseJson(script.selectorText),
params: safeParseJson(script.paramsText),
script: script.scriptText,
notes: script.notes,
targetConfig: script.targetConfig,
publicAPI: script.publicAPI,
source: script.source,
},
null,
2,
);
}
function safeParseJson(text: string): unknown {
try {
return JSON.parse(text);
} catch {
return text;
}
}