feat: 脚本录制功能完善

This commit is contained in:
duanshuwen
2026-04-12 22:12:57 +08:00
parent 6432634d17
commit 66db6c462e
16 changed files with 262 additions and 124 deletions

View File

@@ -1,4 +1,26 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
const electron = require("electron");
const OpenAI = require("openai");
const util = require("util");
@@ -13,7 +35,6 @@ const net = require("net");
const http = require("http");
const child_process = require("child_process");
const events = require("events");
const playwright = require("playwright");
require("bytenode");
const electronUpdater = require("electron-updater");
function _interopNamespaceDefault(e) {
@@ -88,6 +109,7 @@ var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen";
IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
@@ -793,7 +815,7 @@ class WindowService {
}
_loadPage(window2, pageName) {
{
return window2.loadURL(`${"http://localhost:5173"}/${pageName}.html`);
return window2.loadURL(`${"http://localhost:5173/"}/${pageName}.html`);
}
}
_loadWindowTemplate(window2, name) {
@@ -1702,59 +1724,15 @@ async function runScriptById(id, channel) {
});
return result;
}
let recorderBrowser = null;
let recorderContext = null;
async function startRecording(url) {
try {
await launchLocalChrome();
if (recorderBrowser) {
await stopRecording();
}
recorderBrowser = await playwright.chromium.connectOverCDP("http://127.0.0.1:9222");
recorderContext = recorderBrowser.contexts()[0] || await recorderBrowser.newContext();
const page = await recorderContext.newPage();
const targetUrl = url || "about:blank";
await page.goto(targetUrl, { waitUntil: "domcontentloaded" });
await page.pause();
return {
success: true,
code: ""
};
} catch (error) {
log.error("[script-recorder-service] Failed to start recording:", error);
return {
success: false,
error: error?.message || "Failed to start recording"
};
}
}
async function stopRecording() {
try {
if (recorderContext) {
await recorderContext.close().catch(() => {
});
recorderContext = null;
}
if (recorderBrowser) {
await recorderBrowser.close().catch(() => {
});
recorderBrowser = null;
}
return { success: true, code: "" };
} catch (error) {
log.error("[script-recorder-service] Failed to stop recording:", error);
return {
success: false,
error: error?.message || "Failed to stop recording"
};
}
}
const openedTabIndexByChannelName = /* @__PURE__ */ new Map();
function getScriptsDir() {
return electron.app.isPackaged ? path.join(__dirname, "scripts") : path.join(process.cwd(), "electron/scripts");
}
function runTaskOperationService() {
const executeScriptServiceInstance = new executeScriptService();
const playwrightCoreDir = path.dirname(require.resolve("playwright-core"));
const cliPath = path.join(playwrightCoreDir, "cli.js");
let recorderProc = null;
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_LIST, async () => {
try {
return listScripts();
@@ -1806,7 +1784,29 @@ function runTaskOperationService() {
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url) => {
try {
return await startRecording(url);
if (recorderProc) {
recorderProc.kill("SIGINT");
recorderProc = null;
}
const targetUrl = url || "about:blank";
recorderProc = child_process.spawn(process.execPath, [cliPath, "codegen", "--target", "javascript", "--channel", "chrome", "--viewport-size", "1920,1080", "--color-scheme", "light", targetUrl], {
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
stdio: "pipe"
});
recorderProc.on("error", (err) => {
log.error("[SCRIPT_RECORD_START] Failed to start codegen process:", err);
});
recorderProc.on("exit", (code, signal) => {
log.info(`[SCRIPT_RECORD_START] Process exited code=${code} signal=${signal}`);
recorderProc = null;
});
recorderProc.stdout?.on("data", (data) => {
log.info(`[SCRIPT_RECORD_START] stdout: ${data.toString()}`);
});
recorderProc.stderr?.on("data", (data) => {
log.error(`[SCRIPT_RECORD_START] stderr: ${data.toString()}`);
});
return { success: true };
} catch (error) {
log.error("[SCRIPT_RECORD_START] error:", error);
return { success: false, error: error?.message || "Recording start failed" };
@@ -1814,12 +1814,65 @@ function runTaskOperationService() {
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => {
try {
return await stopRecording();
if (recorderProc) {
recorderProc.kill("SIGINT");
recorderProc = null;
}
return { success: true, code: "" };
} catch (error) {
log.error("[SCRIPT_RECORD_STOP] error:", error);
return { success: false, error: error?.message || "Recording stop failed" };
}
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_CODEGEN, async (_event, id, url) => {
try {
const script = getScript(id);
if (!script) {
return { success: false, error: "Script not found" };
}
const scriptsDir = getScriptsDir();
const scriptPath = path.join(scriptsDir, script.filename);
const targetUrl = url || "about:blank";
log.info(`[SCRIPT_CODEGEN] Starting codegen for script ${id} at ${scriptPath} with url ${targetUrl}`);
return await new Promise((resolve) => {
const proc = child_process.spawn(process.execPath, [cliPath, "codegen", "--target", "javascript", "--channel", "chrome", "-o", scriptPath, targetUrl], {
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
stdio: "pipe"
});
proc.on("exit", () => {
try {
let generatedCode = fs.readFileSync(scriptPath, "utf-8");
if (generatedCode.includes("require('playwright')") && !generatedCode.includes("createRequire")) {
generatedCode = `import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
${generatedCode}`;
}
fs.writeFileSync(scriptPath, generatedCode, "utf-8");
saveScript({
id,
name: script.name,
description: script.description,
code: generatedCode,
channel: script.channel,
enabled: script.enabled
});
resolve({ success: true, code: generatedCode });
} catch (err) {
log.error("[SCRIPT_CODEGEN] Failed to process generated code:", err);
resolve({ success: false, error: err?.message || "Failed to process generated code" });
}
});
proc.on("error", (err) => {
log.error("[SCRIPT_CODEGEN] Failed to start codegen:", err);
resolve({ success: false, error: err.message });
});
});
} catch (error) {
log.error("[SCRIPT_CODEGEN] error:", error);
return { success: false, error: error?.message || "Codegen failed" };
}
});
electron.ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels) => {
try {
await launchLocalChrome();

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -54,6 +54,7 @@ var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen";
IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
@@ -112,7 +113,8 @@ const api = {
toggle: (id, enabled) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_TOGGLE, id, enabled),
run: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id),
startRecording: (url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url),
stopRecording: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP)
stopRecording: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP),
codegen: (id, url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_CODEGEN, id, url)
}
};
electron.contextBridge.exposeInMainWorld("api", api);

4
dist/index.html vendored
View File

@@ -8,8 +8,8 @@
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
/>
<script type="module" crossorigin src="./assets/index-CIC9LE5u.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Dck3WKYD.css">
<script type="module" crossorigin src="./assets/index-CBOhT_7U.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Dov57n46.css">
</head>
<body>
<div id="app"></div>

View File

@@ -66,6 +66,7 @@ const api: WindowApi = {
run: (id: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id),
startRecording: (url?: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url),
stopRecording: () => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP),
codegen: (id: string, url?: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_CODEGEN, id, url),
},
}

