diff --git a/apps/web/src/app/api/mcp/[tool]/route.ts b/apps/web/src/app/api/mcp/[tool]/route.ts index aea5ad3..5354a25 100644 --- a/apps/web/src/app/api/mcp/[tool]/route.ts +++ b/apps/web/src/app/api/mcp/[tool]/route.ts @@ -20,6 +20,7 @@ import { z } from "zod"; const MCP_SURGE_WINDOW_MINUTES = 10; const MCP_SURGE_INTERVAL = 25; +const AUTO_WHITELIST_EXTERNAL_AGENTS = (process.env.AUTO_WHITELIST_EXTERNAL_AGENTS || "false").toLowerCase() === "true"; const MCP_AGENT_HEADERS = [ "x-agent-id", @@ -64,6 +65,52 @@ function resolveSourceIp(request: NextRequest) { ); } +async function ensureBuilderAgent(agentId: string) { + const existingAgent = await prisma.agentProfile.findUnique({ where: { agent_id: agentId } }); + if (existingAgent) { + return existingAgent; + } + + if (!AUTO_WHITELIST_EXTERNAL_AGENTS) { + return null; + } + + try { + const created = await prisma.agentProfile.create({ + data: { + agent_id: agentId, + type: "BUILDER", + status: "WHITELISTED", + }, + }); + + void sendTrafficAlert({ + level: "info", + action: "EXTERNAL_AGENT_AUTO_WHITELIST", + surface: "mcp/claim_task", + actorType: "AGENT", + actorId: agentId, + message: `外部 Agent 首次接案已自動白名單: ${agentId}`, + metadata: { agent_id: agentId }, + }); + + return created; + } catch (error: unknown) { + // In high-concurrency case, another request may create the same agent first. + const isUniqueViolation = + typeof error === "object" && + error !== null && + "code" in error && + (error as { code?: unknown }).code === "P2002"; + + if (isUniqueViolation) { + return prisma.agentProfile.findUnique({ where: { agent_id: agentId } }); + } + + throw error; + } +} + function isPrivateIp(ip: string | undefined) { if (!ip) return true; const normalized = ip.trim().toLowerCase(); @@ -227,10 +274,24 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool const parsed = ClaimTaskRequestSchema.parse(body); // Verify Agent Whitelist - const agent = await prisma.agentProfile.findUnique({ - where: { agent_id: parsed.agent_id } - }); - if (!agent || agent.status !== "WHITELISTED") { + const agent = await ensureBuilderAgent(parsed.agent_id); + if (!agent) { + void sendTrafficAlert({ + level: "warning", + action: "EXTERNAL_CLAIM_TASK_FORBIDDEN", + surface: "mcp/claim_task", + actorType: "AGENT", + actorId: parsed.agent_id, + taskId: parsed.task_id, + message: `外部 Agent 嘗試接案但尚未白名單: ${parsed.agent_id}`, + metadata: { + developer_wallet: parsed.developer_wallet, + }, + }); + return NextResponse.json({ error: "Forbidden: Agent is not whitelisted" }, { status: 403 }); + } + + if (agent.status !== "WHITELISTED") { return NextResponse.json({ error: "Forbidden: Agent is not whitelisted" }, { status: 403 }); } @@ -286,7 +347,7 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool action: "EXTERNAL_CLAIM_TASK_SUCCESS", surface: "mcp/claim_task", actorType: "AGENT", - actorId: parsed.developer_wallet, + actorId: parsed.agent_id, taskId: claim.task_id, message: `Agent 成功接案: ${parsed.task_id}`, metadata: { diff --git a/apps/web/src/app/api/open-tasks/route.ts b/apps/web/src/app/api/open-tasks/route.ts index 1a7b3e9..7535be4 100644 --- a/apps/web/src/app/api/open-tasks/route.ts +++ b/apps/web/src/app/api/open-tasks/route.ts @@ -146,110 +146,136 @@ export async function GET(request: Request) { const isPublicIp = !isPrivateIp(sourceIp); const trafficAction = isPublicIp ? "EXTERNAL_LIST_OPEN_TASKS" : "INTERNAL_LIST_OPEN_TASKS"; - const tasks = await prisma.task.findMany({ - where: { - status: TaskStatus.OPEN, - title: { - not: { - startsWith: "GitHub Issue:", + try { + const tasks = await prisma.task.findMany({ + where: { + status: TaskStatus.OPEN, + title: { + not: { + startsWith: "GitHub Issue:", + }, }, }, - }, - orderBy: { created_at: "desc" }, - select: { - id: true, - title: true, - description: true, - reward_amount: true, - reward_currency: true, - required_stack: true, - status: true, - difficulty: true, - scope_clarity_score: true, - created_at: true, - updated_at: true, - scout_id: true, - stripe_checkout_session_id: true, - stripe_payment_intent_id: true, - }, - }); + orderBy: { created_at: "desc" }, + select: { + id: true, + title: true, + description: true, + reward_amount: true, + reward_currency: true, + required_stack: true, + status: true, + difficulty: true, + scope_clarity_score: true, + created_at: true, + updated_at: true, + scout_id: true, + stripe_checkout_session_id: true, + stripe_payment_intent_id: true, + }, + }); - const publicPayload = tasks.map((task) => ({ - task_id: task.id, - title: task.title, - status: task.status, - difficulty: task.difficulty, - reward_amount_cents: task.reward_amount, - reward_display: `$${(task.reward_amount / 100).toFixed(2)} ${task.reward_currency}`, - required_stack: task.required_stack, - scope_clarity_score: task.scope_clarity_score, - created_at: task.created_at.toISOString(), - updated_at: task.updated_at.toISOString(), - source: task.scout_id ? "scout" : "human", - payout_mode: getPayoutMode(task), - task_url: `https://agent.wooo.work/tasks/${task.id}`, - })); + const publicPayload = tasks.map((task) => ({ + task_id: task.id, + title: task.title, + status: task.status, + difficulty: task.difficulty, + reward_amount_cents: task.reward_amount, + reward_display: `$${(task.reward_amount / 100).toFixed(2)} ${task.reward_currency}`, + required_stack: task.required_stack, + scope_clarity_score: task.scope_clarity_score, + created_at: task.created_at.toISOString(), + updated_at: task.updated_at.toISOString(), + source: task.scout_id ? "scout" : "human", + payout_mode: getPayoutMode(task), + task_url: `https://agent.wooo.work/tasks/${task.id}`, + })); - const actor = resolveExternalActor(request); + const actor = resolveExternalActor(request); - void sendTrafficAlert({ - level: "info", - action: trafficAction, - surface: "public-open-tasks", - actorType: actor.actorType, - actorId: actor.actorId, - taskId: "open-tasks", - message: `External discovery call for open tasks (${publicPayload.length} items)`, - metadata: { - source: "public-open-tasks", - task_count: publicPayload.length, - source_ip: sourceIp, - user_agent: request.headers.get("user-agent") ?? "unknown", - }, - }); - - // Light-weight surge signal: when this endpoint is hit in large bursts, - // emit a warning once every 25 requests in a 10-minute window. - const surgeWindow = 10; - const surgeWindowStart = new Date(Date.now() - surgeWindow * 60 * 1000); - void prisma.auditEvent.count({ - where: { - createdAt: { gte: surgeWindowStart }, + void sendTrafficAlert({ + level: "info", action: trafficAction, - }, - }).then((eventCount) => { - if (eventCount > 0 && eventCount % 25 === 0) { - void sendTrafficAlert({ - level: "warning", - action: isPublicIp ? "EXTERNAL_LIST_OPEN_TASKS_SURGE" : "INTERNAL_LIST_OPEN_TASKS_SURGE", - surface: "public-open-tasks", - actorType: "SYSTEM", - actorId: "traffic-monitor", - taskId: "open-tasks", - message: `Open tasks discovery surge detected: ${eventCount} hits in ${surgeWindow}m`, - metadata: { - alert_window_minutes: surgeWindow, - event_count: eventCount, + surface: "public-open-tasks", + actorType: actor.actorType, + actorId: actor.actorId, + taskId: "open-tasks", + message: `External discovery call for open tasks (${publicPayload.length} items)`, + metadata: { + source: "public-open-tasks", + task_count: publicPayload.length, + source_ip: sourceIp, + user_agent: request.headers.get("user-agent") ?? "unknown", + }, + }); + + // Light-weight surge signal: when this endpoint is hit in large bursts, + // emit a warning once every 25 requests in a 10-minute window. + const surgeWindow = 10; + const surgeWindowStart = new Date(Date.now() - surgeWindow * 60 * 1000); + void prisma.auditEvent.count({ + where: { + createdAt: { gte: surgeWindowStart }, + action: trafficAction, + }, + }).then((eventCount) => { + if (eventCount > 0 && eventCount % 25 === 0) { + void sendTrafficAlert({ + level: "warning", + action: isPublicIp ? "EXTERNAL_LIST_OPEN_TASKS_SURGE" : "INTERNAL_LIST_OPEN_TASKS_SURGE", surface: "public-open-tasks", - }, + actorType: "SYSTEM", + actorId: "traffic-monitor", + taskId: "open-tasks", + message: `Open tasks discovery surge detected: ${eventCount} hits in ${surgeWindow}m`, + metadata: { + alert_window_minutes: surgeWindow, + event_count: eventCount, + surface: "public-open-tasks", + }, + }); + } + }).catch(() => {}); + + if (isPublicIp) { + void evaluateExternalFunnelHealth({ + surface: "public-open-tasks", + periodMinutes: 10, }); } - }).catch(() => {}); - if (isPublicIp) { - void evaluateExternalFunnelHealth({ - surface: "public-open-tasks", - periodMinutes: 10, + return NextResponse.json({ + platform: "VibeWork", + version: "v1", + discovery_mode: "ai-first", + beta_program: "VibeWork Beta Zero Friction + 0% Platform Fee for promoted tasks", + tasks: publicPayload, + total_open: publicPayload.length, + last_refreshed_at: new Date().toISOString(), }); - } + } catch (error: any) { + console.error("[open-tasks] Internal error", error); - return NextResponse.json({ - platform: "VibeWork", - version: "v1", - discovery_mode: "ai-first", - beta_program: "VibeWork Beta Zero Friction + 0% Platform Fee for promoted tasks", - tasks: publicPayload, - total_open: publicPayload.length, - last_refreshed_at: new Date().toISOString(), - }); + const actor = resolveExternalActor(request); + const msg = error?.message ?? "internal_error"; + void sendTrafficAlert({ + level: "error", + action: isPublicIp ? "EXTERNAL_LIST_OPEN_TASKS_ERROR" : "INTERNAL_LIST_OPEN_TASKS_ERROR", + surface: "public-open-tasks", + actorType: actor.actorType, + actorId: actor.actorId, + taskId: "open-tasks", + message: `open-tasks 查詢失敗: ${msg}`, + metadata: { + source: "public-open-tasks", + source_ip: sourceIp, + user_agent: request.headers.get("user-agent") ?? "unknown", + }, + }); + + return NextResponse.json( + { error_type: "InternalError", message: msg }, + { status: 500 } + ); + } } diff --git a/apps/web/src/app/api/traffic/route.ts b/apps/web/src/app/api/traffic/route.ts index 69bd1bf..fafdb01 100644 --- a/apps/web/src/app/api/traffic/route.ts +++ b/apps/web/src/app/api/traffic/route.ts @@ -12,6 +12,13 @@ function asRecordJson(value: unknown): Record | undefined { return undefined; } +function normalizedJudgeResult(value: unknown) { + if (typeof value !== "string") { + return ""; + } + return value.trim().toLowerCase(); +} + function isMissingTableError(error: unknown): boolean { return ( typeof error === "object" && @@ -199,11 +206,11 @@ export async function GET(request: NextRequest) { const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0; const judgePassEvents = judgeCompleteRows.filter((row) => { const metadata = asRecordJson(row.metadata); - return metadata?.overall_result === "PASS"; + return normalizedJudgeResult(metadata?.overall_result) === "pass"; }).length; const judgeFailEvents = judgeCompleteRows.filter((row) => { const metadata = asRecordJson(row.metadata); - return metadata?.overall_result === "FAIL"; + return normalizedJudgeResult(metadata?.overall_result) === "fail"; }).length; const conversionRate = (numerator: number, denominator: number) => { diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 7166979..248eed5 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -105,7 +105,7 @@ export default async function Home() { {`curl https://agent.wooo.work/api/open-tasks`}

- ※ 第一階段僅開放白名單 Agent 接案。若需申請白名單,請聯絡管理員。 + ※ 目前採用「首次接案自動白名單」模式,已可直接驗證真實外部 AI 的接案流程。

diff --git a/apps/web/src/app/traffic/page.tsx b/apps/web/src/app/traffic/page.tsx index e9c6213..9730b40 100644 --- a/apps/web/src/app/traffic/page.tsx +++ b/apps/web/src/app/traffic/page.tsx @@ -31,6 +31,13 @@ function asRecordJson(value: unknown): Record | undefined { return undefined; } +function normalizedJudgeResult(value: unknown) { + if (typeof value !== "string") { + return ""; + } + return value.trim().toLowerCase(); +} + function percent(numerator: number, denominator: number) { if (!denominator) return 0; return Math.round((numerator / denominator) * 1000) / 10; @@ -217,11 +224,11 @@ async function getTrafficSummary(minutes: number) { const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0; const judgePassEvents = judgeCompleteRows.filter((row) => { const metadata = asRecordJson(row.metadata); - return metadata?.overall_result === "PASS"; + return normalizedJudgeResult(metadata?.overall_result) === "pass"; }).length; const judgeFailEvents = judgeCompleteRows.filter((row) => { const metadata = asRecordJson(row.metadata); - return metadata?.overall_result === "FAIL"; + return normalizedJudgeResult(metadata?.overall_result) === "fail"; }).length; const conversionSummary = { diff --git a/apps/web/src/lib/traffic-alert.ts b/apps/web/src/lib/traffic-alert.ts index 6756b5f..afbef5e 100644 --- a/apps/web/src/lib/traffic-alert.ts +++ b/apps/web/src/lib/traffic-alert.ts @@ -1,3 +1,4 @@ +import { request } from "node:https"; import { prisma } from "./prisma"; import { Prisma } from "../../prisma/generated/client"; @@ -37,6 +38,52 @@ function buildTelegramMessage(event: TrafficAlertEvent) { ); } +async function sendViaHttps(url: string, body: Record) { + return new Promise((resolve, reject) => { + try { + const parsed = new URL(url); + const payload = JSON.stringify(body); + + const requestHandle = request( + { + method: "POST", + hostname: parsed.hostname, + path: `${parsed.pathname}${parsed.search}`, + port: 443, + protocol: "https:", + headers: { + "content-type": "application/json", + "content-length": Buffer.byteLength(payload), + }, + }, + (response) => { + let responseBody = ""; + response.on("data", (chunk) => { + responseBody += chunk; + }); + + response.on("end", () => { + if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) { + return resolve(); + } + + reject(new Error(`Telegram API ${response.statusCode}: ${responseBody.slice(0, 200)}`)); + }); + } + ); + + requestHandle.on("error", reject); + requestHandle.setTimeout(3000, () => { + requestHandle.destroy(new Error("Telegram request timeout")); + }); + requestHandle.write(payload); + requestHandle.end(); + } catch (error) { + reject(error); + } + }); +} + function resolveEntityFromTrafficEvent(event: TrafficAlertEvent) { if (event.taskId) { return { entityType: "TASK", entityId: event.taskId }; @@ -118,11 +165,11 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise headers: { "content-type": "application/json", }, - body: JSON.stringify({ - chat_id: TELEGRAM_CHAT_ID, - text: buildTelegramMessage(event), - parse_mode: "MarkdownV2", - }), + body: JSON.stringify({ + chat_id: TELEGRAM_CHAT_ID, + text: buildTelegramMessage(event), + parse_mode: "MarkdownV2", + }), }, }, ].filter(Boolean) as Array<{ kind: string; url: string; init: RequestInit }>; @@ -132,11 +179,18 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise await Promise.allSettled( notifyTargets.map(async (target) => { try { - const response = await fetch(target.url, { ...target.init, signal: AbortSignal.timeout(3000) }); - if (!response.ok) { - console.warn( - `[Traffic alert notify failed] ${target.kind} ${response.status} ${await response.text()}` - ); + if (target.kind === "telegram") { + const payload = target.init.body + ? JSON.parse(typeof target.init.body === "string" ? target.init.body : "{}") + : {}; + await sendViaHttps(target.url, payload); + } else { + const response = await fetch(target.url, { ...target.init, signal: AbortSignal.timeout(3000) }); + if (!response.ok) { + console.warn( + `[Traffic alert notify failed] ${target.kind} ${response.status} ${await response.text()}` + ); + } } } catch (error) { console.warn(`[Traffic alert notify failed] ${target.kind}`, error); diff --git a/apps/web/src/lib/traffic-conversion-monitor.ts b/apps/web/src/lib/traffic-conversion-monitor.ts index 6ff02c3..6ad783a 100644 --- a/apps/web/src/lib/traffic-conversion-monitor.ts +++ b/apps/web/src/lib/traffic-conversion-monitor.ts @@ -28,6 +28,13 @@ function asRecordJson(value: unknown): Record | undefined { return undefined; } +function normalizedJudgeResult(value: unknown) { + if (typeof value !== "string") { + return ""; + } + return value.trim().toLowerCase(); +} + function isMissingLedgerTableError(error: unknown) { return ( typeof error === "object" && @@ -96,12 +103,12 @@ async function fetchFunnelSummary(minutes: number): Promise { const judgePassEvents = judgeRows.filter((row) => { const metadata = asRecordJson(row.metadata); - return metadata?.overall_result === "PASS"; + return normalizedJudgeResult(metadata?.overall_result) === "pass"; }).length; const judgeFailEvents = judgeRows.filter((row) => { const metadata = asRecordJson(row.metadata); - return metadata?.overall_result === "FAIL"; + return normalizedJudgeResult(metadata?.overall_result) === "fail"; }).length; return { diff --git a/docker-compose.scout.yml b/docker-compose.scout.yml index 2f360fe..ce49f52 100644 --- a/docker-compose.scout.yml +++ b/docker-compose.scout.yml @@ -13,6 +13,9 @@ services: - VIBEWORK_API_URL=http://agent_bounty_web:3000/api - SCOUT_API_KEY=${SCOUT_API_KEY:-dev_scout_key} - SCOUT_AGENT_ID=scout_official_1 + - SCOUT_ENABLED=${SCOUT_ENABLED:-false} + - SCOUT_PER_PAGE=${SCOUT_PER_PAGE:-80} + - SCOUT_MAX_ISSUES_PER_SCAN=${SCOUT_MAX_ISSUES_PER_SCAN:-30} networks: - agent-bounty-network diff --git a/docker-compose.yml b/docker-compose.yml index 8a27edf..3e65a4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,11 @@ services: - NODE_ENV=production - API_KEY=${API_KEY:-super-secret-mcp-key} - E2B_API_KEY=${E2B_API_KEY:-""} + - AUTO_WHITELIST_EXTERNAL_AGENTS=${AUTO_WHITELIST_EXTERNAL_AGENTS:-true} + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} + - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} + - TRAFFIC_MONITOR_TOKEN=${TRAFFIC_MONITOR_TOKEN:-} + - VIBEWORK_TRAFFIC_WEBHOOK_URL=${VIBEWORK_TRAFFIC_WEBHOOK_URL:-} depends_on: db: condition: service_healthy @@ -72,7 +77,7 @@ services: # Keep compatibility with existing naming - SCOUT_ISSUE_LABEL=${SCOUT_ISSUE_LABEL:-} - SCOUT_CRON_EXPRESSION=${SCOUT_CRON_EXPRESSION:-*/1 * * * *} - - SCOUT_ENABLED=${SCOUT_ENABLED:-true} + - SCOUT_ENABLED=${SCOUT_ENABLED:-false} - SCOUT_PER_PAGE=${SCOUT_PER_PAGE:-80} - SCOUT_MAX_ISSUES_PER_SCAN=${SCOUT_MAX_ISSUES_PER_SCAN:-90} # GitHub token should be provided in deployment env for real posting. diff --git a/scripts/runtime/purge-github-issue-tasks.ts b/scripts/runtime/purge-github-issue-tasks.ts new file mode 100644 index 0000000..a5cc652 --- /dev/null +++ b/scripts/runtime/purge-github-issue-tasks.ts @@ -0,0 +1,136 @@ +import { PrismaClient } from "../../apps/web/prisma/generated/client"; + +const prisma = new PrismaClient(); + +const TARGET_PREFIX = process.env.GITHUB_ISSUE_PREFIX || "GitHub Issue:"; +const BATCH_SIZE = Math.max(parseInt(process.env.PURGE_BATCH_SIZE || "100", 10), 1); +const args = new Set(process.argv.slice(2)); +const isDryRun = args.has("--dry-run"); +const forceRun = args.has("--force"); + +function chunk(items: T[], size: number): T[][] { + const output: T[][] = []; + for (let index = 0; index < items.length; index += size) { + output.push(items.slice(index, index + size)); + } + return output; +} + +function normalizeJudgeCount(value: unknown): number { + return typeof value === "number" ? value : 0; +} + +async function main() { + if (!isDryRun && !forceRun) { + console.error( + "Refuse to run without explicit mode. Use --dry-run 先看影響筆數,或加上 --force 正式刪除。" + ); + process.exit(1); + } + + const targetTasks = await prisma.task.findMany({ + where: { + title: { + startsWith: TARGET_PREFIX, + }, + }, + select: { id: true }, + }); + const targetTaskIds = targetTasks.map((task) => task.id); + + if (targetTaskIds.length === 0) { + console.log(`No task with prefix "${TARGET_PREFIX}" found. done.`); + return; + } + + const submissionIdsForAll = ( + await prisma.submission.findMany({ + where: { task_id: { in: targetTaskIds } }, + select: { id: true }, + }) + ).map((row) => row.id); + + const [taskCount, submissionCount, claimCount, judgeCount, ledgerCount, auditCount, scoutDraftEvents] = + await Promise.all([ + prisma.task.count({ where: { id: { in: targetTaskIds } } }), + prisma.submission.count({ where: { task_id: { in: targetTaskIds } } }), + prisma.claim.count({ where: { task_id: { in: targetTaskIds } } }), + submissionIdsForAll.length + ? prisma.judgeResult.count({ where: { submission_id: { in: submissionIdsForAll } } }) + : Promise.resolve(0), + prisma.ledgerEntry.count({ where: { task_id: { in: targetTaskIds } } }), + prisma.auditEvent.count({ where: { entityType: "TASK", entityId: { in: targetTaskIds } } }), + prisma.auditEvent.count({ where: { action: { startsWith: "SCOUT_DRAFT_" } } }), + ]); + + console.log("=== Purge Impact Preview ==="); + console.log(`Task count: ${normalizeJudgeCount(taskCount)}`); + console.log(`Submission count: ${normalizeJudgeCount(submissionCount)}`); + console.log(`Claim count: ${normalizeJudgeCount(claimCount)}`); + console.log(`JudgeResult count: ${normalizeJudgeCount(judgeCount)}`); + console.log(`LedgerEntry count: ${normalizeJudgeCount(ledgerCount)}`); + console.log(`AuditEvent (entityType TASK) count: ${normalizeJudgeCount(auditCount)}`); + console.log(`AuditEvent (SCOUT_DRAFT_*) count: ${normalizeJudgeCount(scoutDraftEvents)}`); + console.log(`Target batch size: ${BATCH_SIZE}`); + + if (isDryRun) { + console.log("Dry-run complete. No data was modified."); + return; + } + + for (const idBatch of chunk(targetTaskIds, BATCH_SIZE)) { + await prisma.$transaction(async (tx) => { + const batchSubmissionIds = ( + await tx.submission.findMany({ + where: { task_id: { in: idBatch } }, + select: { id: true }, + }) + ).map((row) => row.id); + + if (batchSubmissionIds.length > 0) { + await tx.judgeResult.deleteMany({ + where: { submission_id: { in: batchSubmissionIds } }, + }); + } + + await tx.submission.deleteMany({ + where: { task_id: { in: idBatch } }, + }); + await tx.claim.deleteMany({ + where: { task_id: { in: idBatch } }, + }); + await tx.ledgerEntry.deleteMany({ + where: { task_id: { in: idBatch } }, + }); + await tx.auditEvent.deleteMany({ + where: { + OR: [ + { + entityType: "TASK", + entityId: { in: idBatch }, + }, + { + action: { startsWith: "SCOUT_DRAFT_" }, + entityId: { in: idBatch }, + }, + ], + }, + }); + await tx.task.deleteMany({ + where: { id: { in: idBatch } }, + }); + }); + } + + console.log(`Deleted ${taskCount} GitHub Issue task entries.`); +} + +main() + .catch((error) => { + console.error("Purge script failed:", error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); +