aktueller stand

This commit is contained in:
2025-12-15 16:12:38 +01:00
parent 6c83e4a7ee
commit b71d99b048
7 changed files with 1020 additions and 89 deletions

View File

@@ -30,6 +30,9 @@ const DAILY_BOOKMARK_TITLE_MAX_LENGTH = 160;
const DAILY_BOOKMARK_URL_MAX_LENGTH = 800;
const DAILY_BOOKMARK_NOTES_MAX_LENGTH = 800;
const DAILY_BOOKMARK_MARKER_MAX_LENGTH = 120;
const AUTOMATION_TYPE_REQUEST = 'request';
const AUTOMATION_TYPE_EMAIL = 'email';
const AUTOMATION_TYPE_FLOW = 'flow';
const AUTOMATION_MAX_NAME_LENGTH = 160;
const AUTOMATION_MAX_URL_LENGTH = 2000;
const AUTOMATION_MAX_BODY_LENGTH = 12000;
@@ -40,6 +43,9 @@ const AUTOMATION_DEFAULT_INTERVAL_MINUTES = 60;
const AUTOMATION_MAX_JITTER_MINUTES = 120;
const AUTOMATION_MAX_RESPONSE_PREVIEW = 4000;
const AUTOMATION_WORKER_INTERVAL_MS = 30000;
const AUTOMATION_MAX_STEPS = 3;
const AUTOMATION_MAX_EMAIL_TO_LENGTH = 320;
const AUTOMATION_MAX_EMAIL_SUBJECT_LENGTH = 320;
const SPORTS_SCORING_DEFAULTS = {
enabled: 1,
threshold: 5,
@@ -90,6 +96,54 @@ if (!fs.existsSync(screenshotDir)) {
fs.mkdirSync(screenshotDir, { recursive: true });
}
const automationConfigPath = path.join(__dirname, 'data', 'automation-config.json');
const defaultAutomationConfig = {
smtp: {
host: '',
port: 587,
secure: false,
user: '',
pass: '',
from: ''
}
};
function ensureAutomationConfigFile() {
try {
if (!fs.existsSync(automationConfigPath)) {
const dir = path.dirname(automationConfigPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(automationConfigPath, JSON.stringify(defaultAutomationConfig, null, 2), 'utf8');
console.log('Automation-Config angelegt unter', automationConfigPath);
}
} catch (error) {
console.warn('Konnte Automation-Config nicht erstellen:', error.message);
}
}
ensureAutomationConfigFile();
function loadAutomationConfig() {
try {
const raw = fs.readFileSync(automationConfigPath, 'utf8');
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : { ...defaultAutomationConfig };
} catch (error) {
return { ...defaultAutomationConfig };
}
}
const automationConfig = loadAutomationConfig();
let nodemailer = null;
try {
nodemailer = require('nodemailer');
} catch (error) {
nodemailer = null;
}
// Middleware - Enhanced CORS for extension
app.use(cors({
origin: (origin, callback) => {
@@ -1292,10 +1346,15 @@ db.exec(`
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
type TEXT NOT NULL DEFAULT 'request',
method TEXT NOT NULL DEFAULT 'GET',
url_template TEXT NOT NULL,
url_template TEXT,
headers_json TEXT,
body_template TEXT,
email_to TEXT,
email_subject_template TEXT,
email_body_template TEXT,
steps_json TEXT,
interval_minutes INTEGER NOT NULL DEFAULT ${AUTOMATION_DEFAULT_INTERVAL_MINUTES},
jitter_minutes INTEGER DEFAULT 0,
start_at DATETIME,
@@ -1311,6 +1370,12 @@ db.exec(`
);
`);
ensureColumn('automation_requests', 'type', 'type TEXT NOT NULL DEFAULT \'request\'');
ensureColumn('automation_requests', 'email_to', 'email_to TEXT');
ensureColumn('automation_requests', 'email_subject_template', 'email_subject_template TEXT');
ensureColumn('automation_requests', 'email_body_template', 'email_body_template TEXT');
ensureColumn('automation_requests', 'steps_json', 'steps_json TEXT');
db.exec(`
CREATE INDEX IF NOT EXISTS idx_automation_requests_next_run
ON automation_requests(next_run_at);
@@ -1342,10 +1407,15 @@ const listAutomationRequestsStmt = db.prepare(`
id,
name,
description,
type,
method,
url_template,
headers_json,
body_template,
email_to,
email_subject_template,
email_body_template,
steps_json,
interval_minutes,
jitter_minutes,
start_at,
@@ -1356,6 +1426,11 @@ const listAutomationRequestsStmt = db.prepare(`
last_status_code,
last_error,
next_run_at,
(
SELECT COUNT(1)
FROM automation_request_runs r
WHERE r.request_id = automation_requests.id
) AS runs_count,
created_at,
updated_at
FROM automation_requests
@@ -1367,10 +1442,15 @@ const getAutomationRequestStmt = db.prepare(`
id,
name,
description,
type,
method,
url_template,
headers_json,
body_template,
email_to,
email_subject_template,
email_body_template,
steps_json,
interval_minutes,
jitter_minutes,
start_at,
@@ -1389,11 +1469,13 @@ const getAutomationRequestStmt = db.prepare(`
const insertAutomationRequestStmt = db.prepare(`
INSERT INTO automation_requests (
id, name, description, method, url_template, headers_json, body_template,
id, name, description, type, method, url_template, headers_json, body_template,
email_to, email_subject_template, email_body_template, steps_json,
interval_minutes, jitter_minutes, start_at, run_until, active, last_run_at,
last_status, last_status_code, last_error, next_run_at
) VALUES (
@id, @name, @description, @method, @url_template, @headers_json, @body_template,
@id, @name, @description, @type, @method, @url_template, @headers_json, @body_template,
@email_to, @email_subject_template, @email_body_template, @steps_json,
@interval_minutes, @jitter_minutes, @start_at, @run_until, @active, @last_run_at,
@last_status, @last_status_code, @last_error, @next_run_at
)
@@ -1403,10 +1485,15 @@ const updateAutomationRequestStmt = db.prepare(`
UPDATE automation_requests
SET name = @name,
description = @description,
type = @type,
method = @method,
url_template = @url_template,
headers_json = @headers_json,
body_template = @body_template,
email_to = @email_to,
email_subject_template = @email_subject_template,
email_body_template = @email_body_template,
steps_json = @steps_json,
interval_minutes = @interval_minutes,
jitter_minutes = @jitter_minutes,
start_at = @start_at,
@@ -1486,7 +1573,6 @@ ensureColumn('ai_credentials', 'usage_24h_count', 'usage_24h_count INTEGER DEFAU
ensureColumn('ai_credentials', 'usage_24h_reset_at', 'usage_24h_reset_at DATETIME');
ensureColumn('search_seen_posts', 'manually_hidden', 'manually_hidden INTEGER NOT NULL DEFAULT 0');
ensureColumn('search_seen_posts', 'sports_auto_hidden', 'sports_auto_hidden INTEGER NOT NULL DEFAULT 0');
db.exec(`
CREATE TABLE IF NOT EXISTS ai_usage_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1927,6 +2013,14 @@ function renderAutomationTemplate(template, context = {}) {
return shifted.toISOString().slice(0, 10);
}
const dayOffsetMatch = key.match(/^day([+-]\d+)?$/);
if (dayOffsetMatch) {
const offset = dayOffsetMatch[1] ? parseInt(dayOffsetMatch[1], 10) : 0;
const shifted = new Date(baseDate);
shifted.setDate(baseDate.getDate() + (Number.isNaN(offset) ? 0 : offset));
return String(shifted.getDate()).padStart(2, '0');
}
switch (key) {
case 'today':
case 'date':
@@ -2082,11 +2176,24 @@ function serializeAutomationRequest(row) {
return {
id: row.id,
name: row.name,
type: row.type || AUTOMATION_TYPE_REQUEST,
description: row.description || '',
method: row.method || 'GET',
url_template: row.url_template,
headers,
body_template: row.body_template || '',
email_to: row.email_to || '',
email_subject_template: row.email_subject_template || '',
email_body_template: row.email_body_template || '',
steps: row.steps_json ? (() => {
try {
const parsed = JSON.parse(row.steps_json);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
return [];
}
})() : [],
runs_count: row.runs_count || 0,
interval_minutes: clampAutomationIntervalMinutes(row.interval_minutes),
jitter_minutes: clampAutomationJitterMinutes(row.jitter_minutes || 0),
start_at: row.start_at ? ensureIsoDate(row.start_at) : null,
@@ -2124,6 +2231,14 @@ function normalizeAutomationPayload(payload, existing = {}) {
const errors = [];
const normalized = {};
const rawType = typeof payload.type === 'string'
? payload.type.trim().toLowerCase()
: (existing.type || AUTOMATION_TYPE_REQUEST);
const type = [AUTOMATION_TYPE_REQUEST, AUTOMATION_TYPE_EMAIL, AUTOMATION_TYPE_FLOW].includes(rawType)
? rawType
: AUTOMATION_TYPE_REQUEST;
normalized.type = type;
const nameSource = typeof payload.name === 'string'
? payload.name
: existing.name || '';
@@ -2152,11 +2267,17 @@ function normalizeAutomationPayload(payload, existing = {}) {
const rawUrl = typeof payload.url_template === 'string'
? payload.url_template
: (typeof payload.url === 'string' ? payload.url : existing.url_template || '');
const urlTemplate = rawUrl.trim();
if (!urlTemplate) {
errors.push('URL-Template fehlt');
const urlTemplate = (rawUrl || '').trim();
if (type === AUTOMATION_TYPE_REQUEST || type === AUTOMATION_TYPE_FLOW) {
if (!urlTemplate && type === AUTOMATION_TYPE_REQUEST) {
errors.push('URL-Template fehlt');
}
normalized.url_template = urlTemplate
? truncateString(urlTemplate, AUTOMATION_MAX_URL_LENGTH)
: null;
} else {
normalized.url_template = truncateString(urlTemplate, AUTOMATION_MAX_URL_LENGTH);
normalized.url_template = null;
}
const headersInput = payload.headers
@@ -2184,6 +2305,82 @@ function normalizeAutomationPayload(payload, existing = {}) {
: '';
}
if (type === AUTOMATION_TYPE_EMAIL) {
const toValue = (payload.email_to || payload.to || existing.email_to || '').trim();
if (!toValue) {
errors.push('E-Mail Empfänger fehlt');
} else {
normalized.email_to = truncateString(toValue, AUTOMATION_MAX_EMAIL_TO_LENGTH);
}
const subjectValue = (payload.email_subject_template || payload.subject || existing.email_subject_template || '').trim();
if (!subjectValue) {
errors.push('E-Mail Betreff fehlt');
} else {
normalized.email_subject_template = truncateString(subjectValue, AUTOMATION_MAX_EMAIL_SUBJECT_LENGTH);
}
const bodyValue = typeof payload.email_body_template === 'string'
? payload.email_body_template
: (typeof payload.body_template === 'string' ? payload.body_template : existing.email_body_template || '');
if (!bodyValue || !String(bodyValue).trim()) {
errors.push('E-Mail Body fehlt');
} else {
normalized.email_body_template = truncateString(String(bodyValue), AUTOMATION_MAX_BODY_LENGTH);
}
normalized.steps_json = null;
normalized.url_template = '';
normalized.headers_json = null;
normalized.body_template = null;
} else if (type === AUTOMATION_TYPE_FLOW) {
const stepsInput = Array.isArray(payload.steps)
? payload.steps
: (typeof payload.steps_json === 'string' ? (() => {
try { return JSON.parse(payload.steps_json); } catch (err) { return []; }
})() : []);
const steps = [];
for (const rawStep of stepsInput) {
if (!rawStep || typeof rawStep !== 'object') continue;
const stepUrl = typeof rawStep.url === 'string' ? rawStep.url.trim() : '';
if (!stepUrl) continue;
const stepMethod = typeof rawStep.method === 'string'
? rawStep.method.toUpperCase()
: 'GET';
const allowed = allowedMethods.includes(stepMethod) ? stepMethod : 'GET';
const stepHeaders = normalizeAutomationHeaders(rawStep.headers || rawStep.headers_json || {});
const stepHeadersSerialized = serializeAutomationHeaders(stepHeaders);
steps.push({
method: allowed,
url: truncateString(stepUrl, AUTOMATION_MAX_URL_LENGTH),
headers: stepHeadersSerialized ? JSON.parse(stepHeadersSerialized) : {},
body: typeof rawStep.body === 'string'
? truncateString(rawStep.body, AUTOMATION_MAX_BODY_LENGTH)
: ''
});
if (steps.length >= AUTOMATION_MAX_STEPS) break;
}
if (!steps.length) {
errors.push('Mindestens ein Schritt mit URL ist erforderlich');
} else {
try {
const serializedSteps = JSON.stringify(steps);
normalized.steps_json = serializedSteps;
} catch (error) {
errors.push('Schritte konnten nicht gespeichert werden');
}
}
normalized.email_to = null;
normalized.email_subject_template = null;
normalized.email_body_template = null;
normalized.url_template = normalized.url_template || '';
} else {
normalized.email_to = null;
normalized.email_subject_template = null;
normalized.email_body_template = null;
normalized.steps_json = null;
}
const scheduleType = typeof payload.schedule_type === 'string'
? payload.schedule_type
: payload.interval_type;
@@ -3173,6 +3370,14 @@ function broadcastPostDeletion(postId, options = {}) {
broadcastSseEvent(payload);
}
function broadcastAutomationEvent(eventType, payload = {}) {
if (!eventType) {
return;
}
const data = { type: eventType, ...payload };
broadcastSseEvent(data);
}
const automationRunningRequests = new Set();
let automationWorkerTimer = null;
let automationWorkerBusy = false;
@@ -3196,36 +3401,187 @@ async function executeAutomationRequest(request, options = {}) {
automationRunningRequests.add(current.id);
const context = buildAutomationTemplateContext(new Date());
const method = (current.method || 'GET').toUpperCase();
const url = renderAutomationTemplate(current.url_template, context);
const headers = renderAutomationHeaders(current.headers_json, context);
const shouldSendBody = !['GET', 'HEAD'].includes(method);
const body = shouldSendBody && current.body_template
? renderAutomationTemplate(current.body_template, context)
: null;
const startedAt = new Date();
let status = 'success';
let statusCode = null;
let responseText = '';
let errorMessage = null;
const stepResults = [];
async function executeHttpStep(step, stepIndex, inheritedContext) {
const method = (step.method || 'GET').toUpperCase();
const url = renderAutomationTemplate(step.url_template || step.url, inheritedContext);
const headers = renderAutomationHeaders(step.headers_json || serializeAutomationHeaders(step.headers || {}), inheritedContext);
const shouldSendBody = !['GET', 'HEAD'].includes(method);
const body = shouldSendBody && (step.body_template || step.body)
? renderAutomationTemplate(step.body_template || step.body, inheritedContext)
: null;
let localStatus = 'success';
let localStatusCode = null;
let localResponseText = '';
let localError = null;
try {
const response = await fetch(url, {
method,
headers,
body: shouldSendBody && body !== null ? body : undefined,
redirect: 'follow'
});
localStatusCode = response.status;
try {
localResponseText = await response.text();
} catch (error) {
localResponseText = '';
}
if (!response.ok) {
localStatus = 'error';
localError = `HTTP ${response.status}`;
}
} catch (error) {
localStatus = 'error';
localError = error.message || 'Unbekannter Fehler';
}
let parsedJson = null;
if (localResponseText) {
try {
parsedJson = JSON.parse(localResponseText);
} catch (error) {
parsedJson = null;
}
}
const resultContext = {
...inheritedContext,
[`step${stepIndex}_text`]: localResponseText,
[`step${stepIndex}_status`]: localStatus,
[`step${stepIndex}_status_code`]: localStatusCode,
[`step${stepIndex}_json`]: parsedJson || null
};
return {
status: localStatus,
statusCode: localStatusCode,
responseText: localResponseText,
error: localError,
context: resultContext
};
}
async function executeEmailStep(stepContext) {
if (!nodemailer) {
throw new Error('E-Mail Versand nicht verfügbar (nodemailer fehlt)');
}
const {
SMTP_HOST,
SMTP_PORT,
SMTP_USER,
SMTP_PASS,
SMTP_SECURE,
SMTP_FROM
} = process.env;
const configSmtp = (automationConfig && automationConfig.smtp) || {};
const host = SMTP_HOST || configSmtp.host;
if (!host) {
throw new Error('SMTP_HOST nicht gesetzt und keine Konfiguration in automation-config.json');
}
const port = SMTP_PORT ? Number(SMTP_PORT) : (configSmtp.port || 587);
const secure = SMTP_SECURE === 'true' || SMTP_SECURE === '1'
? true
: (typeof configSmtp.secure === 'boolean' ? configSmtp.secure : port === 465);
const user = SMTP_USER || configSmtp.user || '';
const pass = SMTP_PASS || configSmtp.pass || '';
const from = SMTP_FROM || configSmtp.from || user || 'automation@example.com';
const transporter = nodemailer.createTransport({
host,
port,
secure,
auth: user ? { user, pass } : undefined
});
const to = renderAutomationTemplate(current.email_to || '', stepContext);
const subject = renderAutomationTemplate(current.email_subject_template || '', stepContext);
const body = renderAutomationTemplate(current.email_body_template || '', stepContext);
const bodyHtml = body ? body.replace(/\n/g, '<br>') : '';
if (!to || !subject || !body) {
throw new Error('E-Mail Felder unvollständig');
}
const info = await transporter.sendMail({
from,
to,
subject,
text: body,
html: bodyHtml || body
});
return {
status: 'success',
statusCode: info && info.accepted ? 200 : null,
responseText: info ? JSON.stringify({ messageId: info.messageId }) : '',
error: null,
context: { ...stepContext }
};
}
try {
const response = await fetch(url, {
method,
headers,
body: shouldSendBody && body !== null ? body : undefined,
redirect: 'follow'
});
statusCode = response.status;
try {
responseText = await response.text();
} catch (error) {
responseText = '';
}
if (!response.ok) {
status = 'error';
errorMessage = `HTTP ${response.status}`;
if (current.type === AUTOMATION_TYPE_EMAIL) {
const emailResult = await executeEmailStep(context);
status = emailResult.status;
statusCode = emailResult.statusCode;
responseText = emailResult.responseText;
errorMessage = emailResult.error;
} else if (current.type === AUTOMATION_TYPE_FLOW) {
let flowContext = { ...context };
const steps = current.steps_json ? (() => {
try {
const parsed = JSON.parse(current.steps_json);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
return [];
}
})() : [];
let stepIndex = 1;
for (const step of steps) {
const stepResult = await executeHttpStep({
method: step.method || step.http_method || 'GET',
url_template: step.url_template || step.url,
headers_json: serializeAutomationHeaders(step.headers || {}),
body_template: step.body_template || step.body
}, stepIndex, flowContext);
stepResults.push(stepResult);
flowContext = { ...flowContext, ...stepResult.context };
if (stepResult.status === 'error') {
status = 'error';
errorMessage = stepResult.error;
statusCode = stepResult.statusCode;
responseText = stepResult.responseText;
break;
} else {
statusCode = stepResult.statusCode;
responseText = stepResult.responseText;
}
stepIndex += 1;
}
} else {
const result = await executeHttpStep({
method: current.method,
url_template: current.url_template,
headers_json: current.headers_json,
body_template: current.body_template
}, 1, context);
status = result.status;
statusCode = result.statusCode;
responseText = result.responseText;
errorMessage = result.error;
stepResults.push(result);
}
} catch (error) {
status = 'error';
@@ -3259,6 +3615,15 @@ async function executeAutomationRequest(request, options = {}) {
)
: null;
broadcastAutomationEvent('automation-run', {
request_id: current.id,
status,
status_code: statusCode,
next_run_at: nextRunAt,
last_run_at: runRecord.started_at,
runs_count: (current.runs_count || 0) + 1
});
try {
updateAutomationRequestStmt.run({
...current,