feat: 脚本录制功能完善

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

No files matched your search

+104 -51
View File
@@ -1,4 +1,26 @@
"use strict"; "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 electron = require("electron");
const OpenAI = require("openai"); const OpenAI = require("openai");
const util = require("util"); const util = require("util");
@@ -13,7 +35,6 @@ const net = require("net");
const http = require("http"); const http = require("http");
const child_process = require("child_process"); const child_process = require("child_process");
const events = require("events"); const events = require("events");
const playwright = require("playwright");
require("bytenode"); require("bytenode");
const electronUpdater = require("electron-updater"); const electronUpdater = require("electron-updater");
function _interopNamespaceDefault(e) { function _interopNamespaceDefault(e) {
@@ -88,6 +109,7 @@ var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
IPC_EVENTS2["SCRIPT_RUN"] = "script:run"; IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start"; IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop"; IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen";
IPC_EVENTS2["UPDATE_CHECK"] = "update:check"; IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download"; IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install"; IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
@@ -793,7 +815,7 @@ class WindowService {
} }
_loadPage(window2, pageName) { _loadPage(window2, pageName) {
{ {
return window2.loadURL(`${"http://localhost:5173"}/${pageName}.html`); return window2.loadURL(`${"http://localhost:5173/"}/${pageName}.html`);
} }
} }
_loadWindowTemplate(window2, name) { _loadWindowTemplate(window2, name) {
@@ -1702,59 +1724,15 @@ async function runScriptById(id, channel) {
}); });
return result; 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(); const openedTabIndexByChannelName = /* @__PURE__ */ new Map();
function getScriptsDir() { function getScriptsDir() {
return electron.app.isPackaged ? path.join(__dirname, "scripts") : path.join(process.cwd(), "electron/scripts"); return electron.app.isPackaged ? path.join(__dirname, "scripts") : path.join(process.cwd(), "electron/scripts");
} }
function runTaskOperationService() { function runTaskOperationService() {
const executeScriptServiceInstance = new executeScriptService(); 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 () => { electron.ipcMain.handle(IPC_EVENTS.SCRIPT_LIST, async () => {
try { try {
return listScripts(); return listScripts();
@@ -1806,7 +1784,29 @@ function runTaskOperationService() {
}); });
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url) => { electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url) => {
try { 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) { } catch (error) {
log.error("[SCRIPT_RECORD_START] error:", error); log.error("[SCRIPT_RECORD_START] error:", error);
return { success: false, error: error?.message || "Recording start failed" }; return { success: false, error: error?.message || "Recording start failed" };
@@ -1814,12 +1814,65 @@ function runTaskOperationService() {
}); });
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => { electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => {
try { try {
return await stopRecording(); if (recorderProc) {
recorderProc.kill("SIGINT");
recorderProc = null;
}
return { success: true, code: "" };
} catch (error) { } catch (error) {
log.error("[SCRIPT_RECORD_STOP] error:", error); log.error("[SCRIPT_RECORD_STOP] error:", error);
return { success: false, error: error?.message || "Recording stop failed" }; 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) => { electron.ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels) => {
try { try {
await launchLocalChrome(); await launchLocalChrome();
+4 -1
View File
@@ -1 +1,4 @@
"use strict";const s=require("electron"),ae=require("openai"),$=require("util"),h=require("electron-log"),U=require("path"),ee=require("fs"),te=require("js-base64"),C=require("node:path"),ce=require("crypto"),le=require("electron-squirrel-startup"),de=require("net"),ue=require("http"),he=require("child_process"),pe=require("events");require("bytenode");const y=require("electron-updater");function ne(n){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const t in n)if(t!=="default"){const i=Object.getOwnPropertyDescriptor(n,t);Object.defineProperty(e,t,i.get?i:{enumerable:!0,get:()=>n[t]})}}return e.default=n,Object.freeze(e)}const S=ne(U),v=ne(ee);var r=(n=>(n.EXTERNAL_OPEN="external-open",n.WINDOW_MINIMIZE="window-minimize",n.WINDOW_MAXIMIZE="window-maximize",n.WINDOW_CLOSE="window-close",n.IS_WINDOW_MAXIMIZED="is-window-maximized",n.APP_SET_FRAMELESS="app:set-frameless",n.APP_LOAD_PAGE="app:load-page",n.TAB_CREATE="tab:create",n.TAB_LIST="tab:list",n.TAB_NAVIGATE="tab:navigate",n.TAB_RELOAD="tab:reload",n.TAB_BACK="tab:back",n.TAB_FORWARD="tab:forward",n.TAB_SWITCH="tab:switch",n.TAB_CLOSE="tab:close",n.LOG_TO_MAIN="log-to-main",n.READ_FILE="read-file",n.INVOKE="ipc:invoke",n.INVOKE_ASYNC="ipc:invokeAsync",n.APP_MINIMIZE="app:minimize",n.APP_MAXIMIZE="app:maximize",n.APP_QUIT="app:quit",n.FILE_READ="file:read",n.FILE_WRITE="file:write",n.GET_WINDOW_ID="get-window-id",n.CUSTOM_EVENT="custom:event",n.TIME_UPDATE="time:update",n.RENDERER_IS_READY="renderer-ready",n.SHOW_CONTEXT_MENU="show-context-menu",n.START_A_DIALOGUE="start-a-dialogue",n.OPEN_WINDOW="open-window",n.LOG_DEBUG="log-debug",n.LOG_INFO="log-info",n.LOG_WARN="log-warn",n.LOG_ERROR="log-error",n.CONFIG_UPDATED="config-updated",n.SET_CONFIG="set-config",n.GET_CONFIG="get-config",n.UPDATE_CONFIG="update-config",n.SET_THEME_MODE="set-theme-mode",n.GET_THEME_MODE="get-theme-mode",n.IS_DARK_THEME="is-dark-theme",n.THEME_MODE_UPDATED="theme-mode-updated",n.EXECUTE_SCRIPT="execute-script",n.OPEN_CHANNEL="open-channel",n.UPDATE_CHECK="update:check",n.UPDATE_DOWNLOAD="update:download",n.UPDATE_INSTALL="update:install",n.UPDATE_VERSION="update:version",n.UPDATE_STATUS_CHANGED="update:status-changed",n))(r||{});const ie={width:1440,height:900,minWidth:1440,minHeight:900};var w=(n=>(n.MAIN="main",n.SETTING="setting",n.DIALOG="dialog",n.LOADING="loading",n))(w||{}),g=(n=>(n.THEME_MODE="themeMode",n.PRIMARY_COLOR="primaryColor",n.LANGUAGE="language",n.FONT_SIZE="fontSize",n.MINIMIZE_TO_TRAY="minimizeToTray",n.PROVIDER="provider",n.DEFAULT_MODEL="defaultModel",n.AUTO_CHECK_UPDATE="autoCheckUpdate",n.AUTO_DOWNLOAD_UPDATE="autoDownloadUpdate",n))(g||{}),E=(n=>(n.CONVERSATION_ITEM="conversation-item",n.CONVERSATION_LIST="conversation-list",n.MESSAGE_ITEM="message-item",n))(E||{}),D=(n=>(n.PIN="pin",n.RENAME="rename",n.DEL="del",n))(D||{}),_=(n=>(n.NEW_CONVERSATION="newConversation",n.SORT_BY="sortBy",n.SORT_BY_CREATE_TIME="sortByCreateTime",n.SORT_BY_UPDATE_TIME="sortByUpdateTime",n.SORT_BY_NAME="sortByName",n.SORT_BY_MODEL="sortByModel",n.SORT_ASCENDING="sortAscending",n.SORT_DESCENDING="sortDescending",n.BATCH_OPERATIONS="batchOperations",n))(_||{}),O=(n=>(n.COPY="copy",n.DELETE="delete",n.SELECT="select",n))(O||{});class ge{}const fe=$.promisify(v.readdir),_e=$.promisify(v.stat),me=$.promisify(v.unlink);class z{static _instance;LOG_RETENTION_DAYS=7;CLEANUP_INTERVAL_MS=1440*60*1e3;constructor(){const e=S.join(s.app.getPath("userData"),"logs");try{v.existsSync(e)||v.mkdirSync(e,{recursive:!0})}catch(t){this.error("Failed to create log directory:",t)}h.transports.file.resolvePathFn=()=>{const t=new Date,i=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`;return S.join(e,`${i}.log`)},h.transports.file.format="[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}",h.transports.file.maxSize=10*1024*1024,h.transports.console.level=process.env.NODE_ENV==="development"?"debug":"info",h.transports.file.level="debug",this._setupIpcEvents(),this._rewriteConsole(),this.info("LogService initialized successfully."),this._cleanupOldLogs(),setInterval(()=>this._cleanupOldLogs(),this.CLEANUP_INTERVAL_MS)}_setupIpcEvents(){s.ipcMain.on(r.LOG_DEBUG,(e,t,...i)=>this.debug(t,...i)),s.ipcMain.on(r.LOG_INFO,(e,t,...i)=>this.info(t,...i)),s.ipcMain.on(r.LOG_WARN,(e,t,...i)=>this.warn(t,...i)),s.ipcMain.on(r.LOG_ERROR,(e,t,...i)=>this.error(t,...i))}_rewriteConsole(){console.debug=h.debug,console.log=h.info,console.info=h.info,console.warn=h.warn,console.error=h.error}async _cleanupOldLogs(){try{const e=S.join(s.app.getPath("userData"),"logs");if(!v.existsSync(e))return;const t=new Date,i=new Date(t.getTime()-this.LOG_RETENTION_DAYS*24*60*60*1e3),o=await fe(e);let a=0;for(const c of o){if(!c.endsWith(".log"))continue;const d=S.join(e,c);try{const f=await _e(d);f.isFile()&&f.birthtime<i&&(await me(d),a++)}catch(f){this.error(`Failed to delete old log file ${d}:`,f)}}a>0&&this.info(`Successfully cleaned up ${a} oLine truncated "use strict";var we=Object.create;var re=Object.defineProperty;var Ae=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var Se=Object.getPrototypeOf,Ee=Object.prototype.hasOwnProperty;var ve=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ye(e))!Ee.call(n,s)&&s!==t&&re(n,s,{get:()=>e[s],enumerable:!(i=Ae(e,s))||i.enumerable});return n};var Ce=(n,e,t)=>(t=n!=null?we(Se(n)):{},ve(e||!n||!n.__esModule?re(t,"default",{value:n,enumerable:!0}):t,n));const o=require("electron"),De=require("openai"),K=require("util"),d=require("electron-log"),y=require("path"),A=require("fs"),de=require("js-base64"),N=require("node:path"),Ie=require("crypto"),Me=require("electron-squirrel-startup"),Re=require("net"),Oe=require("http"),X=require("child_process"),be=require("events");require("bytenode");const C=require("electron-updater");function ue(n){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const t in n)if(t!=="default"){const i=Object.getOwnPropertyDescriptor(n,t);Object.defineProperty(e,t,i.get?i:{enumerable:!0,get:()=>n[t]})}}return e.default=n,Object.freeze(e)}const W=ue(y),I=ue(A);var c=(n=>(n.EXTERNAL_OPEN="external-open",n.WINDOW_MINIMIZE="window-minimize",n.WINDOW_MAXIMIZE="window-maximize",n.WINDOW_CLOSE="window-close",n.IS_WINDOW_MAXIMIZED="is-window-maximized",n.APP_SET_FRAMELESS="app:set-frameless",n.APP_LOAD_PAGE="app:load-page",n.TAB_CREATE="tab:create",n.TAB_LIST="tab:list",n.TAB_NAVIGATE="tab:navigate",n.TAB_RELOAD="tab:reload",n.TAB_BACK="tab:back",n.TAB_FORWARD="tab:forward",n.TAB_SWITCH="tab:switch",n.TAB_CLOSE="tab:close",n.LOG_TO_MAIN="log-to-main",n.READ_FILE="read-file",n.INVOKE="ipc:invoke",n.INVOKE_ASYNC="ipc:invokeAsync",n.APP_MINIMIZE="app:minimize",n.APP_MAXIMIZE="app:maximize",n.APP_QUIT="app:quit",n.FILE_READ="file:read",n.FILE_WRITE="file:write",n.GET_WINDOW_ID="get-window-id",n.CUSTOM_EVENT="custom:event",n.TIME_UPDATE="time:update",n.RENDERER_IS_READY="renderer-ready",n.SHOW_CONTEXT_MENU="show-context-menu",n.START_A_DIALOGUE="start-a-dialogue",n.OPEN_WINDOW="open-window",n.LOG_DEBUG="log-debug",n.LOG_INFO="log-info",n.LOG_WARN="log-warn",n.LOG_ERROR="log-error",n.CONFIG_UPDATED="config-updated",n.SET_CONFIG="set-config",n.GET_CONFIG="get-config",n.UPDATE_CONFIG="update-config",n.SET_THEME_MODE="set-theme-mode",n.GET_THEME_MODE="get-theme-mode",n.IS_DARK_THEME="is-dark-theme",n.THEME_MODE_UPDATED="theme-mode-updated",n.EXECUTE_SCRIPT="execute-script",n.OPEN_CHANNEL="open-channel",n.SCRIPT_LIST="script:list",n.SCRIPT_GET="script:get",n.SCRIPT_SAVE="script:save",n.SCRIPT_DELETE="script:delete",n.SCRIPT_TOGGLE="script:toggle",n.SCRIPT_RUN="script:run",n.SCRIPT_RECORD_START="script:record-start",n.SCRIPT_RECORD_STOP="script:record-stop",n.SCRIPT_CODEGEN="script:codegen",n.UPDATE_CHECK="update:check",n.UPDATE_DOWNLOAD="update:download",n.UPDATE_INSTALL="update:install",n.UPDATE_VERSION="update:version",n.UPDATE_STATUS_CHANGED="update:status-changed",n))(c||{});const pe={width:1440,height:900,minWidth:1440,minHeight:900};var E=(n=>(n.MAIN="main",n.SETTING="setting",n.DIALOG="dialog",n.LOADING="loading",n))(E||{}),_=(n=>(n.THEME_MODE="themeMode",n.PRIMARY_COLOR="primaryColor",n.LANGUAGE="language",n.FONT_SIZE="fontSize",n.MINIMIZE_TO_TRAY="minimizeToTray",n.PROVIDER="provider",n.DEFAULT_MODEL="defaultModel",n.AUTO_CHECK_UPDATE="autoCheckUpdate",n.AUTO_DOWNLOAD_UPDATE="autoDownloadUpdate",n))(_||{}),D=(n=>(n.CONVERSATION_ITEM="conversation-item",n.CONVERSATION_LIST="conversation-list",n.MESSAGE_ITEM="message-item",n))(D||{}),O=(n=>(n.PIN="pin",n.RENAME="rename",n.DEL="del",n))(O||{}),w=(n=>(n.NEW_CONVERSATION="newConversation",n.SORT_BY="sortBy",n.SORT_BY_CREATE_TIME="sortByCreateTime",n.SORT_BY_UPDATE_TIME="sortByUpdateTime",n.SORT_BY_NAME="sortByName",n.SORT_BY_MODEL="sortByModel",n.SORT_ASCENDING="sortAscending",n.SORT_DESCENDING="sortDescending",n.BATCH_OPERATIONS="batchOperations",n))(w||{}),b=(n=>(n.COPY="copy",n.DELETE="delete",n.SELECT="select",n))(b||{});class Ne{}const Le=K.promisify(I.readdir),Pe=K.promisify(I.stat),We=K.promisify(I.unlink);class J{static _instance;LOG_RETENTION_DAYS=7;CLEANUP_INTERVAL_MS=1440*60*1e3;constructor(){const e=W.join(o.app.getPath("userData"),"logs");try{I.existsSync(e)||I.mkdirSync(e,{recursive:!0})}catch(t){this.error("Failed to create log directory:",t)}d.transports.file.resolvePathFn=()=>{const t=new Date,i=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`;return W.join(e,`${i}.log`)},d.transports.file.format="[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}",d.transports.file.maxSize=10*1024*1024,d.transports.console.level=process.env.NODE_ENV==="development"?"debug":"info",d.transports.file.level="debug",this._setupIpcEvents(),this._rewriteConsole(),this.info("LogService initialized successfully."),this._cleanupOldLogs(),setInterval(()=>this._cleanupOldLogs(),this.CLEANUP_INTERVAL_MS)}_setupIpcEvents(){o.ipcMain.on(c.LOG_DEBUG,(e,t,..Line truncated
const require = createRequire(import.meta.url);
${p}`),A.writeFileSync(u,p,"utf-8"),le({id:r,name:l.name,description:l.description,code:p,channel:l.channel,enabled:l.enabled}),m({success:!0,code:p})}catch(p){d.error("[SCRIPT_CODEGEN] Failed to process generated code:",p),m({success:!1,error:p?.message||"Failed to process generated code"})}}),T.on("error",p=>{d.error("[SCRIPT_CODEGEN] Failed to start codegen:",p),m({success:!1,error:p.message})})})}catch(l){return d.error("[SCRIPT_CODEGEN] error:",l),{success:!1,error:l?.message||"Codegen failed"}}}),o.ipcMain.handle(c.OPEN_CHANNEL,async(s,r)=>{try{await _t();const a=Y(),l=y.join(a,"open_all_channel.js");if(q.clear(),Array.isArray(r))for(let u=0;u<r.length;u++){const f=r[u]?.channelName;f&&q.set(String(f),u)}return{success:!0,result:await n.executeScript(l,{channels:r})}}catch(a){return{success:!1,error:a?.message||"open channel failed"}}}),o.ipcMain.handle(c.EXECUTE_SCRIPT,async(s,r)=>{try{const a=r.roomList.find(T=>T.id===r.roomType),g=[["fzName","fg_trace.js"],["mtName","mt_trace.js"],["dyHotelName","dy_hotel_trace.js"],["dyHotSpringName","dy_hot_spring_trace.js"]].filter(([T])=>a?.[T]),u=Y(),f=g.map(([T,p])=>{const M=y.join(u,p);if(!A.existsSync(M))throw new Error(`Script not found for channel ${T}: ${M}`);return{channel:T,scriptPath:M}}),m=[];for(let T=0;T<f.length;T++){const p=f[T],M={fzName:"fliggy",mtName:"meituan",dyHotelName:"douyin",dyHotSpringName:"douyin"},k={fliggy:0,meituan:1,douyin:2},S=M[p.channel],R=S?q.get(S)??k[S]??T:T;d.info(`Launching script for channel ${p.channel}: ${p.scriptPath} (tabIndex: ${R})`);const Te=await n.executeScript(p.scriptPath,{roomType:a[p.channel],startTime:r.startTime,endTime:r.endTime,operation:r.operation,tabIndex:R});m.push({channel:p.channel,scriptPath:p.scriptPath,...Te})}return{success:!0,result:m}}catch(a){return{success:!1,error:a.message}}})}class se{mainWindow=null;static _instance;_initialized=!1;constructor(){C.autoUpdater.autoDownload=!1}init(){this._initialized||(this._initialized=!0,this.setupListeners(),this.registerHandlers())}static getInstance(){return this._instance||(this._instance=new se),this._instance}setMainWindow(e){this.mainWindow=e}setupListeners(){C.autoUpdater.on("checking-for-update",()=>this.sendToRenderer(c.UPDATE_STATUS_CHANGED,{status:"checking"})),C.autoUpdater.on("update-available",e=>this.sendToRenderer(c.UPDATE_STATUS_CHANGED,{status:"available",info:e})),C.autoUpdater.on("update-not-available",()=>this.sendToRenderer(c.UPDATE_STATUS_CHANGED,{status:"not-available"})),C.autoUpdater.on("download-progress",e=>this.sendToRenderer(c.UPDATE_STATUS_CHANGED,{status:"downloading",progress:e})),C.autoUpdater.on("update-downloaded",e=>this.sendToRenderer(c.UPDATE_STATUS_CHANGED,{status:"downloaded",info:e})),C.autoUpdater.on("error",e=>this.sendToRenderer(c.UPDATE_STATUS_CHANGED,{status:"error",error:e.message}))}sendToRenderer(e,t){o.BrowserWindow.getAllWindows().forEach(i=>{i.isDestroyed()||i.webContents.send(e,t)})}registerHandlers(){o.ipcMain.handle(c.UPDATE_CHECK,()=>o.app.isPackaged?C.autoUpdater.checkForUpdates():(this.sendToRenderer(c.UPDATE_STATUS_CHANGED,{status:"checking"}),setTimeout(()=>{this.sendToRenderer(c.UPDATE_STATUS_CHANGED,{status:"not-available"})},1500),null)),o.ipcMain.handle(c.UPDATE_DOWNLOAD,()=>o.app.isPackaged?C.autoUpdater.downloadUpdate():null),o.ipcMain.handle(c.UPDATE_INSTALL,()=>o.app.isPackaged?C.autoUpdater.quitAndInstall():null),o.ipcMain.handle(c.UPDATE_VERSION,()=>o.app.getVersion())}}const Ot=se.getInstance();Ot.init();Me&&o.app.quit();o.app.whenReady().then(()=>{fe(),yt(),Rt()});o.app.on("window-all-closed",()=>{process.platform!=="darwin"&&!v.get(_.MINIMIZE_TO_TRAY)&&(d.info("app closing due to all windows being closed"),o.app.quit())});o.app.on("activate",()=>{o.BrowserWindow.getAllWindows().length===0&&fe()});
Binary file not shown.
+3 -1
View File
@@ -54,6 +54,7 @@ var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
IPC_EVENTS2["SCRIPT_RUN"] = "script:run"; IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start"; IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop"; IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen";
IPC_EVENTS2["UPDATE_CHECK"] = "update:check"; IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download"; IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install"; IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
@@ -112,7 +113,8 @@ const api = {
toggle: (id, enabled) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_TOGGLE, id, enabled), toggle: (id, enabled) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_TOGGLE, id, enabled),
run: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id), run: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id),
startRecording: (url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url), 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); electron.contextBridge.exposeInMainWorld("api", api);
+2 -2
View File
@@ -8,8 +8,8 @@
http-equiv="Content-Security-Policy" 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" 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> <script type="module" crossorigin src="./assets/index-CBOhT_7U.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Dck3WKYD.css"> <link rel="stylesheet" crossorigin href="./assets/index-Dov57n46.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+1
View File
@@ -66,6 +66,7 @@ const api: WindowApi = {
run: (id: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id), run: (id: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id),
startRecording: (url?: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url), startRecording: (url?: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url),
stopRecording: () => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP), stopRecording: () => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP),
codegen: (id: string, url?: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_CODEGEN, id, url),
}, },
} }
+97 -6
View File
@@ -11,12 +11,9 @@ import {
toggleScript, toggleScript,
} from '@electron/service/script-store-service'; } from '@electron/service/script-store-service';
import { runScriptById } from '@electron/service/script-execution-service'; import { runScriptById } from '@electron/service/script-execution-service';
import {
startRecording,
stopRecording,
} from '@electron/service/script-recorder-service';
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { spawn } from 'child_process'
import log from 'electron-log'; import log from 'electron-log';
const openedTabIndexByChannelName = new Map<string, number>() const openedTabIndexByChannelName = new Map<string, number>()
@@ -29,6 +26,9 @@ function getScriptsDir() {
export function runTaskOperationService() { export function runTaskOperationService() {
const executeScriptServiceInstance = new executeScriptService(); 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 // 脚本管理 IPC
ipcMain.handle(IPC_EVENTS.SCRIPT_LIST, async () => { 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) => { ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url?: string) => {
try { 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) { } catch (error: any) {
log.error('[SCRIPT_RECORD_START] error:', error); log.error('[SCRIPT_RECORD_START] error:', error);
return { success: false, error: error?.message || 'Recording start failed' }; return { success: false, error: error?.message || 'Recording start failed' };
@@ -97,13 +125,76 @@ export function runTaskOperationService() {
ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => { ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => {
try { try {
return await stopRecording(); if (recorderProc) {
recorderProc.kill('SIGINT');
recorderProc = null;
}
return { success: true, code: '' };
} catch (error: any) { } catch (error: any) {
log.error('[SCRIPT_RECORD_STOP] error:', error); log.error('[SCRIPT_RECORD_STOP] error:', error);
return { success: false, error: error?.message || 'Recording stop failed' }; 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) => { ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels: any) => {
try { try {
+6 -1
View File
@@ -28,7 +28,12 @@
"enabled": true, "enabled": true,
"channel": "fliggy", "channel": "fliggy",
"createdAt": "2026-04-09T19:35:34.000Z", "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", "id": "script-mt-trace",
@@ -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',
};
}
}
Vendored
+5
View File
@@ -88,6 +88,10 @@ declare global {
params: [] params: []
return: Promise<{ success: boolean; code?: string; error?: string }> 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 type TabId = string
@@ -142,6 +146,7 @@ declare global {
run: (id: string) => Promise<ScriptExecutionResult>, run: (id: string) => Promise<ScriptExecutionResult>,
startRecording: (url?: string) => Promise<{ success: boolean; code?: string; error?: string }>, startRecording: (url?: string) => Promise<{ success: boolean; code?: string; error?: string }>,
stopRecording: () => 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 }>,
}, },
} }
+1
View File
@@ -68,6 +68,7 @@ export enum IPC_EVENTS {
SCRIPT_RUN = 'script:run', SCRIPT_RUN = 'script:run',
SCRIPT_RECORD_START = 'script:record-start', SCRIPT_RECORD_START = 'script:record-start',
SCRIPT_RECORD_STOP = 'script:record-stop', SCRIPT_RECORD_STOP = 'script:record-stop',
SCRIPT_CODEGEN = 'script:codegen',
// 更新 // 更新
UPDATE_CHECK = 'update:check', UPDATE_CHECK = 'update:check',
+2
View File
@@ -15,4 +15,6 @@ export const scriptApi = {
window.api.scriptApi.startRecording(url), window.api.scriptApi.startRecording(url),
stopRecording: (): Promise<{ success: boolean; code?: string; error?: string }> => stopRecording: (): Promise<{ success: boolean; code?: string; error?: string }> =>
window.api.scriptApi.stopRecording(), window.api.scriptApi.stopRecording(),
codegen: (id: string, url?: string): Promise<{ success: boolean; code?: string; error?: string }> =>
window.api.scriptApi.codegen(id, url),
}; };
@@ -71,7 +71,7 @@
:loading="saving" :loading="saving"
class="!rounded-full !px-6 !h-[42px] !text-[13px] !font-semibold" 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> </el-button>
</div> </div>
</div> </div>
@@ -124,7 +124,6 @@ const form = ref({
name: '', name: '',
description: '', description: '',
channel: '', channel: '',
enabled: true,
}); });
function resetForm() { function resetForm() {
@@ -132,7 +131,6 @@ function resetForm() {
name: '', name: '',
description: '', description: '',
channel: '', channel: '',
enabled: true,
}; };
} }
@@ -149,7 +147,7 @@ function handleSubmit() {
description: form.value.description.trim(), description: form.value.description.trim(),
code: defaultScriptTemplate, code: defaultScriptTemplate,
channel: form.value.channel, channel: form.value.channel,
enabled: form.value.enabled, enabled: true,
}; };
emit('save', payload); emit('save', payload);
visible.value = false; visible.value = false;
+11 -2
View File
@@ -193,7 +193,7 @@ function openEditDialog(script: AutomationScript) {
} }
function handleCreateDialogClose() { function handleCreateDialogClose() {
editingScript.value = undefined; // editingScript lifecycle is handled by handleSave / handleEditDialogClose
} }
function handleEditDialogClose() { function handleEditDialogClose() {
@@ -207,8 +207,17 @@ async function handleSave(input: ScriptSaveInput) {
await store.saveScript(input); await store.saveScript(input);
ElMessage.success(t('script.toast.updated')); ElMessage.success(t('script.toast.updated'));
} else { } else {
await store.saveScript(input); const result = await store.saveScript(input);
ElMessage.success(t('script.toast.created')); 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) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err); const msg = err instanceof Error ? err.message : String(err);
+15
View File
@@ -140,6 +140,20 @@ export const useScriptStore = defineStore('script', () => {
recordingStatus.value = 'idle'; 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 { return {
scripts, scripts,
loading, loading,
@@ -157,5 +171,6 @@ export const useScriptStore = defineStore('script', () => {
startRecording, startRecording,
stopRecording, stopRecording,
resetRecording, resetRecording,
codegen,
}; };
}); });
+9
View File
@@ -72,6 +72,9 @@ export default defineConfig(({ mode, command }) => {
rollupOptions: { rollupOptions: {
external: isMainProcessExternal, external: isMainProcessExternal,
}, },
watch: {
exclude: ['**/electron/scripts/**', '**/scripts.meta.json'],
},
}, },
resolve: { resolve: {
alias: { alias: {
@@ -100,6 +103,9 @@ export default defineConfig(({ mode, command }) => {
entryFileNames: 'preload.js', entryFileNames: 'preload.js',
}, },
}, },
watch: {
exclude: ['**/electron/scripts/**', '**/scripts.meta.json'],
},
}, },
resolve: { resolve: {
alias: { alias: {
@@ -139,6 +145,9 @@ export default defineConfig(({ mode, command }) => {
server: { server: {
port: 5173, port: 5173,
watch: {
ignored: ['**/electron/scripts/**', '**/scripts.meta.json'],
},
proxy: { proxy: {
'/ingress': { '/ingress': {
target: 'http://8.138.234.141', target: 'http://8.138.234.141',