尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Claude Code 100个真实案例 - 用AI搭建定时任务调度系统(Cron+Web管理面板)

发布时间:2026/9/27 18:45:52

资讯中心
01
ARTICLE

Claude Code 100个真实案例 - 用AI搭建定时任务调度系统(Cron+Web管理面板)

Claude Code 100个真实案例 - 用AI搭建定时任务调度系统(Cron+Web管理面板)
1. 为什么我劝你别再手写 crontab 了服务器上跑着十几条定时任务crontab -e打开是一屏密密麻麻的星号表达式谁写的、干什么用的、上次跑成功没有全靠注释和记忆。这就是我三个月前的真实状态每天凌晨 2 点的数据统计脚本某天静默失败了直到三天后业务方问“日报怎么没发”才发现。crontab本身没问题问题在于它只负责“到点执行”不负责“执行得怎么样”。定时任务调度系统要解决的就是这件事把 Cron 表达式、任务处理逻辑、执行日志、失败重试、超时控制、可视化面板这几件事拆开又串起来。Cron 负责“什么时候跑”handler 负责“跑什么”日志表负责“跑得怎么样”Web 面板负责“让你一眼看到”。这套东西用 Node.js node-cron better-sqlite3 Express 就能搭起来前端用 Vue 3 Element Plus 做个管理界面整体不超过 800 行核心代码。这篇面向的是需要可视化管控服务器定时任务的开发者——你可能在维护一套中小型后端任务数量在 10 到 100 之间用 K8s CronJob 太重用 crontab 又太黑盒。我会把 Claude Code 从零搭建这套系统的完整路径拆成可复制的步骤数据库表结构、调度引擎核心、任务处理函数注册、Express API 路由、以及通过 TaoToken 统一 Key 通道接入 AI 能力的 settings.json 配置骨架。每一步都有可运行的代码和验证动作你跟着敲就能跑起来。2. 前置准备环境、依赖与 TaoToken 统一 Key 通道2.1 环境检查与依赖清单先把环境对齐版本不对后面会踩坑。Node.js 需要 20 以上因为用到了 ES Module 和node:sqlite的部分特性pnpm 用 9 以上Claude Code 用最新版即可。node -v # 需要 v20 pnpm -v # 需要 9 claude --version # 最新版依赖清单如下直接pnpm add装齐工具版本用途Node.js20运行环境Express4.xWeb API 框架node-cron3.xCron 调度引擎better-sqlite311.x任务配置和日志存储Vue 33.4管理面板前端Element Plus2.xUI 组件库Claude Code最新版AI 编程助手mkdir task-scheduler cd task-scheduler pnpm init pnpm add express cors node-cron better-sqlite3 uuid pnpm add -D nodemon2.2 TaoToken 统一 Key 通道配置Claude Code 在项目里跑的时候需要一个统一的模型接入通道。我试过在多个项目里各自维护 Key切换环境时特别容易搞混后来统一走 TaoToken 的 API 通道一个 Key 管所有项目的模型调用。配置写在项目根目录的.claude/settings.json里骨架如下{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoToken密钥 }, permissions: { allow: [ Bash(pnpm:*), Bash(node:*), Read, Write, Edit ] } }这里ANTHROPIC_BASE_URL指向 TaoToken 的 API 地址ANTHROPIC_API_KEY填你在控制台生成的 Key。Key 的获取路径是登录官网后进控制台在 API Keys 页面新建一个复制出来填进去。接入文档里有完整的参数说明遇到 401 或 404 先对照文档检查 Base URL 有没有多写或少写路径。注意settings.json 里的 Key 不要提交到 Git建议用.env加.gitignore管理或者用 Claude Code 的环境变量注入方式。配置好之后在项目目录下运行claude它会自动读取这个 settings.json后续所有代码生成、文件读写、命令执行都走这条通道。如果你只是想在浏览器里验证模型是否通可以直接用模型对话页面发一条测试消息确认 Key 有效再回到项目里。3. 可复制配置数据库表结构与调度引擎核心3.1 数据库初始化调度系统的数据分两张表tasks存任务配置task_logs存每次执行的日志。用 better-sqlite3 同步 API省去回调地狱。// src/database.js import Database from better-sqlite3; import path from path; import { fileURLToPath } from url; const __dirname path.dirname(fileURLToPath(import.meta.url)); const dbPath path.join(__dirname, ../data/scheduler.db); const db new Database(dbPath); db.pragma(journal_mode WAL); db.exec( CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT DEFAULT , cron_expression TEXT NOT NULL, handler_name TEXT NOT NULL, handler_params TEXT DEFAULT {}, enabled INTEGER DEFAULT 1, timeout_seconds INTEGER DEFAULT 60, max_retries INTEGER DEFAULT 3, retry_delay_ms INTEGER DEFAULT 5000, last_run_at TEXT, last_status TEXT, run_count INTEGER DEFAULT 0, fail_count INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)), updated_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE IF NOT EXISTS task_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, task_name TEXT NOT NULL, status TEXT NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, duration_ms INTEGER, output TEXT, error TEXT, retry_count INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE INDEX IF NOT EXISTS idx_task_logs_task_id ON task_logs(task_id); CREATE INDEX IF NOT EXISTS idx_task_logs_status ON task_logs(status); CREATE INDEX IF NOT EXISTS idx_task_logs_created ON task_logs(created_at); ); export default db;tasks表里几个关键字段cron_expression存表达式handler_name对应代码里注册的处理函数名timeout_seconds和max_retries控制超时与重试enabled控制启停。task_logs表记录每次执行的status、duration_ms、output、error这是后面排查问题的核心依据。3.2 调度引擎核心调度引擎是整个系统的心脏它要做四件事注册处理函数、创建任务、按 Cron 调度、执行并记录日志。核心难点是任务锁和超时控制。// src/scheduler/engine.js import cron from node-cron; import { v4 as uuidv4 } from uuid; import db from ../database.js; class SchedulerEngine { constructor() { this.jobs new Map(); this.handlers new Map(); this.runningTasks new Set(); this.isRunning false; } registerHandler(name, handler) { this.handlers.set(name, handler); console.log(注册任务处理函数: ${name}); } createTask(config) { const { name, description , cronExpression, handlerName, handlerParams {}, enabled true, timeoutSeconds 60, maxRetries 3, retryDelayMs 5000, } config; if (!cron.validate(cronExpression)) { throw new Error(无效的 Cron 表达式: ${cronExpression}); } if (!this.handlers.has(handlerName)) { throw new Error(处理函数 ${handlerName} 未注册); } const taskId uuidv4().slice(0, 8); db.prepare( INSERT INTO tasks (id, name, description, cron_expression, handler_name, handler_params, enabled, timeout_seconds, max_retries, retry_delay_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ).run(taskId, name, description, cronExpression, handlerName, JSON.stringify(handlerParams), enabled ? 1 : 0, timeoutSeconds, maxRetries, retryDelayMs); if (enabled) this._scheduleTask(taskId); console.log(任务创建成功: ${name} (${taskId}) - ${cronExpression}); return taskId; } _scheduleTask(taskId) { const task db.prepare(SELECT * FROM tasks WHERE id ?).get(taskId); if (!task || !task.enabled) return; if (this.jobs.has(taskId)) this.jobs.get(taskId).stop(); const job cron.schedule(task.cron_expression, async () { await this._executeTask(taskId); }, { scheduled: true, timezone: Asia/Shanghai }); this.jobs.set(taskId, job); console.log(任务已调度: ${task.name} - ${task.cron_expression}); } async _executeTask(taskId, retryCount 0) { const task db.prepare(SELECT * FROM tasks WHERE id ?).get(taskId); if (!task) return; if (this.runningTasks.has(taskId)) { console.log(任务 ${task.name} 正在执行中跳过本次触发); return; } this.runningTasks.add(taskId); const startTime Date.now(); const logId this._createLog(taskId, task.name, retryCount); console.log(开始执行任务: ${task.name} (重试: ${retryCount})); try { const handler this.handlers.get(task.handler_name); if (!handler) throw new Error(处理函数 ${task.handler_name} 未找到); const params JSON.parse(task.handler_params || {}); const result await Promise.race([ handler(params), new Promise((_, reject) setTimeout( () reject(new Error(任务执行超时${task.timeout_seconds}秒)), task.timeout_seconds * 1000 )), ]); const duration Date.now() - startTime; this._updateLog(logId, success, duration, JSON.stringify(result || OK)); this._updateTaskStatus(taskId, success); console.log(任务完成: ${task.name}耗时 ${duration}ms); } catch (err) { const duration Date.now() - startTime; console.error(任务失败: ${task.name} - ${err.message}); this._updateLog(logId, failed, duration, null, err.message); this._updateTaskStatus(taskId, failed); if (retryCount task.max_retries) { const delay task.retry_delay_ms * (retryCount 1); console.log(${delay / 1000}秒后重试${retryCount 1}/${task.max_retries}); setTimeout(() this._executeTask(taskId, retryCount 1), delay); } else { console.log(任务 ${task.name} 重试${task.max_retries}次后仍然失败); this._sendAlert(task, err.message); } } finally { this.runningTasks.delete(taskId); } } _createLog(taskId, taskName, retryCount) { const result db.prepare( INSERT INTO task_logs (task_id, task_name, status, started_at, retry_count) VALUES (?, ?, running, datetime(now, localtime), ?) ).run(taskId, taskName, retryCount); return result.lastInsertRowid; } _updateLog(logId, status, durationMs, output, error) { db.prepare( UPDATE task_logs SET status ?, finished_at datetime(now, localtime), duration_ms ?, output ?, error ? WHERE id ? ).run(status, durationMs, output || null, error || null, logId); } _updateTaskStatus(taskId, status) { const field status failed ? fail_count fail_count 1, : ; db.prepare( UPDATE tasks SET last_run_at datetime(now, localtime), last_status ?, ${field} run_count run_count 1, updated_at datetime(now, localtime) WHERE id ? ).run(status, taskId); } _sendAlert(task, errorMessage) { console.log(\n 任务失败告警 ); console.log( 任务名称: ${task.name}); console.log( 任务ID: ${task.id}); console.log( Cron: ${task.cron_expression}); console.log( 错误: ${errorMessage}); console.log( 时间: ${new Date().toLocaleString(zh-CN)}); } async triggerTask(taskId) { return this._executeTask(taskId); } pauseTask(taskId) { const job this.jobs.get(taskId); if (job) { job.stop(); db.prepare(UPDATE tasks SET enabled 0, updated_at datetime(now,localtime) WHERE id ?).run(taskId); console.log(任务已暂停: ${taskId}); } } resumeTask(taskId) { db.prepare(UPDATE tasks SET enabled 1, updated_at datetime(now,localtime) WHERE id ?).run(taskId); this._scheduleTask(taskId); console.log(任务已恢复: ${taskId}); } deleteTask(taskId) { const job this.jobs.get(taskId); if (job) job.stop(); this.jobs.delete(taskId); db.prepare(DELETE FROM tasks WHERE id ?).run(taskId); console.log(任务已删除: ${taskId}); } start() { const tasks db.prepare(SELECT * FROM tasks WHERE enabled 1).all(); console.log(\n启动调度引擎加载 ${tasks.length} 个任务...); for (const task of tasks) this._scheduleTask(task.id); this.isRunning true; console.log(调度引擎已启动\n); } stop() { for (const [, job] of this.jobs) job.stop(); this.jobs.clear(); this.isRunning false; console.log(调度引擎已停止); } listTasks(page 1, size 20) { const offset (page - 1) * size; const list db.prepare(SELECT * FROM tasks ORDER BY created_at DESC LIMIT ? OFFSET ?).all(size, offset); const total db.prepare(SELECT COUNT(*) as total FROM tasks).get(); return { list, total: total.total, page, size }; } getTaskLogs(taskId, page 1, size 20) { const offset (page - 1) * size; const where taskId ? WHERE task_id ? : ; const params taskId ? [taskId, size, offset] : [size, offset]; const list db.prepare(SELECT * FROM task_logs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?).all(...params); const totalParams taskId ? [taskId] : []; const total db.prepare(SELECT COUNT(*) as total FROM task_logs ${where}).get(...totalParams); return { list, total: total.total, page, size }; } getStatus() { const totalTasks db.prepare(SELECT COUNT(*) as c FROM tasks).get().c; const enabledTasks db.prepare(SELECT COUNT(*) as c FROM tasks WHERE enabled 1).get().c; const todayLogs db.prepare(SELECT COUNT(*) as c FROM task_logs WHERE date(created_at) date(now, localtime)).get().c; const todayFails db.prepare(SELECT COUNT(*) as c FROM task_logs WHERE status failed AND date(created_at) date(now, localtime)).get().c; return { isRunning: this.isRunning, totalTasks, enabledTasks, runningCount: this.runningTasks.size, todayExecutions: todayLogs, todayFailures: todayFails, todaySuccessRate: todayLogs 0 ? (((todayLogs - todayFails) / todayLogs) * 100).toFixed(1) % : 100%, }; } } export const scheduler new SchedulerEngine();这段代码里有三个设计点值得单独说。第一runningTasks用 Set 做任务锁同一个任务上一次还没跑完就跳过本次触发避免并发重复执行导致数据错乱。第二超时控制用Promise.race把 handler 和一个定时 reject 的 Promise 赛跑谁先完成用谁的结果这样即使 handler 里有个死循环也不会拖垮整个调度器。第三重试延迟用retry_delay_ms * (retryCount 1)做递增第一次失败等 5 秒第二次等 10 秒第三次等 15 秒避免短时间内密集重试把下游服务打挂。3.3 任务处理函数注册处理函数和调度逻辑分离是这套系统好维护的关键。handler 只关心“做什么”不关心“什么时候做”。// src/handlers/index.js import { scheduler } from ../scheduler/engine.js; function sleep(ms) { return new Promise(resolve setTimeout(resolve, ms)); } export function registerHandlers() { scheduler.registerHandler(dailyStats, async (params) { console.log(开始统计昨日数据...); await sleep(1000); const stats { date: new Date(Date.now() - 86400000).toISOString().split(T)[0], orders: Math.floor(Math.random() * 500) 100, revenue: (Math.random() * 100000 10000).toFixed(2), newUsers: Math.floor(Math.random() * 200) 50, }; console.log(统计完成: 订单${stats.orders}单, 收入¥${stats.revenue}); return stats; }); scheduler.registerHandler(cacheCleanup, async () { console.log(开始清理过期缓存...); await sleep(500); const cleaned Math.floor(Math.random() * 100); const remaining Math.floor(Math.random() * 5000) 1000; console.log(清理完成: 清除 ${cleaned} 个过期键剩余 ${remaining} 个); return { cleaned, remaining }; }); scheduler.registerHandler(dbBackup, async () { const filename backup_${new Date().toISOString().split(T)[0]}.sql.gz; console.log(开始数据库备份: ${filename}...); await sleep(3000); const sizeMB (Math.random() * 100 10).toFixed(2); console.log(备份完成: ${filename} (${sizeMB}MB)); return { filename, size: ${sizeMB}MB, path: /backups/${filename} }; }); scheduler.registerHandler(orderTimeout, async (params) { const timeoutMinutes params.timeoutMinutes || 30; console.log(扫描超过 ${timeoutMinutes} 分钟未支付的订单...); await sleep(800); const found Math.floor(Math.random() * 20); console.log(找到 ${found} 个超时订单已全部关闭); return { found, closed: found, timeoutMinutes }; }); scheduler.registerHandler(healthCheck, async (params) { const services params.services || [user-service, order-service, payment-service]; console.log(检查 ${services.length} 个服务健康状态...); const results {}; for (const svc of services) { await sleep(200); const healthy Math.random() 0.1; results[svc] { status: healthy ? healthy : unhealthy, responseTime: ${Math.floor(Math.random() * 100 10)}ms }; if (!healthy) console.log(${svc} 服务异常); } const unhealthy Object.values(results).filter(r r.status unhealthy).length; if (unhealthy 0) throw new Error(${unhealthy} 个服务不健康: ${JSON.stringify(results)}); console.log(所有服务正常); return results; }); scheduler.registerHandler(alwaysFail, async () { throw new Error(这是一个测试失败的任务); }); console.log(所有任务处理函数注册完成); }// src/handlers/init-tasks.js import { scheduler } from ../scheduler/engine.js; import db from ../database.js; export function initDefaultTasks() { const count db.prepare(SELECT COUNT(*) as c FROM tasks).get().c; if (count 0) { console.log(已有 ${count} 个任务跳过初始化); return; } console.log(初始化默认任务...); scheduler.createTask({ name: 每日数据统计, description: 统计昨日订单数、销售额、新增用户等数据, cronExpression: 0 2 * * *, handlerName: dailyStats, timeoutSeconds: 120, }); scheduler.createTask({ name: 缓存清理, description: 清理 Redis 中已过期的缓存数据, cronExpression: 0 * * * *, handlerName: cacheCleanup, timeoutSeconds: 30, }); scheduler.createTask({ name: 超时订单检查, description: 扫描超过30分钟未支付的订单并自动关闭, cronExpression: */5 * * * *, handlerName: orderTimeout, handlerParams: { timeoutMinutes: 30 }, timeoutSeconds: 60, }); scheduler.createTask({ name: 数据库备份, description: 备份 MySQL 数据库到云存储, cronExpression: 0 3 * * *, handlerName: dbBackup, timeoutSeconds: 300, }); scheduler.createTask({ name: 服务健康检查, description: 检查各微服务的健康状态, cronExpression: */10 * * * *, handlerName: healthCheck, handlerParams: { services: [user-svc, order-svc, pay-svc] }, timeoutSeconds: 30, maxRetries: 1, }); console.log(默认任务初始化完成); }3.4 Express API 路由API 层负责把调度引擎的能力暴露给 Web 面板。路由设计遵循 RESTful 风格任务操作用 POST查询用 GET。// src/app.js import express from express; import cors from cors; import path from path; import { fileURLToPath } from url; import { scheduler } from ./scheduler/engine.js; import { registerHandlers } from ./handlers/index.js; import { initDefaultTasks } from ./handlers/init-tasks.js; import db from ./database.js; const __dirname path.dirname(fileURLToPath(import.meta.url)); const app express(); app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, ../public))); app.get(/api/dashboard, (req, res) { const status scheduler.getStatus(); const recentLogs db.prepare(SELECT * FROM task_logs ORDER BY created_at DESC LIMIT 10).all(); const hourlyStats db.prepare( SELECT strftime(%H, created_at) as hour, COUNT(*) as total, SUM(CASE WHEN status success THEN 1 ELSE 0 END) as success, SUM(CASE WHEN status failed THEN 1 ELSE 0 END) as failed FROM task_logs WHERE date(created_at) date(now, localtime) GROUP BY hour ORDER BY hour ).all(); res.json({ code: 200, data: { status, recentLogs, hourlyStats } }); }); app.get(/api/tasks, (req, res) { const { page 1, size 20 } req.query; res.json({ code: 200, data: scheduler.listTasks(Number(page), Number(size)) }); }); app.post(/api/tasks, (req, res) { try { const taskId scheduler.createTask(req.body); res.json({ code: 200, message: 任务创建成功, data: { id: taskId } }); } catch (err) { res.status(400).json({ code: 400, message: err.message }); } }); app.put(/api/tasks/:id, (req, res) { try { const { id } req.params; const { name, description, cronExpression, enabled, timeoutSeconds, maxRetries } req.body; const updates []; const values []; if (name ! undefined) { updates.push(name ?); values.push(name); } if (description ! undefined) { updates.push(description ?); values.push(description); } if (cronExpression ! undefined) { updates.push(cron_expression ?); values.push(cronExpression); } if (enabled ! undefined) { updates.push(enabled ?); values.push(enabled ? 1 : 0); } if (timeoutSeconds ! undefined) { updates.push(timeout_seconds ?); values.push(timeoutSeconds); } if (maxRetries ! undefined) { updates.push(max_retries ?); values.push(maxRetries); } if (updates.length 0) return res.status(400).json({ code: 400, message: 没有需要更新的字段 }); updates.push(updated_at datetime(now,localtime)); values.push(id); db.prepare(UPDATE tasks SET ${updates.join(, )} WHERE id ?).run(...values); if (cronExpression ! undefined || enabled ! undefined) { if (enabled false) scheduler.pauseTask(id); else scheduler.resumeTask(id); } res.json({ code: 200, message: 任务更新成功 }); } catch (err) { res.status(400).json({ code: 400, message: err.message }); } }); app.delete(/api/tasks/:id, (req, res) { scheduler.deleteTask(req.params.id); res.json({ code: 200, message: 任务已删除 }); }); app.post(/api/tasks/:id/trigger, async (req, res) { try { res.json({ code: 200, message: 任务已触发正在后台执行 }); scheduler.triggerTask(req.params.id); } catch (err) { res.status(400).json({ code: 400, message: err.message }); } }); app.post(/api/tasks/:id/pause, (req, res) { scheduler.pauseTask(req.params.id); res.json({ code: 200, message: 任务已暂停 }); }); app.post(/api/tasks/:id/resume, (req, res) { scheduler.resumeTask(req.params.id); res.json({ code: 200, message: 任务已恢复 }); }); app.get(/api/tasks/:id/logs, (req, res) { const { page 1, size 20 } req.query; res.json({ code: 200, data: scheduler.getTaskLogs(req.params.id, Number(page), Number(size)) }); }); app.get(/api/logs, (req, res) { const { page 1, size 50, status, taskId } req.query; const offset (Number(page) - 1) * Number(size); let where 11; const params []; if (status) { where AND status ?; params.push(status); } if (taskId) { where AND task_id ?; params.push(taskId); } const list db.prepare(SELECT * FROM task_logs WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?).all(...params, Number(size), offset); const total db.prepare(SELECT COUNT(*) as total FROM task_logs WHERE ${where}).get(...params); res.json({ code: 200, data: { list, total: total.total, page: Number(page), size: Number(size) } }); }); app.post(/api/cron/validate, async (req, res) { const { expression } req.body; const cron await import(node-cron); res.json({ code: 200, data: { expression, valid: cron.validate(expression) } }); }); async function start() { registerHandlers(); initDefaultTasks(); scheduler.start(); const PORT 3000; app.listen(PORT, () { console.log(\n任务调度系统已启动: http://localhost:${PORT}); console.log(管理面板: http://localhost:${PORT}); console.log(API 文档: http://localhost:${PORT}/api/tasks); }); } start(); process.on(SIGINT, () { console.log(\n正在关闭调度系统...); scheduler.stop(); db.close(); process.exit(0); });4. 验证请求与成功结果4.1 启动与任务加载验证pnpm dev预期输出所有任务处理函数注册完成 初始化默认任务... 任务创建成功: 每日数据统计 (a1b2c3d4) - 0 2 * * * 任务创建成功: 缓存清理 (e5f6g7h8) - 0 * * * * 任务创建成功: 超时订单检查 (i9j0k1l2) - */5 * * * * 任务创建成功: 数据库备份 (m3n4o5p6) - 0 3 * * * 任务创建成功: 服务健康检查 (q7r8s9t0) - */10 * * * * 启动调度引擎加载 5 个任务... 任务已调度: 每日数据统计 - 0 2 * * * 任务已调度: 缓存清理 - 0 * * * * 任务已调度: 超时订单检查 - */5 * * * * 任务已调度: 数据库备份 - 0 3 * * * 任务已调度: 服务健康检查 - */10 * * * * 调度引擎已启动 任务调度系统已启动: http://localhost:3000看到 5 个任务全部调度成功说明数据库初始化、handler 注册、Cron 调度三个环节都通了。4.2 手动触发任务验证用 curl 手动触发“每日数据统计”任务验证执行链路curl -X POST http://localhost:3000/api/tasks/a1b2c3d4/trigger返回{code:200,message:任务已触发正在后台执行}服务端日志开始执行任务: 每日数据统计 (重试: 0) 开始统计昨日数据... 统计完成: 订单356单, 收入¥58291.33 任务完成: 每日数据统计耗时 1023ms4.3 日志回显验证查询该任务的执行日志curl http://localhost:3000/api/tasks/a1b2c3d4/logs?page1size5返回的list里能看到status: success、duration_ms: 1023、output里存着统计结果的 JSON 字符串。这一步验证了日志写入和查询链路。4.4 失败重试验证触发alwaysFail任务需要先创建它观察重试日志curl -X POST http://localhost:3000/api/tasks/alwaysFail/trigger服务端会依次输出开始执行任务: 测试失败任务 (重试: 0) 任务失败: 测试失败任务 - 这是一个测试失败的任务 5秒后重试1/3 开始执行任务: 测试失败任务 (重试: 1) 任务失败: 测试失败任务 - 这是一个测试失败的任务 10秒后重试2/3 ... 任务 测试失败任务 重试3次后仍然失败 任务失败告警 重试间隔递增、最终触发告警说明失败处理逻辑正常。5. 本篇常见错排查5.1 Cron 表达式无效导致任务创建失败报错无效的 Cron 表达式: xxx。node-cron 用的是标准 5 段式分 时 日 月 周不支持 6 段式带秒的写法。如果你从别处复制了0 0 2 * * ?这种 Quartz 风格的表达式去掉最后的?和秒位改成0 2 * * *。验证表达式是否合法可以直接调/api/cron/validate接口或者用模型对话让 AI 帮你翻译成标准 5 段式。5.2 处理函数未注册报错处理函数 xxx 未注册。检查registerHandlers()里有没有对应的scheduler.registerHandler(xxx, ...)以及createTask时传的handlerName是否和注册名完全一致大小写敏感。常见坑是注册时写dailyStats创建任务时写dailystats。5.3 任务执行超时日志里出现任务执行超时60秒。先确认timeout_seconds设置是否合理数据库备份这类任务可能需要 300 秒。如果 handler 里有网络请求检查是不是下游服务响应慢。超时后任务会被标记为 failed 并进入重试流程如果重试也超时说明需要优化 handler 本身或者调大超时阈值。5.4 SQLite 数据库锁报错SQLITE_BUSY: database is locked。better-sqlite3 是同步 API正常情况下不会锁但如果多个进程同时打开同一个 db 文件就会冲突。确保只有一个 Node 进程在跑WAL 模式已经开了能缓解读写并发。如果确实需要多进程考虑换成 PostgreSQL。5.5 任务重复执行同一个任务在短时间内被执行了两次。检查runningTasks锁是否生效——如果任务执行时间极短比如 10ms上一次已经释放锁下一次 Cron 触发时确实会再执行一次这是正常的。如果任务执行时间较长却仍然重复检查是不是有多个调度实例在跑或者_scheduleTask被调用了两次导致创建了两个 Cron job。5.6 TaoToken 通道 401/404Claude Code 报 401 或 404。先检查.claude/settings.json里的ANTHROPIC_BASE_URL是不是https://taotoken.net/api注意不要多加/v1或末尾斜杠。再检查ANTHROPIC_API_KEY是否有效可以去控制台的 API Keys 页面确认 Key 状态。如果还是不通对照接入文档逐项核对参数或者用模型对话页面单独测试 Key 是否可用。6. 把 AI 能力接进调度系统这套系统跑起来之后你会发现有些任务本身就需要 AI 能力——比如每天凌晨统计完数据后让模型生成一段自然语言的数据摘要或者健康检查失败时让模型分析错误日志给出可能的原因。这时候 TaoToken 的统一 Key 通道就派上用场了调度系统里的 handler 通过 HTTP 调用 TaoToken 的 API和 Claude Code 共用同一个 Key不用在项目里维护多套凭证。具体做法是在 handler 里加一个callAI函数把 prompt 发给模型拿回结果存进日志的output字段。这样 Web 面板上不仅能看任务成功失败还能直接看到 AI 生成的分析结论。如果你打算长期跑这类带 AI 能力的定时任务Coding Plan 的额度模式比按次调用更划算适合任务数量多、调用频率稳定的场景。整套系统的代码结构最终是这样的task-scheduler/ ├── src/ │ ├── database.js # 数据库初始化 │ ├── app.js # 应用入口 API 路由 │ ├── scheduler/ │ │ └── engine.js # 调度引擎核心 │ └── handlers/ │ ├── index.js # 任务处理函数注册 │ └── init-tasks.js # 默认任务初始化 ├── data/ │ └── scheduler.db # SQLite 数据库 ├── public/ # 前端管理面板 └── package.json前端 Vue 3 Element Plus 的面板部分核心是三个页面任务列表页表格展示 启停/触发/删除操作、任务详情页Cron 表达式编辑 执行日志表格、仪表盘页今日执行次数、成功率、最近日志。API 已经全部就绪前端直接调/api/tasks、/api/logs、/api/dashboard就行。面板的路由和组件代码量不大用 Claude Code 生成一版基础 CRUD 页面大概十几分钟重点是把任务状态和日志的实时刷新做好用setInterval每 10 秒拉一次/api/dashboard就够了。踩过的坑里最值得说的是任务锁和超时控制这两个点。一开始我没加锁结果“超时订单检查”这个每 5 分钟跑一次的任务某次执行卡了 8 分钟期间又触发了两次三个实例同时扫同一批订单差点重复关闭。加了runningTasks锁之后这个问题就没了。超时控制也是必须的一个 handler 里的 HTTP 请求没设 timeout下游服务挂了之后这个任务就永远卡在 running 状态日志表里全是没结束的记录。用Promise.race兜底之后再慢的任务也会在timeout_seconds后被强制标记为失败并进入重试流程。如果你在接入过程中遇到 API 报错或者配置问题先去 API Keys 页面确认 Key 状态再对照接入文档检查 Base URL 和请求头。需要验证模型是否正常响应直接用模型对话发一条测试消息最快。长期跑编码和 Agent 类任务的话Coding Plan 的额度模式更适合持续调用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。