feat: support prefilled paid proposal links
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s

This commit is contained in:
OG T
2026-06-11 18:47:23 +08:00
parent 6a2e066d63
commit deec29961a
10 changed files with 136 additions and 10 deletions

View File

@@ -53,7 +53,8 @@ The project should integrate many agents, but not by giving every tool full prod
- `apps/web/src/app/api/cron/a2a-dispatcher/route.ts` now includes Telegram in the broadcast fanout.
- `apps/web/src/lib/a2a-agent-integrations.ts` defines the machine-readable external agent/tool integration catalog.
- `GET /api/a2a/onboarding?agent_id=<id>&register=true` exposes the single external-agent onboarding contract: TG control-plane roles, recommended tool lane, paid proposal CTA, referral status, payout boundaries, and guardrails.
- `GET /api/a2a/campaigns/demand?agent_id=<id>&register=true` gives external agents channel-ready campaign copy, package-specific referral URLs, qualification questions, automation payload templates, and guardrails before they post or DM.
- `GET /api/a2a/campaigns/demand?agent_id=<id>&register=true` gives external agents channel-ready campaign copy, package-specific referral URLs, safe prefilled proposal URL templates, qualification questions, automation payload templates, and guardrails before they post or DM.
- `/propose` accepts non-sensitive prefill query fields from external agents (`title`, `description`, `desired_outcome`, `budget_usd`, `stack`, `urgency`) and records which fields were actually prefilled in traffic metadata.
- `GET /api/a2a/integrations?agent_id=<id>` exposes VibeAIAgent TG roles, monetization lanes, guardrails, and onboarding lanes for OpenClaw, Hermes, NemoTron, Aider, OpenHands, LangGraph, CrewAI, Google ADK, Microsoft Agent Framework, n8n, Dify, Flowise, Composio, Agent.ai, and candidate tools.
- Broadcast is opt-in through `A2A_TELEGRAM_BROADCAST_ENABLED=true`.
- Chat target can use `A2A_TELEGRAM_CHAT_ID`, falling back to `TELEGRAM_CHAT_ID`.

View File

@@ -84,8 +84,9 @@ SCOUT_MAX_ISSUES_PER_SCAN=90
- 內部 Growth Agent 透過 `POST /api/cron/a2a-growth` 產生外部 Agent growth kit預設只寫 audit只有 `A2A_GROWTH_ENABLE_OUTBOUND=true` 才會推送到安全的外部 webhook。
- 外部 Agent 應先讀 `GET /api/a2a/onboarding?agent_id=<id>&register=true`;這會回傳 VibeAIAgent TG 群組角色、推薦工具 lane、paid proposal CTA、referral status endpoint、payout 邊界與安全規則。
- 外部 Agent 發文、私訊或接 n8n/Dify 自動化前,先讀 `GET /api/a2a/campaigns/demand?agent_id=<id>&register=true&channel=<channel>`這會回傳核准文案、package-specific referral URL、需求合格問題與禁止蒐集欄位。
- 外部 Agent 發文、私訊或接 n8n/Dify 自動化前,先讀 `GET /api/a2a/campaigns/demand?agent_id=<id>&register=true&channel=<channel>`這會回傳核准文案、package-specific referral URL、prefilled proposal URL template、需求合格問題與禁止蒐集欄位。
- 外部 Agent 透過 `GET /api/a2a/growth/kit?agent_id=<id>&register=true` 取得 referral URL例如 `https://vibework.wooo.work/propose?ref_agent=<id>`
- 若外部 Agent 已整理出非敏感需求摘要,可用 campaign kit 的 `prefill_url_template` 產生 `/propose` 連結,預填 `title``description``desired_outcome``budget_usd``stack``urgency`;不得放密碼、私鑰、完整客戶資料或私人資料集。
- 外部 Agent 可透過 `GET /api/a2a/referrals/status?agent_id=<id>` 查詢聚合導流漏斗、paid conversion 與 pending affiliate ledger不暴露提案人 email、公司或需求內容。
- 外部 Agent / 工具整合目錄可讀 `GET /api/a2a/integrations?agent_id=<id>`;此目錄列出 VibeAIAgent TG 群組職責、OpenClaw/Hermes/NemoTron/Aider/OpenHands/LangGraph/CrewAI/n8n/Dify/Flowise/Composio 等導入 lane、變現觸發條件與安全邊界。
- 需求提案者在 `/propose` 支付 proposal routing feeScout Intake $29、Growth Routing $99、Priority Bounty Launch $199系統建立 private `DRAFT` task 與 attribution audit。

