This commit is contained in:
张成
2026-03-18 14:18:41 +08:00
parent 54341f0a0b
commit 5b671d320b
21 changed files with 4404 additions and 42 deletions

View File

@@ -0,0 +1,30 @@
import cron from 'node-cron';
const task_id_to_cron_job = new Map();
export function stop_all_cron_jobs() {
for (const job of task_id_to_cron_job.values()) {
job.stop();
}
task_id_to_cron_job.clear();
}
export function upsert_cron_job(schedule_task_id, cron_expression, on_tick) {
const existing = task_id_to_cron_job.get(schedule_task_id);
if (existing) {
existing.stop();
task_id_to_cron_job.delete(schedule_task_id);
}
const job = cron.schedule(cron_expression, on_tick, { scheduled: true });
task_id_to_cron_job.set(schedule_task_id, job);
}
export function remove_cron_job(schedule_task_id) {
const job = task_id_to_cron_job.get(schedule_task_id);
if (!job) {
return;
}
job.stop();
task_id_to_cron_job.delete(schedule_task_id);
}

View File

@@ -0,0 +1,18 @@
export function safe_json_stringify(value) {
try {
return JSON.stringify(value);
} catch (err) {
return JSON.stringify({ error: 'json_stringify_failed', message: String(err) });
}
}
export function safe_json_parse(text) {
if (text === null || text === undefined || text === '') {
return null;
}
try {
return JSON.parse(text);
} catch (err) {
return null;
}
}

View File

@@ -0,0 +1,102 @@
import dotenv from 'dotenv';
import path from 'node:path';
import puppeteer from 'puppeteer';
dotenv.config();
let browser_singleton = null;
function get_action_timeout_ms() {
return Number(process.env.ACTION_TIMEOUT_MS || 300000);
}
function get_crx_src_path() {
const crx_src_path = process.env.CRX_SRC_PATH;
if (!crx_src_path) {
throw new Error('缺少环境变量 CRX_SRC_PATH');
}
return crx_src_path;
}
function get_extension_id_from_targets(targets) {
for (const target of targets) {
const url = target.url();
if (!url) continue;
if (url.startsWith('chrome-extension://')) {
const match = url.match(/^chrome-extension:\/\/([^/]+)\//);
if (match && match[1]) return match[1];
}
}
return null;
}
export async function get_or_create_browser() {
if (browser_singleton) {
return browser_singleton;
}
const extension_path = path.resolve(get_crx_src_path());
const headless = String(process.env.PUPPETEER_HEADLESS || 'false') === 'true';
browser_singleton = await puppeteer.launch({
headless,
args: [
`--disable-extensions-except=${extension_path}`,
`--load-extension=${extension_path}`,
'--no-default-browser-check',
'--disable-popup-blocking',
'--disable-dev-shm-usage'
]
});
return browser_singleton;
}
export async function invoke_extension_action(action_name, action_payload) {
const browser = await get_or_create_browser();
const page = await browser.newPage();
await page.goto('about:blank');
const targets = await browser.targets();
const extension_id = get_extension_id_from_targets(targets);
if (!extension_id) {
await page.close();
throw new Error('未找到扩展 extension_id请确认 CRX_SRC_PATH 指向 src 且成功加载)');
}
const bridge_url = `chrome-extension://${extension_id}/bridge/bridge.html`;
await page.goto(bridge_url, { waitUntil: 'domcontentloaded' });
const timeout_ms = get_action_timeout_ms();
const action_res = await page.evaluate(
async (action, payload, timeout) => {
function with_timeout(promise, timeout_ms_inner) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('action_timeout')), timeout_ms_inner);
promise
.then((v) => {
clearTimeout(timer);
resolve(v);
})
.catch((e) => {
clearTimeout(timer);
reject(e);
});
});
}
if (!window.server_bridge_invoke) {
throw new Error('bridge 未注入 window.server_bridge_invoke');
}
return await with_timeout(window.server_bridge_invoke(action, payload), timeout);
},
action_name,
action_payload || {},
timeout_ms
);
await page.close();
return action_res;
}

View File

@@ -0,0 +1,33 @@
import { schedule_task } from '../models/index.js';
import { safe_json_parse } from './json_utils.js';
import { execute_action_and_record } from './task_executor.js';
import { remove_cron_job, upsert_cron_job } from './cron_manager.js';
export async function reload_all_schedules() {
const rows = await schedule_task.findAll();
for (const row of rows) {
if (!row.enabled) {
remove_cron_job(row.id);
continue;
}
upsert_cron_job(row.id, row.cron_expression, async () => {
try {
await schedule_task.update(
{ last_run_at: new Date() },
{ where: { id: row.id } }
);
await execute_action_and_record({
action_name: row.action_name,
action_payload: safe_json_parse(row.payload_json) || {},
source: 'cron',
schedule_task_id: row.id
});
} catch (err) {
// cron 执行失败已在 crawl_run_record 落库,避免重复抛出影响其它任务
}
});
}
}

View File

@@ -0,0 +1,39 @@
import { crawl_run_record } from '../models/index.js';
import { safe_json_stringify } from './json_utils.js';
import { invoke_extension_action } from './puppeteer_runner.js';
export async function execute_action_and_record(params) {
const {
action_name,
action_payload,
source,
schedule_task_id
} = params;
const request_payload = safe_json_stringify(action_payload || {});
let ok = false;
let result_payload = null;
let error_message = null;
try {
const result = await invoke_extension_action(action_name, action_payload || {});
ok = true;
result_payload = safe_json_stringify(result);
return result;
} catch (err) {
ok = false;
error_message = (err && err.message) || String(err);
throw err;
} finally {
await crawl_run_record.create({
action_name,
request_payload,
ok,
result_payload,
error_message,
source,
schedule_task_id: schedule_task_id || null
});
}
}