cdp-edge 1.23.2 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -21
- package/bin/cdp-edge.js +10 -1
- package/contracts/agent-versions.json +42 -41
- package/contracts/types.ts +81 -0
- package/dist/commands/install.js +6 -1
- package/dist/commands/server.js +4 -4
- package/docs/whatsapp-ctwa.md +3 -2
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +5 -4
- package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +0 -1
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +4 -4
- package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +81 -70
- package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +6 -2
- package/extracted-skill/tracking-events-generator/cdpTrack.js +7 -0
- package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
- package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
- package/package.json +9 -5
- package/server-edge-tracker/INSTALAR.md +5 -5
- package/server-edge-tracker/{index.js → index.ts} +186 -72
- package/server-edge-tracker/modules/{db.js → db.ts} +180 -69
- package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
- package/server-edge-tracker/modules/dispatch/meta.ts +138 -0
- package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
- package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
- package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +59 -25
- package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
- package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
- package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +49 -56
- package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
- package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +179 -83
- package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
- package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
- package/server-edge-tracker/modules/utils.ts +186 -0
- package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
- package/server-edge-tracker/types.ts +251 -0
- package/server-edge-tracker/wrangler.toml +24 -6
- package/templates/lancamento-imobiliario.md +344 -0
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
- package/server-edge-tracker/modules/dispatch/meta.js +0 -119
- package/server-edge-tracker/modules/ml/segmentation.js +0 -316
- package/server-edge-tracker/modules/utils.js +0 -89
- package/server-edge-tracker/worker.js +0 -4577
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CDP Edge — ML Clustering (Fase 1)
|
|
3
|
-
* Handlers das rotas /api/segmentation/*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { tryParseJson } from '../utils.js';
|
|
7
|
-
|
|
8
|
-
// ── POST /api/segmentation/cluster ────────────────────────────────────────────
|
|
9
|
-
export async function handleSegmentationCluster(env, request, headers) {
|
|
10
|
-
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
11
|
-
if (!env.AI) return new Response(JSON.stringify({ error: 'Workers AI não configurado (verifique binding AI no wrangler.toml)' }), { status: 503, headers });
|
|
12
|
-
|
|
13
|
-
const url = new URL(request.url);
|
|
14
|
-
const algorithm = url.searchParams.get('algorithm') || 'kmeans';
|
|
15
|
-
const nClusters = Math.min(10, Math.max(3, parseInt(url.searchParams.get('n_clusters') || '5')));
|
|
16
|
-
const clientVertical = url.searchParams.get('vertical') || 'general';
|
|
17
|
-
const forceRecluster = url.searchParams.get('force') === 'true';
|
|
18
|
-
|
|
19
|
-
if (!['kmeans', 'dbscan', 'hierarchical'].includes(algorithm)) {
|
|
20
|
-
return new Response(JSON.stringify({ error: 'algorithm deve ser: kmeans, dbscan ou hierarchical', received: algorithm }), { status: 400, headers });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
// 1. Cluster recente? Evitar re-clustering desnecessário (< 7 dias)
|
|
25
|
-
if (!forceRecluster) {
|
|
26
|
-
const existing = await env.DB.prepare(`
|
|
27
|
-
SELECT id, created_at, cluster_name FROM ml_segments
|
|
28
|
-
WHERE clustering_algorithm = ? AND is_active = 1 AND client_vertical = ?
|
|
29
|
-
ORDER BY created_at DESC LIMIT 1
|
|
30
|
-
`).bind(algorithm, clientVertical).first();
|
|
31
|
-
|
|
32
|
-
if (existing) {
|
|
33
|
-
const ageDays = (Date.now() - new Date(existing.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
34
|
-
if (ageDays < 7) {
|
|
35
|
-
return new Response(JSON.stringify({
|
|
36
|
-
success: true, message: 'Cluster existente ainda válido (< 7 dias). Use ?force=true para re-clustering.',
|
|
37
|
-
cluster_id: existing.id, cluster_name: existing.cluster_name,
|
|
38
|
-
age_days: Math.round(ageDays * 10) / 10, use_existing: true,
|
|
39
|
-
}), { status: 200, headers });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 2. Extrair leads históricos do D1 (últimos 6 meses, excluindo bots confirmados)
|
|
45
|
-
const leadsRes = await env.DB.prepare(`
|
|
46
|
-
SELECT id, predicted_ltv_class, engagement_score, intention_level,
|
|
47
|
-
country, state, utm_source, utm_medium, bot_score,
|
|
48
|
-
CAST(strftime('%H', created_at) AS INTEGER) AS hour_of_day,
|
|
49
|
-
CAST(julianday('now') - julianday(created_at) AS INTEGER) AS days_since_lead,
|
|
50
|
-
CASE WHEN strftime('%w', created_at) IN ('0','6') THEN 1 ELSE 0 END AS is_weekend
|
|
51
|
-
FROM leads
|
|
52
|
-
WHERE created_at >= datetime('now', '-6 months')
|
|
53
|
-
AND (bot_score IS NULL OR bot_score < 2)
|
|
54
|
-
ORDER BY RANDOM()
|
|
55
|
-
LIMIT 2000
|
|
56
|
-
`).all();
|
|
57
|
-
|
|
58
|
-
const leads = leadsRes.results || [];
|
|
59
|
-
|
|
60
|
-
if (leads.length < 50) {
|
|
61
|
-
return new Response(JSON.stringify({
|
|
62
|
-
error: 'Dados insuficientes para clustering. Mínimo: 50 leads nos últimos 6 meses.',
|
|
63
|
-
leads_found: leads.length, required: 50,
|
|
64
|
-
}), { status: 400, headers });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 3. Feature Engineering — normalização 0–1
|
|
68
|
-
const features = leads.map(l => ({
|
|
69
|
-
id: l.id,
|
|
70
|
-
ltv: l.predicted_ltv_class === 'High' ? 1 : (l.predicted_ltv_class === 'Medium' ? 0.5 : 0),
|
|
71
|
-
engagement: Math.min((l.engagement_score || 0) / 100, 1),
|
|
72
|
-
intention: l.intention_level === 'comprador' || l.intention_level === 'high_intent' ? 1
|
|
73
|
-
: l.intention_level === 'interessado' ? 0.6
|
|
74
|
-
: l.intention_level === 'curioso' ? 0.3 : 0,
|
|
75
|
-
recency: Math.max(0, 1 - (l.days_since_lead || 0) / 180),
|
|
76
|
-
hour: (l.hour_of_day || 12) / 23,
|
|
77
|
-
is_weekend: l.is_weekend || 0,
|
|
78
|
-
is_br: l.country === 'BR' ? 1 : 0,
|
|
79
|
-
is_paid: ['facebook','google','tiktok','instagram','youtube'].includes((l.utm_source || '').toLowerCase()) ? 1 : 0,
|
|
80
|
-
}));
|
|
81
|
-
|
|
82
|
-
// 4. Prompt para Workers AI
|
|
83
|
-
const sampleSize = Math.min(features.length, 100);
|
|
84
|
-
const sample = features.slice(0, sampleSize);
|
|
85
|
-
|
|
86
|
-
const clusteringPrompt =
|
|
87
|
-
`You are a customer segmentation ML expert. Perform ${algorithm} clustering on ${sampleSize} customers into ${nClusters} segments.
|
|
88
|
-
|
|
89
|
-
Customer features (all normalized 0-1):
|
|
90
|
-
- ltv: predicted lifetime value (0=Low, 0.5=Medium, 1=High)
|
|
91
|
-
- engagement: browser engagement score
|
|
92
|
-
- intention: purchase intention (0=none, 0.3=curious, 0.6=interested, 1=buyer)
|
|
93
|
-
- recency: lead recency (1=today, 0=6 months ago)
|
|
94
|
-
- hour: conversion hour of day
|
|
95
|
-
- is_weekend: converted on weekend (0/1)
|
|
96
|
-
- is_br: lead from Brazil (0/1)
|
|
97
|
-
- is_paid: from paid traffic channel (0/1)
|
|
98
|
-
|
|
99
|
-
Data (${sampleSize} customers): ${JSON.stringify(sample.slice(0, 50))}
|
|
100
|
-
|
|
101
|
-
Return ONLY valid JSON, zero explanation:
|
|
102
|
-
{
|
|
103
|
-
"clusters": [
|
|
104
|
-
{
|
|
105
|
-
"cluster_id": 0,
|
|
106
|
-
"name": "[Nome Descritivo em Português]",
|
|
107
|
-
"size": ${Math.round(sampleSize / nClusters)},
|
|
108
|
-
"percentage": ${Math.round(100 / nClusters)},
|
|
109
|
-
"characteristics": {
|
|
110
|
-
"avg_ltv_class": 0.5,
|
|
111
|
-
"avg_behavior_score": 0.5,
|
|
112
|
-
"avg_engagement_score": 0.5,
|
|
113
|
-
"avg_intention_level": 0.5,
|
|
114
|
-
"avg_days_since_lead": 30,
|
|
115
|
-
"dominant_countries": ["BR"],
|
|
116
|
-
"dominant_states": ["SP", "RJ"],
|
|
117
|
-
"dominant_utm_sources": ["facebook"],
|
|
118
|
-
"top_features": ["ltv", "engagement"]
|
|
119
|
-
},
|
|
120
|
-
"centroid": { "ltv": 0.5, "engagement": 0.5, "intention": 0.5 },
|
|
121
|
-
"action_recommendation": "[Recomendação de campanha específica para este segmento]"
|
|
122
|
-
}
|
|
123
|
-
],
|
|
124
|
-
"silhouette_score": 0.65,
|
|
125
|
-
"total_processed": ${sampleSize}
|
|
126
|
-
}`;
|
|
127
|
-
|
|
128
|
-
// 5. Workers AI
|
|
129
|
-
const startTime = Date.now();
|
|
130
|
-
const aiRes = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
|
|
131
|
-
messages: [{ role: 'user', content: clusteringPrompt }],
|
|
132
|
-
max_tokens: 2000,
|
|
133
|
-
});
|
|
134
|
-
const duration = Date.now() - startTime;
|
|
135
|
-
|
|
136
|
-
if (!aiRes?.response) throw new Error('Workers AI não retornou resposta');
|
|
137
|
-
|
|
138
|
-
const jsonMatch = aiRes.response.trim().match(/\{[\s\S]*\}/);
|
|
139
|
-
if (!jsonMatch) throw new Error('Resposta do Workers AI não contém JSON válido');
|
|
140
|
-
const mlResult = JSON.parse(jsonMatch[0]);
|
|
141
|
-
|
|
142
|
-
if (!Array.isArray(mlResult.clusters) || mlResult.clusters.length === 0) {
|
|
143
|
-
throw new Error('Workers AI não retornou clusters válidos');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 6. Inativar clusters anteriores
|
|
147
|
-
await env.DB.prepare(`UPDATE ml_segments SET is_active = 0 WHERE clustering_algorithm = ? AND client_vertical = ? AND is_active = 1`).bind(algorithm, clientVertical).run();
|
|
148
|
-
|
|
149
|
-
// 7. Persistir novos clusters
|
|
150
|
-
const now = new Date().toISOString();
|
|
151
|
-
for (const cluster of mlResult.clusters) {
|
|
152
|
-
const ch = cluster.characteristics || {};
|
|
153
|
-
await env.DB.prepare(`
|
|
154
|
-
INSERT INTO ml_segments (
|
|
155
|
-
cluster_id, cluster_name, clustering_algorithm, client_vertical,
|
|
156
|
-
size, percentage, avg_ltv_class, avg_behavior_score, avg_engagement_score,
|
|
157
|
-
avg_intention_level, avg_days_since_lead,
|
|
158
|
-
dominant_countries, dominant_states, dominant_utm_sources, dominant_features,
|
|
159
|
-
silhouette_score, action_recommendations, bid_recommendations, campaign_recommendations,
|
|
160
|
-
is_active, created_at, updated_at
|
|
161
|
-
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)
|
|
162
|
-
`).bind(
|
|
163
|
-
cluster.cluster_id || 0, cluster.name || `Segmento ${cluster.cluster_id}`, algorithm, clientVertical,
|
|
164
|
-
cluster.size || 0, cluster.percentage || 0,
|
|
165
|
-
ch.avg_ltv_class || 0, ch.avg_behavior_score || 0, ch.avg_engagement_score || 0,
|
|
166
|
-
ch.avg_intention_level || 0, ch.avg_days_since_lead || 0,
|
|
167
|
-
JSON.stringify(ch.dominant_countries || ['BR']), JSON.stringify(ch.dominant_states || []),
|
|
168
|
-
JSON.stringify(ch.dominant_utm_sources || []), JSON.stringify(ch.top_features || []),
|
|
169
|
-
mlResult.silhouette_score || 0,
|
|
170
|
-
JSON.stringify([cluster.action_recommendation || '']), JSON.stringify([]), JSON.stringify([]),
|
|
171
|
-
now, now,
|
|
172
|
-
).run();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 8. Log no histórico
|
|
176
|
-
try {
|
|
177
|
-
await env.DB.prepare(`
|
|
178
|
-
INSERT INTO ml_clustering_history (
|
|
179
|
-
clustering_id, started_at, completed_at, algorithm,
|
|
180
|
-
n_leads_processed, n_clusters_created, total_duration_ms,
|
|
181
|
-
workers_ai_neurons_used, status, parameters, results_summary
|
|
182
|
-
) VALUES (0, ?, datetime('now'), ?, ?, ?, ?, ?, 'completed', ?, ?)
|
|
183
|
-
`).bind(
|
|
184
|
-
new Date(startTime).toISOString(), algorithm, leads.length, mlResult.clusters.length,
|
|
185
|
-
duration, Math.ceil(duration * 0.01),
|
|
186
|
-
JSON.stringify({ algorithm, n_clusters: nClusters, vertical: clientVertical }),
|
|
187
|
-
JSON.stringify({ clusters: mlResult.clusters.length, silhouette: mlResult.silhouette_score }),
|
|
188
|
-
).run();
|
|
189
|
-
} catch (e) { console.error('[Segmentation] history log error:', e.message); }
|
|
190
|
-
|
|
191
|
-
return new Response(JSON.stringify({
|
|
192
|
-
success: true, algorithm, n_clusters: mlResult.clusters.length, client_vertical: clientVertical,
|
|
193
|
-
leads_analyzed: leads.length, duration_ms: duration, silhouette_score: mlResult.silhouette_score || null,
|
|
194
|
-
clusters: mlResult.clusters, generated_at: now,
|
|
195
|
-
}), { status: 200, headers });
|
|
196
|
-
|
|
197
|
-
} catch (err) {
|
|
198
|
-
console.error('[Segmentation] cluster error:', err.message);
|
|
199
|
-
try {
|
|
200
|
-
if (env.DB) {
|
|
201
|
-
await env.DB.prepare(`
|
|
202
|
-
INSERT INTO ml_clustering_history (clustering_id, started_at, algorithm, n_leads_processed, n_clusters_created, total_duration_ms, workers_ai_neurons_used, status, error_message, parameters, results_summary)
|
|
203
|
-
VALUES (0, datetime('now'), ?, 0, 0, 0, 0, 'failed', ?, ?, '{}')
|
|
204
|
-
`).bind(algorithm, err.message, JSON.stringify({ algorithm, n_clusters: nClusters })).run();
|
|
205
|
-
}
|
|
206
|
-
} catch { /* não bloquear a resposta de erro */ }
|
|
207
|
-
|
|
208
|
-
return new Response(JSON.stringify({ error: 'Erro ao executar clustering', message: err.message }), { status: 500, headers });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ── GET /api/segmentation/list ────────────────────────────────────────────────
|
|
213
|
-
export async function handleSegmentationList(env, request, headers) {
|
|
214
|
-
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
215
|
-
|
|
216
|
-
const url = new URL(request.url);
|
|
217
|
-
const algorithm = url.searchParams.get('algorithm') || null;
|
|
218
|
-
const vertical = url.searchParams.get('vertical') || null;
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
const conditions = ['is_active = 1'];
|
|
222
|
-
const bindings = [];
|
|
223
|
-
if (algorithm) { conditions.push('clustering_algorithm = ?'); bindings.push(algorithm); }
|
|
224
|
-
if (vertical) { conditions.push('client_vertical = ?'); bindings.push(vertical); }
|
|
225
|
-
|
|
226
|
-
const result = await env.DB.prepare(`
|
|
227
|
-
SELECT id, cluster_id, cluster_name, clustering_algorithm, client_vertical,
|
|
228
|
-
size, percentage, avg_ltv_class, avg_behavior_score, avg_engagement_score,
|
|
229
|
-
avg_intention_level, avg_days_since_lead, silhouette_score,
|
|
230
|
-
dominant_countries, dominant_states, dominant_utm_sources, dominant_features,
|
|
231
|
-
action_recommendations, bid_recommendations, campaign_recommendations,
|
|
232
|
-
is_active, created_at, updated_at
|
|
233
|
-
FROM ml_segments
|
|
234
|
-
WHERE ${conditions.join(' AND ')}
|
|
235
|
-
ORDER BY created_at DESC
|
|
236
|
-
LIMIT 50
|
|
237
|
-
`).bind(...bindings).all();
|
|
238
|
-
|
|
239
|
-
const segments = (result.results || []).map(s => ({
|
|
240
|
-
...s,
|
|
241
|
-
dominant_countries: tryParseJson(s.dominant_countries, []),
|
|
242
|
-
dominant_states: tryParseJson(s.dominant_states, []),
|
|
243
|
-
dominant_utm_sources: tryParseJson(s.dominant_utm_sources, []),
|
|
244
|
-
dominant_features: tryParseJson(s.dominant_features, []),
|
|
245
|
-
action_recommendations: tryParseJson(s.action_recommendations, []),
|
|
246
|
-
bid_recommendations: tryParseJson(s.bid_recommendations, []),
|
|
247
|
-
campaign_recommendations: tryParseJson(s.campaign_recommendations, []),
|
|
248
|
-
}));
|
|
249
|
-
|
|
250
|
-
return new Response(JSON.stringify({ success: true, total: segments.length, segments }), { status: 200, headers });
|
|
251
|
-
} catch (err) {
|
|
252
|
-
console.error('[Segmentation] list error:', err.message);
|
|
253
|
-
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ── GET /api/segmentation/outliers ───────────────────────────────────────────
|
|
258
|
-
export async function handleSegmentationOutliers(env, request, headers) {
|
|
259
|
-
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
260
|
-
|
|
261
|
-
const url = new URL(request.url);
|
|
262
|
-
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 200);
|
|
263
|
-
const days = parseInt(url.searchParams.get('days') || '30');
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
const result = await env.DB.prepare(`
|
|
267
|
-
SELECT msm.lead_id, msm.cluster_id, msm.confidence, msm.is_outlier, msm.outlier_reason, msm.assigned_at,
|
|
268
|
-
l.email, l.phone, l.country, l.state, l.city, l.utm_source, l.bot_score, l.engagement_score, l.intention_level, l.created_at AS lead_created_at
|
|
269
|
-
FROM ml_segment_members msm
|
|
270
|
-
LEFT JOIN leads l ON CAST(msm.lead_id AS INTEGER) = l.id
|
|
271
|
-
WHERE msm.is_outlier = 1 AND msm.assigned_at >= datetime('now', '-' || ? || ' days')
|
|
272
|
-
ORDER BY msm.assigned_at DESC
|
|
273
|
-
LIMIT ?
|
|
274
|
-
`).bind(days, limit).all();
|
|
275
|
-
|
|
276
|
-
return new Response(JSON.stringify({ success: true, total: (result.results || []).length, period_days: days, outliers: result.results || [] }), { status: 200, headers });
|
|
277
|
-
} catch (err) {
|
|
278
|
-
console.error('[Segmentation] outliers error:', err.message);
|
|
279
|
-
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ── PUT /api/segmentation/update ─────────────────────────────────────────────
|
|
284
|
-
export async function handleSegmentationUpdate(env, request, headers) {
|
|
285
|
-
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
286
|
-
|
|
287
|
-
let body;
|
|
288
|
-
try { body = await request.json(); }
|
|
289
|
-
catch { return new Response(JSON.stringify({ error: 'JSON inválido no body da requisição' }), { status: 400, headers }); }
|
|
290
|
-
|
|
291
|
-
const { cluster_id, action_recommendations, bid_recommendations, campaign_recommendations } = body;
|
|
292
|
-
if (cluster_id === undefined || cluster_id === null) {
|
|
293
|
-
return new Response(JSON.stringify({ error: 'cluster_id é obrigatório' }), { status: 400, headers });
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
const sets = [];
|
|
298
|
-
const bindings = [];
|
|
299
|
-
if (action_recommendations !== undefined) { sets.push('action_recommendations = ?'); bindings.push(JSON.stringify(action_recommendations)); }
|
|
300
|
-
if (bid_recommendations !== undefined) { sets.push('bid_recommendations = ?'); bindings.push(JSON.stringify(bid_recommendations)); }
|
|
301
|
-
if (campaign_recommendations !== undefined) { sets.push('campaign_recommendations = ?'); bindings.push(JSON.stringify(campaign_recommendations)); }
|
|
302
|
-
|
|
303
|
-
if (sets.length === 0) {
|
|
304
|
-
return new Response(JSON.stringify({ error: 'Nenhum campo válido para atualizar (action_recommendations, bid_recommendations, campaign_recommendations)' }), { status: 400, headers });
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
sets.push("updated_at = datetime('now')");
|
|
308
|
-
bindings.push(cluster_id);
|
|
309
|
-
|
|
310
|
-
await env.DB.prepare(`UPDATE ml_segments SET ${sets.join(', ')} WHERE id = ?`).bind(...bindings).run();
|
|
311
|
-
return new Response(JSON.stringify({ success: true, cluster_id, fields_updated: sets.length - 1 }), { status: 200, headers });
|
|
312
|
-
} catch (err) {
|
|
313
|
-
console.error('[Segmentation] update error:', err.message);
|
|
314
|
-
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CDP Edge — Utilities
|
|
3
|
-
* Funções puras sem dependências externas.
|
|
4
|
-
* Importadas por todos os outros módulos.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// ── CORS ──────────────────────────────────────────────────────────────────────
|
|
8
|
-
export function isAllowedOrigin(origin, siteDomain) {
|
|
9
|
-
if (!origin || !siteDomain) return false;
|
|
10
|
-
return origin === `https://${siteDomain}`
|
|
11
|
-
|| origin.endsWith(`.${siteDomain}`)
|
|
12
|
-
|| origin === 'http://localhost:3000'
|
|
13
|
-
|| origin === 'http://localhost:5173';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function corsHeaders(origin, siteDomain) {
|
|
17
|
-
const allowed = isAllowedOrigin(origin, siteDomain) ? origin : `https://${siteDomain}`;
|
|
18
|
-
return {
|
|
19
|
-
'Access-Control-Allow-Origin': allowed,
|
|
20
|
-
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
|
21
|
-
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With',
|
|
22
|
-
'Access-Control-Max-Age': '86400',
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ── SHA-256 via WebCrypto (obrigatório no Cloudflare Workers) ─────────────────
|
|
27
|
-
export async function sha256(value) {
|
|
28
|
-
if (!value) return undefined;
|
|
29
|
-
const clean = String(value).toLowerCase().trim();
|
|
30
|
-
if (!clean) return undefined;
|
|
31
|
-
const buf = await crypto.subtle.digest(
|
|
32
|
-
'SHA-256',
|
|
33
|
-
new TextEncoder().encode(clean)
|
|
34
|
-
);
|
|
35
|
-
return Array.from(new Uint8Array(buf))
|
|
36
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
37
|
-
.join('');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ── Normalização de telefone → somente dígitos + DDI 55 ──────────────────────
|
|
41
|
-
export function normalizePhone(phone) {
|
|
42
|
-
if (!phone) return undefined;
|
|
43
|
-
let digits = String(phone).replace(/\D/g, '');
|
|
44
|
-
if (digits.length === 11 && !digits.startsWith('55')) digits = '55' + digits;
|
|
45
|
-
if (digits.length === 10 && !digits.startsWith('55')) digits = '55' + digits;
|
|
46
|
-
return digits.length >= 10 ? digits : undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── Normalização de cidade → lowercase sem acentos ────────────────────────────
|
|
50
|
-
export function normalizeCity(city) {
|
|
51
|
-
if (!city) return undefined;
|
|
52
|
-
return String(city)
|
|
53
|
-
.toLowerCase()
|
|
54
|
-
.normalize('NFD')
|
|
55
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
56
|
-
.replace(/[^a-z0-9]/g, '');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── Parse seguro de JSON armazenado como TEXT no D1 ───────────────────────────
|
|
60
|
-
export function tryParseJson(str, fallback) {
|
|
61
|
-
if (!str) return fallback !== undefined ? fallback : null;
|
|
62
|
-
try { return JSON.parse(str); } catch { return fallback !== undefined ? fallback : null; }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── Mapa Meta → GA4 event names ───────────────────────────────────────────────
|
|
66
|
-
export const META_TO_GA4 = {
|
|
67
|
-
PageView: 'page_view',
|
|
68
|
-
ViewContent: 'view_item',
|
|
69
|
-
Lead: 'generate_lead',
|
|
70
|
-
Contact: 'generate_lead',
|
|
71
|
-
Schedule: 'generate_lead',
|
|
72
|
-
InitiateCheckout: 'begin_checkout',
|
|
73
|
-
AddToCart: 'add_to_cart',
|
|
74
|
-
AddPaymentInfo: 'add_payment_info',
|
|
75
|
-
Purchase: 'purchase',
|
|
76
|
-
CompleteRegistration: 'sign_up',
|
|
77
|
-
Subscribe: 'subscribe',
|
|
78
|
-
StartTrial: 'start_trial',
|
|
79
|
-
Search: 'search',
|
|
80
|
-
AddToWishlist: 'add_to_wishlist',
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// ── Lista canônica de eventos válidos (19 eventos) ────────────────────────────
|
|
84
|
-
export const VALID_EVENT_NAMES = new Set([
|
|
85
|
-
'PageView','ViewContent','Lead','Purchase','InitiateCheckout',
|
|
86
|
-
'AddToCart','CompleteRegistration','Contact','Schedule',
|
|
87
|
-
'StartTrial','Subscribe','SubmitApplication','Search',
|
|
88
|
-
'video_start','video_25','video_50','video_75','video_complete',
|
|
89
|
-
]);
|