feat: 新增脚本录制功能
This commit is contained in:
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