R33: 工作台热点数据页更新按钮(纯图标+30min冷却防重+整页重建+CDP冒烟全绿)
This commit is contained in:
1 parent
9445b76485
commit
fa55d3eecb
6 files changed
+243
-4
No files matched your search
@@ -275,6 +275,66 @@ function log(msg) {
|
||||
console.log('[' + new Date().toLocaleTimeString('zh-CN', { hour12: false }) + '] ' + msg);
|
||||
}
|
||||
|
||||
// ---------- 榜单数据更新任务(09-03 新增:热点数据页「更新榜单」按钮后台任务)----------
|
||||
// 任务名固定用于防重识别(同名最近一次任务状态 = 是否有更新在跑 / 上次执行结果)
|
||||
const RANK_UPDATE_NAME = '更新榜单数据';
|
||||
const RANK_UPDATE_COOLDOWN_MS = 30 * 60 * 1000; // 防重复更新窗口:距上次成功落盘 < 30 分钟拦截(可 force 跳过)
|
||||
|
||||
// workbuddy 宿主库(automations/sessions/automation_runs),与 /api/run 同款路径解析
|
||||
function wbDb() {
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
return new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'));
|
||||
}
|
||||
|
||||
// 创建一次性后台任务(08-31 实测约定:scheduled_at 与 next_run_at 均取 +5s 未来,调度器按 next_run_at 扫描拾取;
|
||||
// 模型跟随用户最近活跃手动会话选择,兜底 'auto')——逻辑与 /api/run 完全一致,供更新榜单等新入口复用
|
||||
function createOnceAutomation(name, prompt, skillsArr) {
|
||||
const db = wbDb();
|
||||
try {
|
||||
const now = Date.now();
|
||||
const id = 'automation-' + now;
|
||||
const cwd = 'D:\\AgentSkill\\mcn-workshop'; // 工作台触发的会话归入 mcn-workshop 空间分组
|
||||
const d = new Date(now + 5 * 1000);
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const scheduledAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
const nextRunAt = now + 5 * 1000;
|
||||
const uidRow = db.prepare("SELECT user_id FROM sessions WHERE user_id IS NOT NULL AND user_id <> '' ORDER BY created_at DESC LIMIT 1").get();
|
||||
const ownerUserId = uidRow ? uidRow.user_id : '';
|
||||
const modelRow = db.prepare(`SELECT model FROM sessions WHERE deleted_at IS NULL AND COALESCE(is_background_automation,0)=0 AND model IS NOT NULL AND model <> '' ORDER BY COALESCE(last_activity_at, updated_at) DESC LIMIT 1`).get();
|
||||
const modelId = (modelRow && modelRow.model) ? modelRow.model : 'auto';
|
||||
db.prepare(`INSERT INTO automations (id,name,prompt,status,schedule_type,scheduled_at,next_run_at,rrule,cwds,created_at,updated_at,skills_json,connector_ids_json,model_id,permission_mode,owner_user_id,owner_status)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
||||
.run(id, name, prompt, 'ACTIVE', 'once', scheduledAt, nextRunAt, '', JSON.stringify([cwd]), now, now, JSON.stringify(Array.isArray(skillsArr) ? skillsArr : []), '[]', modelId, 'fullAccess', ownerUserId, 'confirmed');
|
||||
return { id };
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 最近一次「更新榜单数据」任务状态(running/queued/pending = 进行中;done/error/review = 已结束)
|
||||
function rankLastTaskState() {
|
||||
try {
|
||||
const db = wbDb();
|
||||
try {
|
||||
const row = db.prepare('SELECT id, status, deleted_at FROM automations WHERE name=? AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1').get(RANK_UPDATE_NAME);
|
||||
if (!row) return null;
|
||||
const st = db.prepare('SELECT running, last_error FROM automation_runtime_state WHERE automation_id=?').get(row.id);
|
||||
const runs = db.prepare('SELECT status FROM automation_runs WHERE automation_id=? ORDER BY created_at DESC LIMIT 1').get(row.id);
|
||||
let state = 'queued';
|
||||
if (st && Number(st.running) === 1) state = 'running';
|
||||
else if (runs) {
|
||||
const s = String(runs.status || '').toUpperCase();
|
||||
if (s === 'IN_PROGRESS') state = 'running';
|
||||
else if (['DONE', 'COMPLETED', 'SUCCESS', 'FINISHED', 'ACCEPTED'].includes(s)) state = 'done';
|
||||
else if (['ERROR', 'CANCELLED', 'FAILED', 'INTERRUPTED'].includes(s)) state = 'error';
|
||||
else if (s === 'PENDING_REVIEW') state = 'review';
|
||||
else state = 'pending';
|
||||
}
|
||||
return { id: row.id, state, error: (st && st.last_error) || null };
|
||||
} finally { db.close(); }
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
// ---- 静态文件 / API ----
|
||||
const server = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://localhost');
|
||||
@@ -527,6 +587,53 @@ const server = http.createServer((req, res) => {
|
||||
} catch (e) { return sendErr(500, e.message); }
|
||||
}
|
||||
|
||||
// ---------- 榜单数据更新(09-03 新增:热点数据页「更新榜单」按钮;执行前防重复校验)----------
|
||||
// GET:返回数据新鲜度(最近成功落盘时刻 = RANKING_DIR 最新 json mtime)+ 最近同名更新任务状态,供前端展示与预拦截
|
||||
if (p === '/api/dsh/ranking-update' && req.method === 'GET') {
|
||||
try {
|
||||
const fr = dsh.rankingFreshness();
|
||||
return sendJSON(200, {
|
||||
ok: true,
|
||||
exists: fr.exists, files: fr.files,
|
||||
lastUpdateAt: fr.latestMtime || null,
|
||||
lastUpdateDate: fr.latestDate || '',
|
||||
lastUpdateAgeMin: fr.latestMtime ? Math.floor((Date.now() - fr.latestMtime) / 60000) : null,
|
||||
cooldownMin: RANK_UPDATE_COOLDOWN_MS / 60000,
|
||||
task: rankLastTaskState(),
|
||||
});
|
||||
} catch (e) { return sendErr(500, '查询更新状态失败: ' + e.message); }
|
||||
}
|
||||
// POST:防重校验(① 更新任务进行中拦截;② 距上次落盘 < 冷却窗口且未 force 拦截)→ 创建后台抓取任务
|
||||
if (p === '/api/dsh/ranking-update' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { prompt, force } = JSON.parse(body || '{}');
|
||||
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) return sendErr(400, 'prompt 不能为空');
|
||||
// 防重①:进行中(running/queued/pending)→ 拦截
|
||||
const task = rankLastTaskState();
|
||||
if (task && ['running', 'queued', 'pending'].includes(task.state)) {
|
||||
return sendJSON(200, { ok: false, error: '已有榜单更新任务在执行中(可在左侧会话栏查看进度),请勿重复提交' });
|
||||
}
|
||||
// 防重②:距上次成功落盘 < 冷却窗口且未 force → 拦截(needForce 供前端提示仍可强制更新)
|
||||
const fr = dsh.rankingFreshness();
|
||||
if (!force && fr.latestMtime && Date.now() - fr.latestMtime < RANK_UPDATE_COOLDOWN_MS) {
|
||||
const ageMin = Math.floor((Date.now() - fr.latestMtime) / 60000);
|
||||
return sendJSON(200, {
|
||||
ok: false, needForce: true,
|
||||
error: `榜单数据 ${Math.max(ageMin, 1)} 分钟前刚更新过(${fr.latestDate || ''}),${RANK_UPDATE_COOLDOWN_MS / 60000} 分钟内防重复更新;如确需强制刷新请再次点击并选择「仍要更新」`,
|
||||
});
|
||||
}
|
||||
// 通过 → 创建后台更新任务(skills 挂 mcn-data-insight:三榜抓取脚本与落盘规范所在)
|
||||
const r = createOnceAutomation(RANK_UPDATE_NAME, prompt.trim(), ['mcn-data-insight']);
|
||||
log('已提交榜单更新任务: ' + r.id);
|
||||
return sendJSON(200, { ok: true, id: r.id, message: '榜单更新任务已提交,请在左侧会话栏查看执行' });
|
||||
} catch (e) { return sendErr(500, '任务提交失败: ' + e.message); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- 提交 AI 任务(Automation 一次性任务,WorkBuddy 自动执行,左侧会话栏可见)----------
|
||||
if (p === '/api/run' && req.method === 'POST') {
|
||||
let body = '';
|
||||
|
||||
Reference in new issue
Block a user