feat: 新增脚本录制功能
This commit is contained in:
@@ -4,6 +4,7 @@ import { setupMainWindow } from './wins';
|
||||
import started from 'electron-squirrel-startup'
|
||||
import configManager from '@electron/service/config-service'
|
||||
import { runTaskOperationService } from '@electron/process/runTaskOperationService'
|
||||
import { initScriptStoreService } from '@electron/service/script-store-service'
|
||||
import log from 'electron-log';
|
||||
import 'bytenode'; // Ensure bytenode is bundled/externalized correctly
|
||||
import { appUpdater } from '@electron/service/updater';
|
||||
@@ -30,6 +31,9 @@ if (started) {
|
||||
app.whenReady().then(() => {
|
||||
setupMainWindow();
|
||||
|
||||
// 初始化脚本存储服务
|
||||
initScriptStoreService()
|
||||
|
||||
// 开启任务操作子进程
|
||||
runTaskOperationService()
|
||||
|
||||
|
||||
@@ -55,6 +55,18 @@ const api: WindowApi = {
|
||||
|
||||
// 打开渠道
|
||||
openChannel: (channels: any) => ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels),
|
||||
|
||||
// 脚本管理
|
||||
scriptApi: {
|
||||
list: () => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_LIST),
|
||||
get: (id: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_GET, id),
|
||||
save: (input: any) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_SAVE, input),
|
||||
delete: (id: string) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_DELETE, id),
|
||||
toggle: (id: string, enabled: boolean) => ipcRenderer.invoke(IPC_EVENTS.SCRIPT_TOGGLE, id, enabled),
|
||||
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),
|
||||
},
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
@@ -3,6 +3,18 @@ import { ipcMain, app } from 'electron';
|
||||
import { IPC_EVENTS } from '@lib/constants';
|
||||
import { launchLocalChrome } from '@electron/utils/chrome/launchLocalChrome'
|
||||
import { executeScriptService } from '@electron/service/execute-script-service';
|
||||
import {
|
||||
listScripts,
|
||||
getScript,
|
||||
saveScript,
|
||||
deleteScript,
|
||||
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 log from 'electron-log';
|
||||
@@ -17,6 +29,81 @@ function getScriptsDir() {
|
||||
|
||||
export function runTaskOperationService() {
|
||||
const executeScriptServiceInstance = new executeScriptService();
|
||||
|
||||
// 脚本管理 IPC
|
||||
ipcMain.handle(IPC_EVENTS.SCRIPT_LIST, async () => {
|
||||
try {
|
||||
return listScripts();
|
||||
} catch (error: any) {
|
||||
log.error('[SCRIPT_LIST] error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.SCRIPT_GET, async (_event, id: string) => {
|
||||
try {
|
||||
return getScript(id);
|
||||
} catch (error: any) {
|
||||
log.error('[SCRIPT_GET] error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.SCRIPT_SAVE, async (_event, input: any) => {
|
||||
try {
|
||||
return saveScript(input);
|
||||
} catch (error: any) {
|
||||
log.error('[SCRIPT_SAVE] error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.SCRIPT_DELETE, async (_event, id: string) => {
|
||||
try {
|
||||
return deleteScript(id);
|
||||
} catch (error: any) {
|
||||
log.error('[SCRIPT_DELETE] error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.SCRIPT_TOGGLE, async (_event, id: string, enabled: boolean) => {
|
||||
try {
|
||||
return toggleScript(id, enabled);
|
||||
} catch (error: any) {
|
||||
log.error('[SCRIPT_TOGGLE] error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.SCRIPT_RUN, async (_event, id: string) => {
|
||||
try {
|
||||
const script = getScript(id);
|
||||
return await runScriptById(id, script?.channel);
|
||||
} catch (error: any) {
|
||||
log.error('[SCRIPT_RUN] error:', error);
|
||||
return { success: false, exitCode: null, stdoutTail: '', stderrTail: '', error: error?.message || 'Run failed' };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url?: string) => {
|
||||
try {
|
||||
return await startRecording(url);
|
||||
} catch (error: any) {
|
||||
log.error('[SCRIPT_RECORD_START] error:', error);
|
||||
return { success: false, error: error?.message || 'Recording start failed' };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => {
|
||||
try {
|
||||
return await stopRecording();
|
||||
} catch (error: any) {
|
||||
log.error('[SCRIPT_RECORD_STOP] error:', error);
|
||||
return { success: false, error: error?.message || 'Recording stop failed' };
|
||||
}
|
||||
});
|
||||
|
||||
// 打开渠道
|
||||
ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels: any) => {
|
||||
try {
|
||||
|
||||
18
electron/scripts/change-model.mjs
Normal file
18
electron/scripts/change-model.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { preparePage, safeDisconnectBrowser } from './common/tabs.js';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
const { page } = await preparePage(browser, {
|
||||
targetUrl: 'about:blank',
|
||||
});
|
||||
|
||||
// Example: navigate and click an element
|
||||
// await page.goto('https://example.com');
|
||||
// await page.click('text=Change Model');
|
||||
|
||||
console.log('Change model script executed');
|
||||
|
||||
await safeDisconnectBrowser(browser);
|
||||
process.exit(0);
|
||||
})();
|
||||
18
electron/scripts/pause-resume.mjs
Normal file
18
electron/scripts/pause-resume.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { preparePage, safeDisconnectBrowser } from './common/tabs.js';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
const { page } = await preparePage(browser, {
|
||||
targetUrl: 'about:blank',
|
||||
});
|
||||
|
||||
// Example: fill a form and submit
|
||||
// await page.fill('input[name="username"]', 'test');
|
||||
// await page.click('button[type="submit"]');
|
||||
|
||||
console.log('Pause resume script executed');
|
||||
|
||||
await safeDisconnectBrowser(browser);
|
||||
process.exit(0);
|
||||
})();
|
||||
18
electron/scripts/pause.mjs
Normal file
18
electron/scripts/pause.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { preparePage, safeDisconnectBrowser } from './common/tabs.js';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
const { page } = await preparePage(browser, {
|
||||
targetUrl: 'about:blank',
|
||||
});
|
||||
|
||||
// Example: wait for a specific element or timeout
|
||||
// await page.waitForSelector('[data-testid="status-paused"]');
|
||||
// await page.waitForTimeout(2000);
|
||||
|
||||
console.log('Pause script executed');
|
||||
|
||||
await safeDisconnectBrowser(browser);
|
||||
process.exit(0);
|
||||
})();
|
||||
34
electron/scripts/scripts.meta.json
Normal file
34
electron/scripts/scripts.meta.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"scripts": [
|
||||
{
|
||||
"id": "seed-change-model",
|
||||
"name": "change-model",
|
||||
"description": "",
|
||||
"filename": "change-model.mjs",
|
||||
"enabled": true,
|
||||
"channel": "common",
|
||||
"createdAt": "2026-04-12T05:27:08.543Z",
|
||||
"updatedAt": "2026-04-12T05:27:08.543Z"
|
||||
},
|
||||
{
|
||||
"id": "seed-pause-resume",
|
||||
"name": "pause-resume",
|
||||
"description": "",
|
||||
"filename": "pause-resume.mjs",
|
||||
"enabled": true,
|
||||
"channel": "common",
|
||||
"createdAt": "2026-04-12T05:27:08.544Z",
|
||||
"updatedAt": "2026-04-12T05:27:08.544Z"
|
||||
},
|
||||
{
|
||||
"id": "seed-pause",
|
||||
"name": "pause",
|
||||
"description": "",
|
||||
"filename": "pause.mjs",
|
||||
"enabled": true,
|
||||
"channel": "common",
|
||||
"createdAt": "2026-04-12T05:27:08.544Z",
|
||||
"updatedAt": "2026-04-12T05:27:08.544Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
electron/scripts/seed/change-model.mjs
Normal file
18
electron/scripts/seed/change-model.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { preparePage, safeDisconnectBrowser } from './common/tabs.js';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
const { page } = await preparePage(browser, {
|
||||
targetUrl: 'about:blank',
|
||||
});
|
||||
|
||||
// Example: navigate and click an element
|
||||
// await page.goto('https://example.com');
|
||||
// await page.click('text=Change Model');
|
||||
|
||||
console.log('Change model script executed');
|
||||
|
||||
await safeDisconnectBrowser(browser);
|
||||
process.exit(0);
|
||||
})();
|
||||
18
electron/scripts/seed/pause-resume.mjs
Normal file
18
electron/scripts/seed/pause-resume.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { preparePage, safeDisconnectBrowser } from './common/tabs.js';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
const { page } = await preparePage(browser, {
|
||||
targetUrl: 'about:blank',
|
||||
});
|
||||
|
||||
// Example: fill a form and submit
|
||||
// await page.fill('input[name="username"]', 'test');
|
||||
// await page.click('button[type="submit"]');
|
||||
|
||||
console.log('Pause resume script executed');
|
||||
|
||||
await safeDisconnectBrowser(browser);
|
||||
process.exit(0);
|
||||
})();
|
||||
18
electron/scripts/seed/pause.mjs
Normal file
18
electron/scripts/seed/pause.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { preparePage, safeDisconnectBrowser } from './common/tabs.js';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
const { page } = await preparePage(browser, {
|
||||
targetUrl: 'about:blank',
|
||||
});
|
||||
|
||||
// Example: wait for a specific element or timeout
|
||||
// await page.waitForSelector('[data-testid="status-paused"]');
|
||||
// await page.waitForTimeout(2000);
|
||||
|
||||
console.log('Pause script executed');
|
||||
|
||||
await safeDisconnectBrowser(browser);
|
||||
process.exit(0);
|
||||
})();
|
||||
37
electron/service/script-execution-service/index.ts
Normal file
37
electron/service/script-execution-service/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { executeScriptService } from '@electron/service/execute-script-service';
|
||||
import {
|
||||
getScriptPathById,
|
||||
updateLastRun,
|
||||
} from '@electron/service/script-store-service';
|
||||
import type { ScriptExecutionResult } from '@lib/script-types';
|
||||
|
||||
const executor = new executeScriptService();
|
||||
|
||||
export async function runScriptById(
|
||||
id: string,
|
||||
channel?: string,
|
||||
): Promise<ScriptExecutionResult> {
|
||||
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: new Date().toISOString(),
|
||||
success: result.success,
|
||||
error: result.error,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
56
electron/service/script-recorder-service/index.ts
Normal file
56
electron/service/script-recorder-service/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
246
electron/service/script-store-service/index.ts
Normal file
246
electron/service/script-store-service/index.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { app } from 'electron';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import log from 'electron-log';
|
||||
import type {
|
||||
AutomationScript,
|
||||
ScriptMetaItem,
|
||||
ScriptsMeta,
|
||||
ScriptSaveInput,
|
||||
} from '@lib/script-types';
|
||||
|
||||
const META_FILENAME = 'scripts.meta.json';
|
||||
const SEED_DIR = 'seed';
|
||||
|
||||
function getScriptsDir(): string {
|
||||
return app.isPackaged
|
||||
? path.join(__dirname, 'scripts')
|
||||
: path.join(process.cwd(), 'electron/scripts');
|
||||
}
|
||||
|
||||
function ensureScriptsDir(): void {
|
||||
const dir = getScriptsDir();
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function getMetaPath(): string {
|
||||
return path.join(getScriptsDir(), META_FILENAME);
|
||||
}
|
||||
|
||||
function readMeta(): ScriptsMeta {
|
||||
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 as ScriptsMeta;
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn('[script-store-service] Failed to read meta:', err);
|
||||
}
|
||||
return { scripts: [] };
|
||||
}
|
||||
|
||||
function writeMeta(meta: ScriptsMeta): void {
|
||||
ensureScriptsDir();
|
||||
const metaPath = getMetaPath();
|
||||
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
function sanitizeFilename(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
|| 'script';
|
||||
}
|
||||
|
||||
function generateUniqueFilename(name: string, existingNames: Set<string>): string {
|
||||
const base = sanitizeFilename(name);
|
||||
let filename = `${base}.mjs`;
|
||||
let counter = 1;
|
||||
while (existingNames.has(filename)) {
|
||||
filename = `${base}-${counter}.mjs`;
|
||||
counter++;
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
function seedScripts(): void {
|
||||
const scriptsDir = getScriptsDir();
|
||||
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: ScriptsMeta = { 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 = 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);
|
||||
}
|
||||
|
||||
export function initScriptStoreService(): void {
|
||||
ensureScriptsDir();
|
||||
seedScripts();
|
||||
}
|
||||
|
||||
export function listScripts(): AutomationScript[] {
|
||||
const meta = readMeta();
|
||||
return meta.scripts
|
||||
.map((item) => enrichWithCode(item))
|
||||
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
}
|
||||
|
||||
export function getScript(id: string): AutomationScript | null {
|
||||
const meta = readMeta();
|
||||
const item = meta.scripts.find((s) => s.id === id);
|
||||
if (!item) return null;
|
||||
return enrichWithCode(item);
|
||||
}
|
||||
|
||||
export function getScriptPathById(id: string): string | null {
|
||||
const meta = readMeta();
|
||||
const item = meta.scripts.find((s) => s.id === id);
|
||||
if (!item) return null;
|
||||
return path.join(getScriptsDir(), item.filename);
|
||||
}
|
||||
|
||||
export function saveScript(input: ScriptSaveInput): AutomationScript {
|
||||
const meta = readMeta();
|
||||
const scriptsDir = getScriptsDir();
|
||||
const existingNames = new Set(meta.scripts.map((s) => s.filename));
|
||||
const now = 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 filePath = path.join(scriptsDir, existing.filename);
|
||||
fs.writeFileSync(filePath, 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: ScriptMetaItem = {
|
||||
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);
|
||||
}
|
||||
|
||||
export function deleteScript(id: string): boolean {
|
||||
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(), 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;
|
||||
}
|
||||
|
||||
export function toggleScript(id: string, enabled: boolean): boolean {
|
||||
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 = new Date().toISOString();
|
||||
writeMeta(meta);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function updateLastRun(id: string, lastRun: NonNullable<AutomationScript['lastRun']>): boolean {
|
||||
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 = new Date().toISOString();
|
||||
writeMeta(meta);
|
||||
return true;
|
||||
}
|
||||
|
||||
function enrichWithCode(item: ScriptMetaItem): AutomationScript {
|
||||
const scriptsDir = getScriptsDir();
|
||||
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,
|
||||
} as AutomationScript;
|
||||
}
|
||||
Reference in New Issue
Block a user