fix automation script import and local import UI

This commit is contained in:
ant-black
2026-06-23 09:30:15 +08:00
parent bf85bc122c
commit b260f7b0b8
13 changed files with 4165 additions and 106 deletions
+2 -1
View File
@@ -8,10 +8,11 @@ import (
)
const (
automationScriptDefaultsMarkerName = "defaults-seeded-v10"
automationScriptDefaultsMarkerName = "defaults-seeded-v11"
)
var automationScriptDefaultsLegacyMarkerNames = []string{
"defaults-seeded-v10",
"defaults-seeded-v9",
"defaults-seeded-v8",
"defaults-seeded-v7",
@@ -13,6 +13,7 @@ import (
const (
DualInstanceRuntimeScriptID = "dual-instance-runtime-switch"
NewsQueryTXTScriptID = "news-query-txt"
ProtonMailFirstMessageID = "proton-mail-first-message"
WebImageGenerateScriptID = "web-image-generate-download"
builtinScriptLibraryRoot = "demo-library"
)
@@ -20,12 +21,14 @@ const (
var builtinScriptLibraryPackageDirs = map[string]string{
DualInstanceRuntimeScriptID: builtinScriptLibraryRoot + "/" + DualInstanceRuntimeScriptID,
NewsQueryTXTScriptID: builtinScriptLibraryRoot + "/" + NewsQueryTXTScriptID,
ProtonMailFirstMessageID: builtinScriptLibraryRoot + "/" + ProtonMailFirstMessageID,
WebImageGenerateScriptID: builtinScriptLibraryRoot + "/" + WebImageGenerateScriptID,
}
var builtinScriptLibraryDefaultOrder = []string{
DualInstanceRuntimeScriptID,
NewsQueryTXTScriptID,
ProtonMailFirstMessageID,
WebImageGenerateScriptID,
}
@@ -120,6 +123,7 @@ func importBuiltinScriptLibraryBundle(packageDir string) (ImportedBundle, error)
Format: mapStringValueAny(descriptor, "format"),
PackageFormat: mapStringValueAny(descriptor, "packageFormat"),
ManifestVersion: mapIntValueAny(descriptor, "manifestVersion"),
ID: mapStringValueAny(descriptor, "id"),
Name: mapStringValueAny(descriptor, "name"),
Description: mapStringValueAny(descriptor, "description"),
Type: mapStringValueAny(descriptor, "type"),
@@ -7,8 +7,8 @@ func TestDefaultScriptBundles(t *testing.T) {
if err != nil {
t.Fatalf("DefaultScriptBundles returned error: %v", err)
}
if len(bundles) != 3 {
t.Fatalf("expected three default script bundles, got %d", len(bundles))
if len(bundles) != 4 {
t.Fatalf("expected four default script bundles, got %d", len(bundles))
}
expected := []struct {
@@ -29,6 +29,12 @@ func TestDefaultScriptBundles(t *testing.T) {
uri: "repo://backend/internal/automation/demo-library/news-query-txt",
publicAPI: "",
},
{
id: ProtonMailFirstMessageID,
name: "Proton 邮件搜索并读取最新邮件",
uri: "repo://backend/internal/automation/demo-library/proton-mail-first-message",
publicAPI: "mail/proton-first-message",
},
{
id: WebImageGenerateScriptID,
name: "网页图片生成并下载",
@@ -5,5 +5,7 @@
!dual-instance-runtime-switch/**
!news-query-txt/
!news-query-txt/**
!proton-mail-first-message/
!proton-mail-first-message/**
!web-image-generate-download/
!web-image-generate-download/**
@@ -0,0 +1,76 @@
{
"createdAt": "2026-05-17T16:05:38+08:00",
"description": "打开 Proton Inbox,用查询语句搜索邮件,再用发件邮箱和收件人约束结果,返回命中邮件的正文内容和验证码。",
"entryFile": "index.cjs",
"format": "ant-automation-script",
"id": "proton-mail-first-message",
"manifestVersion": 1,
"name": "Proton 邮件搜索并读取最新邮件",
"notes": "searchQuery 只写入 Proton 搜索框;senderEmail 作为高级搜索约束,recipient 留空也可直接跑。若同一邮箱里 OpenAI / ChatGPT 邮件较多,再补 recipient 缩小范围。旧字段 recipientQuery 仍兼容。",
"packageFormat": "ant-automation-script",
"params": {
"recipient": "",
"searchQuery": "OpenAI, ChatGPT",
"senderEmail": ""
},
"publicAPI": {
"enabled": true,
"method": "POST",
"path": "mail/proton-first-message",
"requestMode": "params-only",
"responseMode": "envelope",
"timeoutMs": 120000,
"requestBodyText": "",
"responseBodyText": "{\r\n \"ok\": true,\r\n \"scriptId\": \"proton-mail-first-message\",\r\n \"scriptName\": \"Proton 邮件搜索并读取最新邮件\",\r\n \"status\": \"success\",\r\n \"summary\": \"已返回当前打开邮件内容\",\r\n \"error\": \"\",\r\n \"runId\": \"xxx\",\r\n \"durationMs\": 5902,\r\n \"result\": {\r\n \"verificationCode\": \"856734\",\r\n \"subject\": \"Your temporary ChatGPT login code\",\r\n \"senderEmail\": \"noreply@example.com\",\r\n \"recipientEmail\": \"target@example.com\",\r\n \"contentText\": \"OpenAI\\nChatGPT Log-in Code\\nIf that was you, enter this code:\\n856734\\nBest,\\nOpenAI\"\r\n }\r\n}",
"variables": [
]
},
"selector": {
"code": ""
},
"source": {
"importedAt": "",
"path": "proton-mail-first-message",
"ref": "HEAD",
"type": "builtin",
"uri": "repo://backend/internal/automation/demo-library/proton-mail-first-message"
},
"status": "ready",
"tags": [
"Playwright",
"邮箱",
"Proton",
"邮件处理"
],
"targetConfig": {
"mode": "manual",
"selector": {
"code": "",
"profileId": "",
"profileName": "",
"groupId": "",
"keywords": [
],
"tags": [
]
},
"templateSelector": {
"code": "",
"profileId": "",
"profileName": "",
"groupId": "",
"keywords": [
],
"tags": [
]
},
"createNameTemplate": ""
},
"type": "playwright-cdp",
"updatedAt": "2026-05-24T02:24:39.006Z"
}
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,7 @@ type scriptImportEnvelope struct {
PackageFormat string `json:"packageFormat"`
ManifestVersion int `json:"manifestVersion"`
Manifest map[string]any `json:"manifest"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
@@ -118,6 +118,7 @@ func importDirectoryBundle(packageRoot string, entryPath string, descriptor map[
Format: mapStringValueAny(descriptor, "format"),
PackageFormat: mapStringValueAny(descriptor, "packageFormat"),
ManifestVersion: mapIntValueAny(descriptor, "manifestVersion"),
ID: mapStringValueAny(descriptor, "id"),
Name: mapStringValueAny(descriptor, "name"),
Description: mapStringValueAny(descriptor, "description"),
Type: mapStringValueAny(descriptor, "type"),
@@ -130,6 +131,8 @@ func importDirectoryBundle(packageRoot string, entryPath string, descriptor map[
ParamsText: descriptor["paramsText"],
ScriptText: string(entryData),
Notes: appendImportSourceNote(mapStringValueAny(descriptor, "notes"), sourceLabel),
TargetConfig: mapObjectValue(descriptor, "targetConfig"),
PublicAPI: mapObjectValue(descriptor, "publicAPI"),
Source: mapObjectValue(descriptor, "source"),
}, filepath.Base(packageRoot), sourceLabel)
if err != nil {
@@ -49,6 +49,7 @@ func buildImportedRecord(envelope scriptImportEnvelope, defaultName string, sour
}
mergeDescriptorValue(descriptor, "format", envelope.Format)
mergeDescriptorValue(descriptor, "packageFormat", envelope.PackageFormat)
mergeDescriptorValue(descriptor, "id", envelope.ID)
if envelope.ManifestVersion > 0 {
descriptor["manifestVersion"] = envelope.ManifestVersion
}
@@ -99,18 +100,27 @@ func buildImportedRecord(envelope scriptImportEnvelope, defaultName string, sour
}
now := time.Now().Format(time.RFC3339)
packageFormat := normalizeScriptPackageFormat(firstNonEmpty(mapStringValueAny(descriptor, "packageFormat"), mapStringValueAny(descriptor, "format")))
recordID := uuid.NewString()
recordStatus := "draft"
if packageFormat == defaultScriptPackageFormat {
if manifestID := strings.TrimSpace(mapStringValueAny(descriptor, "id")); manifestID != "" {
recordID = manifestID
}
recordStatus = mapStringValueAny(descriptor, "status")
}
source := inferImportSource(sourceLabel)
if explicitSource, ok := descriptor["source"].(map[string]any); ok {
source = mergeImportedSource(source, explicitSource)
}
return normalizeScriptRecord(ScriptRecord{
PackageFormat: normalizeScriptPackageFormat(firstNonEmpty(mapStringValueAny(descriptor, "packageFormat"), mapStringValueAny(descriptor, "format"))),
PackageFormat: packageFormat,
ManifestVersion: normalizeScriptManifestVersion(mapIntValueAny(descriptor, "manifestVersion"), 0),
ID: uuid.NewString(),
ID: recordID,
Name: firstNonEmpty(mapStringValueAny(descriptor, "name"), defaultName, "导入脚本"),
Description: mapStringValueAny(descriptor, "description"),
Type: mapStringValueAny(descriptor, "type"),
Status: "draft",
Status: recordStatus,
EntryFile: normalizeScriptEntryFile(firstNonEmpty(mapStringValueAny(descriptor, "entryFile"), defaultEntryFileForName(defaultName))),
Tags: mapStringSliceValue(descriptor, "tags"),
SelectorText: stringifyImportJSONValue(firstNonNil(descriptor["selectorText"], descriptor["selector"])),
@@ -98,6 +98,54 @@ func TestImportBundleFromBytesSupportsZipPackage(t *testing.T) {
}
}
func TestImportBundleFromZipPreservesPackageIdentityAndPublicAPI(t *testing.T) {
zipPath := filepath.Join(t.TempDir(), "proton-package.zip")
if err := os.WriteFile(zipPath, buildScriptPackageZipBytes(t, map[string]string{
scriptPackageManifestName: `{
"format": "ant-automation-script",
"packageFormat": "ant-automation-script",
"manifestVersion": 1,
"id": "proton-mail-first-message",
"name": "Proton 邮件搜索并读取最新邮件",
"type": "playwright-cdp",
"status": "ready",
"entryFile": "index.cjs",
"targetConfig": {
"mode": "manual"
},
"publicAPI": {
"enabled": true,
"method": "POST",
"path": "mail/proton-first-message",
"requestMode": "params-only",
"responseMode": "envelope",
"timeoutMs": 120000
}
}`,
"index.cjs": "module.exports.run = async () => ({ ok: true })",
}), 0o644); err != nil {
t.Fatalf("write zip failed: %v", err)
}
bundle, err := ImportBundleFromZip(zipPath, "本地文件 "+zipPath)
if err != nil {
t.Fatalf("ImportBundleFromZip returned error: %v", err)
}
if bundle.Record.ID != "proton-mail-first-message" {
t.Fatalf("expected package id to round-trip, got %q", bundle.Record.ID)
}
if bundle.Record.Status != "ready" {
t.Fatalf("expected package status to round-trip, got %q", bundle.Record.Status)
}
if !bundle.Record.PublicAPI.Enabled || bundle.Record.PublicAPI.Path != "mail/proton-first-message" {
t.Fatalf("expected public api to round-trip, got %+v", bundle.Record.PublicAPI)
}
if bundle.Record.TargetConfig.Mode != "manual" {
t.Fatalf("expected target config to round-trip, got %+v", bundle.Record.TargetConfig)
}
}
func TestImportBundleFromZipSupportsSingleRootDirectory(t *testing.T) {
zipPath := filepath.Join(t.TempDir(), "nested.zip")
if err := os.WriteFile(zipPath, buildScriptPackageZipBytes(t, map[string]string{
@@ -10,12 +10,9 @@ import {
import type { BrowserProfile } from "../types";
export type ImportMode =
| "text"
| "local-file"
| "local-dir"
| "local-library"
| "remote-url"
| "local"
| "git";
export type LocalImportKind = "file" | "directory";
export const DUAL_INSTANCE_SCRIPT_ID = "dual-instance-runtime-switch";
export const NEWS_SCRIPT_ID = "news-query-txt";
@@ -10,9 +10,6 @@ import {
importAutomationScriptFromGit,
importAutomationScriptFromLocalDirectory,
importAutomationScriptFromLocalFile,
importAutomationScriptFromLocalLibrary,
importAutomationScriptFromRemote,
importAutomationScriptFromText,
saveAutomationScript,
} from "../automationScriptApi";
import {
@@ -37,6 +34,7 @@ import {
resolveDualLaunchCodes,
type AutomationCardPresentation,
type ImportMode,
type LocalImportKind,
} from "./AutomationPage.helpers";
export function AutomationPage() {
const navigate = useNavigate();
@@ -56,9 +54,8 @@ export function AutomationPage() {
const [createType, setCreateType] =
useState<AutomationScriptType>("playwright-cdp");
const [createName, setCreateName] = useState("");
const [importMode, setImportMode] = useState<ImportMode>("text");
const [importText, setImportText] = useState("");
const [remoteURL, setRemoteURL] = useState("");
const [importMode, setImportMode] = useState<ImportMode>("local");
const [localImportKind, setLocalImportKind] = useState<LocalImportKind>("file");
const [gitURL, setGitURL] = useState("");
const [gitRef, setGitRef] = useState("");
const [gitScriptPath, setGitScriptPath] = useState("");
@@ -202,9 +199,8 @@ export function AutomationPage() {
};
const resetImportModal = () => {
setImportMode("text");
setImportText("");
setRemoteURL("");
setImportMode("local");
setLocalImportKind("file");
setGitURL("");
setGitRef("");
setGitScriptPath("");
@@ -250,24 +246,12 @@ export function AutomationPage() {
let failedCount = 0;
switch (importMode) {
case "text": {
imported = [await importAutomationScriptFromText(importText)];
break;
}
case "local-file":
imported = [await importAutomationScriptFromLocalFile()];
break;
case "local-dir":
imported = [await importAutomationScriptFromLocalDirectory()];
break;
case "local-library": {
const result = await importAutomationScriptFromLocalLibrary();
imported = result.imported;
failedCount = result.failed.length;
break;
}
case "remote-url":
imported = [await importAutomationScriptFromRemote(remoteURL)];
case "local":
imported = [
localImportKind === "directory"
? await importAutomationScriptFromLocalDirectory()
: await importAutomationScriptFromLocalFile(),
];
break;
case "git":
imported = [
@@ -407,16 +391,14 @@ export function AutomationPage() {
open={importOpen}
busyAction={modalBusyAction}
importMode={importMode}
importText={importText}
remoteURL={remoteURL}
localImportKind={localImportKind}
gitURL={gitURL}
gitRef={gitRef}
gitScriptPath={gitScriptPath}
onClose={closeImportModal}
onImport={handleImport}
onImportModeChange={setImportMode}
onImportTextChange={setImportText}
onRemoteURLChange={setRemoteURL}
onLocalImportKindChange={setLocalImportKind}
onGitURLChange={setGitURL}
onGitRefChange={setGitRef}
onGitScriptPathChange={setGitScriptPath}
@@ -1,6 +1,6 @@
import { Button, FormItem, Input, Modal, Select, Textarea } from "../../../shared/components";
import { Button, FormItem, Input, Modal, Select } from "../../../shared/components";
import { AUTOMATION_SCRIPT_TYPE_OPTIONS, type AutomationScriptType } from "../automationScripts";
import type { ImportMode } from "./AutomationPage.helpers";
import type { ImportMode, LocalImportKind } from "./AutomationPage.helpers";
interface CreateAutomationScriptModalProps {
open: boolean;
@@ -73,16 +73,14 @@ interface ImportAutomationScriptModalProps {
open: boolean;
busyAction: "none" | "create" | "import";
importMode: ImportMode;
importText: string;
remoteURL: string;
localImportKind: LocalImportKind;
gitURL: string;
gitRef: string;
gitScriptPath: string;
onClose: () => void;
onImport: () => Promise<void>;
onImportModeChange: (value: ImportMode) => void;
onImportTextChange: (value: string) => void;
onRemoteURLChange: (value: string) => void;
onLocalImportKindChange: (value: LocalImportKind) => void;
onGitURLChange: (value: string) => void;
onGitRefChange: (value: string) => void;
onGitScriptPathChange: (value: string) => void;
@@ -92,16 +90,14 @@ export function ImportAutomationScriptModal({
open,
busyAction,
importMode,
importText,
remoteURL,
localImportKind,
gitURL,
gitRef,
gitScriptPath,
onClose,
onImport,
onImportModeChange,
onImportTextChange,
onRemoteURLChange,
onLocalImportKindChange,
onGitURLChange,
onGitRefChange,
onGitScriptPathChange,
@@ -133,73 +129,50 @@ export function ImportAutomationScriptModal({
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{[
{ value: "text", label: "文本" },
{ value: "local-file", label: "本地文件" },
{ value: "local-dir", label: "本地目录" },
{ value: "local-library", label: "脚本库" },
{ value: "remote-url", label: "远程 URL" },
{ value: "git", label: "Git" },
{ value: "local", label: "本地", disabled: false },
{ value: "git", label: "Git(进行中)", disabled: false },
{ value: "script-library", label: "脚本库(计划中)", disabled: true },
].map((item) => (
<Button
key={item.value}
size="sm"
variant={importMode === item.value ? "primary" : "secondary"}
onClick={() => onImportModeChange(item.value as ImportMode)}
disabled={busyAction !== "none"}
onClick={() => {
if (item.value === "script-library") {
return;
}
onImportModeChange(item.value as ImportMode);
}}
disabled={busyAction !== "none" || item.disabled}
>
{item.label}
</Button>
))}
</div>
{importMode === "text" ? (
<>
<div className="text-sm text-[var(--color-text-secondary)]">
JSON稿
{importMode === "local" ? (
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{[
{ value: "file", label: "ZIP / 文件" },
{ value: "directory", label: "文件夹" },
].map((item) => (
<Button
key={item.value}
size="sm"
variant={localImportKind === item.value ? "primary" : "secondary"}
onClick={() => onLocalImportKindChange(item.value as LocalImportKind)}
disabled={busyAction !== "none"}
>
{item.label}
</Button>
))}
</div>
<FormItem label="脚本 JSON">
<Textarea
rows={18}
value={importText}
onChange={(event) => onImportTextChange(event.target.value)}
className="font-mono"
placeholder='{"manifest":{"name":"示例脚本"}}'
/>
</FormItem>
</>
) : null}
{importMode === "local-file" ? (
<div className="rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] px-4 py-4 text-sm text-[var(--color-text-secondary)]">
`.js/.cjs/.mjs`
`.json` `.zip` `.ts/.cts/.mts` TypeScript
</div>
) : null}
{importMode === "local-dir" ? (
<div className="rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] px-4 py-4 text-sm text-[var(--color-text-secondary)]">
Git `.ts/.cts/.mts` TypeScript
</div>
) : null}
{importMode === "local-library" ? (
<div className="rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] px-4 py-4 text-sm text-[var(--color-text-secondary)]">
Git
</div>
) : null}
{importMode === "remote-url" ? (
<div className="space-y-4">
<div className="text-sm text-[var(--color-text-secondary)]">
JSON ZIP使 Git `.ts/.cts/.mts` TypeScript
<div className="rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] px-4 py-4 text-sm text-[var(--color-text-secondary)]">
{localImportKind === "directory"
? "点击导入后选择脚本文件夹,适合一整套本地脚本目录。"
: "点击导入后选择本地文件,支持标准 ZIP 脚本包、JSON 模板和单文件脚本。"}
</div>
<FormItem label="远程地址">
<Input
value={remoteURL}
onChange={(event) => onRemoteURLChange(event.target.value)}
placeholder="https://example.com/script.cjs"
/>
</FormItem>
</div>
) : null}