feat: add A2A agent integration control plane
All checks were successful
CI and Production Smoke / smoke (push) Successful in 10s
All checks were successful
CI and Production Smoke / smoke (push) Successful in 10s
This commit is contained in:
@@ -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}®ister=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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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®ister=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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}. 這筆費用是提案 intake、AI 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>
|
||||
|
||||
77
apps/web/src/lib/a2a-broadcasters/telegram.ts
Normal file
77
apps/web/src/lib/a2a-broadcasters/telegram.ts
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 triage、referral 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 review、bounty conversion、agent broadcast prep.",
|
||||
deliverable: "適合急件、跨系統整合、需要平台協助拆任務的需求。",
|
||||
reviewWindow: "fast-track review",
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user