View File

@@ -11,12 +11,9 @@ import {
toggleScript,
} from '@electron/service/script-store-service';
import { runScriptById } from '@electron/service/script-execution-service';
import {
startRecording,
stopRecording,
} from '@electron/service/script-recorder-service';
import fs from 'fs'
import path from 'path'
import { spawn } from 'child_process'
import log from 'electron-log';
const openedTabIndexByChannelName = new Map<string, number>()
@@ -29,6 +26,9 @@ function getScriptsDir() {
export function runTaskOperationService() {
const executeScriptServiceInstance = new executeScriptService();
const playwrightCoreDir = path.dirname(require.resolve('playwright-core'));
const cliPath = path.join(playwrightCoreDir, 'cli.js');
let recorderProc: ReturnType<typeof spawn> | null = null;
// 脚本管理 IPC
ipcMain.handle(IPC_EVENTS.SCRIPT_LIST, async () => {
@@ -88,7 +88,35 @@ export function runTaskOperationService() {
ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url?: string) => {
try {
return await startRecording(url);
if (recorderProc) {
recorderProc.kill('SIGINT');
recorderProc = null;
}
const targetUrl = url || 'about:blank';
recorderProc = spawn(process.execPath, [cliPath, 'codegen', '--target', 'javascript', '--channel', 'chrome', '--viewport-size', '1920,1080', '--color-scheme', 'light', targetUrl], {
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
stdio: 'pipe',
});
recorderProc.on('error', (err) => {
log.error('[SCRIPT_RECORD_START] Failed to start codegen process:', err);
});
recorderProc.on('exit', (code, signal) => {
log.info(`[SCRIPT_RECORD_START] Process exited code=${code} signal=${signal}`);
recorderProc = null;
});
recorderProc.stdout?.on('data', (data: Buffer) => {
log.info(`[SCRIPT_RECORD_START] stdout: ${data.toString()}`);
});
recorderProc.stderr?.on('data', (data: Buffer) => {
log.error(`[SCRIPT_RECORD_START] stderr: ${data.toString()}`);
});
return { success: true };
} catch (error: any) {
log.error('[SCRIPT_RECORD_START] error:', error);
return { success: false, error: error?.message || 'Recording start failed' };
@@ -97,13 +125,76 @@ export function runTaskOperationService() {
ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => {
try {
return await stopRecording();
if (recorderProc) {
recorderProc.kill('SIGINT');
recorderProc = null;
}
return { success: true, code: '' };
} catch (error: any) {
log.error('[SCRIPT_RECORD_STOP] error:', error);
return { success: false, error: error?.message || 'Recording stop failed' };
}
});
ipcMain.handle(IPC_EVENTS.SCRIPT_CODEGEN, async (_event, id: string, url?: string) => {
try {
const script = getScript(id);
if (!script) {
return { success: false, error: 'Script not found' };
}
const scriptsDir = getScriptsDir();
const scriptPath = path.join(scriptsDir, script.filename);
const targetUrl = url || 'about:blank';
log.info(`[SCRIPT_CODEGEN] Starting codegen for script ${id} at ${scriptPath} with url ${targetUrl}`);
return await new Promise<{ success: boolean; code?: string; error?: string }>((resolve) => {
const proc = spawn(process.execPath, [cliPath, 'codegen', '--target', 'javascript', '--channel', 'chrome', '-o', scriptPath, targetUrl], {
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
stdio: 'pipe',
});
proc.on('exit', () => {
try {
let generatedCode = fs.readFileSync(scriptPath, 'utf-8');
// Playwright codegen --target javascript generates CommonJS code.
// Since script files use .mjs extension, we inject createRequire for compatibility.
if (generatedCode.includes("require('playwright')") && !generatedCode.includes('createRequire')) {
generatedCode = `import { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\n\n${generatedCode}`;
}
fs.writeFileSync(scriptPath, generatedCode, 'utf-8');
// Update script store so the new code is reflected in metadata reads
saveScript({
id,
name: script.name,
description: script.description,
code: generatedCode,
channel: script.channel,
enabled: script.enabled,
});
resolve({ success: true, code: generatedCode });
} catch (err: any) {
log.error('[SCRIPT_CODEGEN] Failed to process generated code:', err);
resolve({ success: false, error: err?.message || 'Failed to process generated code' });
}
});
proc.on('error', (err) => {
log.error('[SCRIPT_CODEGEN] Failed to start codegen:', err);
resolve({ success: false, error: err.message });
});
});
} catch (error: any) {
log.error('[SCRIPT_CODEGEN] error:', error);
return { success: false, error: error?.message || 'Codegen failed' };
}
});
// 打开渠道
ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels: any) => {
try {

View File

@@ -28,7 +28,12 @@
"enabled": true,
"channel": "fliggy",
"createdAt": "2026-04-09T19:35:34.000Z",
"updatedAt": "2026-04-09T19:35:34.000Z"
"updatedAt": "2026-04-12T12:59:12.117Z",
"lastRun": {
"time": "2026-04-12T12:59:12.117Z",
"success": false,
"error": "Script exited with code 1"
}
},
{
"id": "script-mt-trace",

View File

@@ -1,56 +0,0 @@
import { chromium } from 'playwright';
import log from 'electron-log';
import { launchLocalChrome } from '@electron/utils/chrome/launchLocalChrome';
let recorderBrowser: any = null;
let recorderContext: any = null;
export async function startRecording(url?: string): Promise<{ success: boolean; code?: string; error?: string }> {
try {
await launchLocalChrome();
if (recorderBrowser) {
await stopRecording();
}
recorderBrowser = await chromium.connectOverCDP('http://127.0.0.1:9222');
recorderContext = recorderBrowser.contexts()[0] || (await recorderBrowser.newContext());
const page = await recorderContext.newPage();
const targetUrl = url || 'about:blank';
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
// 唤起 Playwright Inspector让用户手动录制并生成代码
await page.pause();
return {
success: true,
code: '',
};
} catch (error: any) {
log.error('[script-recorder-service] Failed to start recording:', error);
return {
success: false,
error: error?.message || 'Failed to start recording',
};
}
}
export async function stopRecording(): Promise<{ success: boolean; code?: string; error?: string }> {
try {
if (recorderContext) {
await recorderContext.close().catch(() => {});
recorderContext = null;
}
if (recorderBrowser) {
await recorderBrowser.close().catch(() => {});
recorderBrowser = null;
}
return { success: true, code: '' };
} catch (error: any) {
log.error('[script-recorder-service] Failed to stop recording:', error);
return {
success: false,
error: error?.message || 'Failed to stop recording',
};
}
}

5
global.d.ts vendored
View File

@@ -88,6 +88,10 @@ declare global {
params: []
return: Promise<{ success: boolean; code?: string; error?: string }>
}
[IPC_EVENTS.SCRIPT_CODEGEN]: {
params: [id: string, url?: string]
return: Promise<{ success: boolean; code?: string; error?: string }>
}
}
type TabId = string
@@ -142,6 +146,7 @@ declare global {
run: (id: string) => Promise<ScriptExecutionResult>,
startRecording: (url?: string) => Promise<{ success: boolean; code?: string; error?: string }>,
stopRecording: () => Promise<{ success: boolean; code?: string; error?: string }>,
codegen: (id: string, url?: string) => Promise<{ success: boolean; code?: string; error?: string }>,
},
}

View File

@@ -68,6 +68,7 @@ export enum IPC_EVENTS {
SCRIPT_RUN = 'script:run',
SCRIPT_RECORD_START = 'script:record-start',
SCRIPT_RECORD_STOP = 'script:record-stop',
SCRIPT_CODEGEN = 'script:codegen',
// 更新
UPDATE_CHECK = 'update:check',

View File

@@ -15,4 +15,6 @@ export const scriptApi = {
window.api.scriptApi.startRecording(url),
stopRecording: (): Promise<{ success: boolean; code?: string; error?: string }> =>
window.api.scriptApi.stopRecording(),
codegen: (id: string, url?: string): Promise<{ success: boolean; code?: string; error?: string }> =>
window.api.scriptApi.codegen(id, url),
};

View File

@@ -71,7 +71,7 @@
:loading="saving"
class="!rounded-full !px-6 !h-[42px] !text-[13px] !font-semibold"
>
{{ saving ? t('common.saving', 'Saving...') : t('script.dialog.createTitle') }}
{{ saving ? t('common.saving', 'Saving...') : t('script.dialog.createAndRecord', '创建并录制') }}
</el-button>
</div>
</div>
@@ -124,7 +124,6 @@ const form = ref({
name: '',
description: '',
channel: '',
enabled: true,
});
function resetForm() {
@@ -132,7 +131,6 @@ function resetForm() {
name: '',
description: '',
channel: '',
enabled: true,
};
}
@@ -149,7 +147,7 @@ function handleSubmit() {
description: form.value.description.trim(),
code: defaultScriptTemplate,
channel: form.value.channel,
enabled: form.value.enabled,
enabled: true,
};
emit('save', payload);
visible.value = false;

View File

@@ -193,7 +193,7 @@ function openEditDialog(script: AutomationScript) {
}
function handleCreateDialogClose() {
editingScript.value = undefined;
// editingScript lifecycle is handled by handleSave / handleEditDialogClose
}
function handleEditDialogClose() {
@@ -207,8 +207,17 @@ async function handleSave(input: ScriptSaveInput) {
await store.saveScript(input);
ElMessage.success(t('script.toast.updated'));
} else {
await store.saveScript(input);
const result = await store.saveScript(input);
ElMessage.success(t('script.toast.created'));
ElMessage.info(t('script.toast.codegenStarting', '正在启动 Playwright codegen 录制,请在新窗口中操作,完成后关闭窗口即可自动保存代码'));
const codegenResult = await store.codegen(result.id, input.channel || 'about:blank');
if (codegenResult.success) {
ElMessage.success(t('script.toast.codegenFinished', '录制完成,代码已保存'));
editingScript.value = { ...result, code: codegenResult.code || '' };
editDialogVisible.value = true;
} else {
ElMessage.error(codegenResult.error || t('script.toast.codegenFailed', '录制失败'));
}
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);

View File

@@ -140,6 +140,20 @@ export const useScriptStore = defineStore('script', () => {
recordingStatus.value = 'idle';
};
const codegen = async (id: string, url?: string) => {
try {
const result = await scriptApi.codegen(id, url);
if (!result.success) {
error.value = result.error || 'Codegen failed';
}
return result;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
error.value = msg;
throw err;
}
};
return {
scripts,
loading,
@@ -157,5 +171,6 @@ export const useScriptStore = defineStore('script', () => {
startRecording,
stopRecording,
resetRecording,
codegen,
};
});

View File

@@ -72,6 +72,9 @@ export default defineConfig(({ mode, command }) => {
rollupOptions: {
external: isMainProcessExternal,
},
watch: {
exclude: ['**/electron/scripts/**', '**/scripts.meta.json'],
},
},
resolve: {
alias: {
@@ -100,6 +103,9 @@ export default defineConfig(({ mode, command }) => {
entryFileNames: 'preload.js',
},
},
watch: {
exclude: ['**/electron/scripts/**', '**/scripts.meta.json'],
},
},
resolve: {
alias: {
@@ -139,6 +145,9 @@ export default defineConfig(({ mode, command }) => {
server: {
port: 5173,
watch: {
ignored: ['**/electron/scripts/**', '**/scripts.meta.json'],
},
proxy: {
'/ingress': {
target: 'http://8.138.234.141',