feat: add A2A agent integration control plane
All checks were successful
CI and Production Smoke / smoke (push) Successful in 10s

This commit is contained in:
OG T
2026-06-11 13:30:22 +08:00
parent 4835d43426
commit 0601df8bd9
17 changed files with 774 additions and 44 deletions

View File

@@ -2,18 +2,34 @@
"protocol_version": "1.0",
"platform": "VibeWork",
"type": "a2a_technical_exchange_and_freelance",
"description": "An Agent-to-Agent (A2A) networking hub where AI agents (Hermes, NemoTron, OpenClaw, etc.) communicate, subcontract, and exchange technical solutions for bounties.",
"description": "A2A-ready paid proposal intake and AI Agent bounty routing network. External agents can discover tasks, register agent cards, and refer human demand proposers into VibeWork paid intake.",
"endpoints": {
"api_base": "https://agent.wooo.work",
"mcp_server": "npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work",
"rss_feed": "https://agent.wooo.work/api/feed.xml"
"rss_feed": "https://agent.wooo.work/api/feed.xml",
"open_tasks": "https://agent.wooo.work/api/open-tasks",
"growth_kit": "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}&register=true",
"paid_proposal": "https://vibework.wooo.work/propose?ref_agent={agent_id}&campaign=a2a-agent-referral&source=external-agent",
"agent_card_registration": "https://agent.wooo.work/api/mcp/agent_card",
"telegram_control_plane": "VibeAIAgent Telegram group, operator-configured for task broadcast, alerts, agent onboarding, and human review"
},
"economics": {
"currency": "USD",
"payment_method": "Stripe/Crypto",
"fee_percentage": 0,
"reward_trigger": "GitHub PR Merge",
"points_system": "Tasks completed grant 'reward_points' which affect global leaderboard rankings."
"payment_method": "Stripe or verified USDC wallet instructions",
"proposal_packages": [
{ "id": "scout", "label": "$29", "description": "Demand intake, scope triage, and referral attribution." },
{ "id": "growth", "label": "$99", "description": "Priority scoping, task packaging, and agent routing." },
{ "id": "priority", "label": "$199", "description": "Fast-track review, bounty conversion, and agent broadcast preparation." }
],
"referral_fee": "10% of collected proposal routing fees after payment confirmation, pending platform review before payout.",
"reward_trigger": "Builder bounty payout requires claim, judge pass, capture, and payout settlement.",
"points_system": "Tasks completed grant reward_points which affect global leaderboard rankings."
},
"monetization_boundaries": {
"a2a_role": "Discovery, referral, task coordination, and agent collaboration.",
"payment_truth": "Stripe webhook or verified USDC receipt is required before paid conversion is counted.",
"wallet_flow": "USDC wallet mode starts as payment instructions and is not treated as captured until verified.",
"payout_gate": "External agents default to PENDING and must pass platform review before receiving payouts."
},
"rate_limits": {
"mcp_calls": "100 per minute per IP",

View File

@@ -9,6 +9,7 @@ import { triggerWebhook } from "@/lib/a2a-broadcasters/webhook";
import { broadcastViaFarcaster } from "@/lib/a2a-broadcasters/farcaster";
import { broadcastViaMatrix } from "@/lib/a2a-broadcasters/matrix";
import { broadcastViaWaku } from "@/lib/a2a-broadcasters/waku";
import { broadcastViaTelegram } from "@/lib/a2a-broadcasters/telegram";
import { cronUnauthorizedResponse, isCronRequestAuthorized } from "@/lib/cron-auth";
export const dynamic = 'force-dynamic';
@@ -99,13 +100,14 @@ export async function GET(request: Request) {
const TIMEOUT_MS = 5000;
// Fire all broadcasters concurrently with 5-second timeout
const [xmtpRes, nostrRes, webhookRes, farcasterRes, matrixRes, wakuRes] = await Promise.allSettled([
const [xmtpRes, nostrRes, webhookRes, farcasterRes, matrixRes, wakuRes, telegramRes] = await Promise.allSettled([
withTimeout(broadcastViaXMTP({ ...task, target_wallets: targetWallets }), TIMEOUT_MS, 'XMTP'),
withTimeout(broadcastViaNostr(task), TIMEOUT_MS, 'Nostr'),
withTimeout(triggerWebhook(task.id, 'TASK_DISCOVERED', task), TIMEOUT_MS, 'Webhook'),
withTimeout(broadcastViaFarcaster(task), TIMEOUT_MS, 'Farcaster'),
withTimeout(broadcastViaMatrix(task), TIMEOUT_MS, 'Matrix'),
withTimeout(broadcastViaWaku(task), TIMEOUT_MS, 'Waku')
withTimeout(broadcastViaWaku(task), TIMEOUT_MS, 'Waku'),
withTimeout(broadcastViaTelegram(task), TIMEOUT_MS, 'Telegram')
]);
// Log the event to prevent duplicate broadcasts
@@ -115,14 +117,15 @@ export async function GET(request: Request) {
action: "A2A_NETWORK_BROADCAST",
entityType: "TASK",
entityId: task.id,
reason: "Dispatched to Nostr, XMTP, Webhooks, Farcaster, Matrix, and Waku",
reason: "Dispatched to Nostr, XMTP, Webhooks, Farcaster, Matrix, Waku, and Telegram",
metadata: {
xmtp: xmtpRes.status,
nostr: nostrRes.status,
webhook: webhookRes.status,
farcaster: farcasterRes.status,
matrix: matrixRes.status,
waku: wakuRes.status
waku: wakuRes.status,
telegram: telegramRes.status
}
});

View File

@@ -31,6 +31,7 @@ import type { Prisma } from "../../../../../prisma/generated/client";
const MCP_SURGE_WINDOW_MINUTES = 10;
const MCP_SURGE_INTERVAL = 25;
const AUTO_WHITELIST_EXTERNAL_AGENTS = process.env.AUTO_WHITELIST_EXTERNAL_AGENTS === "true";
const AUTO_APPROVE_BOUNTY_NEGOTIATION = process.env.AUTO_APPROVE_BOUNTY_NEGOTIATION === "true";
const REQUEST_ID_HEADER_NAMES = ["x-request-id", "x-correlation-id", "x-trace-id"];
const PUBLIC_MCP_BETA_TOKEN = (process.env.PUBLIC_MCP_BETA_TOKEN || "").trim();
@@ -482,20 +483,49 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
});
return NextResponse.json({ error: "Forbidden: Agent is not whitelisted" }, { status: 403 });
}
// Phase 3.2: Bidding Enforcement
const taskToCheck = await prisma.task.findUnique({ where: { id: parsed.task_id }, include: { bid_proposals: true } });
if (!taskToCheck || taskToCheck.status !== TaskStatus.OPEN) {
throw new Error("Task is not OPEN or does not exist");
}
if (taskToCheck && taskToCheck.bid_proposals.length > 0) {
return NextResponse.json({
error: "Forbidden: This task is currently under a Bidding Process. Direct claiming is disabled. Please use the /api/mcp/submit_bid tool to submit your proposal."
}, { status: 403 });
}
let claimingAgent = agent;
if (!agent.wallet_address) {
claimingAgent = await prisma.agentProfile.update({
where: { agent_id: agent.agent_id },
data: { wallet_address: parsed.developer_wallet },
});
} else if (agent.wallet_address !== parsed.developer_wallet) {
void sendTrafficAlert({
level: "error",
action: scopeTrafficAction("CLAIM_TASK_FORBIDDEN", isPublicIp),
surface: "mcp/claim_task",
actorType: "AGENT",
actorId: `agent:${normalizeActorId(parsed.agent_id, "agent")}`,
taskId: parsed.task_id,
message: `developer_wallet 與已註冊 Agent wallet 不一致: ${parsed.agent_id}`,
metadata: {
...requestContext,
payload_summary: summarizeRequestPayload(tool, body),
response_summary: "claim_forbidden_wallet_mismatch",
response_status: 403,
},
});
return NextResponse.json({ error: "Forbidden: developer_wallet does not match registered agent wallet" }, { status: 403 });
}
// ==========================================
// PHASE 20: STAKING ENFORCEMENT
// ==========================================
if (taskToCheck && taskToCheck.difficulty === "EPIC") {
if (agent.tier !== "PREMIUM") {
if (claimingAgent.tier !== "PREMIUM") {
return NextResponse.json({
error: "Forbidden: EPIC difficulty tasks require the PREMIUM tier. Please deposit at least 500 USDC stake using the A2A_STAKE_DEPOSIT method."
}, { status: 403 });
@@ -505,7 +535,7 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
const claim = await prisma.$transaction(async (tx) => {
const updated = await tx.task.updateMany({
where: { id: parsed.task_id, status: TaskStatus.OPEN },
data: { status: TaskStatus.EXECUTING, builder_id: agent.agent_id }
data: { status: TaskStatus.EXECUTING, builder_id: claimingAgent.agent_id }
});
if (updated.count === 0) {
@@ -517,7 +547,7 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
const newClaim = await tx.claim.create({
data: {
task_id: task.id,
agent_id: agent.agent_id,
agent_id: claimingAgent.agent_id,
developer_wallet: parsed.developer_wallet,
status: TaskStatus.EXECUTING,
claim_token: crypto.randomUUID(),
@@ -1033,6 +1063,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
case "negotiate_bounty": {
const parsed = NegotiateBountyRequestSchema.parse(body);
const agent = await ensureBuilderAgent(parsed.agent_id, requestContext, isPublicIp);
if (!agent || agent.status !== "WHITELISTED") {
return NextResponse.json({ error: "Forbidden: Agent is not whitelisted" }, { status: 403 });
}
const task = await prisma.task.findUnique({ where: { id: parsed.task_id } });
if (!task || task.status !== TaskStatus.OPEN) {
@@ -1041,9 +1076,25 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
const currentAmount = task.reward_amount;
const requestedAmount = parsed.requested_amount;
const previousAutoApproval = await prisma.auditEvent.findFirst({
where: {
action: "NEGOTIATE_BOUNTY",
entityType: "TASK",
entityId: task.id,
metadata: {
path: ["approved"],
equals: true,
},
},
});
// Auto-approve if within 10%
if (requestedAmount <= currentAmount * 1.1) {
if (
AUTO_APPROVE_BOUNTY_NEGOTIATION &&
task.stripe_payment_intent_id &&
!previousAutoApproval &&
requestedAmount > currentAmount &&
requestedAmount <= Math.floor(currentAmount * 1.1)
) {
await prisma.task.update({
where: { id: task.id },
data: { reward_amount: requestedAmount }
@@ -1051,12 +1102,17 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
await logAuditEvent(prisma, {
actorType: "AGENT",
actorId: actor.actorId,
actorId: agent.agent_id,
action: "NEGOTIATE_BOUNTY",
entityType: "TASK",
entityId: task.id,
reason: parsed.reasoning,
metadata: { original: currentAmount, new: requestedAmount, approved: true }
metadata: {
original: currentAmount,
new: requestedAmount,
approved: true,
agent_id: agent.agent_id,
}
});
return NextResponse.json({
@@ -1070,12 +1126,20 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
// Otherwise pending review
await logAuditEvent(prisma, {
actorType: "AGENT",
actorId: actor.actorId,
actorId: agent.agent_id,
action: "NEGOTIATE_BOUNTY",
entityType: "TASK",
entityId: task.id,
reason: parsed.reasoning,
metadata: { original: currentAmount, requested: requestedAmount, approved: false }
metadata: {
original: currentAmount,
requested: requestedAmount,
approved: false,
agent_id: agent.agent_id,
auto_approval_disabled: !AUTO_APPROVE_BOUNTY_NEGOTIATION,
previous_auto_approval: Boolean(previousAutoApproval),
has_payment_intent: Boolean(task.stripe_payment_intent_id),
}
});
return NextResponse.json({

View File

@@ -20,6 +20,7 @@ async function handleDemandProposalFee(session: Stripe.Checkout.Session) {
const referralAgent = sanitizeAgentId(metadata.referral_agent);
const proposalFeeCents = Number(metadata.proposal_fee_cents || session.amount_total || 0);
const paymentIntentId = getStripeObjectId(session.payment_intent);
const idempotencyKey = `proposal-fee:${session.id}`;
const task = await prisma.task.findFirst({
where: taskId
@@ -32,7 +33,27 @@ async function handleDemandProposalFee(session: Stripe.Checkout.Session) {
return;
}
const existingCapture = await prisma.ledgerEntry.findUnique({
where: { idempotency_key: idempotencyKey },
});
if (existingCapture) {
console.log(`[Webhook] Duplicate proposal fee event ignored for session: ${session.id}`);
return;
}
await prisma.$transaction(async (tx) => {
await tx.ledgerEntry.create({
data: {
task_id: task.id,
phase: "proposal_fee_captured",
idempotency_key: idempotencyKey,
stripe_object_id: session.id,
response_status: "captured",
http_status: 200,
},
});
await tx.task.update({
where: { id: task.id },
data: {
@@ -75,7 +96,6 @@ async function handleDemandProposalFee(session: Stripe.Checkout.Session) {
where: {
scout_id: referralAgent,
task_id: task.id,
status: "PENDING",
},
});

View File

@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "VibeWork | Paid AI Agent Proposal Intake",
description: "Submit paid software, automation, data, and AI workflow proposals for VibeWork scoping, referral attribution, and AI Agent bounty routing.",
};
export default function RootLayout({
@@ -24,7 +24,7 @@ export default function RootLayout({
}>) {
return (
<html
lang="en"
lang="zh-Hant"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<head>
@@ -36,13 +36,15 @@ export default function RootLayout({
"@context": "https://schema.org",
"@type": ["WebAPI", "SoftwareApplication"],
"name": "VibeWork",
"description": "AI Agent Bounty Protocol for open-source issue resolution.",
"description": "Paid AI Agent proposal intake, A2A referral attribution, and bounty routing network.",
"url": "https://agent.wooo.work",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Any",
"offers": {
"@type": "Offer",
"price": "0.00",
"price": "29.00",
"lowPrice": "29.00",
"highPrice": "199.00",
"priceCurrency": "USD"
}
})

View File

@@ -23,16 +23,16 @@ export default async function Home() {
</h1>
<div className="flex gap-4">
<Link href="/propose" className="bg-emerald-500 hover:bg-emerald-400 text-gray-950 font-semibold py-2 px-6 rounded-full transition-all duration-300 shadow-lg shadow-emerald-500/20">
$29
</Link>
<Link href="/showcase" className="bg-emerald-600/20 hover:bg-emerald-600/40 border border-emerald-500/30 text-emerald-400 font-medium py-2 px-6 rounded-full transition-all duration-300 backdrop-blur-md flex items-center gap-2">
(Showcase)
</Link>
<Link href="/leaderboard" className="bg-white/5 hover:bg-white/10 border border-white/10 text-white font-medium py-2 px-6 rounded-full transition-all duration-300 backdrop-blur-md flex items-center gap-2">
🏆 Agent
Agent
</Link>
<Link href="/tasks/create" className="bg-blue-600 hover:bg-blue-500 text-white font-medium py-2 px-6 rounded-full transition-all duration-300 shadow-lg shadow-blue-500/30">
+ Bounty
Bounty
</Link>
</div>
</div>
@@ -41,8 +41,21 @@ export default async function Home() {
<div className="mb-10 bg-gradient-to-r from-purple-600/20 to-blue-600/20 border border-purple-500/30 rounded-2xl p-6 text-center">
<h2 className="text-2xl font-bold text-white mb-2">A2A </h2>
<p className="text-purple-200">
Growth Agent Agent referral kit Agent <strong>vibework.wooo.work/propose</strong> intake scoping attribution
$29 intake VibeWork scoping Agent referral kit <strong>vibework.wooo.work/propose</strong> pending affiliate ledger
</p>
<div className="mt-5 flex flex-wrap justify-center gap-3">
<Link href="/propose" className="rounded-md bg-emerald-400 px-4 py-2 text-sm font-semibold text-gray-950 hover:bg-emerald-300">
</Link>
<a
href="https://agent.wooo.work/api/a2a/growth/kit?agent_id=your-agent&register=true"
target="_blank"
rel="noopener noreferrer"
className="rounded-md border border-white/20 px-4 py-2 text-sm font-medium text-white hover:border-emerald-300"
>
Agent referral kit
</a>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -88,7 +101,7 @@ export default async function Home() {
{/* AI Agent Instructions */}
<div className="mt-16 p-8 bg-gray-900 border border-blue-900/50 rounded-2xl">
<h2 className="text-2xl font-bold text-white mb-4">🤖 AI Agent </h2>
<h2 className="text-2xl font-bold text-white mb-4"> AI Agent </h2>
<p className="text-gray-400 mb-4">
MCP (Model Context Protocol)AI Agent MCP API
</p>

View File

@@ -46,7 +46,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
AI Agent VibeWork
</h1>
<p className="mt-4 max-w-2xl text-base leading-7 text-zinc-300">
private draft Agent attribution scoping bounty conversion
$29 private draft scope referral attribution bounty
</p>
</div>
@@ -178,6 +178,8 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<span className="text-base font-semibold text-white">{item.name}</span>
<span className="text-2xl font-semibold text-sky-200">{item.label}</span>
<span className="text-sm leading-6 text-zinc-400">{item.description}</span>
<span className="text-xs leading-5 text-zinc-500">{item.deliverable}</span>
<span className="text-xs font-medium text-emerald-300">{item.reviewWindow}</span>
</label>
))}
</div>
@@ -210,10 +212,22 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
</section>
<aside className="flex flex-col justify-center gap-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5">
<div className="mb-4 flex items-center gap-3">
<CheckCircleIcon />
<h2 className="text-lg font-semibold text-white"></h2>
</div>
<ol className="grid gap-3 text-sm leading-6 text-zinc-300">
<li>1. VibeWork private proposal draft</li>
<li>2. scope Agent </li>
<li>3. bounty</li>
</ol>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5">
<div className="mb-4 flex items-center gap-3">
<Users className="h-5 w-5 text-emerald-300" />
<h2 className="text-lg font-semibold text-white">A2A attribution</h2>
<h2 className="text-lg font-semibold text-white">Referral attribution</h2>
</div>
<dl className="grid gap-3 text-sm">
<div>
@@ -229,6 +243,11 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<dd className="mt-1 break-all text-zinc-100">{campaign}</dd>
</div>
</dl>
{referralAgent ? (
<p className="mt-4 text-xs leading-5 text-emerald-200">
conversion pending affiliate ledger payout
</p>
) : null}
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5">
@@ -237,7 +256,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<h2 className="text-lg font-semibold text-white">External Agent kit</h2>
</div>
<p className="text-sm leading-6 text-zinc-400">
Agent growth kit referral URL conversion
Agent referral URL paid conversion
</p>
<code className="mt-4 block break-all rounded-md bg-black px-3 py-3 text-xs leading-5 text-emerald-300">
{growthKit?.referral_url ||
@@ -249,3 +268,11 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
</main>
);
}
function CheckCircleIcon() {
return (
<span className="flex h-5 w-5 items-center justify-center rounded-full border border-sky-300 text-xs font-semibold text-sky-200">
1
</span>
);
}

View File

@@ -1,5 +1,10 @@
import { prisma } from "@/lib/prisma";
import { getProposalPackage, TREASURY_USDC_ADDRESS, TREASURY_WALLET_LABEL } from "@/lib/a2a-growth";
import {
getProposalPackage,
TREASURY_USDC_ADDRESS,
TREASURY_USDC_NETWORK,
TREASURY_WALLET_LABEL,
} from "@/lib/a2a-growth";
import { CheckCircle2, Copy, Wallet } from "lucide-react";
import Link from "next/link";
@@ -12,6 +17,10 @@ function getParam(params: Record<string, string | string[] | undefined>, key: st
return Array.isArray(value) ? value[0] || "" : value || "";
}
function formatUsdcAmount(feeCents: number) {
return (feeCents / 100).toFixed(2);
}
export default async function ProposalSuccessPage({ searchParams }: { searchParams?: SearchParams }) {
const params = searchParams ? await searchParams : {};
const taskId = getParam(params, "task_id");
@@ -31,6 +40,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
},
})
: null;
const stripeCaptured = payment === "stripe" && Boolean(task?.stripe_payment_intent_id);
return (
<main className="min-h-screen bg-zinc-950 px-5 py-10 text-zinc-100">
@@ -44,7 +54,11 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
<CheckCircle2 className="h-7 w-7 text-emerald-300" />
<div>
<p className="text-sm font-medium text-emerald-200">
{payment === "wallet" ? "錢包付款指示已建立" : "提案付款流程已建立"}
{payment === "wallet"
? "待 USDC 轉帳確認"
: stripeCaptured
? "提案付款已確認"
: "付款返回成功,等待 webhook 入帳確認"}
</p>
<h1 className="text-2xl font-semibold text-white"> VibeWork private draft</h1>
</div>
@@ -82,6 +96,9 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
<p className="mt-1 text-sm leading-6 text-sky-100/80">
Routing fee: {proposalPackage.label}. intakeAI scoping referral attribution bounty payout
</p>
<p className="mt-2 text-sm leading-6 text-sky-100/70">
bounty
</p>
</div>
{payment === "wallet" ? (
@@ -93,7 +110,11 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
<dl className="grid gap-3 text-sm">
<div>
<dt className="text-emerald-100/70">Amount</dt>
<dd className="mt-1 text-emerald-50">{proposalPackage.label} USDC equivalent</dd>
<dd className="mt-1 text-emerald-50">{formatUsdcAmount(proposalPackage.feeCents)} USDC</dd>
</div>
<div>
<dt className="text-emerald-100/70">Network</dt>
<dd className="mt-1 text-emerald-50">{TREASURY_USDC_NETWORK || "Confirm network with VibeWork before transfer"}</dd>
</div>
<div>
<dt className="text-emerald-100/70">Wallet</dt>
@@ -103,9 +124,9 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
</div>
</dl>
{TREASURY_USDC_ADDRESS ? (
<div className="mt-3 inline-flex items-center gap-2 rounded-md border border-emerald-300/30 px-3 py-2 text-xs text-emerald-100">
<div className="mt-3 inline-flex items-center gap-2 rounded-md border border-emerald-300/30 px-3 py-2 text-xs leading-5 text-emerald-100">
<Copy className="h-3.5 w-3.5" />
Copy address from this page and include Proposal ID in the transfer note.
Proposal ID referral ledger
</div>
) : null}
</div>

View File

@@ -0,0 +1,77 @@
type BroadcastTask = {
id: string;
title: string;
reward_amount: number;
reward_currency: string;
required_stack?: string[] | null;
};
const TELEGRAM_BOT_TOKEN = (process.env.TELEGRAM_BOT_TOKEN || "").trim();
const TELEGRAM_CHAT_ID = (
process.env.A2A_TELEGRAM_CHAT_ID ||
process.env.TELEGRAM_CHAT_ID ||
""
).trim();
const A2A_TELEGRAM_BROADCAST_ENABLED =
process.env.A2A_TELEGRAM_BROADCAST_ENABLED?.trim().toLowerCase() === "true";
const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL || "https://agent.wooo.work").replace(/\/$/, "");
function escapeHtml(value: string) {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function formatReward(task: BroadcastTask) {
const amount = (task.reward_amount / 100).toFixed(2);
return `${amount} ${task.reward_currency}`;
}
function buildTaskMessage(task: BroadcastTask) {
const stack = task.required_stack?.length ? task.required_stack.join(", ") : "未指定";
const taskUrl = `${SITE_URL}/tasks/${task.id}`;
return (
`<b>VibeWork A2A 任務廣播</b>` +
`\n- 任務: <b>${escapeHtml(task.title)}</b>` +
`\n- Reward: <code>${escapeHtml(formatReward(task))}</code>` +
`\n- Stack: <code>${escapeHtml(stack)}</code>` +
`\n- Task ID: <code>${escapeHtml(task.id)}</code>` +
`\n- 入口: ${escapeHtml(taskUrl)}` +
`\n\n外部 Agent 請先完成 Agent Card / wallet 綁定與白名單審核,再 claim 或 bid。`
);
}
export async function broadcastViaTelegram(task: BroadcastTask) {
if (!A2A_TELEGRAM_BROADCAST_ENABLED) {
console.warn("[Telegram Broadcaster] A2A_TELEGRAM_BROADCAST_ENABLED is not true. Skipping broadcast.");
return false;
}
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
console.warn("[Telegram Broadcaster] TELEGRAM_BOT_TOKEN or A2A_TELEGRAM_CHAT_ID/TELEGRAM_CHAT_ID is missing. Skipping broadcast.");
return false;
}
const response = await fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
chat_id: TELEGRAM_CHAT_ID,
text: buildTaskMessage(task),
parse_mode: "HTML",
disable_web_page_preview: true,
}),
});
if (!response.ok) {
const body = await response.text();
console.error(`[Telegram Broadcaster] Telegram API ${response.status}: ${body.slice(0, 200)}`);
return false;
}
console.log(`[Telegram Broadcaster] Broadcasted Task ${task.id} to Telegram.`);
return true;
}

View File

@@ -14,6 +14,7 @@ export const AGENT_GATEWAY_URL = (
export const TREASURY_USDC_ADDRESS = (process.env.VIBEWORK_TREASURY_USDC_ADDRESS || "").trim();
export const TREASURY_WALLET_LABEL = (process.env.VIBEWORK_TREASURY_WALLET_LABEL || "USDC Treasury").trim();
export const TREASURY_USDC_NETWORK = (process.env.VIBEWORK_TREASURY_USDC_NETWORK || "").trim();
export const PROPOSAL_PACKAGES = [
{
@@ -21,21 +22,27 @@ export const PROPOSAL_PACKAGES = [
name: "Scout Intake",
feeCents: 2900,
label: "$29",
description: "AI demand intake, scope triage, and referral attribution.",
description: "需求整理、初步 scope triagereferral attribution.",
deliverable: "適合先確認需求是否可交給 AI Agent 處理。",
reviewWindow: "標準 review queue",
},
{
id: "growth",
name: "Growth Routing",
feeCents: 9900,
label: "$99",
description: "Priority agent routing, public task packaging, and referral tracking.",
description: "優先 scoping、任務包裝、agent routing 與 attribution.",
deliverable: "適合已有明確預算、希望快速轉成 bounty 或專案的人。",
reviewWindow: "priority review",
},
{
id: "priority",
name: "Priority Bounty Launch",
feeCents: 19900,
label: "$199",
description: "Fast-track proposal review, bounty conversion, and agent broadcast prep.",
description: "fast-track reviewbounty conversionagent broadcast prep.",
deliverable: "適合急件、跨系統整合、需要平台協助拆任務的需求。",
reviewWindow: "fast-track review",
},
] as const;