import { env } from "cloudflare:workers"; import { getRequestExecutionContext } from "vinext/shims/request-context"; import { backfillAccountProfiles } from "../../../lib/account-enrichment-service"; import { ensureSchema, getDashboardData, getRawDb, uid, } from "../../../lib/mvp-db"; import { collectDistributionMetrics, createCollectionRunTasks, retryFailedCollections, runDueScheduledCollections, shanghaiDateFromTimestamp, } from "../../../lib/collection-service"; import { resolveCollectionMcpConfig, type CollectionMcpBindings, } from "../../../lib/mcp-collection-client"; import { adminForbidden, isAdminRequest } from "../../../lib/admin-auth"; import { isManagerRequest } from "../../../lib/user-auth"; import { FeishuSourceError, readFeishuSource, type FeishuBindings, } from "../../../lib/feishu-client"; import { createDistributionTask } from "../../../lib/task-service"; import { DistributionReleaseError, releaseUnfinishedDistribution, } from "../../../lib/distribution-release-service"; export const runtime = "edge"; type ActionBody = { action?: string; [key: string]: unknown; }; function numberValue(value: unknown, fallback = 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } export async function POST(request: Request) { if (!(await isAdminRequest(request))) return adminForbidden(); try { await ensureSchema(); const body = (await request.json()) as ActionBody; const db = getRawDb(); if (body.action === "inspect_feishu") { const source = await readFeishuSource( String(body.feishuUrl ?? "").trim(), env as unknown as FeishuBindings, ); return Response.json({ sheetId: source.sheetId, sheetName: source.sheetName, syncedAt: source.syncedAt, rowCount: source.rows.length, columns: source.columns, preview: source.rows.slice(0, 3), }); } if (body.action === "create_task") { const name = String(body.name ?? "").trim(); const brand = String(body.brand ?? "").trim(); const dueAt = String(body.dueAt ?? "").trim(); if (!name || !brand || !dueAt) { return Response.json( { error: "请补全任务名称、品牌和截止日期" }, { status: 400 }, ); } await createDistributionTask( { feishuUrl: String(body.feishuUrl ?? "").trim(), name, brand, dueAt, }, env as unknown as FeishuBindings, ); } else if (body.action === "claim") { const partnerId = String(body.partnerId ?? ""); const taskId = String(body.taskId ?? ""); const quantity = Math.max(1, Math.min(50, numberValue(body.quantity, 1))); const available = await db .prepare( `SELECT id FROM contents WHERE task_id = ? AND status = 'available' ORDER BY created_at, id LIMIT ?`, ) .bind(taskId, quantity) .all<{ id: string }>(); if (available.results.length === 0) { return Response.json({ error: "当前没有可领取内容" }, { status: 409 }); } const statements = available.results.flatMap((content) => [ db .prepare( `INSERT INTO distributions (id, task_id, content_id, partner_id, status) VALUES (?, ?, ?, ?, 'claimed')`, ) .bind(uid("dist"), taskId, content.id, partnerId), db .prepare("UPDATE contents SET status = 'allocated' WHERE id = ?") .bind(content.id), ]); statements.push( db .prepare( `UPDATE tasks SET claimed_quantity = claimed_quantity + ? WHERE id = ?`, ) .bind(available.results.length, taskId), db .prepare( `UPDATE partners SET claimed_total = claimed_total + ? WHERE id = ?`, ) .bind(available.results.length, partnerId), ); await db.batch(statements); } else if (body.action === "release_distribution") { if (!(await isManagerRequest(request))) return adminForbidden(); await releaseUnfinishedDistribution( db, String(body.distributionId ?? "").trim(), ); } else if (body.action === "save_collection_schedule") { const taskId = String(body.taskId ?? "").trim(); const startDate = String(body.startDate ?? "").trim(); const days = Array.isArray(body.days) ? [...new Set(body.days.map(Number))] .filter( (day) => Number.isInteger(day) && day >= 1 && day <= 7, ) .sort((a, b) => a - b) : []; if (!taskId || !/^\d{4}-\d{2}-\d{2}$/.test(startDate)) { return Response.json( { error: "请选择有效的开始采集日期" }, { status: 400 }, ); } if (days.length === 0) { return Response.json( { error: "请至少选择一个自动采集日" }, { status: 400 }, ); } const task = await db .prepare("SELECT id FROM tasks WHERE id = ?") .bind(taskId) .first<{ id: string }>(); if (!task) { return Response.json({ error: "任务不存在" }, { status: 404 }); } await db.batch([ db .prepare( `UPDATE tasks SET collection_start_date = ?, collection_days = ?, collection_schedule_updated_at = CURRENT_TIMESTAMP WHERE id = ?`, ) .bind(startDate, JSON.stringify(days), taskId), db .prepare( `UPDATE distributions SET collection_status = CASE WHEN latest_likes IS NULL THEN 'scheduled' ELSE collection_status END, collection_status_description = CASE WHEN latest_likes IS NULL THEN ? ELSE collection_status_description END, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND publish_url IS NOT NULL AND publish_url != ''`, ) .bind( `已安排${days.length}个采集日,每日09:00执行`, taskId, ), ]); await createCollectionRunTasks(db, taskId, startDate, days); const catchup = runDueScheduledCollections( db, Date.now(), resolveCollectionMcpConfig( env as unknown as CollectionMcpBindings, ), "catchup", taskId, ).catch(() => undefined); const executionContext = getRequestExecutionContext(); if (executionContext) { executionContext.waitUntil(catchup); } else { await catchup; } } else if (body.action === "run_due_collections") { await runDueScheduledCollections( db, Date.now(), resolveCollectionMcpConfig( env as unknown as CollectionMcpBindings, ), "catchup", ); } else if ( body.action === "collect_now" || body.action === "collect" ) { const distributionId = String(body.distributionId ?? ""); const rawDay = body.day === null || body.day === undefined ? null : numberValue(body.day); const day = rawDay !== null && Number.isInteger(rawDay) && rawDay >= 1 && rawDay <= 7 ? rawDay : null; if (body.action === "collect" && day === null) { return Response.json({ error: "采集周期无效" }, { status: 400 }); } await collectDistributionMetrics( db, distributionId, shanghaiDateFromTimestamp(Date.now()), day, "manual", resolveCollectionMcpConfig( env as unknown as CollectionMcpBindings, ), ); } else if (body.action === "retry_failed_collections") { const taskId = String(body.taskId ?? "").trim(); if (!taskId) { return Response.json({ error: "请选择需要补采的任务" }, { status: 400 }); } await retryFailedCollections( db, taskId, resolveCollectionMcpConfig( env as unknown as CollectionMcpBindings, ), ); } else if (body.action === "backfill_account_profiles") { const backfill = backfillAccountProfiles( db, resolveCollectionMcpConfig( env as unknown as CollectionMcpBindings, ), Math.max(1, Math.min(25, numberValue(body.limit, 10))), ).catch(() => undefined); const executionContext = getRequestExecutionContext(); if (executionContext) { executionContext.waitUntil(backfill); } else { await backfill; } } else if (body.action === "set_public_account_ids") { const items = Array.isArray(body.items) ? body.items.slice(0, 50) : []; const normalized = items .map((item) => { const record = item && typeof item === "object" ? (item as Record) : {}; return { accountId: String(record.accountId ?? "").trim().slice(0, 80), publicAccountId: String(record.publicAccountId ?? "") .trim() .slice(0, 80), }; }) .filter( (item) => item.accountId && /^[\p{L}\p{N}._-]{2,80}$/u.test(item.publicAccountId), ); if (normalized.length === 0) { return Response.json( { error: "没有可回填的账号号值" }, { status: 400 }, ); } await db.batch( normalized.map((item) => db .prepare( `UPDATE accounts SET public_account_id = ?, last_seen_at = CURRENT_TIMESTAMP WHERE id = ?`, ) .bind(item.publicAccountId, item.accountId), ), ); } else if (body.action === "manual_metrics") { const distributionId = String(body.distributionId ?? ""); const exposure = Math.max(0, numberValue(body.exposure)); const views = Math.max(0, numberValue(body.views)); await db .prepare( `UPDATE distributions SET exposure = ?, views = ?, ocr_status = 'manual', status = CASE WHEN d7_likes IS NOT NULL THEN 'complete' ELSE status END, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, ) .bind(exposure, views, distributionId) .run(); } else { return Response.json({ error: "不支持的操作" }, { status: 400 }); } return Response.json(await getDashboardData()); } catch (error) { return Response.json( { error: error instanceof Error ? error.message : "操作失败" }, { status: error instanceof FeishuSourceError || error instanceof DistributionReleaseError ? error.status : 500, }, ); } }