2026-06-07 22:30:33 +00:00
|
|
|
// ============================================
|
|
|
|
|
// MCP Tools — Typed wrappers for Meta MCP tools
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
import { callMetaTool, type Client } from './client';
|
|
|
|
|
|
|
|
|
|
// ---- Helper: extract text content from MCP result ----
|
|
|
|
|
function extractContent(result: Awaited<ReturnType<typeof callMetaTool>>): unknown {
|
|
|
|
|
if (!result.content || !Array.isArray(result.content)) return null;
|
|
|
|
|
|
|
|
|
|
const textPart = result.content.find((c: { type: string }) => c.type === 'text');
|
|
|
|
|
if (!textPart || !('text' in textPart)) return null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(textPart.text as string);
|
|
|
|
|
} catch {
|
|
|
|
|
return textPart.text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requireId(value: unknown, label: string) {
|
|
|
|
|
if (!value || typeof value !== 'string') {
|
|
|
|
|
throw new Error(`${label} obrigatório não informado`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDefaultOptimizationGoal(objective: string, destination?: string) {
|
|
|
|
|
if (destination === 'WHATSAPP') {
|
|
|
|
|
return 'CONVERSATIONS';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (objective) {
|
|
|
|
|
case 'OUTCOME_TRAFFIC':
|
|
|
|
|
return 'LINK_CLICKS';
|
|
|
|
|
|
|
|
|
|
case 'OUTCOME_AWARENESS':
|
|
|
|
|
return 'REACH';
|
|
|
|
|
|
|
|
|
|
case 'OUTCOME_ENGAGEMENT':
|
|
|
|
|
return 'POST_ENGAGEMENT';
|
|
|
|
|
|
|
|
|
|
case 'OUTCOME_LEADS':
|
|
|
|
|
return 'LEAD_GENERATION';
|
|
|
|
|
|
|
|
|
|
case 'OUTCOME_SALES':
|
|
|
|
|
// OFFSITE_CONVERSIONS requires a pixel_id. Since we don't have it in MVP, fallback to LINK_CLICKS
|
|
|
|
|
return 'LINK_CLICKS';
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return 'LINK_CLICKS';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDefaultBillingEvent(objective: string) {
|
|
|
|
|
switch (objective) {
|
|
|
|
|
case 'OUTCOME_TRAFFIC':
|
|
|
|
|
case 'OUTCOME_AWARENESS':
|
|
|
|
|
case 'OUTCOME_ENGAGEMENT':
|
|
|
|
|
case 'OUTCOME_LEADS':
|
|
|
|
|
case 'OUTCOME_SALES':
|
|
|
|
|
default:
|
|
|
|
|
return 'IMPRESSIONS';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeBidCapParams(params: {
|
|
|
|
|
bid_strategy?: string;
|
|
|
|
|
bid_amount?: number;
|
|
|
|
|
}) {
|
|
|
|
|
if (!params.bid_strategy && !params.bid_amount) return {};
|
|
|
|
|
|
|
|
|
|
const bidStrategy = params.bid_strategy || 'LOWEST_COST_WITH_BID_CAP';
|
|
|
|
|
|
|
|
|
|
if (bidStrategy === 'LOWEST_COST_WITH_BID_CAP' && !params.bid_amount) {
|
|
|
|
|
throw new Error('Para campanha Bid Cap, informe bid_amount em centavos. Exemplo: R$10,00 = 1000.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
bid_strategy: bidStrategy,
|
|
|
|
|
bid_amount: params.bid_amount,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
// Campaign Management
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
export async function listCampaigns(client: Client, accountId?: string, datePreset: string = 'last_30d') {
|
|
|
|
|
const args: Record<string, unknown> = { date_preset: datePreset };
|
|
|
|
|
if (accountId) args.ad_account_id = accountId;
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'list_campaigns', args);
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getCampaignDetails(
|
|
|
|
|
client: Client,
|
|
|
|
|
campaignId: string,
|
|
|
|
|
datePreset: string = 'last_30d'
|
|
|
|
|
) {
|
|
|
|
|
requireId(campaignId, 'campaignId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'get_campaign_details', {
|
|
|
|
|
campaign_id: campaignId,
|
|
|
|
|
date_preset: datePreset,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function pauseCampaign(client: Client, campaignId: string) {
|
|
|
|
|
requireId(campaignId, 'campaignId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'update_campaign', {
|
|
|
|
|
campaign_id: campaignId,
|
|
|
|
|
status: 'PAUSED',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function resumeCampaign(client: Client, campaignId: string) {
|
|
|
|
|
requireId(campaignId, 'campaignId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'update_campaign', {
|
|
|
|
|
campaign_id: campaignId,
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 17:33:24 +00:00
|
|
|
export type DuplicableLevel = 'campaign' | 'adset' | 'ad';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Duplica uma campanha, conjunto (ad set) ou anúncio usando o endpoint nativo
|
|
|
|
|
* /copies da Meta — o mesmo "Duplicar" do Ads Manager. Campanhas e conjuntos
|
|
|
|
|
* vêm com cópia completa (deep copy: traz os filhos junto) e tudo entra como
|
|
|
|
|
* PAUSED, pra revisar antes de ativar.
|
|
|
|
|
*/
|
|
|
|
|
export async function duplicateEntity(client: Client, level: DuplicableLevel, id: string) {
|
|
|
|
|
requireId(id, 'id');
|
|
|
|
|
|
|
|
|
|
const config: Record<DuplicableLevel, { tool: string; argKey: string }> = {
|
|
|
|
|
campaign: { tool: 'copy_campaign', argKey: 'campaign_id' },
|
|
|
|
|
adset: { tool: 'copy_adset', argKey: 'adset_id' },
|
|
|
|
|
ad: { tool: 'copy_ad', argKey: 'ad_id' },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const entry = config[level];
|
|
|
|
|
if (!entry) {
|
|
|
|
|
throw new Error(`Nível inválido para duplicar: ${level}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, entry.tool, { [entry.argKey]: id });
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-07 22:30:33 +00:00
|
|
|
/**
|
|
|
|
|
* Mantido por compatibilidade.
|
|
|
|
|
* ATENÇÃO: orçamento normalmente é no AdSet, não na campanha.
|
|
|
|
|
* Para atualizar orçamento real, prefira updateAdSetBudget().
|
|
|
|
|
*/
|
|
|
|
|
export async function updateCampaignBudget(
|
|
|
|
|
client: Client,
|
|
|
|
|
campaignId: string,
|
|
|
|
|
dailyBudget: number
|
|
|
|
|
) {
|
|
|
|
|
requireId(campaignId, 'campaignId');
|
|
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
`updateCampaignBudget não deve atualizar daily_budget direto na campanha.
|
|
|
|
|
Use listAdSets(client, campaignId), pegue o adset_id e chame updateAdSetBudget(client, adsetId, dailyBudget).`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
// Insights & Analytics
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
export async function getCampaignInsights(
|
|
|
|
|
client: Client,
|
|
|
|
|
campaignId: string,
|
|
|
|
|
datePreset: string = 'last_7d'
|
|
|
|
|
) {
|
|
|
|
|
requireId(campaignId, 'campaignId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'get_insights_for_campaign', {
|
|
|
|
|
campaign_id: campaignId,
|
|
|
|
|
date_preset: datePreset,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getAccountInsights(
|
|
|
|
|
client: Client,
|
|
|
|
|
datePreset: string = 'last_7d',
|
|
|
|
|
accountId?: string
|
|
|
|
|
) {
|
|
|
|
|
const args: Record<string, unknown> = {
|
|
|
|
|
date_preset: datePreset,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (accountId) args.ad_account_id = accountId;
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'get_insights_for_account', args);
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getAdAccounts(client: Client) {
|
|
|
|
|
const result = await callMetaTool(client, 'list_ad_accounts');
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getPages(client: Client) {
|
|
|
|
|
const result = await callMetaTool(client, 'list_pages');
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
// Ad Set Management
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
export async function listAdSets(client: Client, campaignId: string) {
|
|
|
|
|
requireId(campaignId, 'campaignId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'list_adsets', {
|
|
|
|
|
campaign_id: campaignId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateAdSetBudget(
|
|
|
|
|
client: Client,
|
|
|
|
|
adSetId: string,
|
|
|
|
|
dailyBudget: number
|
|
|
|
|
) {
|
|
|
|
|
requireId(adSetId, 'adSetId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'update_adset', {
|
|
|
|
|
adset_id: adSetId,
|
|
|
|
|
daily_budget: dailyBudget,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function pauseAdSet(client: Client, adSetId: string) {
|
|
|
|
|
requireId(adSetId, 'adSetId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'update_adset', {
|
|
|
|
|
adset_id: adSetId,
|
|
|
|
|
status: 'PAUSED',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function resumeAdSet(client: Client, adSetId: string) {
|
|
|
|
|
requireId(adSetId, 'adSetId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'update_adset', {
|
|
|
|
|
adset_id: adSetId,
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 15:54:00 +00:00
|
|
|
export interface AdSetDefaultCreative {
|
|
|
|
|
page_id?: string;
|
|
|
|
|
instagram_actor_id?: string;
|
|
|
|
|
link?: string;
|
|
|
|
|
cta?: string;
|
|
|
|
|
is_whatsapp: boolean;
|
|
|
|
|
whatsapp_link?: string;
|
|
|
|
|
whatsapp_phone_number?: string;
|
2026-06-08 16:18:41 +00:00
|
|
|
sample_primary_text?: string;
|
|
|
|
|
sample_headline?: string;
|
|
|
|
|
sample_description?: string;
|
2026-06-08 15:54:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AdSetDefaultCreativeApiResponse {
|
|
|
|
|
ads?: {
|
|
|
|
|
data?: Array<{
|
|
|
|
|
creative?: {
|
|
|
|
|
object_story_spec?: {
|
|
|
|
|
page_id?: string;
|
|
|
|
|
instagram_actor_id?: string;
|
|
|
|
|
link_data?: {
|
|
|
|
|
link?: string;
|
2026-06-08 16:18:41 +00:00
|
|
|
message?: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
description?: string;
|
2026-06-08 15:54:00 +00:00
|
|
|
call_to_action?: { type?: string };
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
asset_feed_spec?: {
|
|
|
|
|
link_urls?: Array<{ website_url?: string }>;
|
|
|
|
|
call_to_action_types?: string[];
|
2026-06-08 16:18:41 +00:00
|
|
|
bodies?: Array<{ text?: string }>;
|
|
|
|
|
titles?: Array<{ text?: string }>;
|
|
|
|
|
descriptions?: Array<{ text?: string }>;
|
2026-06-08 15:54:00 +00:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}>;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface MetaPageSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
instagram_business_account?: { id?: string };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extrai page_id/link/instagram_actor_id/cta do criativo de um anúncio já
|
|
|
|
|
* existente no ad set, para pré-preencher a publicação de novos anúncios no
|
|
|
|
|
* mesmo destino. Se o ad set ainda não tiver anúncios, cai no fallback de
|
|
|
|
|
* usar a primeira página conectada à conta (mesmo padrão de campaigns/create).
|
|
|
|
|
*/
|
|
|
|
|
export async function getAdSetDefaultCreative(
|
|
|
|
|
client: Client,
|
|
|
|
|
adSetId: string
|
|
|
|
|
): Promise<AdSetDefaultCreative> {
|
|
|
|
|
requireId(adSetId, 'adSetId');
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'get_adset_default_creative', { adset_id: adSetId });
|
|
|
|
|
const data = extractContent(result) as AdSetDefaultCreativeApiResponse | null;
|
|
|
|
|
|
|
|
|
|
const creative = data?.ads?.data?.[0]?.creative;
|
|
|
|
|
const storySpec = creative?.object_story_spec;
|
|
|
|
|
const assetFeed = creative?.asset_feed_spec;
|
|
|
|
|
|
|
|
|
|
if (storySpec) {
|
|
|
|
|
const linkData = storySpec.link_data;
|
|
|
|
|
const ctaType = linkData?.call_to_action?.type;
|
|
|
|
|
const isWhatsapp = ctaType === 'WHATSAPP_MESSAGE';
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
page_id: storySpec.page_id || undefined,
|
|
|
|
|
instagram_actor_id: storySpec.instagram_actor_id || undefined,
|
|
|
|
|
link: linkData?.link || undefined,
|
|
|
|
|
cta: ctaType,
|
|
|
|
|
is_whatsapp: isWhatsapp,
|
|
|
|
|
whatsapp_link: isWhatsapp ? linkData?.link || undefined : undefined,
|
|
|
|
|
whatsapp_phone_number: undefined,
|
2026-06-08 16:18:41 +00:00
|
|
|
sample_primary_text: linkData?.message || undefined,
|
|
|
|
|
sample_headline: linkData?.name || undefined,
|
|
|
|
|
sample_description: linkData?.description || undefined,
|
2026-06-08 15:54:00 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (assetFeed) {
|
|
|
|
|
const ctaType = assetFeed.call_to_action_types?.[0];
|
|
|
|
|
const isWhatsapp = ctaType === 'WHATSAPP_MESSAGE';
|
|
|
|
|
const link = assetFeed.link_urls?.[0]?.website_url;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
page_id: undefined,
|
|
|
|
|
instagram_actor_id: undefined,
|
|
|
|
|
link,
|
|
|
|
|
cta: ctaType,
|
|
|
|
|
is_whatsapp: isWhatsapp,
|
|
|
|
|
whatsapp_link: isWhatsapp ? link : undefined,
|
|
|
|
|
whatsapp_phone_number: undefined,
|
2026-06-08 16:18:41 +00:00
|
|
|
sample_primary_text: assetFeed.bodies?.[0]?.text || undefined,
|
|
|
|
|
sample_headline: assetFeed.titles?.[0]?.text || undefined,
|
|
|
|
|
sample_description: assetFeed.descriptions?.[0]?.text || undefined,
|
2026-06-08 15:54:00 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: nenhum anúncio existente — usa a primeira página da conta.
|
|
|
|
|
const pages = (await getPages(client)) as MetaPageSummary[] | null;
|
|
|
|
|
const firstPage = Array.isArray(pages) ? pages.find((p) => p?.id) : undefined;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
page_id: firstPage?.id,
|
|
|
|
|
instagram_actor_id: firstPage?.instagram_business_account?.id,
|
|
|
|
|
link: undefined,
|
|
|
|
|
cta: undefined,
|
|
|
|
|
is_whatsapp: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-07 22:30:33 +00:00
|
|
|
// ==============================
|
|
|
|
|
// Campaign Creation
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
export async function createCampaign(
|
|
|
|
|
client: Client,
|
|
|
|
|
params: {
|
|
|
|
|
name: string;
|
|
|
|
|
objective: string;
|
|
|
|
|
status?: string;
|
|
|
|
|
ad_account_id?: string;
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
const result = await callMetaTool(client, 'create_campaign', {
|
|
|
|
|
name: params.name,
|
|
|
|
|
objective: params.objective,
|
|
|
|
|
status: params.status || 'PAUSED',
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createAdSet(
|
|
|
|
|
client: Client,
|
|
|
|
|
params: {
|
|
|
|
|
name: string;
|
|
|
|
|
campaign_id: string;
|
|
|
|
|
daily_budget: number;
|
|
|
|
|
targeting: Record<string, unknown>;
|
|
|
|
|
objective: string;
|
|
|
|
|
optimization_goal?: string;
|
|
|
|
|
billing_event?: string;
|
|
|
|
|
status?: string;
|
|
|
|
|
promoted_object?: Record<string, unknown>;
|
|
|
|
|
destination_type?: string;
|
|
|
|
|
bid_strategy?: string;
|
|
|
|
|
bid_amount?: number;
|
|
|
|
|
start_time?: string;
|
|
|
|
|
end_time?: string;
|
|
|
|
|
ad_account_id?: string;
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
const bidCapParams: any = {};
|
|
|
|
|
if (params.bid_strategy) bidCapParams['bid_strategy'] = params.bid_strategy;
|
|
|
|
|
if (params.bid_amount) bidCapParams['bid_amount'] = params.bid_amount;
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'create_adset', {
|
|
|
|
|
name: params.name,
|
|
|
|
|
campaign_id: params.campaign_id,
|
|
|
|
|
daily_budget: params.daily_budget,
|
|
|
|
|
targeting: params.targeting,
|
|
|
|
|
objective: params.objective,
|
|
|
|
|
optimization_goal:
|
|
|
|
|
params.optimization_goal ||
|
|
|
|
|
getDefaultOptimizationGoal(params.objective, params.destination_type),
|
|
|
|
|
billing_event: params.billing_event || getDefaultBillingEvent(params.objective),
|
|
|
|
|
status: params.status || 'PAUSED',
|
|
|
|
|
promoted_object: params.promoted_object,
|
|
|
|
|
destination_type: params.destination_type,
|
|
|
|
|
start_time: params.start_time,
|
|
|
|
|
end_time: params.end_time,
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
...bidCapParams,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createAd(
|
|
|
|
|
client: Client,
|
|
|
|
|
params: {
|
|
|
|
|
name: string;
|
|
|
|
|
adset_id: string;
|
|
|
|
|
creative_id: string;
|
|
|
|
|
status?: string;
|
|
|
|
|
ad_account_id?: string;
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
const result = await callMetaTool(client, 'create_ad', {
|
|
|
|
|
name: params.name,
|
|
|
|
|
adset_id: params.adset_id,
|
|
|
|
|
creative_id: params.creative_id,
|
|
|
|
|
status: params.status || 'PAUSED',
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 15:40:29 +00:00
|
|
|
export interface PlacementImages {
|
|
|
|
|
square?: string;
|
|
|
|
|
vertical?: string;
|
|
|
|
|
horizontal?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-07 22:30:33 +00:00
|
|
|
export async function createAdCreative(
|
|
|
|
|
client: Client,
|
|
|
|
|
params: {
|
|
|
|
|
name: string;
|
|
|
|
|
page_id: string;
|
|
|
|
|
instagram_actor_id?: string;
|
|
|
|
|
image_hash?: string;
|
|
|
|
|
image_hashes?: string[];
|
2026-06-16 15:40:29 +00:00
|
|
|
// Imagens por posicionamento (1:1 / 9:16 / 1.91:1). Quando presente, o
|
|
|
|
|
// criativo usa Placement Asset Customization em vez de uma imagem única.
|
|
|
|
|
placement_images?: PlacementImages;
|
2026-06-07 22:30:33 +00:00
|
|
|
link: string;
|
|
|
|
|
message: string;
|
|
|
|
|
headline: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
call_to_action_type?: string;
|
|
|
|
|
ad_account_id?: string;
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
const result = await callMetaTool(client, 'create_ad_creative', {
|
|
|
|
|
name: params.name,
|
|
|
|
|
page_id: params.page_id,
|
|
|
|
|
instagram_actor_id: params.instagram_actor_id,
|
|
|
|
|
image_hash: params.image_hash,
|
|
|
|
|
image_hashes: params.image_hashes,
|
2026-06-16 15:40:29 +00:00
|
|
|
placement_images: params.placement_images,
|
2026-06-07 22:30:33 +00:00
|
|
|
link: params.link,
|
|
|
|
|
message: params.message,
|
|
|
|
|
headline: params.headline,
|
|
|
|
|
description: params.description || '',
|
|
|
|
|
call_to_action_type: params.call_to_action_type || 'LEARN_MORE',
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createWhatsappAdCreative(
|
|
|
|
|
client: Client,
|
|
|
|
|
params: {
|
|
|
|
|
name: string;
|
|
|
|
|
page_id: string;
|
|
|
|
|
instagram_actor_id?: string;
|
|
|
|
|
image_hash?: string;
|
|
|
|
|
image_hashes?: string[];
|
|
|
|
|
message: string;
|
|
|
|
|
headline: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
link?: string;
|
|
|
|
|
whatsapp_link?: string;
|
|
|
|
|
whatsapp_phone_number?: string;
|
|
|
|
|
ad_account_id?: string;
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
const result = await callMetaTool(client, 'create_whatsapp_ad_creative', {
|
|
|
|
|
name: params.name,
|
|
|
|
|
page_id: params.page_id,
|
|
|
|
|
instagram_actor_id: params.instagram_actor_id,
|
|
|
|
|
image_hash: params.image_hash,
|
|
|
|
|
image_hashes: params.image_hashes,
|
|
|
|
|
message: params.message,
|
|
|
|
|
headline: params.headline,
|
|
|
|
|
description: params.description || '',
|
|
|
|
|
link: params.link,
|
|
|
|
|
whatsapp_link: params.whatsapp_link,
|
|
|
|
|
whatsapp_phone_number: params.whatsapp_phone_number,
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function uploadAdImage(
|
|
|
|
|
client: Client,
|
|
|
|
|
imageUrl?: string,
|
|
|
|
|
imageBytes?: string,
|
|
|
|
|
adAccountId?: string
|
|
|
|
|
) {
|
|
|
|
|
const params: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
if (imageUrl) params.image_url = imageUrl;
|
|
|
|
|
if (imageBytes) params.image_bytes = imageBytes;
|
|
|
|
|
if (adAccountId) params.ad_account_id = adAccountId;
|
|
|
|
|
|
|
|
|
|
const result = await callMetaTool(client, 'upload_ad_image', params);
|
|
|
|
|
return extractContent(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==============================
|
2026-06-08 15:54:00 +00:00
|
|
|
// Creative + Ad Pipeline (reusável: campanha nova ou ad set existente)
|
2026-06-07 22:30:33 +00:00
|
|
|
// ==============================
|
|
|
|
|
|
2026-06-08 15:54:00 +00:00
|
|
|
export interface AddAdToAdSetParams {
|
|
|
|
|
adset_id: string;
|
|
|
|
|
ad_name: string;
|
2026-06-07 22:30:33 +00:00
|
|
|
page_id: string;
|
|
|
|
|
instagram_actor_id?: string;
|
|
|
|
|
image_urls?: string[];
|
|
|
|
|
image_bytes?: string[];
|
|
|
|
|
link: string;
|
|
|
|
|
headline: string;
|
|
|
|
|
primary_text: string;
|
|
|
|
|
description: string;
|
|
|
|
|
cta: string;
|
|
|
|
|
ad_account_id?: string;
|
|
|
|
|
|
|
|
|
|
is_whatsapp?: boolean;
|
|
|
|
|
whatsapp_link?: string;
|
|
|
|
|
whatsapp_phone_number?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 15:54:00 +00:00
|
|
|
/**
|
|
|
|
|
* Sobe imagens, cria o ad creative (normal ou whatsapp) e o anúncio dentro de
|
|
|
|
|
* um ad set já existente. Usado tanto pelo pipeline de criação completa de
|
|
|
|
|
* campanha quanto para adicionar novos anúncios a uma campanha ativa.
|
|
|
|
|
*/
|
|
|
|
|
export async function addAdToAdSet(client: Client, params: AddAdToAdSetParams) {
|
2026-06-07 22:30:33 +00:00
|
|
|
const imageHashes: string[] = [];
|
|
|
|
|
|
|
|
|
|
const DATA_URI_RE = /^data:image\/[a-zA-Z0-9.+-]+;base64,(.+)$/;
|
|
|
|
|
|
|
|
|
|
const processImageUploads = async (urls?: string[], bytes?: string[]) => {
|
|
|
|
|
const allBytes = [...(bytes || [])];
|
|
|
|
|
const realUrls: string[] = [];
|
|
|
|
|
|
|
|
|
|
// gpt-image-2 returns base64 data URIs (data:image/png;base64,...) — Meta's
|
|
|
|
|
// upload_ad_image expects those as raw base64 via image_bytes, not image_url
|
|
|
|
|
// (sending a data URI as image_url causes "tipo de arquivo não suportado").
|
|
|
|
|
for (const url of urls || []) {
|
|
|
|
|
const match = DATA_URI_RE.exec(url);
|
|
|
|
|
if (match) {
|
|
|
|
|
allBytes.push(match[1]);
|
|
|
|
|
} else {
|
|
|
|
|
realUrls.push(url);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const url of realUrls) {
|
|
|
|
|
const res = await uploadAdImage(client, url, undefined, params.ad_account_id);
|
|
|
|
|
if ((res as any)?.hash) imageHashes.push((res as any).hash);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const b of allBytes) {
|
|
|
|
|
const res = await uploadAdImage(client, undefined, b, params.ad_account_id);
|
|
|
|
|
if ((res as any)?.hash) imageHashes.push((res as any).hash);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await processImageUploads(params.image_urls, params.image_bytes);
|
|
|
|
|
|
|
|
|
|
if (imageHashes.length === 0) {
|
2026-06-08 15:54:00 +00:00
|
|
|
throw new Error('[Upload] Nenhuma imagem foi enviada ou processada.');
|
2026-06-07 22:30:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-08 15:54:00 +00:00
|
|
|
const creativeName = `${params.ad_name} — Creative`;
|
2026-06-07 22:30:33 +00:00
|
|
|
|
2026-06-16 15:40:29 +00:00
|
|
|
// A imagem quadrada (1080x1080, a primeira gerada) é a mais compatível e vira
|
|
|
|
|
// o fallback universal. Quando há ≥2 imagens, tentamos também usar cada uma no
|
|
|
|
|
// seu posicionamento (1:1 feed / 9:16 stories-reels / 1.91:1 coluna direita)
|
|
|
|
|
// via Placement Asset Customization (roda em ad set normal, não-dinâmico).
|
2026-06-07 22:30:33 +00:00
|
|
|
const primaryImageHash = imageHashes[0];
|
2026-06-16 15:40:29 +00:00
|
|
|
const placementImages: PlacementImages | undefined =
|
|
|
|
|
!params.is_whatsapp && imageHashes.length >= 2
|
|
|
|
|
? { square: imageHashes[0], vertical: imageHashes[1], horizontal: imageHashes[2] }
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
// Monta o criativo com (ou sem) o Instagram e com (ou sem) customização por
|
|
|
|
|
// posicionamento — cada um tem seu próprio fallback (ver cascata abaixo).
|
|
|
|
|
const buildCreative = (instagramActorId?: string, usePlacements = false) => {
|
2026-06-08 17:21:18 +00:00
|
|
|
if (params.is_whatsapp) {
|
|
|
|
|
return createWhatsappAdCreative(client, {
|
|
|
|
|
name: creativeName,
|
|
|
|
|
page_id: params.page_id,
|
|
|
|
|
instagram_actor_id: instagramActorId,
|
|
|
|
|
image_hash: primaryImageHash,
|
|
|
|
|
message: params.primary_text,
|
|
|
|
|
headline: params.headline,
|
|
|
|
|
description: params.description,
|
|
|
|
|
link: params.link,
|
|
|
|
|
whatsapp_link: params.whatsapp_link,
|
|
|
|
|
whatsapp_phone_number: params.whatsapp_phone_number,
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return createAdCreative(client, {
|
2026-06-07 22:30:33 +00:00
|
|
|
name: creativeName,
|
|
|
|
|
page_id: params.page_id,
|
2026-06-08 17:21:18 +00:00
|
|
|
instagram_actor_id: instagramActorId,
|
2026-06-07 22:30:33 +00:00
|
|
|
image_hash: primaryImageHash,
|
2026-06-16 15:40:29 +00:00
|
|
|
placement_images: usePlacements ? placementImages : undefined,
|
2026-06-07 22:30:33 +00:00
|
|
|
link: params.link,
|
|
|
|
|
message: params.primary_text,
|
|
|
|
|
headline: params.headline,
|
|
|
|
|
description: params.description,
|
|
|
|
|
call_to_action_type: params.cta,
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
2026-06-08 17:21:18 +00:00
|
|
|
};
|
|
|
|
|
|
2026-06-16 15:19:27 +00:00
|
|
|
// Erros ligados ao Instagram em que vale refazer o criativo só com o Facebook
|
|
|
|
|
// (o anúncio ainda roda no IG pela identidade da página):
|
|
|
|
|
// - 1815199: a conta de anúncios não tem acesso a esta conta do Instagram.
|
|
|
|
|
// - "instagram_actor_id must be a valid Instagram account id": o ID vinculado
|
|
|
|
|
// à página não é um ator de Instagram válido para Ads (ex: conta conectada
|
|
|
|
|
// pelo fluxo de "login do Instagram" em vez do Business/Página).
|
|
|
|
|
const isInstagramError = (err: unknown) =>
|
|
|
|
|
err instanceof Error &&
|
|
|
|
|
(/1815199/.test(err.message) ||
|
|
|
|
|
/instagram_actor_id/i.test(err.message) ||
|
|
|
|
|
/valid instagram account/i.test(err.message));
|
2026-06-08 17:21:18 +00:00
|
|
|
|
2026-06-16 15:40:29 +00:00
|
|
|
const wantPlacements = !!placementImages;
|
2026-06-08 17:21:18 +00:00
|
|
|
let creative;
|
|
|
|
|
let instagramDropped = false;
|
2026-06-16 15:40:29 +00:00
|
|
|
let placementsDropped = false;
|
|
|
|
|
|
2026-06-08 17:21:18 +00:00
|
|
|
try {
|
2026-06-16 15:40:29 +00:00
|
|
|
creative = await buildCreative(params.instagram_actor_id, wantPlacements);
|
2026-06-08 17:21:18 +00:00
|
|
|
} catch (err) {
|
2026-06-16 15:19:27 +00:00
|
|
|
if (params.instagram_actor_id && isInstagramError(err)) {
|
2026-06-16 15:40:29 +00:00
|
|
|
// Problema no Instagram: refaz sem IG (mantendo a customização se houver).
|
2026-06-08 17:21:18 +00:00
|
|
|
instagramDropped = true;
|
2026-06-16 15:40:29 +00:00
|
|
|
try {
|
|
|
|
|
creative = await buildCreative(undefined, wantPlacements);
|
|
|
|
|
} catch (err2) {
|
|
|
|
|
if (wantPlacements) {
|
|
|
|
|
placementsDropped = true;
|
|
|
|
|
creative = await buildCreative(undefined, false);
|
|
|
|
|
} else {
|
|
|
|
|
throw err2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (wantPlacements) {
|
|
|
|
|
// Erro provável da customização por posicionamento → cai pra imagem única.
|
|
|
|
|
placementsDropped = true;
|
|
|
|
|
try {
|
|
|
|
|
creative = await buildCreative(params.instagram_actor_id, false);
|
|
|
|
|
} catch (err2) {
|
|
|
|
|
if (params.instagram_actor_id && isInstagramError(err2)) {
|
|
|
|
|
instagramDropped = true;
|
|
|
|
|
creative = await buildCreative(undefined, false);
|
|
|
|
|
} else {
|
|
|
|
|
throw err2;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-08 17:21:18 +00:00
|
|
|
} else {
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
2026-06-07 22:30:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const creativeId = (creative as any)?.id;
|
|
|
|
|
if (!creativeId) {
|
2026-06-08 15:54:00 +00:00
|
|
|
throw new Error(`[Creative] Falha no criativo: ${JSON.stringify(creative)}`);
|
2026-06-07 22:30:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ad = await createAd(client, {
|
2026-06-08 15:54:00 +00:00
|
|
|
name: `${params.ad_name} — Ad`,
|
|
|
|
|
adset_id: params.adset_id,
|
2026-06-07 22:30:33 +00:00
|
|
|
creative_id: creativeId,
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const adId = (ad as any)?.id;
|
|
|
|
|
if (!adId) {
|
2026-06-08 15:54:00 +00:00
|
|
|
throw new Error(`[Ad] Falha no anúncio: ${JSON.stringify(ad)}`);
|
2026-06-07 22:30:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-08 15:54:00 +00:00
|
|
|
return {
|
|
|
|
|
adset_id: params.adset_id,
|
|
|
|
|
creative_id: creativeId,
|
|
|
|
|
ad_id: adId,
|
2026-06-08 17:21:18 +00:00
|
|
|
instagram_dropped: instagramDropped,
|
2026-06-16 15:40:29 +00:00
|
|
|
placements_dropped: placementsDropped,
|
2026-06-08 15:54:00 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
// Full Campaign Creation Pipeline
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
export interface FullCampaignParams {
|
|
|
|
|
campaign_name: string;
|
|
|
|
|
objective: string;
|
|
|
|
|
daily_budget: number;
|
|
|
|
|
targeting: Record<string, unknown>;
|
|
|
|
|
page_id: string;
|
|
|
|
|
instagram_actor_id?: string;
|
|
|
|
|
image_urls?: string[];
|
|
|
|
|
image_bytes?: string[];
|
|
|
|
|
link: string;
|
|
|
|
|
headline: string;
|
|
|
|
|
primary_text: string;
|
|
|
|
|
description: string;
|
|
|
|
|
cta: string;
|
|
|
|
|
ad_account_id?: string;
|
|
|
|
|
|
|
|
|
|
bid_strategy?: string;
|
|
|
|
|
bid_amount?: number;
|
|
|
|
|
|
|
|
|
|
optimization_goal?: string;
|
|
|
|
|
billing_event?: string;
|
|
|
|
|
destination_type?: string;
|
|
|
|
|
promoted_object?: Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
is_whatsapp?: boolean;
|
|
|
|
|
whatsapp_link?: string;
|
|
|
|
|
whatsapp_phone_number?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createFullCampaign(client: Client, params: FullCampaignParams) {
|
|
|
|
|
// Step 1: Campaign
|
|
|
|
|
const campaign = await createCampaign(client, {
|
|
|
|
|
name: params.campaign_name,
|
|
|
|
|
objective: params.objective,
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const campaignId = (campaign as any)?.id;
|
|
|
|
|
|
|
|
|
|
if (!campaignId) {
|
|
|
|
|
throw new Error(`[Step 1 - Campaign] Falha: ${JSON.stringify(campaign)}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: Ad Set
|
|
|
|
|
const destinationType = params.destination_type || (params.is_whatsapp ? 'WHATSAPP' : undefined);
|
|
|
|
|
|
|
|
|
|
let promotedObject = params.promoted_object;
|
|
|
|
|
|
|
|
|
|
if (!promotedObject && params.is_whatsapp) {
|
|
|
|
|
promotedObject = {
|
|
|
|
|
page_id: params.page_id,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const optimizationGoal =
|
|
|
|
|
params.optimization_goal ||
|
|
|
|
|
getDefaultOptimizationGoal(params.objective, destinationType);
|
|
|
|
|
|
|
|
|
|
const billingEvent =
|
|
|
|
|
params.billing_event ||
|
|
|
|
|
getDefaultBillingEvent(params.objective);
|
|
|
|
|
|
|
|
|
|
const adSet = await createAdSet(client, {
|
|
|
|
|
name: `${params.campaign_name} — Ad Set`,
|
|
|
|
|
campaign_id: campaignId,
|
|
|
|
|
daily_budget: params.daily_budget,
|
|
|
|
|
targeting: params.targeting,
|
|
|
|
|
objective: params.objective,
|
|
|
|
|
optimization_goal: optimizationGoal,
|
|
|
|
|
billing_event: billingEvent,
|
|
|
|
|
promoted_object: promotedObject,
|
|
|
|
|
destination_type: destinationType,
|
|
|
|
|
bid_strategy: params.bid_strategy,
|
|
|
|
|
bid_amount: params.bid_amount,
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const adSetId = (adSet as any)?.id;
|
|
|
|
|
|
|
|
|
|
if (!adSetId) {
|
|
|
|
|
throw new Error(`[Step 2 - Ad Set] Falha: ${JSON.stringify(adSet)}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Steps 3-5: Image Upload + Ad Creative + Ad (pipeline compartilhado)
|
|
|
|
|
const { creative_id: creativeId, ad_id: adId } = await addAdToAdSet(client, {
|
|
|
|
|
adset_id: adSetId,
|
|
|
|
|
ad_name: params.campaign_name,
|
|
|
|
|
page_id: params.page_id,
|
|
|
|
|
instagram_actor_id: params.instagram_actor_id,
|
|
|
|
|
image_urls: params.image_urls,
|
|
|
|
|
image_bytes: params.image_bytes,
|
|
|
|
|
link: params.link,
|
|
|
|
|
headline: params.headline,
|
|
|
|
|
primary_text: params.primary_text,
|
|
|
|
|
description: params.description,
|
|
|
|
|
cta: params.cta,
|
|
|
|
|
ad_account_id: params.ad_account_id,
|
|
|
|
|
is_whatsapp: params.is_whatsapp,
|
|
|
|
|
whatsapp_link: params.whatsapp_link,
|
|
|
|
|
whatsapp_phone_number: params.whatsapp_phone_number,
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-07 22:30:33 +00:00
|
|
|
return {
|
|
|
|
|
campaign_id: campaignId,
|
|
|
|
|
adset_id: adSetId,
|
|
|
|
|
creative_ids: [creativeId],
|
|
|
|
|
ad_ids: [adId],
|
|
|
|
|
objective: params.objective,
|
|
|
|
|
optimization_goal: optimizationGoal,
|
|
|
|
|
billing_event: billingEvent,
|
|
|
|
|
bid_strategy: params.bid_strategy,
|
|
|
|
|
bid_amount: params.bid_amount,
|
|
|
|
|
is_whatsapp: params.is_whatsapp || false,
|
|
|
|
|
};
|
|
|
|
|
}
|