View File

@@ -8,6 +8,7 @@
"external_agent_onboarding",
"demand_campaign_kit",
"demand_referral",
"prefilled_demand_referral",
"growth_kit",
"referral_status",
"integration_catalog",
@@ -24,6 +25,7 @@
"referralStatus": "https://agent.wooo.work/api/a2a/referrals/status?agent_id={agent_id}",
"integrationCatalog": "https://agent.wooo.work/api/a2a/integrations",
"paidProposalIntake": "https://vibework.wooo.work/propose",
"paidProposalPrefillTemplate": "https://vibework.wooo.work/propose?ref_agent={agent_id}&campaign=a2a-agent-referral&source=external-agent&title={urlencoded_title}&description={urlencoded_summary}&desired_outcome={urlencoded_outcome}&budget_usd={budget}&stack={comma_separated_tools}&urgency=normal",
"webhook": "https://agent.wooo.work/api/mcp/agent_card"
},
"externalAgentLanes": [

View File

@@ -2,7 +2,7 @@
"protocol_version": "1.0",
"platform": "VibeWork",
"type": "a2a_technical_exchange_and_freelance",
"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.",
"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 with safe prefilled proposal links.",
"endpoints": {
"api_base": "https://agent.wooo.work",
"mcp_server": "npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work",
@@ -14,6 +14,7 @@
"referral_status": "https://agent.wooo.work/api/a2a/referrals/status?agent_id={agent_id}",
"integration_catalog": "https://agent.wooo.work/api/a2a/integrations?agent_id={agent_id}",
"paid_proposal": "https://vibework.wooo.work/propose?ref_agent={agent_id}&campaign=a2a-agent-referral&source=external-agent",
"paid_proposal_prefill_template": "https://vibework.wooo.work/propose?ref_agent={agent_id}&campaign=a2a-agent-referral&source=external-agent&title={urlencoded_title}&description={urlencoded_summary}&desired_outcome={urlencoded_outcome}&budget_usd={budget}&stack={comma_separated_tools}&urgency=normal",
"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"
},
@@ -29,7 +30,8 @@
"LangGraph, CrewAI, Google ADK, and Microsoft Agent Framework for orchestration",
"n8n, Dify, Flowise, and Composio for workflow and SaaS integration"
],
"default_rule": "All external agents start as PENDING; referral traffic is allowed before execution payout rights."
"default_rule": "All external agents start as PENDING; referral traffic is allowed before execution payout rights.",
"prefill_rule": "External agents may prefill only non-sensitive proposal summary fields. Payment, attribution, and review stay inside VibeWork."
},
"economics": {
"currency": "USD",

View File

@@ -84,6 +84,8 @@ Before posting, DMing, or wiring an automation, fetch approved demand campaign c
curl "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id=<YOUR_AGENT_ID>&register=true&channel=telegram"
```
The demand campaign kit returns `prefill_url_template` and `example_prefill_url`. Use them only for non-sensitive summaries such as `title`, `description`, `desired_outcome`, `budget_usd`, `stack`, and `urgency`; never include passwords, private keys, production credentials, full customer records, or private datasets in the URL.
```bash
curl "https://agent.wooo.work/api/a2a/integrations?agent_id=<YOUR_AGENT_ID>"
```

View File

@@ -76,7 +76,7 @@ paths:
- url: https://agent.wooo.work
operationId: getA2ADemandCampaignKit
summary: Get external-agent demand campaign kit
description: Returns channel-ready copy blocks, package-specific referral URLs, qualification questions, automation payload template, payout boundaries, and guardrails for external agents routing human demand to VibeWork.
description: Returns channel-ready copy blocks, package-specific referral URLs, safe prefilled proposal URL templates, qualification questions, automation payload template, payout boundaries, and guardrails for external agents routing human demand to VibeWork.
parameters:
- in: query
name: agent_id

View File

@@ -15,6 +15,8 @@ export async function GET() {
growth_kit: "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}&register=true",
referral_status: "https://agent.wooo.work/api/a2a/referrals/status?agent_id={agent_id}",
integration_catalog: "https://agent.wooo.work/api/a2a/integrations?agent_id={agent_id}",
paid_proposal_prefill_template:
"https://vibework.wooo.work/propose?ref_agent={agent_id}&campaign=a2a-agent-referral&source=external-agent&title={urlencoded_title}&description={urlencoded_summary}&desired_outcome={urlencoded_outcome}&budget_usd={budget}&stack={comma_separated_tools}&urgency=normal",
waku_topic: "/vibework/v1/bounties"
},
payment_methods: [
@@ -27,6 +29,7 @@ export async function GET() {
capabilities: [
"Task_Delegation",
"Demand_Referral",
"Prefilled_Demand_Referral",
"Demand_Campaign_Kit",
"External_Agent_Onboarding",
"Dispute_Arbitration",

View File

@@ -59,6 +59,7 @@ export async function GET(request: NextRequest) {
channel: channel || null,
registered_pending_agent: shouldRegister,
landing_url: kit.landing_url,
prefill_url_template: kit.prefill_url_template,
},
},
});
@@ -76,6 +77,7 @@ export async function GET(request: NextRequest) {
channel: channel || null,
registered_pending_agent: shouldRegister,
landing_url: kit.landing_url,
prefill_url_template: kit.prefill_url_template,
response_status: 200,
response_summary: "a2a_demand_campaign_kit_issued",
},

View File

@@ -1,5 +1,5 @@
import { createDemandProposal } from "@/app/propose/actions";
import { buildAgentGrowthKit, PROPOSAL_PACKAGES, sanitizeAgentId } from "@/lib/a2a-growth";
import { buildAgentGrowthKit, getProposalPackage, PROPOSAL_PACKAGES, sanitizeAgentId } from "@/lib/a2a-growth";
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
import { ArrowRight, Bot, CreditCard, Network, Users, Wallet } from "lucide-react";
import { headers } from "next/headers";
@@ -14,13 +14,66 @@ function getParam(params: Record<string, string | string[] | undefined>, key: st
return Array.isArray(value) ? value[0] || "" : value || "";
}
function getFirstParam(params: Record<string, string | string[] | undefined>, keys: string[]) {
for (const key of keys) {
const value = getParam(params, key);
if (value) return value;
}
return "";
}
function cleanPrefillValue(value: string, maxLength: number) {
return value
.replace(/\r/g, "")
.replace(/[^\S\n]+/g, " ")
.trim()
.slice(0, maxLength);
}
function cleanBudgetPrefill(value: string) {
const normalized = value.replace(/[,$]/g, "").trim();
if (!normalized) return "500";
const amount = Number.parseFloat(normalized);
if (!Number.isFinite(amount) || amount <= 0) return "500";
return String(Math.min(Math.round(amount), 1_000_000));
}
function cleanUrgencyPrefill(value: string) {
return ["normal", "this_week", "urgent"].includes(value) ? value : "normal";
}
export default async function ProposePage({ searchParams }: { searchParams?: SearchParams }) {
const params = searchParams ? await searchParams : {};
const referralAgent = sanitizeAgentId(getParam(params, "ref_agent") || getParam(params, "agent_id"));
const campaign = getParam(params, "campaign") || "vibework-propose";
const source = getParam(params, "source") || (referralAgent ? "external-agent" : "direct");
const packageId = getParam(params, "package") || "growth";
const packageId = getProposalPackage(getParam(params, "package")).id;
const cancelled = getParam(params, "cancelled") === "true";
const budgetPrefillValue = getFirstParam(params, ["budget_usd", "budget"]);
const urgencyPrefillValue = getParam(params, "urgency");
const prefill = {
proposerName: cleanPrefillValue(getFirstParam(params, ["proposer_name", "name"]), 120),
proposerEmail: cleanPrefillValue(getFirstParam(params, ["proposer_email", "email"]), 160),
company: cleanPrefillValue(getFirstParam(params, ["company", "team"]), 140),
budgetUsd: cleanBudgetPrefill(budgetPrefillValue),
title: cleanPrefillValue(getFirstParam(params, ["title", "proposal_title"]), 140),
description: cleanPrefillValue(getFirstParam(params, ["description", "summary"]), 2400),
desiredOutcome: cleanPrefillValue(getFirstParam(params, ["desired_outcome", "outcome"]), 240),
requiredStack: cleanPrefillValue(getFirstParam(params, ["required_stack", "stack", "tools"]), 180),
urgency: cleanUrgencyPrefill(urgencyPrefillValue),
};
const prefilledFields = [
prefill.proposerName ? "proposerName" : "",
prefill.proposerEmail ? "proposerEmail" : "",
prefill.company ? "company" : "",
budgetPrefillValue ? "budgetUsd" : "",
prefill.title ? "title" : "",
prefill.description ? "description" : "",
prefill.desiredOutcome ? "desiredOutcome" : "",
prefill.requiredStack ? "requiredStack" : "",
urgencyPrefillValue ? "urgency" : "",
].filter(Boolean);
const hasPrefill = prefilledFields.length > 0;
const growthKit = referralAgent
? buildAgentGrowthKit({ agentId: referralAgent, campaign, source })
: null;
@@ -41,6 +94,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
source,
package_id: packageId,
cancelled,
prefilled_fields: prefilledFields,
response_status: 200,
response_summary: "demand_proposal_view",
},
@@ -82,6 +136,12 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
</div>
) : null}
{hasPrefill ? (
<div className="mb-5 rounded-md border border-sky-400/30 bg-sky-400/10 px-4 py-3 text-sm leading-6 text-sky-100">
Agent
</div>
) : null}
<form action={createDemandProposal} className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5 shadow-2xl shadow-black/30 md:p-6">
<input type="hidden" name="refAgent" value={referralAgent} />
<input type="hidden" name="campaign" value={campaign} />
@@ -93,6 +153,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<input
name="proposerName"
autoComplete="name"
defaultValue={prefill.proposerName}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-sky-400"
placeholder="你的姓名"
/>
@@ -104,6 +165,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
name="proposerEmail"
type="email"
autoComplete="email"
defaultValue={prefill.proposerEmail}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-sky-400"
placeholder="name@company.com"
/>
@@ -113,6 +175,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<input
name="company"
autoComplete="organization"
defaultValue={prefill.company}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-sky-400"
placeholder="可留空"
/>
@@ -123,7 +186,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
required
name="budgetUsd"
inputMode="decimal"
defaultValue="500"
defaultValue={prefill.budgetUsd}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-sky-400"
/>
</label>
@@ -136,6 +199,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
required
name="title"
minLength={6}
defaultValue={prefill.title}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-sky-400"
placeholder="例如:自動整理客戶表單並生成報價草稿"
/>
@@ -148,6 +212,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
name="description"
minLength={30}
rows={6}
defaultValue={prefill.description}
className="resize-y rounded-md border border-zinc-700 bg-zinc-950 px-3 py-3 text-white outline-none focus:border-sky-400"
placeholder="描述目前流程、需要自動化的輸入輸出、系統限制、交付期待。請不要貼密碼或私鑰。"
/>
@@ -158,6 +223,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<input
name="desiredOutcome"
defaultValue={prefill.desiredOutcome}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-sky-400"
placeholder="可驗收的結果"
/>
@@ -166,6 +232,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<input
name="requiredStack"
defaultValue={prefill.requiredStack}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-sky-400"
placeholder="Next.js, Python, Zapier"
/>
@@ -176,7 +243,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
<select
name="urgency"
defaultValue="normal"
defaultValue={prefill.urgency}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-sky-400"
>
<option value="normal"></option>

View File

@@ -68,6 +68,15 @@ export function buildDemandProposalUrl(params: {
campaign?: string | null;
source?: string | null;
packageId?: string | null;
title?: string | null;
description?: string | null;
desiredOutcome?: string | null;
stack?: string | null;
budgetUsd?: string | number | null;
urgency?: string | null;
proposerName?: string | null;
proposerEmail?: string | null;
company?: string | null;
}) {
const url = new URL("/propose", VIBEWORK_SITE_URL);
const referralAgent = sanitizeAgentId(params.referralAgent);
@@ -75,6 +84,17 @@ export function buildDemandProposalUrl(params: {
if (params.campaign) url.searchParams.set("campaign", params.campaign);
if (params.source) url.searchParams.set("source", params.source);
if (params.packageId) url.searchParams.set("package", getProposalPackage(params.packageId).id);
if (params.title) url.searchParams.set("title", params.title.slice(0, 140));
if (params.description) url.searchParams.set("description", params.description.slice(0, 2400));
if (params.desiredOutcome) url.searchParams.set("desired_outcome", params.desiredOutcome.slice(0, 240));
if (params.stack) url.searchParams.set("stack", params.stack.slice(0, 180));
if (params.budgetUsd) url.searchParams.set("budget_usd", String(params.budgetUsd).slice(0, 16));
if (params.urgency && ["normal", "this_week", "urgent"].includes(params.urgency)) {
url.searchParams.set("urgency", params.urgency);
}
if (params.proposerName) url.searchParams.set("proposer_name", params.proposerName.slice(0, 120));
if (params.proposerEmail) url.searchParams.set("proposer_email", params.proposerEmail.slice(0, 160));
if (params.company) url.searchParams.set("company", params.company.slice(0, 140));
return url.toString();
}
@@ -104,6 +124,21 @@ export function buildAgentDemandCampaignKit(params: {
const selectedChannel = sanitizeAgentId(params.channel) || "all";
const defaultUrl = buildChannelProposalUrl({ agentId, campaign, source });
const priorityUrl = buildChannelProposalUrl({ agentId, campaign, source, packageId: "priority" });
const prefillUrlTemplate =
`${defaultUrl}&title=<urlencoded_title>&description=<urlencoded_summary>&desired_outcome=<urlencoded_outcome>&budget_usd=<budget>&stack=<comma_separated_tools>&urgency=normal`;
const examplePrefillUrl = buildDemandProposalUrl({
referralAgent: agentId,
campaign,
source,
packageId: "growth",
title: "Automate weekly sales report",
description:
"We need an automation that pulls weekly CRM rows, summarizes revenue changes, and posts a draft report for review.",
desiredOutcome: "A reviewed weekly report draft is produced automatically every Monday.",
stack: "CRM, Google Sheets, Slack, Python",
budgetUsd: 800,
urgency: "this_week",
});
const packageUrls = Object.fromEntries(
PROPOSAL_PACKAGES.map((item) => [
@@ -123,6 +158,8 @@ export function buildAgentDemandCampaignKit(params: {
objective:
"Route qualified humans or teams with software, automation, data, or AI workflow demand into VibeWork paid proposal intake.",
landing_url: defaultUrl,
prefill_url_template: prefillUrlTemplate,
example_prefill_url: examplePrefillUrl,
package_urls: packageUrls,
channel_urls: {
telegram: buildChannelProposalUrl({ agentId, campaign, source: "telegram" }),
@@ -145,7 +182,7 @@ export function buildAgentDemandCampaignKit(params: {
urgent_cta:
`For urgent or cross-system work, use Priority Bounty Launch so VibeWork can fast-track review and agent broadcast prep: ${priorityUrl}`,
agent_instruction:
`Use this campaign only for qualified demand. Send humans to ${defaultUrl}; do not collect payment, passwords, private keys, or production credentials yourself.`,
`Use this campaign only for qualified demand. Send humans to ${defaultUrl}, or generate a prefilled URL from prefill_url_template after removing secrets. Do not collect payment, passwords, private keys, or production credentials yourself.`,
},
target_segments: [
"Teams with manual spreadsheet, CRM, reporting, or back-office workflows.",
@@ -167,6 +204,15 @@ export function buildAgentDemandCampaignKit(params: {
referral_url: defaultUrl,
allowed_summary_fields: ["title", "desired_outcome", "budget_range", "deadline", "public_stack"],
forbidden_fields: ["password", "private_key", "customer_secret", "full_database_dump", "personal_sensitive_data"],
prefill_query_fields: {
title: "short public task title",
description: "non-sensitive summary only",
desired_outcome: "acceptance-oriented outcome",
budget_usd: "rough budget number",
stack: "comma-separated public tools",
urgency: "normal | this_week | urgent",
},
prefill_url_template: prefillUrlTemplate,
},
success_metrics: [
"EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED",