@enerlence/suntropy-cli 0.11.9 → 0.12.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 +81 -1
- package/dist/bin/suntropy.js +1179 -19
- package/dist/bin/suntropy.js.map +1 -1
- package/package.json +1 -1
- package/skills/satvolt-api.md +279 -0
- package/skills/satvolt-campaign.md +228 -0
- package/skills/satvolt-cli.md +185 -0
- package/skills/satvolt-lead-troubleshooting.md +89 -0
- package/skills/satvolt-probe-to-full-campaign.md +185 -0
package/package.json
CHANGED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Referencia de la API pública HTTP de Satvolt (`/api/v1`), la misma que usa `suntropy satvolt`. Úsala para integrar Satvolt desde código o `curl`: crear y configurar campañas, seguirlas, ampliarlas, leer y exportar leads, gestionar plantillas y consultar el consumo de créditos. Si tienes la CLI a mano, es más cómodo `satvolt-cli`.
|
|
2
|
+
|
|
3
|
+
## Base, autenticación y formato
|
|
4
|
+
|
|
5
|
+
| Entorno | Base URL |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Producción | `https://api.enerlence.com/satvolt/api/v1` |
|
|
8
|
+
| Local | `http://localhost:8099/api/v1` |
|
|
9
|
+
|
|
10
|
+
- **Autenticación:** `Authorization: Bearer <JWT>`, el mismo token de Suntropy. Se verifican la firma y la caducidad, y todo queda acotado al `clientUID` del token.
|
|
11
|
+
- `401 MISSING_TOKEN`: falta la cabecera.
|
|
12
|
+
- `401 INVALID_TOKEN`: el token no es válido.
|
|
13
|
+
- `401 TOKEN_EXPIRED`: el token ha caducado.
|
|
14
|
+
- `401 MISSING_CLIENT`: el token no trae `clientUID`.
|
|
15
|
+
- **Respuestas:** con el código HTTP real y un sobre:
|
|
16
|
+
- éxito: `{ "code": 200, "data": ... }`
|
|
17
|
+
- error: `{ "code": 422, "error": { "code": "VALIDATION_ERROR", "message": "...", "details": [...] } }`
|
|
18
|
+
- **Paginación:** `?limit=` (25 por defecto, 200 como máximo; 500 en `export-tables/:id/data`) y `?offset=`. La respuesta trae `{ items, total, limit, offset, hasMore }`.
|
|
19
|
+
- **Campañas de otra empresa:** devuelven `404 CAMPAIGN_NOT_FOUND`, igual que un id inexistente.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
API=https://api.enerlence.com/satvolt/api/v1
|
|
23
|
+
curl -s -H "Authorization: Bearer $TOKEN" "$API/campaigns?state=completed&limit=10"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Catálogo
|
|
27
|
+
|
|
28
|
+
| Método | Ruta | Devuelve |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| GET | `/catalog/actions` | Acciones LEAD visibles: `action`, `name`, `description`, `multiple`, `isAsync`, `creditCost` (fijo por lead; las ejecuciones fallidas o saltadas no cobran), `finalLeadState`, `dependencies`, `resultsPropertyKeys` (solo claves de primer nivel), `configSchema` (JSON Schema de la configuración de entrada). Las rutas de los datos que escribe cada paso salen de `/campaigns/:id/fields` |
|
|
31
|
+
| GET | `/catalog/ai-agents` | Agentes válidos para `AI_AGENT.config.agentId` |
|
|
32
|
+
| GET | `/catalog/business-groups` | Grupos para `businessGroups` |
|
|
33
|
+
| GET | `/catalog/states` | Estados de campaña y de lead |
|
|
34
|
+
|
|
35
|
+
## Campañas
|
|
36
|
+
|
|
37
|
+
| Método | Ruta | Parámetros / body |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| GET | `/campaigns` | `?limit&offset&search&state=a,b&source=maps\|excel\|campaign` |
|
|
40
|
+
| POST | `/campaigns` | Crear (ver abajo) |
|
|
41
|
+
| GET | `/campaigns/:id` | Detalle con `leadStates`, `sectorSearch` y `configuration` |
|
|
42
|
+
| DELETE | `/campaigns/:id` | Borra la campaña con sectores, leads, ejecuciones, configuración y jobs |
|
|
43
|
+
| POST | `/campaigns/:id/start` | Arranca una campaña `queued` (`409 INVALID_CAMPAIGN_STATE` si no lo está) |
|
|
44
|
+
| POST | `/campaigns/:id/reset` | `?start=true` para relanzarla. Borra leads y resultados |
|
|
45
|
+
| POST | `/campaigns/:id/extend` | `{ "maxLeads": 500 }` o `{ "maxLeads": null }` para quitar el límite |
|
|
46
|
+
| POST | `/campaigns/:id/resume` | `{ "step": { "action": "AI_AGENT", "config": {...} } }`: lo añade al final y lo ejecuta sobre los leads |
|
|
47
|
+
| GET | `/campaigns/:id/usage` | `?include=leads`: créditos totales, por paso y, opcionalmente, por lead |
|
|
48
|
+
| GET | `/campaigns/:id/logs` | `?limit&sinceTs&level=debug\|log\|warn\|error` → `{ entries, lastTs }` |
|
|
49
|
+
| GET | `/campaigns/:id/funnel` | Por paso LEAD: `reached`, `success`, `failure`, `skipped`, `processing` y `pending`, más `leadStates`. `criteria` (o `null`) añade `met`/`unmet` según el `successIf` del paso |
|
|
50
|
+
|
|
51
|
+
### Criterio de éxito y reintentos por paso
|
|
52
|
+
|
|
53
|
+
Ejecutar un paso y que traiga el dato son cosas distintas: un agente puede terminar en
|
|
54
|
+
`success` respondiendo que no encontró nada. Dos claves de `config`, admitidas por
|
|
55
|
+
cualquier acción, lo separan:
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{ "successIf": ["response.linkedinUrl"], "maxRetries": 2 }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `successIf`: rutas que deben traer valor. Relativas a lo que el paso escribe (en un
|
|
62
|
+
agente, a su `response`) o absolutas con `fullData.`/`lead.`. Vacío = `null`, `""`,
|
|
63
|
+
`[]` o `{}`; `false` y `0` sí son datos.
|
|
64
|
+
- `maxRetries` (0-5): repite el paso mientras no se cumpla el criterio. Los créditos se
|
|
65
|
+
cobran una sola vez por paso, no por intento. Agotados los intentos, el lead sigue al
|
|
66
|
+
paso siguiente y la ejecución queda con `metadata.successCriteria.met = false`.
|
|
67
|
+
|
|
68
|
+
El criterio se evalúa sobre los datos actuales del lead, así que `funnel` lo recalcula al
|
|
69
|
+
vuelo: cámbialo con `PATCH /campaigns/:id/configuration` y vuelve a medir sin re-ejecutar.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
curl -s -X PATCH -H "$H" "$API/campaigns/$ID/configuration" \
|
|
73
|
+
-d '{"steps":[{"uid":"7f3edaf055d60627","config":{"successIf":["response.linkedinUrl"],"maxRetries":2}}]}'
|
|
74
|
+
curl -s -H "$H" "$API/campaigns/$ID/funnel" | jq '.data.steps[] | {name, reached, criteria}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Crear campaña
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
POST /campaigns
|
|
81
|
+
{
|
|
82
|
+
"name": "Sonda Huévar",
|
|
83
|
+
"area": { "type": "circle", "center": { "lat": 37.3509, "lng": -6.2757 }, "radiusMeters": 5000 },
|
|
84
|
+
"templateId": "Greenvolt industria",
|
|
85
|
+
"maxLeads": 50,
|
|
86
|
+
"start": false
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **`area`** admite tres formas:
|
|
91
|
+
- `{type:"circle", center, radiusMeters}`, de 100 m a 50 km;
|
|
92
|
+
- `{type:"bounds", northWest, southEast}`;
|
|
93
|
+
- `{type:"polygon", coordinates:[[lat,lng],...]}`, que se busca en su rectángulo envolvente (con aviso).
|
|
94
|
+
|
|
95
|
+
Los puntos pueden ir como `{lat,lng}` o `[lat,lng]`.
|
|
96
|
+
- **Base opcional:** `templateId` (id o nombre) o `fromCampaignId` (una campaña de Maps). Aporta `steps`, `businessGroups`, `description`, `searchQuery` y `maxLeads`; lo que se envía explícitamente tiene prioridad. No se pueden usar las dos a la vez.
|
|
97
|
+
- **Resto de campos:**
|
|
98
|
+
- `steps` son solo los pasos LEAD `[{action, config?, uid?, disable?}]`. Se validan contra `configSchema`, se rellenan los `default` y se añaden las dependencias que falten, con aviso.
|
|
99
|
+
- `businessGroups` es `["businesses"]` por defecto si no hay base.
|
|
100
|
+
- Opcionales: `searchQuery`, `description`, `inputAddress` y `region`.
|
|
101
|
+
- **Respuesta:** `{ campaign, area, configuration, estimatedCreditsPerLead, basedOn, started, warnings }`.
|
|
102
|
+
- **Errores:** `422 VALIDATION_ERROR` trae en `details[]` el `index`, `uid`, `action`, `field` y `message` de cada problema. Otros: `400 INVALID_AREA`, `404 TEMPLATE_NOT_FOUND`, `400 UNSUPPORTED_SOURCE`.
|
|
103
|
+
|
|
104
|
+
### Ampliar (`extend`)
|
|
105
|
+
|
|
106
|
+
- **Qué hace:** guarda el nuevo límite y vuelve a buscar solo en los sectores cuya búsqueda no se agotó. Solo los leads nuevos pasan por el pipeline.
|
|
107
|
+
- **Respuesta:** `{ campaignId, previousMaxLeads, maxLeads, currentLeads, sectors: {total, queued, exhausted}, started, campaign }`.
|
|
108
|
+
- **Si no queda nada que buscar:** `sectors.queued = 0`. El límite se guarda, pero no se busca nada.
|
|
109
|
+
- **Errores:**
|
|
110
|
+
- `409 CAMPAIGN_RUNNING`: la campaña está en marcha o tiene búsquedas en cola.
|
|
111
|
+
- `409 CAMPAIGN_NOT_STARTED`: todavía está `queued`.
|
|
112
|
+
- `400 UNSUPPORTED_SOURCE`: no es de Maps.
|
|
113
|
+
- `400 VALIDATION_ERROR`: el límite no es mayor que los leads actuales.
|
|
114
|
+
- **Cuándo tiene sentido:** en `GET /campaigns/:id`, `sectorSearch` da `{ total, exhausted, incomplete, queued, unknown }`. Con `incomplete + unknown > 0` todavía puede aparecer algo.
|
|
115
|
+
|
|
116
|
+
## Pipeline
|
|
117
|
+
|
|
118
|
+
| Método | Ruta | Body |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| GET | `/campaigns/:id/configuration` | → `{ campaignId, source, businessGroups, description, updatedAtMs, steps[{uid, action, target, disable, structural, config}] }` |
|
|
121
|
+
| PUT | `/campaigns/:id/configuration` | `{ steps: [...lista completa de pasos LEAD...], businessGroups?, description? }`. Los estructurales se ignoran, así que se puede reenviar lo que devuelve el GET |
|
|
122
|
+
| PATCH | `/campaigns/:id/configuration` | `{ steps: [parche, ...], businessGroups?, description? }` |
|
|
123
|
+
| GET | `/campaigns/:id/steps` | Pasos con `name`, `creditCost`, `isAsync`, `executedLeads`, `neverExecuted`, `runnable` y `reason` |
|
|
124
|
+
| POST | `/campaigns/:id/steps/:uid/run` | Ejecuta sobre los leads existentes un paso que ningún lead ha ejecutado |
|
|
125
|
+
|
|
126
|
+
**Formas de parche** (en `PATCH configuration` y `PATCH campaign-templates`):
|
|
127
|
+
|
|
128
|
+
| Parche | Efecto |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `{ "uid": "…", "config": { "k": "v", "otra": null } }` | Fusiona (JSON Merge Patch); `null` devuelve la clave a su default |
|
|
131
|
+
| `{ "uid": "…", "replaceConfig": true, "config": {...} }` | Sustituye la config entera |
|
|
132
|
+
| `{ "uid": "…", "disable": true }` | Desactiva el paso |
|
|
133
|
+
| `{ "uid": "…", "before": "<uid>" }` / `"after"` | Mueve el paso |
|
|
134
|
+
| `{ "uid": "…", "remove": true }` | Lo quita |
|
|
135
|
+
| `{ "action": "QUALIFY", "config": {...} }` (sin uid) | Lo añade antes de COMPLETE, o donde indiquen `before`/`after` |
|
|
136
|
+
|
|
137
|
+
La `action` de un paso existente no se puede cambiar. Si la campaña está en marcha, cambiar el orden o quitar pasos devuelve un aviso.
|
|
138
|
+
|
|
139
|
+
## Leads
|
|
140
|
+
|
|
141
|
+
| Método | Ruta | Parámetros |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| GET | `/campaigns/:id/leads` | `?limit&offset&name&search&state=a,b&step=<paso>&stepStatus=reached\|success\|failure\|skipped\|processing\|pending&include=steps` |
|
|
144
|
+
| GET | `/campaigns/:id/leads/:leadId` | `?fullData=true` (todo) o `?fullData=clave1,clave2` |
|
|
145
|
+
| POST | `/campaigns/:id/leads/:leadId/steps/:step/run` | `{ "mode": "only" \| "continue", "force": false }` → 202 |
|
|
146
|
+
| GET | `/campaigns/:id/fields` | `?sample=25` (máx. 100) `&step=<paso>`: campos para columnas de exportación, agrupados por paso (ver *Tablas de exportación*) |
|
|
147
|
+
|
|
148
|
+
**`<paso>`** (en `step=` de `/leads` y `/fields`, y en la ruta de `run`): uid, acción si aparece una sola vez, nombre completo del paso (`customName`, sin distinguir mayúsculas ni tildes; no vale un trozo) o clave de fullData donde deja sus datos (`cif`, `qualification_<uid>`). Varias coincidencias: `400 AMBIGUOUS_STEP` con los uids en `details`; ninguna: `404 STEP_NOT_FOUND` con la lista de pasos.
|
|
149
|
+
|
|
150
|
+
**Cada lead de la lista** trae `idLead`, `commercialName`, `state`, `stateError`, `lastAction`, `lastStepUid`, `address`, `phone`, `url`, `googlePlacesType`, `coordinates`, `qualifications` y, con `include=steps`, `steps`.
|
|
151
|
+
|
|
152
|
+
**Ejecutar un paso en un lead:**
|
|
153
|
+
- **`step`:** el uid, o la acción si aparece una vez (`400 AMBIGUOUS_STEP` lista los uids).
|
|
154
|
+
- **`mode: "only"`:** solo ese paso. No encola los siguientes, y un lead `completed` o `unQualified` conserva su estado salvo que cambie el resultado del filtro.
|
|
155
|
+
- **`mode: "continue"`:** sigue el pipeline desde ese paso y vuelve a ejecutar los posteriores.
|
|
156
|
+
- **Respuesta:** `{ campaignId, leadId, stepUid, action, mode, jobId, isAsync, name, creditCost }`.
|
|
157
|
+
- **Errores:**
|
|
158
|
+
- `409 DEPENDENCY_NOT_MET` (con `details.missing`): faltan dependencias; `force: true` las ignora.
|
|
159
|
+
- `409 STEP_IN_PROGRESS`: un paso asíncrono sigue esperando su webhook.
|
|
160
|
+
- `400 STEP_NOT_RUNNABLE`: COMPLETE, paso desactivado o que no es LEAD.
|
|
161
|
+
- `404 LEAD_NOT_FOUND`.
|
|
162
|
+
- `409 CAMPAIGN_NOT_STARTED`.
|
|
163
|
+
|
|
164
|
+
## Tablas de exportación
|
|
165
|
+
|
|
166
|
+
| Método | Ruta | Body / parámetros |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| GET | `/campaigns/:id/export-tables` | Tablas de la campaña |
|
|
169
|
+
| POST | `/campaigns/:id/export-tables` | `{ name, description?, columns: [{ id?, label, path, type? }] }` → 201 con la tabla en `data` (incluye `warnings`) |
|
|
170
|
+
| GET | `/export-tables/:tableId` | Definición |
|
|
171
|
+
| PUT | `/export-tables/:tableId` | `{ name, description?, columns }` (lista completa; conserva los `id`) |
|
|
172
|
+
| PATCH | `/export-tables/:tableId` | `{ name?, description?, columns? }` (`columns` sustituye la lista entera) |
|
|
173
|
+
| DELETE | `/export-tables/:tableId` | Borra la tabla |
|
|
174
|
+
| POST | `/export-tables/:tableId/duplicate` | `{ targetCampaignId, name? }` |
|
|
175
|
+
| GET | `/export-tables/:tableId/data` | `?limit(≤500)&offset&search` → `{ columns, items, total, ... }` |
|
|
176
|
+
| GET | `/export-tables/:tableId/export` | `?format=xlsx\|csv` → fichero con todas las filas (hasta 100.000, sin paginar); cabeceras `Content-Disposition` y `X-Row-Count`. CSV: UTF-8 con BOM, separado por comas, textos con comas, comillas o saltos de línea entre comillas dobles, booleanos `true`/`false` |
|
|
177
|
+
|
|
178
|
+
**Tabla** (respuesta de GET, POST, PUT y PATCH; estas tres últimas añaden `warnings`):
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{ "id": "6650f0c2a1b2c3d4e5f60718", "campaignId": 62, "name": "CRM", "description": null,
|
|
182
|
+
"columns": [{ "id": "c_1a2b3c4d", "label": "Empresa", "path": "lead.commercialName", "type": "string" }],
|
|
183
|
+
"createdAtMs": 1789500035718, "updatedAtMs": 1789500035718, "warnings": [] }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Columnas una a una** (sin reenviar la lista entera):
|
|
187
|
+
|
|
188
|
+
| Método | Ruta | Body |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| GET | `/export-tables/:tableId/columns` | Columnas en orden |
|
|
191
|
+
| POST | `/export-tables/:tableId/columns` | `{ label, path, type?, id?, position? }` → 201 `{ table, column, warnings }` |
|
|
192
|
+
| PATCH | `/export-tables/:tableId/columns/:columnId` | `{ label?, path?, type?, position? }` → `{ table, column, warnings }` |
|
|
193
|
+
| DELETE | `/export-tables/:tableId/columns/:columnId` | → `{ table, column, warnings }` |
|
|
194
|
+
| PUT | `/export-tables/:tableId/columns/order` | `{ columnIds: [...] }` con todas las columnas en el orden nuevo → `{ table, warnings }` |
|
|
195
|
+
|
|
196
|
+
- **`position`:** índice desde 0 (0 = primera columna). Sin `position`, POST añade al final y PATCH deja la columna donde está.
|
|
197
|
+
- **`type`:** opcional al crear. Por defecto se usa el de la columna del lead (`lead.url` → `url`) o el que declara el paso que escribe la ruta (`fullData.consumptionEstimate.annualKwh` → `number`); si no se conoce, `string`. Al cambiar `path` en un PATCH el tipo se mantiene salvo que se mande `type`.
|
|
198
|
+
- **`id`:** letras, dígitos, `_` o `-`, hasta 32 caracteres; se genera si no se manda.
|
|
199
|
+
- **Concurrencia:** cada operación se aplica sobre el estado actual de la tabla, así que dos cambios simultáneos no se pisan. `409 CONCURRENT_UPDATE` si no se pudo aplicar tras varios intentos: repite la llamada.
|
|
200
|
+
- **Errores:** `404 COLUMN_NOT_FOUND`, `400 DUPLICATE_COLUMN_ID`, `400 INVALID_POSITION`, `400 INVALID_ORDER` (faltan o sobran ids en `columnIds`).
|
|
201
|
+
|
|
202
|
+
**Descubrir campos: `GET /campaigns/:id/fields`.** Devuelve los campos agrupados por paso del pipeline. Cada paso trae los campos que declara, así que funciona antes de lanzar la campaña, y la muestra de leads añade cobertura, ejemplos y los campos que dependen de la configuración:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"campaignId": 62, "sampledLeads": 25,
|
|
207
|
+
"lead": [{ "path": "lead.commercialName", "label": "Business name", "type": "string" }],
|
|
208
|
+
"synthetic": [{ "path": "synthetic.googleMapsUrl", "label": "Google Maps URL", "type": "url" }],
|
|
209
|
+
"steps": [{
|
|
210
|
+
"uid": "95161b8b080180e4", "action": "AI_AGENT", "name": "Buscador de CIF y Facturacion",
|
|
211
|
+
"keys": ["cif"], "coverage": 0.32,
|
|
212
|
+
"fields": [
|
|
213
|
+
{ "path": "fullData.cif.response.extras.cnae", "label": "Extras · Cnae", "type": "string",
|
|
214
|
+
"source": "observed", "coverage": 0.32, "example": "2512" }
|
|
215
|
+
],
|
|
216
|
+
"dynamic": [{ "path": "fullData.cif.response", "description": "Agent response: ...",
|
|
217
|
+
"inferredFrom": { "campaignId": 59, "sampledLeads": 25 } }]
|
|
218
|
+
}],
|
|
219
|
+
"other": []
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- **`source`:** `catalog` (lo declara el paso), `observed` (aparece en los leads de la muestra) u `otherCampaign` (deducido de otra campaña tuya con el mismo agente, porque esta aún no tiene respuestas).
|
|
224
|
+
- **`keys`:** claves de `fullData` del paso. Un AI_AGENT o CUSTOM_WEBHOOK con `outputKey` usa el alias; QUALIFY usa `resultKey` o `qualification_<uid>`.
|
|
225
|
+
- **Campos fijos de los pasos más usados:**
|
|
226
|
+
- QUALIFY: `fullData.<clave>.qualifies` (boolean) y `.explanation`.
|
|
227
|
+
- AI_AGENT: `fullData.<clave>.response.<campo>` (depende del agente), más `threadId`, `state` y `completedAt`.
|
|
228
|
+
- ESTIMATE_CONSUMPTION: `fullData.consumptionEstimate.annualKwh`, `.confidence.label`, `.inputsUsed.cnae_2`…
|
|
229
|
+
- FIND_ROOFTOP: `fullData.catastralParcel.catastralReference` y `.area`.
|
|
230
|
+
- `synthetic.googleMapsUrl`: enlace a Google Maps construido con las coordenadas.
|
|
231
|
+
- **`dynamic`:** partes cuya forma depende de la configuración del paso (respuesta de un agente o de un webhook). Sus campos aparecen en `fields` cuando hay leads con respuesta o se deducen de otra campaña; si no hay ninguno, la ruta se completa a mano (`fullData.<clave>.response.<campo>`).
|
|
232
|
+
- **`coverage`:** parte de los leads muestreados con ese dato (null si la campaña no tiene leads). Una cobertura baja en un campo de un paso posterior a un filtro (QUALIFY) es normal.
|
|
233
|
+
- **`other`:** datos de los leads que no escribe ningún paso actual (pasos borrados o alias cambiados).
|
|
234
|
+
|
|
235
|
+
- **Rutas de columna:** `lead.<columna>`, `synthetic.<clave>` y `fullData.<ruta.anidada>` (arrays con índice: `fullData.consumptionEstimate.monthlyKwh[0]`).
|
|
236
|
+
- **Tipos:** `string`, `number`, `boolean`, `date` y `url`.
|
|
237
|
+
- **Avisos:** crear o editar columnas devuelve `warnings` con `UNKNOWN_FULLDATA_KEY` si ningún paso de la campaña escribe la clave de `fullData` de la ruta (errata o alias cambiado). No bloquea: la columna se guarda.
|
|
238
|
+
- **Errores:** `404 EXPORT_TABLE_NOT_FOUND` si el id no existe (o no es un ObjectId) y `400 VALIDATION_ERROR` si alguna ruta o id es inválido.
|
|
239
|
+
|
|
240
|
+
## Plantillas de campaña
|
|
241
|
+
|
|
242
|
+
| Método | Ruta | Body |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| GET | `/campaign-templates` | `?search=` |
|
|
245
|
+
| GET | `/campaign-templates/:idOrName` | Detalle |
|
|
246
|
+
| POST | `/campaign-templates` | `{ name, description?, steps, businessGroups?, configurationDescription?, searchQuery?, maxLeads? }` o `{ fromCampaignId, name, description? }` |
|
|
247
|
+
| PUT | `/campaign-templates/:idOrName` | Plantilla completa (`steps` obligatorio; lo que no se envía se borra) |
|
|
248
|
+
| PATCH | `/campaign-templates/:idOrName` | Campos sueltos; `steps` son parches por uid; `maxLeads: null` o `searchQuery: null` quitan el valor |
|
|
249
|
+
| DELETE | `/campaign-templates/:idOrName` | Borra la plantilla |
|
|
250
|
+
|
|
251
|
+
- **Vista:** `{ id, name, description, source, steps[{uid, action, target, disable, config}], businessGroups, configurationDescription, searchQuery, maxLeads, sourceCampaignId, createdAtMs, updatedAtMs }`.
|
|
252
|
+
- **Errores:** `409 TEMPLATE_NAME_TAKEN` y `404 TEMPLATE_NOT_FOUND`.
|
|
253
|
+
- **uids compartidos:** las campañas creadas desde la misma plantilla comparten uids de pasos.
|
|
254
|
+
|
|
255
|
+
## Ejemplo de principio a fin con curl
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
H="Authorization: Bearer $TOKEN"; J="Content-Type: application/json"
|
|
259
|
+
# 1. Plantilla desde una campaña validada
|
|
260
|
+
curl -s -X POST "$API/campaign-templates" -H "$H" -H "$J" -d '{"fromCampaignId":62,"name":"Greenvolt industria"}'
|
|
261
|
+
# 2. Sonda de 50 leads y arranque
|
|
262
|
+
ID=$(curl -s -X POST "$API/campaigns" -H "$H" -H "$J" -d '{"name":"Sonda Elche","templateId":"Greenvolt industria","maxLeads":50,
|
|
263
|
+
"area":{"type":"circle","center":{"lat":38.293,"lng":-0.617},"radiusMeters":3000},"start":true}' | jq -r .data.campaign.idCampaign)
|
|
264
|
+
# 3. Seguimiento
|
|
265
|
+
curl -s -H "$H" "$API/campaigns/$ID/funnel" | jq '.data.steps[] | {name, success, failure, pending}'
|
|
266
|
+
# 4. Ampliar cuando termine y quede área por buscar
|
|
267
|
+
curl -s -X POST "$API/campaigns/$ID/extend" -H "$H" -H "$J" -d '{"maxLeads":null}'
|
|
268
|
+
# 5. Créditos consumidos
|
|
269
|
+
curl -s -H "$H" "$API/campaigns/$ID/usage" | jq '.data | {totalCredits, avgCreditsPerLead, byStep}'
|
|
270
|
+
# 6. Tabla de exportación: rutas del paso QUALIFY y del agente de CIF, tabla, columna en 2ª posición y CSV
|
|
271
|
+
curl -s -H "$H" "$API/campaigns/$ID/fields?step=QUALIFY" | jq '.data.steps[0].fields[] | {path, type}'
|
|
272
|
+
curl -s -H "$H" "$API/campaigns/$ID/fields?step=cif" | jq '.data.steps[0].fields[] | select(.path | test("cnae")) | .path'
|
|
273
|
+
TABLE=$(curl -s -X POST "$API/campaigns/$ID/export-tables" -H "$H" -H "$J" -d '{"name":"Cualificación","columns":[
|
|
274
|
+
{"label":"Empresa","path":"lead.commercialName"},
|
|
275
|
+
{"label":"Cualifica","path":"fullData.qualification_<uid>.qualifies"},
|
|
276
|
+
{"label":"Motivo","path":"fullData.qualification_<uid>.explanation"}]}' | jq -r .data.id)
|
|
277
|
+
curl -s -X POST "$API/export-tables/$TABLE/columns" -H "$H" -H "$J" -d '{"label":"CNAE","path":"fullData.cif.response.extras.cnae","position":1}'
|
|
278
|
+
curl -s -H "$H" "$API/export-tables/$TABLE/export?format=csv" -o cualificacion.csv
|
|
279
|
+
```
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Crea, configura y explota una campaña de Satvolt (captación de leads B2B desde Google Maps con un pipeline de enriquecimiento) usando `suntropy satvolt`. Todo lo que aquí se hace es lo mismo que permite la web de Satvolt; la CLI habla con la API pública `/satvolt/api/v1` con el mismo token de `suntropy auth`.
|
|
2
|
+
|
|
3
|
+
Cada acción del pipeline gasta créditos por lead (1 crédito = 0,005 €). Antes de arrancar o reanudar una campaña, enseña al usuario el coste estimado y pide confirmación.
|
|
4
|
+
|
|
5
|
+
## Parámetros de entrada
|
|
6
|
+
|
|
7
|
+
| Parámetro | Obligatorio | Default |
|
|
8
|
+
|-----------|-------------|---------|
|
|
9
|
+
| Nombre de la campaña | Sí | - |
|
|
10
|
+
| Área: centro + radio, rectángulo o polígono | Sí | - |
|
|
11
|
+
| Consulta de texto (en vez de búsqueda por cercanía) | No | - |
|
|
12
|
+
| Máximo de leads | No | sin límite |
|
|
13
|
+
| Grupos de negocio | No | `businesses` |
|
|
14
|
+
| Plantilla o campaña base (pasos, grupos, límite) | No | - |
|
|
15
|
+
| Pasos del pipeline (acciones y su config) | No | los de la base; sin base, ninguno (solo descubre leads) |
|
|
16
|
+
| Arrancar al crear | No | no (queda en cola) |
|
|
17
|
+
|
|
18
|
+
## Ejecución
|
|
19
|
+
|
|
20
|
+
### Paso 0: Catálogo
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
suntropy satvolt catalog actions # acciones, créditos por lead, dependencias y JSON Schema de config
|
|
24
|
+
suntropy satvolt catalog business-groups
|
|
25
|
+
suntropy satvolt catalog ai-agents # ids válidos para AI_AGENT config.agentId
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Reglas del catálogo:
|
|
29
|
+
- `configSchema.required` son obligatorios en pasos activos; los `default` se rellenan solos.
|
|
30
|
+
- Solo las acciones con `multiple: true` (QUALIFY, AI_AGENT, CUSTOM_WEBHOOK) pueden repetirse.
|
|
31
|
+
- Si falta una dependencia (p. ej. ESTIMATE_CONSUMPTION necesita FIND_ROOFTOP), el backend la añade y lo avisa en `warnings`.
|
|
32
|
+
|
|
33
|
+
### Paso 1: Pasos del pipeline
|
|
34
|
+
|
|
35
|
+
Si ya hay una plantilla o una campaña con el pipeline que se quiere, úsala como base y sáltate este paso (`templates list`, `--template` o `--from-campaign` en el paso 2). Si el pipeline es nuevo y se va a repetir, guárdalo como plantilla: `templates create --name ... --steps @steps.json`.
|
|
36
|
+
|
|
37
|
+
Escribe los pasos LEAD en orden en un fichero. SECTORIZE, FIND_LEADS y COMPLETE los pone el backend:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cat > steps.json <<'EOF'
|
|
41
|
+
[
|
|
42
|
+
{ "action": "FIND_ROOFTOP" },
|
|
43
|
+
{ "action": "QUALIFY", "config": { "qualificationDefinition": "Nave industrial con cubierta > 1000 m2 y actividad con consumo diurno" } },
|
|
44
|
+
{ "action": "AI_AGENT", "config": { "customName": "Buscador de CIF y Facturacion", "agentId": "<id>", "outputKey": "cif" } },
|
|
45
|
+
{ "action": "ESTIMATE_CONSUMPTION", "config": { "tariffTemplate": "3.0TD", "cnaeTemplate": "{{fullData.cif.response.extras.cnae}}" } }
|
|
46
|
+
]
|
|
47
|
+
EOF
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
ESTIMATE_CONSUMPTION usa la superficie construida del Catastro, el código postal y, si se le pasa, el CNAE. Sin CNAE la confianza no pasa de "media" y la estimación anual de los fabricantes sale muy por debajo. Colócalo después del paso que obtiene el CNAE y referencia su salida en `cnaeTemplate`.
|
|
51
|
+
|
|
52
|
+
### Paso 2: Crear la campaña
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Círculo (100 m – 50 km)
|
|
56
|
+
suntropy satvolt campaigns create --name "<nombre>" \
|
|
57
|
+
--circle <lat>,<lng> --radius <metros> \
|
|
58
|
+
--max-leads 300 --steps @steps.json
|
|
59
|
+
|
|
60
|
+
# Rectángulo
|
|
61
|
+
suntropy satvolt campaigns create --name "<nombre>" --bounds <nwLat>,<nwLng>,<seLat>,<seLng> --steps @steps.json
|
|
62
|
+
|
|
63
|
+
# Polígono ([[lat,lng],...] o GeoJSON). Se busca en su rectángulo envolvente: avisa al usuario.
|
|
64
|
+
suntropy satvolt campaigns create --name "<nombre>" --polygon @area.geojson --steps @steps.json
|
|
65
|
+
|
|
66
|
+
# Desde una plantilla o copiando otra campaña (los flags explícitos tienen prioridad)
|
|
67
|
+
suntropy satvolt campaigns create --name "<nombre>" --template "<plantilla>" --circle <lat>,<lng> --radius <m>
|
|
68
|
+
suntropy satvolt campaigns create --name "<nombre>" --from-campaign <id> --bounds <nwLat>,<nwLng>,<seLat>,<seLng> --max-leads 100
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
La respuesta trae `campaign.idCampaign`, `estimatedCreditsPerLead` y `warnings`. La campaña queda en `queued`.
|
|
72
|
+
|
|
73
|
+
Coste estimado, antes de arrancar:
|
|
74
|
+
- **Máximo:** `estimatedCreditsPerLead × maxLeads` (sin contar FIND_LEADS), como si todos los leads pasaran todos los pasos.
|
|
75
|
+
- **Realista:** si hay una campaña anterior con la misma configuración, `campaigns usage <id>` → `avgCreditsPerLead × maxLeads`. Los filtros (QUALIFY) hacen que la mayoría de leads no pague los pasos caros.
|
|
76
|
+
|
|
77
|
+
Enséñale al usuario el rango y pide confirmación. Las ejecuciones fallidas o saltadas no cobran.
|
|
78
|
+
|
|
79
|
+
### Paso 3: Arrancar y seguir
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
suntropy satvolt campaigns start <campaignId>
|
|
83
|
+
suntropy satvolt campaigns logs <campaignId> --follow --format human # termina solo al acabar la campaña
|
|
84
|
+
suntropy satvolt campaigns funnel <campaignId> --format human # alcanzados/success/failure/processing por paso
|
|
85
|
+
suntropy satvolt campaigns funnel <campaignId> --mode success # en cuántos leads el paso trajo el dato
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Ejecutar ≠ acertar.** Un agente puede terminar sin error respondiendo que no encontró
|
|
89
|
+
nada: el paso cuenta como `success` y la columna se queda vacía. Para medirlo, cada paso
|
|
90
|
+
admite dos claves de config comunes:
|
|
91
|
+
|
|
92
|
+
| Clave | Qué hace |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `successIf` | Rutas que el paso debe rellenar para contar como útil. Relativas a lo que escribe el paso (en un agente, a su respuesta: `response.linkedinUrl`) o absolutas con `fullData.`/`lead.`. Al ser relativas, sobreviven a copiar la campaña o guardarla como plantilla. |
|
|
95
|
+
| `maxRetries` | 0-5. Repite el paso mientras no se cumpla `successIf`. Para pasos no deterministas (agentes, identificación de paneles). Los créditos se cobran una vez por paso, no por intento; agotados los intentos el lead continúa al paso siguiente. |
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
suntropy satvolt steps set <campaignId> <stepUid> \
|
|
99
|
+
--config '{"successIf":["response.linkedinUrl"],"maxRetries":2}'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
El criterio se evalúa sobre los datos actuales del lead, así que se puede cambiar y volver
|
|
103
|
+
a medir una campaña ya terminada sin re-ejecutar nada.
|
|
104
|
+
|
|
105
|
+
### Paso 4: Revisar leads
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
suntropy satvolt leads list <campaignId> --limit 50 --format human
|
|
109
|
+
suntropy satvolt leads list <campaignId> --name "logística"
|
|
110
|
+
suntropy satvolt leads list <campaignId> --step QUALIFY --step-status failure # uid del paso si la acción se repite
|
|
111
|
+
suntropy satvolt leads list <campaignId> --step <uid> --step-status pending
|
|
112
|
+
suntropy satvolt leads get <campaignId> <leadId> --full-data consumptionEstimate,qualification_<uid>
|
|
113
|
+
suntropy satvolt leads full-data <campaignId> <leadId> --path cif.response.extras.cnae
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`--step-status`: `reached` (por defecto), `success`, `failure`, `skipped`, `processing` o `pending`.
|
|
117
|
+
|
|
118
|
+
### Paso 5: Tabla de exportación
|
|
119
|
+
|
|
120
|
+
Descubre qué campos hay y en qué ruta de `fullData` los deja cada paso. Funciona antes de lanzar la campaña: cada paso lista los campos que declara, y los de un agente se deducen de otra campaña con el mismo agente si esta aún no tiene respuestas.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
suntropy satvolt export-tables fields <campaignId> --format human # todos, agrupados por paso
|
|
124
|
+
suntropy satvolt export-tables fields <campaignId> --step <uid|ACCIÓN> --format human
|
|
125
|
+
suntropy satvolt export-tables fields <campaignId> --search cnae --format human # dónde está un dato
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Columnas que salen: `group` (paso), `path` (ruta para la columna), `label`, `type`, `source` (`catalog`, `observed`, `otherCampaign`, `dynamic`), `coverage` (leads de la muestra con el dato) y `example`.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
suntropy satvolt export-tables create <campaignId> --name "CRM" \
|
|
132
|
+
--columns "Empresa=lead.commercialName;Teléfono=lead.phone;Web=lead.url;Consumo anual kWh=fullData.consumptionEstimate.annualKwh;Maps=synthetic.googleMapsUrl"
|
|
133
|
+
|
|
134
|
+
# Ajustar columnas sueltas (por id o etiqueta; posiciones desde 0)
|
|
135
|
+
suntropy satvolt export-tables columns add <tableId> --label CIF --path fullData.cif.response.cif --after Empresa
|
|
136
|
+
suntropy satvolt export-tables columns set <tableId> "Consumo anual kWh" --label "Consumo (kWh/año)"
|
|
137
|
+
suntropy satvolt export-tables columns move <tableId> Maps --position 0
|
|
138
|
+
suntropy satvolt export-tables columns remove <tableId> Teléfono
|
|
139
|
+
suntropy satvolt export-tables columns list <tableId> --format human
|
|
140
|
+
|
|
141
|
+
suntropy satvolt export-tables data <tableId> --limit 20 --format human
|
|
142
|
+
suntropy satvolt export-tables export <tableId> --file-format xlsx --out campaña.xlsx
|
|
143
|
+
suntropy satvolt export-tables export <tableId> --file-format csv --out campaña.csv
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **Tipos de columna:** `string`, `number`, `boolean`, `date`, `url`. Sin tipo, se usa el que declara el paso para esa ruta.
|
|
147
|
+
- **`warnings` con `UNKNOWN_FULLDATA_KEY`:** ningún paso de la campaña escribe esa clave; revisa la ruta con `export-tables fields`.
|
|
148
|
+
|
|
149
|
+
### Paso 6: Cambiar el pipeline
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
suntropy satvolt steps list <campaignId> --format human
|
|
153
|
+
suntropy satvolt steps set <campaignId> <uid> --config '{"enableWebSearch": true}' # fusiona; null devuelve la clave a su valor por defecto
|
|
154
|
+
suntropy satvolt steps add <campaignId> --action AI_AGENT --config @agent.json
|
|
155
|
+
suntropy satvolt steps remove <campaignId> <uid>
|
|
156
|
+
|
|
157
|
+
# JSON completo
|
|
158
|
+
suntropy satvolt config get <campaignId> --save pipeline.json
|
|
159
|
+
suntropy satvolt config update <campaignId> --data @pipeline.json
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Paso 7: Reanudar con un paso nuevo
|
|
163
|
+
|
|
164
|
+
Para añadir una acción a una campaña terminada sin reprocesarla, usa `resume`. Añade el paso al final y lo lanza sobre los leads que llegaron al paso anterior:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
suntropy satvolt catalog ai-agents --format human # id, nombre y descripción de cada agente
|
|
168
|
+
suntropy satvolt campaigns funnel <campaignId> # leads que llegaron al último paso: los que pagarán el nuevo
|
|
169
|
+
suntropy satvolt campaigns resume <campaignId> --action AI_AGENT \
|
|
170
|
+
--config '{"customName":"Web corporativa","agentId":"<id>","outputKey":"web"}'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`--config` es solo el objeto de configuración del paso (el `configSchema` de `catalog actions`), no `{ action, config }`. Coste ≈ leads que llegaron al último paso × `creditCost` de la acción.
|
|
174
|
+
|
|
175
|
+
Si el `agentId` sale de una variable de shell, no lo metas entre comillas simples (no se expande); usa un fichero:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
cat > agent.json <<EOF
|
|
179
|
+
{ "customName": "Web corporativa", "agentId": "$AGENT_ID", "outputKey": "web" }
|
|
180
|
+
EOF
|
|
181
|
+
suntropy satvolt campaigns resume <campaignId> --action AI_AGENT --config @agent.json
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Para guardar el pipeline con el paso nuevo en una plantilla, haz `templates create --from-campaign` después del `resume`: la plantilla copia la configuración que tenga la campaña en ese momento.
|
|
185
|
+
|
|
186
|
+
Si se añadió antes un paso con `steps add` y nunca se ejecutó, `steps list` lo marca con `runnable: true`. Se lanza con:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
suntropy satvolt steps run <campaignId> <uid>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Para repetir un paso en leads concretos (p. ej. tras un fallo) usa `leads run-step <campaignId> <leadId> <uid|ACCIÓN>`: solo ese paso, o `--continue` para seguir el pipeline. La skill `satvolt-lead-troubleshooting` detalla cuándo usar cada uno.
|
|
193
|
+
|
|
194
|
+
### Ampliar una campaña con más leads
|
|
195
|
+
|
|
196
|
+
Para sacar más leads de una campaña de Maps ya terminada (típico tras una sonda) usa `extend`, no `reset`. `reset` borra los leads y vuelve a pagarlos todos. `extend` sube el límite, o lo quita con `--no-limit`, y vuelve a buscar solo en los sectores que se quedaron a medias. Los leads que ya existen no se reprocesan; solo los nuevos pasan por el pipeline.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
suntropy satvolt campaigns get <campaignId> # sectorSearch: incomplete + unknown > 0 → quedan leads por buscar
|
|
200
|
+
suntropy satvolt campaigns extend <campaignId> --max-leads 500
|
|
201
|
+
suntropy satvolt campaigns extend <campaignId> --no-limit # barre entero cada sector pendiente
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- El nuevo límite tiene que ser mayor que los leads actuales. La campaña no puede estar en ejecución (409 `CAMPAIGN_RUNNING`) ni sin arrancar (409 `CAMPAIGN_NOT_STARTED`).
|
|
205
|
+
- Coste ≈ leads nuevos × créditos por lead de la campaña (míralo con `campaigns usage`). Enséñaselo al usuario y pide confirmación antes de ampliar.
|
|
206
|
+
- En las campañas creadas antes de esta función, los sectores salen como `unknown` y cuentan como pendientes: se repiten sus primeras peticiones a Places, pero los duplicados no se crean.
|
|
207
|
+
|
|
208
|
+
### Consumo, reinicio y borrado
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
suntropy satvolt campaigns usage <campaignId> --format human # créditos totales, por lead y por paso
|
|
212
|
+
suntropy satvolt campaigns reset <campaignId> --yes [--start] # BORRA leads y resultados; pide confirmación explícita al usuario
|
|
213
|
+
suntropy satvolt campaigns delete <campaignId> --yes # borra la campaña entera; pide confirmación explícita al usuario
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Para sacar más leads de una campaña ya terminada, usa `extend` (sección anterior), nunca `reset`.
|
|
217
|
+
|
|
218
|
+
## Errores
|
|
219
|
+
|
|
220
|
+
Los errores salen por stderr como `{ error, status, message, details }`:
|
|
221
|
+
|
|
222
|
+
| status / código | Significado |
|
|
223
|
+
|---|---|
|
|
224
|
+
| 422 `VALIDATION_ERROR` | pasos o config inválidos. `details` lista `index`, `uid`, `action`, `field` y `message` de cada problema. |
|
|
225
|
+
| 400 `INVALID_AREA` | área mal formada o fuera de límites. |
|
|
226
|
+
| 409 `INVALID_CAMPAIGN_STATE` | `start` sobre una campaña que no está en cola: hay que hacer `reset` antes. |
|
|
227
|
+
| 409 `PENDING_STEP`, `STEP_NOT_RUNNABLE` | no se puede reanudar; `message` explica por qué. |
|
|
228
|
+
| 401 `TOKEN_EXPIRED` | renueva el token con `suntropy auth refresh`. |
|