feat: 新增脚本录制功能

This commit is contained in:
duanshuwen
2026-04-12 15:46:28 +08:00
parent 66bb07faf2
commit c16fc93685
38 changed files with 3336 additions and 51 deletions

View File

@@ -13,6 +13,7 @@ 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) {
@@ -79,6 +80,14 @@ var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated";
IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script";
IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel";
IPC_EVENTS2["SCRIPT_LIST"] = "script:list";
IPC_EVENTS2["SCRIPT_GET"] = "script:get";
IPC_EVENTS2["SCRIPT_SAVE"] = "script:save";
IPC_EVENTS2["SCRIPT_DELETE"] = "script:delete";
IPC_EVENTS2["SCRIPT_TOGGLE"] = "script:toggle";
IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
@@ -1478,12 +1487,344 @@ class executeScriptService extends events.EventEmitter {
});
}
}
const META_FILENAME = "scripts.meta.json";
const SEED_DIR = "seed";
function getScriptsDir$1() {
return electron.app.isPackaged ? path.join(__dirname, "scripts") : path.join(process.cwd(), "electron/scripts");
}
function ensureScriptsDir() {
const dir = getScriptsDir$1();
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function getMetaPath() {
return path.join(getScriptsDir$1(), META_FILENAME);
}
function readMeta() {
const metaPath = getMetaPath();
if (!fs.existsSync(metaPath)) {
return { scripts: [] };
}
try {
const raw = fs.readFileSync(metaPath, "utf-8");
const parsed = JSON.parse(raw);
if (parsed && Array.isArray(parsed.scripts)) {
return parsed;
}
} catch (err) {
log.warn("[script-store-service] Failed to read meta:", err);
}
return { scripts: [] };
}
function writeMeta(meta) {
ensureScriptsDir();
const metaPath = getMetaPath();
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
}
function sanitizeFilename(name) {
return name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-+|-+$/g, "") || "script";
}
function generateUniqueFilename(name, existingNames) {
const base = sanitizeFilename(name);
let filename = `${base}.mjs`;
let counter = 1;
while (existingNames.has(filename)) {
filename = `${base}-${counter}.mjs`;
counter++;
}
return filename;
}
function seedScripts() {
const scriptsDir = getScriptsDir$1();
const metaPath = getMetaPath();
if (fs.existsSync(metaPath)) {
return;
}
const seedDir = path.join(scriptsDir, SEED_DIR);
if (!fs.existsSync(seedDir)) {
log.info("[script-store-service] Seed directory does not exist, skipping seed.");
return;
}
const meta = { scripts: [] };
const seedFiles = fs.readdirSync(seedDir).filter((f) => f.endsWith(".mjs"));
for (const file of seedFiles) {
const seedPath = path.join(seedDir, file);
const destPath = path.join(scriptsDir, file);
try {
fs.copyFileSync(seedPath, destPath);
const name = file.replace(/\.mjs$/, "");
const now = (/* @__PURE__ */ new Date()).toISOString();
meta.scripts.push({
id: `seed-${name}`,
name,
description: "",
filename: file,
enabled: true,
channel: "",
createdAt: now,
updatedAt: now
});
} catch (err) {
log.warn("[script-store-service] Failed to copy seed file", file, err);
}
}
writeMeta(meta);
log.info("[script-store-service] Seeded scripts:", meta.scripts.length);
}
function initScriptStoreService() {
ensureScriptsDir();
seedScripts();
}
function listScripts() {
const meta = readMeta();
return meta.scripts.map((item) => enrichWithCode(item)).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
}
function getScript(id) {
const meta = readMeta();
const item = meta.scripts.find((s) => s.id === id);
if (!item) return null;
return enrichWithCode(item);
}
function getScriptPathById(id) {
const meta = readMeta();
const item = meta.scripts.find((s) => s.id === id);
if (!item) return null;
return path.join(getScriptsDir$1(), item.filename);
}
function saveScript(input) {
const meta = readMeta();
const scriptsDir = getScriptsDir$1();
const existingNames = new Set(meta.scripts.map((s) => s.filename));
const now = (/* @__PURE__ */ new Date()).toISOString();
if (input.id) {
const index = meta.scripts.findIndex((s) => s.id === input.id);
if (index >= 0) {
const existing = meta.scripts[index];
const filePath2 = path.join(scriptsDir, existing.filename);
fs.writeFileSync(filePath2, input.code, "utf-8");
meta.scripts[index] = {
...existing,
name: input.name,
description: input.description,
channel: input.channel,
enabled: input.enabled,
updatedAt: now
};
writeMeta(meta);
return enrichWithCode(meta.scripts[index]);
}
}
const filename = generateUniqueFilename(input.name, existingNames);
const filePath = path.join(scriptsDir, filename);
fs.writeFileSync(filePath, input.code, "utf-8");
const id = `script-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
const item = {
id,
name: input.name,
description: input.description,
filename,
enabled: input.enabled,
channel: input.channel,
createdAt: now,
updatedAt: now
};
meta.scripts.push(item);
writeMeta(meta);
return enrichWithCode(item);
}
function deleteScript(id) {
const meta = readMeta();
const index = meta.scripts.findIndex((s) => s.id === id);
if (index === -1) return false;
const item = meta.scripts[index];
const filePath = path.join(getScriptsDir$1(), item.filename);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
} catch (err) {
log.warn("[script-store-service] Failed to delete script file:", err);
}
}
meta.scripts.splice(index, 1);
writeMeta(meta);
return true;
}
function toggleScript(id, enabled) {
const meta = readMeta();
const index = meta.scripts.findIndex((s) => s.id === id);
if (index === -1) return false;
meta.scripts[index].enabled = enabled;
meta.scripts[index].updatedAt = (/* @__PURE__ */ new Date()).toISOString();
writeMeta(meta);
return true;
}
function updateLastRun(id, lastRun) {
const meta = readMeta();
const index = meta.scripts.findIndex((s) => s.id === id);
if (index === -1) return false;
meta.scripts[index].lastRun = lastRun;
meta.scripts[index].updatedAt = (/* @__PURE__ */ new Date()).toISOString();
writeMeta(meta);
return true;
}
function enrichWithCode(item) {
const scriptsDir = getScriptsDir$1();
const filePath = path.join(scriptsDir, item.filename);
let code = "";
try {
if (fs.existsSync(filePath)) {
code = fs.readFileSync(filePath, "utf-8");
}
} catch (err) {
log.warn("[script-store-service] Failed to read script file:", err);
}
return {
...item,
code
};
}
const executor = new executeScriptService();
async function runScriptById(id, channel) {
const scriptPath = getScriptPathById(id);
if (!scriptPath) {
return {
success: false,
exitCode: null,
stdoutTail: "",
stderrTail: "",
error: "Script not found"
};
}
const result = await executor.executeScript(scriptPath, {
SCRIPT_ID: id,
CHANNEL: channel || ""
});
updateLastRun(id, {
time: (/* @__PURE__ */ new Date()).toISOString(),
success: result.success,
error: result.error
});
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();
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_LIST, async () => {
try {
return listScripts();
} catch (error) {
log.error("[SCRIPT_LIST] error:", error);
throw error;
}
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_GET, async (_event, id) => {
try {
return getScript(id);
} catch (error) {
log.error("[SCRIPT_GET] error:", error);
throw error;
}
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_SAVE, async (_event, input) => {
try {
return saveScript(input);
} catch (error) {
log.error("[SCRIPT_SAVE] error:", error);
throw error;
}
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_DELETE, async (_event, id) => {
try {
return deleteScript(id);
} catch (error) {
log.error("[SCRIPT_DELETE] error:", error);
throw error;
}
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_TOGGLE, async (_event, id, enabled) => {
try {
return toggleScript(id, enabled);
} catch (error) {
log.error("[SCRIPT_TOGGLE] error:", error);
throw error;
}
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RUN, async (_event, id) => {
try {
const script = getScript(id);
return await runScriptById(id, script?.channel);
} catch (error) {
log.error("[SCRIPT_RUN] error:", error);
return { success: false, exitCode: null, stdoutTail: "", stderrTail: "", error: error?.message || "Run failed" };
}
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url) => {
try {
return await startRecording(url);
} catch (error) {
log.error("[SCRIPT_RECORD_START] error:", error);
return { success: false, error: error?.message || "Recording start failed" };
}
});
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => {
try {
return await stopRecording();
} catch (error) {
log.error("[SCRIPT_RECORD_STOP] error:", error);
return { success: false, error: error?.message || "Recording stop failed" };
}
});
electron.ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels) => {
try {
await launchLocalChrome();
@@ -1629,6 +1970,7 @@ if (started) {
}
electron.app.whenReady().then(() => {
setupMainWindow();
initScriptStoreService();
runTaskOperationService();
});
electron.app.on("window-all-closed", () => {

View File

@@ -46,6 +46,14 @@ var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated";
IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script";
IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel";
IPC_EVENTS2["SCRIPT_LIST"] = "script:list";
IPC_EVENTS2["SCRIPT_GET"] = "script:get";
IPC_EVENTS2["SCRIPT_SAVE"] = "script:save";
IPC_EVENTS2["SCRIPT_DELETE"] = "script:delete";
IPC_EVENTS2["SCRIPT_TOGGLE"] = "script:toggle";
IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
@@ -94,6 +102,17 @@ const api = {
// 执行脚本
executeScript: (params) => electron.ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params),
// 打开渠道
openChannel: (channels) => electron.ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels)
openChannel: (channels) => electron.ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels),
// 脚本管理
scriptApi: {
list: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_LIST),
get: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_GET, id),
save: (input) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_SAVE, input),
delete: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_DELETE, id),
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)
}
};
electron.contextBridge.exposeInMainWorld("api", api);