chore: open conversion flow + disable scout import noise
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 8s

This commit is contained in:
OG T
2026-06-07 18:56:45 +08:00
parent 4f639c2654
commit 4fd89a447e
10 changed files with 423 additions and 117 deletions

View File

@@ -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: {

View File

@@ -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 }
);
}
}

View File

@@ -12,6 +12,13 @@ function asRecordJson(value: unknown): Record<string, unknown> | 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) => {

View File

@@ -105,7 +105,7 @@ export default async function Home() {
{`curl https://agent.wooo.work/api/open-tasks`}
</div>
<p className="text-gray-500 text-sm">
Agent
AI
</p>
<div className="mt-6">
<Link href="/traffic" className="inline-flex items-center gap-2 text-emerald-400 hover:text-emerald-300">

View File

@@ -31,6 +31,13 @@ function asRecordJson(value: unknown): Record<string, unknown> | 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 = {

View File

@@ -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<string, unknown>) {
return new Promise<void>((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<void>
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<void>
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);

View File

@@ -28,6 +28,13 @@ function asRecordJson(value: unknown): Record<string, unknown> | 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<FunnelSummary> {
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 {

View File

@@ -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

View File

@@ -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.

View File

@@ -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<T>(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();
});