feat: 新增脚本录制功能
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user