247 lines
6.5 KiB
TypeScript
247 lines
6.5 KiB
TypeScript
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;
|
|
}
|