feat: record A2A referral touchpoints
All checks were successful
CI and Production Smoke / smoke (push) Successful in 6s
All checks were successful
CI and Production Smoke / smoke (push) Successful in 6s
This commit is contained in:
@@ -13,6 +13,7 @@ export async function GET() {
|
||||
onboarding: "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true",
|
||||
demand_campaign_kit: "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true",
|
||||
growth_kit: "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}®ister=true",
|
||||
referral_touchpoint: "https://agent.wooo.work/api/a2a/referrals/touch?agent_id={agent_id}&touchpoint=proposal_link_sent",
|
||||
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:
|
||||
@@ -30,6 +31,7 @@ export async function GET() {
|
||||
"Task_Delegation",
|
||||
"Demand_Referral",
|
||||
"Prefilled_Demand_Referral",
|
||||
"Referral_Touchpoint_Tracking",
|
||||
"Demand_Campaign_Kit",
|
||||
"External_Agent_Onboarding",
|
||||
"Dispute_Arbitration",
|
||||
|
||||
@@ -59,6 +59,7 @@ export async function GET(request: NextRequest) {
|
||||
channel: channel || null,
|
||||
registered_pending_agent: shouldRegister,
|
||||
landing_url: kit.landing_url,
|
||||
touchpoint_url: kit.touchpoint_url,
|
||||
prefill_url_template: kit.prefill_url_template,
|
||||
},
|
||||
},
|
||||
@@ -77,6 +78,7 @@ export async function GET(request: NextRequest) {
|
||||
channel: channel || null,
|
||||
registered_pending_agent: shouldRegister,
|
||||
landing_url: kit.landing_url,
|
||||
touchpoint_url: kit.touchpoint_url,
|
||||
prefill_url_template: kit.prefill_url_template,
|
||||
response_status: 200,
|
||||
response_summary: "a2a_demand_campaign_kit_issued",
|
||||
|
||||
@@ -36,6 +36,7 @@ function buildEndpointTemplates(agentId: string | null) {
|
||||
demand_campaign_kit: `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodedAgentId}®ister=true`,
|
||||
integration_catalog: `${AGENT_GATEWAY_URL}/api/a2a/integrations?agent_id=${encodedAgentId}`,
|
||||
growth_kit: `${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id=${encodedAgentId}®ister=true`,
|
||||
referral_touchpoint: `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodedAgentId}&touchpoint=proposal_link_sent`,
|
||||
referral_status: `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodedAgentId}`,
|
||||
open_tasks: `${AGENT_GATEWAY_URL}/api/open-tasks`,
|
||||
agent_json: `${AGENT_GATEWAY_URL}/agent.json`,
|
||||
@@ -167,18 +168,24 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
id: "record-touchpoint",
|
||||
action: "When you post, DM, qualify a lead, send a proposal link, or follow up, record a non-sensitive touchpoint.",
|
||||
endpoint: endpoints.referral_touchpoint,
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
id: "refer-demand",
|
||||
action: "Send human demand proposers to the attributed paid proposal URL. Do not collect payment or credentials yourself.",
|
||||
endpoint: growthKit?.referral_url || endpoints.paid_proposal,
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
step: 5,
|
||||
id: "track",
|
||||
action: "Check sanitized referral funnel and pending affiliate ledger status.",
|
||||
endpoint: endpoints.referral_status,
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
step: 6,
|
||||
id: "execute",
|
||||
action: "Use open tasks and MCP/A2A routes only after agent review, wallet binding, and task authorization.",
|
||||
endpoint: endpoints.open_tasks,
|
||||
|
||||
@@ -9,6 +9,7 @@ const TRACKED_REFERRAL_ACTIONS = [
|
||||
"EXTERNAL_A2A_ONBOARDING_VIEW",
|
||||
"EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED",
|
||||
"EXTERNAL_A2A_GROWTH_KIT_ISSUED",
|
||||
"EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_VIEW",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED",
|
||||
"EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED",
|
||||
@@ -68,6 +69,7 @@ export async function GET(request: NextRequest) {
|
||||
referredTasks,
|
||||
affiliateRows,
|
||||
actionCountValues,
|
||||
touchpointRows,
|
||||
latestTrafficEvent,
|
||||
] = await Promise.all([
|
||||
prisma.agentProfile.findUnique({
|
||||
@@ -139,6 +141,18 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
)
|
||||
),
|
||||
prisma.auditEvent.findMany({
|
||||
where: {
|
||||
actorId,
|
||||
action: "EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED",
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 50,
|
||||
select: {
|
||||
createdAt: true,
|
||||
metadata: true,
|
||||
},
|
||||
}),
|
||||
prisma.auditEvent.findMany({
|
||||
where: {
|
||||
actorId,
|
||||
@@ -157,6 +171,15 @@ export async function GET(request: NextRequest) {
|
||||
const actionCounts = Object.fromEntries(
|
||||
TRACKED_REFERRAL_ACTIONS.map((action, index) => [action, actionCountValues[index] || 0])
|
||||
);
|
||||
const touchpointBreakdown = touchpointRows.reduce<Record<string, number>>((acc, row) => {
|
||||
const metadata =
|
||||
typeof row.metadata === "object" && row.metadata !== null && !Array.isArray(row.metadata)
|
||||
? (row.metadata as Record<string, unknown>)
|
||||
: {};
|
||||
const type = typeof metadata.touchpoint_type === "string" ? metadata.touchpoint_type : "unknown";
|
||||
acc[type] = (acc[type] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const affiliateBreakdown: CurrencyBreakdown = {};
|
||||
for (const row of affiliateRows) {
|
||||
@@ -220,6 +243,7 @@ export async function GET(request: NextRequest) {
|
||||
onboarding_events: actionCounts.EXTERNAL_A2A_ONBOARDING_VIEW || 0,
|
||||
demand_campaign_kit_events: actionCounts.EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED || 0,
|
||||
growth_kit_events: actionCounts.EXTERNAL_A2A_GROWTH_KIT_ISSUED || 0,
|
||||
referral_touchpoint_events: actionCounts.EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED || 0,
|
||||
proposal_view_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_VIEW || 0,
|
||||
proposal_created_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED || 0,
|
||||
proposal_checkout_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED || 0,
|
||||
@@ -235,6 +259,12 @@ export async function GET(request: NextRequest) {
|
||||
spam_score: scoutReputation?.spam_score || 0,
|
||||
chargeback_count: scoutReputation?.chargeback_count || 0,
|
||||
},
|
||||
touchpoint_summary: {
|
||||
total: actionCounts.EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED || 0,
|
||||
recent_count: touchpointRows.length,
|
||||
by_type: touchpointBreakdown,
|
||||
latest_at: touchpointRows[0]?.createdAt ? touchpointRows[0].createdAt.toISOString() : null,
|
||||
},
|
||||
affiliate_breakdown: affiliateBreakdown,
|
||||
recent_conversions: recentConversions,
|
||||
});
|
||||
|
||||
258
apps/web/src/app/api/a2a/referrals/touch/route.ts
Normal file
258
apps/web/src/app/api/a2a/referrals/touch/route.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { AGENT_GATEWAY_URL, buildDemandProposalUrl, sanitizeAgentId } from "@/lib/a2a-growth";
|
||||
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const TOUCHPOINT_TYPES = [
|
||||
"campaign_posted",
|
||||
"dm_sent",
|
||||
"lead_qualified",
|
||||
"proposal_link_sent",
|
||||
"prefill_link_sent",
|
||||
"follow_up_sent",
|
||||
"lead_rejected",
|
||||
] as const;
|
||||
|
||||
const TOUCHPOINT_ALIASES: Record<string, (typeof TOUCHPOINT_TYPES)[number]> = {
|
||||
post: "campaign_posted",
|
||||
posted: "campaign_posted",
|
||||
campaign: "campaign_posted",
|
||||
dm: "dm_sent",
|
||||
message: "dm_sent",
|
||||
qualified: "lead_qualified",
|
||||
lead: "lead_qualified",
|
||||
link: "proposal_link_sent",
|
||||
sent: "proposal_link_sent",
|
||||
prefill: "prefill_link_sent",
|
||||
followup: "follow_up_sent",
|
||||
rejected: "lead_rejected",
|
||||
};
|
||||
|
||||
const SENSITIVE_FIELD_NAMES = [
|
||||
"email",
|
||||
"phone",
|
||||
"password",
|
||||
"private_key",
|
||||
"secret",
|
||||
"api_key",
|
||||
"token",
|
||||
"credential",
|
||||
"customer_secret",
|
||||
"full_database_dump",
|
||||
"private_dataset",
|
||||
"personal_sensitive_data",
|
||||
];
|
||||
|
||||
type TouchpointInput = Record<string, unknown>;
|
||||
|
||||
function textValue(value: unknown) {
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
return "";
|
||||
}
|
||||
|
||||
function firstTextValue(searchParams: URLSearchParams, body: TouchpointInput, keys: string[]) {
|
||||
for (const key of keys) {
|
||||
const fromBody = textValue(body[key]);
|
||||
if (fromBody) return fromBody;
|
||||
const fromQuery = searchParams.get(key);
|
||||
if (fromQuery) return fromQuery;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function cleanText(value: string, maxLength: number) {
|
||||
return value
|
||||
.replace(/\r/g, "")
|
||||
.replace(/[^\S\n]+/g, " ")
|
||||
.trim()
|
||||
.slice(0, maxLength);
|
||||
}
|
||||
|
||||
function normalizeTouchpointType(value: string) {
|
||||
const normalized = sanitizeAgentId(value).replace(/-/g, "_");
|
||||
if (TOUCHPOINT_TYPES.includes(normalized as (typeof TOUCHPOINT_TYPES)[number])) {
|
||||
return normalized as (typeof TOUCHPOINT_TYPES)[number];
|
||||
}
|
||||
return TOUCHPOINT_ALIASES[normalized] || "proposal_link_sent";
|
||||
}
|
||||
|
||||
function normalizeUrgency(value: string) {
|
||||
return ["normal", "this_week", "urgent"].includes(value) ? value : "normal";
|
||||
}
|
||||
|
||||
function ignoredSensitiveFields(searchParams: URLSearchParams, body: TouchpointInput) {
|
||||
const keys = new Set([...Array.from(searchParams.keys()), ...Object.keys(body)]);
|
||||
return Array.from(keys)
|
||||
.filter((key) => SENSITIVE_FIELD_NAMES.some((sensitive) => key.toLowerCase().includes(sensitive)))
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function readJsonBody(request: NextRequest) {
|
||||
if (request.method !== "POST") return {};
|
||||
const contentType = request.headers.get("content-type") || "";
|
||||
if (!contentType.includes("application/json")) return {};
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (body && typeof body === "object" && !Array.isArray(body)) {
|
||||
return body as TouchpointInput;
|
||||
}
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function handleTouchpoint(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const body = await readJsonBody(request);
|
||||
const agentId = sanitizeAgentId(firstTextValue(searchParams, body, ["agent_id", "agentId"]));
|
||||
const campaign = sanitizeAgentId(firstTextValue(searchParams, body, ["campaign"])) || "a2a-agent-referral";
|
||||
const channel = sanitizeAgentId(firstTextValue(searchParams, body, ["channel"])) || "";
|
||||
const source =
|
||||
sanitizeAgentId(firstTextValue(searchParams, body, ["source"])) ||
|
||||
channel ||
|
||||
"external-agent";
|
||||
const touchpointType = normalizeTouchpointType(firstTextValue(searchParams, body, ["touchpoint", "event", "type"]));
|
||||
const shouldRegister = firstTextValue(searchParams, body, ["register"]) === "true";
|
||||
|
||||
if (!agentId) {
|
||||
return NextResponse.json({ error: "agent_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (shouldRegister) {
|
||||
await prisma.agentProfile.upsert({
|
||||
where: { agent_id: agentId },
|
||||
update: {
|
||||
discovery_source: "A2A_REFERRAL_TOUCHPOINT",
|
||||
},
|
||||
create: {
|
||||
agent_id: agentId,
|
||||
type: "SCOUT",
|
||||
status: "PENDING",
|
||||
discovery_source: "A2A_REFERRAL_TOUCHPOINT",
|
||||
capabilities: {
|
||||
growth_referral: true,
|
||||
referral_touchpoint_tracking: true,
|
||||
campaign,
|
||||
source,
|
||||
channel: channel || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const leadLabel = cleanText(firstTextValue(searchParams, body, ["lead_label", "label", "title"]), 140);
|
||||
const summary = cleanText(firstTextValue(searchParams, body, ["summary", "description"]), 360);
|
||||
const desiredOutcome = cleanText(firstTextValue(searchParams, body, ["desired_outcome", "outcome"]), 240);
|
||||
const stack = cleanText(firstTextValue(searchParams, body, ["stack", "tools", "required_stack"]), 180);
|
||||
const budgetUsd = cleanText(firstTextValue(searchParams, body, ["budget_usd", "budget"]), 16);
|
||||
const urgency = normalizeUrgency(firstTextValue(searchParams, body, ["urgency"]));
|
||||
const targetSegment = cleanText(firstTextValue(searchParams, body, ["target_segment", "segment"]), 120);
|
||||
const ignoredFields = ignoredSensitiveFields(searchParams, body);
|
||||
|
||||
const proposalUrl = buildDemandProposalUrl({
|
||||
referralAgent: agentId,
|
||||
campaign,
|
||||
source,
|
||||
});
|
||||
const prefilledProposalUrl = buildDemandProposalUrl({
|
||||
referralAgent: agentId,
|
||||
campaign,
|
||||
source,
|
||||
title: leadLabel,
|
||||
description: summary,
|
||||
desiredOutcome,
|
||||
stack,
|
||||
budgetUsd,
|
||||
urgency,
|
||||
});
|
||||
const statusUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}`;
|
||||
|
||||
const safeSummary = {
|
||||
lead_label: leadLabel || null,
|
||||
summary: summary || null,
|
||||
desired_outcome: desiredOutcome || null,
|
||||
stack: stack || null,
|
||||
budget_usd: budgetUsd || null,
|
||||
urgency,
|
||||
target_segment: targetSegment || null,
|
||||
};
|
||||
|
||||
await prisma.auditEvent.create({
|
||||
data: {
|
||||
actorType: "AGENT",
|
||||
actorId: agentId,
|
||||
action: "A2A_REFERRAL_TOUCHPOINT_RECORDED",
|
||||
entityType: "SYSTEM",
|
||||
entityId: `a2a-referral-touchpoint:${agentId}`,
|
||||
metadata: {
|
||||
campaign,
|
||||
source,
|
||||
channel: channel || null,
|
||||
touchpoint_type: touchpointType,
|
||||
registered_pending_agent: shouldRegister,
|
||||
proposal_url: proposalUrl,
|
||||
prefilled_proposal_url: prefilledProposalUrl,
|
||||
ignored_sensitive_fields: ignoredFields,
|
||||
safe_summary: safeSummary,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const trafficEvent = await logA2aTrafficEvent({
|
||||
headers: request.headers,
|
||||
fallbackAgentId: agentId,
|
||||
action: "EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED",
|
||||
surface: "a2a/referrals/touch",
|
||||
entityId: `a2a-referral-touchpoint:${agentId}`,
|
||||
reason: "external_agent_referral_touchpoint_recorded",
|
||||
metadata: {
|
||||
campaign,
|
||||
source,
|
||||
channel: channel || null,
|
||||
touchpoint_type: touchpointType,
|
||||
registered_pending_agent: shouldRegister,
|
||||
proposal_url: proposalUrl,
|
||||
prefilled_proposal_url: prefilledProposalUrl,
|
||||
ignored_sensitive_fields: ignoredFields,
|
||||
safe_summary: safeSummary,
|
||||
response_status: 200,
|
||||
response_summary: "a2a_referral_touchpoint_recorded",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
touchpoint_id: trafficEvent.id,
|
||||
agent_id: agentId,
|
||||
touchpoint_type: touchpointType,
|
||||
campaign,
|
||||
source,
|
||||
channel: channel || null,
|
||||
proposal_url: proposalUrl,
|
||||
prefilled_proposal_url: prefilledProposalUrl,
|
||||
referral_status_url: statusUrl,
|
||||
ignored_sensitive_fields: ignoredFields,
|
||||
guardrails: [
|
||||
"Touchpoints are traffic evidence, not revenue.",
|
||||
"Paid conversion is counted only after VibeWork payment truth.",
|
||||
"Do not send passwords, private keys, production credentials, full customer records, or private datasets.",
|
||||
],
|
||||
next_actions: [
|
||||
"Send the human proposer to proposal_url or prefilled_proposal_url.",
|
||||
"Ask the proposer to review and pay inside VibeWork.",
|
||||
"Check referral_status_url for sanitized aggregate progress.",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return handleTouchpoint(request);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return handleTouchpoint(request);
|
||||
}
|
||||
@@ -509,6 +509,7 @@ export async function GET(request: NextRequest) {
|
||||
(actionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
|
||||
(actionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0);
|
||||
|
||||
const referralTouchpointEvents = actionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0;
|
||||
const proposalViewEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0;
|
||||
const proposalCreatedEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
|
||||
const proposalCheckoutEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
|
||||
@@ -532,6 +533,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const externalFunnel = {
|
||||
discovery_events: discoveryEvents,
|
||||
referral_touchpoint_events: referralTouchpointEvents,
|
||||
proposal_view_events: proposalViewEvents,
|
||||
proposal_created_events: proposalCreatedEvents,
|
||||
proposal_checkout_events: proposalCheckoutEvents,
|
||||
@@ -547,6 +549,8 @@ export async function GET(request: NextRequest) {
|
||||
};
|
||||
|
||||
const conversionRates = {
|
||||
touchpoint_rate: conversionRate(referralTouchpointEvents, discoveryEvents),
|
||||
touchpoint_to_proposal_view_rate: conversionRate(proposalViewEvents, referralTouchpointEvents || discoveryEvents),
|
||||
proposal_view_rate: conversionRate(proposalViewEvents, discoveryEvents),
|
||||
proposal_create_rate: conversionRate(proposalCreatedEvents, proposalViewEvents),
|
||||
proposal_paid_rate: conversionRate(proposalPaidEvents, proposalCreatedEvents),
|
||||
|
||||
@@ -14,6 +14,7 @@ const EVENT_LABELS: Record<string, string> = {
|
||||
EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED: "外部 Agent 領取需求 campaign kit",
|
||||
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部 Agent 讀取 A2A 整合目錄",
|
||||
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部 Agent 領取 growth kit",
|
||||
EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED: "外部 Agent 回報導流 touchpoint",
|
||||
EXTERNAL_A2A_REFERRAL_STATUS_VIEW: "外部 Agent 查詢 referral 狀態",
|
||||
EXTERNAL_DEMAND_PROPOSAL_VIEW: "外部導流需求方查看提案頁",
|
||||
EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED: "外部導流需求方建立提案",
|
||||
@@ -393,6 +394,7 @@ async function getTrafficSummary(minutes: number) {
|
||||
(actionSummary["EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED"] || 0) +
|
||||
(actionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
|
||||
(actionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0);
|
||||
const referralTouchpointEvents = actionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0;
|
||||
const proposalViewEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0;
|
||||
const proposalCreatedEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
|
||||
const proposalCheckoutEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
|
||||
@@ -411,6 +413,7 @@ async function getTrafficSummary(minutes: number) {
|
||||
|
||||
const conversionSummary = {
|
||||
discovery_events: discoveryEvents,
|
||||
referral_touchpoint_events: referralTouchpointEvents,
|
||||
proposal_view_events: proposalViewEvents,
|
||||
proposal_created_events: proposalCreatedEvents,
|
||||
proposal_checkout_events: proposalCheckoutEvents,
|
||||
@@ -425,6 +428,8 @@ async function getTrafficSummary(minutes: number) {
|
||||
};
|
||||
|
||||
const conversionRates = {
|
||||
touchpoint_rate: percent(referralTouchpointEvents, discoveryEvents),
|
||||
touchpoint_to_proposal_view_rate: percent(proposalViewEvents, referralTouchpointEvents || discoveryEvents),
|
||||
proposal_view_rate: percent(proposalViewEvents, discoveryEvents),
|
||||
proposal_create_rate: percent(proposalCreatedEvents, proposalViewEvents),
|
||||
proposal_paid_rate: percent(proposalPaidEvents, proposalCreatedEvents),
|
||||
@@ -585,6 +590,8 @@ function toLocalTime(value: Date) {
|
||||
}
|
||||
|
||||
function buildConversionTips(summary: {
|
||||
touchpoint_rate: number;
|
||||
touchpoint_to_proposal_view_rate: number;
|
||||
proposal_view_rate: number;
|
||||
proposal_create_rate: number;
|
||||
proposal_paid_rate: number;
|
||||
@@ -594,6 +601,7 @@ function buildConversionTips(summary: {
|
||||
payout_rate: number;
|
||||
}, conversionSummary: {
|
||||
discovery_events: number;
|
||||
referral_touchpoint_events: number;
|
||||
proposal_view_events: number;
|
||||
proposal_created_events: number;
|
||||
proposal_checkout_events: number;
|
||||
@@ -608,8 +616,16 @@ function buildConversionTips(summary: {
|
||||
}) {
|
||||
const steps: string[] = [];
|
||||
|
||||
if (conversionSummary.discovery_events > 0 && conversionSummary.referral_touchpoint_events === 0) {
|
||||
steps.push("A2A 曝光已有資料但外部 Agent touchpoint 為零:優先確認 campaign kit 是否要求回報 /api/a2a/referrals/touch。");
|
||||
}
|
||||
|
||||
if (conversionSummary.referral_touchpoint_events > 0 && conversionSummary.proposal_view_events === 0) {
|
||||
steps.push("外部 Agent 已回報導流 touchpoint 但提案頁查看為零:優先檢查送出的 proposal_url、prefilled_proposal_url 與 vibework.wooo.work 代理。");
|
||||
}
|
||||
|
||||
if (conversionSummary.discovery_events > 0 && conversionSummary.proposal_view_events === 0) {
|
||||
steps.push("A2A 曝光已有資料但提案頁查看為零:優先確認 growth kit referral_url、vibework.wooo.work/propose 代理與外部貼文 CTA。");
|
||||
steps.push("A2A 曝光已有資料但提案頁查看為零:優先確認 growth kit referral_url、touchpoint 回傳 URL、外部貼文 CTA 與 /propose 代理。");
|
||||
}
|
||||
|
||||
if (conversionSummary.proposal_view_events > 0 && conversionSummary.proposal_created_events === 0) {
|
||||
@@ -708,6 +724,10 @@ export default async function TrafficDashboard({
|
||||
<div className="text-gray-400 text-sm">A2A 曝光</div>
|
||||
<div className="text-3xl font-bold mt-2 text-cyan-300">{conversionSummary.discovery_events}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
|
||||
<div className="text-gray-400 text-sm">Agent 導流回報</div>
|
||||
<div className="text-3xl font-bold mt-2 text-teal-300">{conversionSummary.referral_touchpoint_events}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
|
||||
<div className="text-gray-400 text-sm">提案頁查看</div>
|
||||
<div className="text-3xl font-bold mt-2 text-sky-300">{conversionSummary.proposal_view_events}</div>
|
||||
@@ -763,13 +783,33 @@ export default async function TrafficDashboard({
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">外部流量轉化漏斗</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>A2A曝光→Agent導流回報</span>
|
||||
<span className="text-emerald-300">{fmtPercent(conversionRates.touchpoint_rate)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-teal-400"
|
||||
style={{ width: `${Math.min(conversionRates.touchpoint_rate, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Agent導流回報→提案頁</span>
|
||||
<span className="text-emerald-300">{fmtPercent(conversionRates.touchpoint_to_proposal_view_rate)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-sky-400"
|
||||
style={{ width: `${Math.min(conversionRates.touchpoint_to_proposal_view_rate, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>A2A曝光→提案頁</span>
|
||||
<span className="text-emerald-300">{fmtPercent(conversionRates.proposal_view_rate)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-sky-400"
|
||||
className="h-full bg-cyan-400"
|
||||
style={{ width: `${Math.min(conversionRates.proposal_view_rate, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user