@danhachuel/prodmaker 0.1.13 → 0.1.14
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/CHANGELOG.md +20 -0
- package/app/api/builder.py +49 -3
- package/app/main.py +2 -2
- package/app/services/builder.py +127 -11
- package/frontend/dist/assets/index-tkOFAfR-.js +77 -0
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +1 -1
- package/seed/prompts/builder-anuncios.md +10 -5
- package/seed/prompts/builder-assets.md +23 -0
- package/seed/prompts/builder-produto.md +19 -0
- package/frontend/dist/assets/index-dz1c2OPb.js +0 -75
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
Todas as versões seguem a etiqueta visual `MAJOR.MINOR.PATCH` com patch de dois dígitos
|
|
4
4
|
(o registry NPM normaliza `0.1.00` → `0.1.0`).
|
|
5
5
|
|
|
6
|
+
## 0.1.14 — 2026-10-31
|
|
7
|
+
|
|
8
|
+
- **Fase 10 — REVISÃO (fechamento das lacunas apontadas na criação de produto/PDF).**
|
|
9
|
+
- **`POST /api/builder/generate-product`:** gera o **conteúdo completo** do produto em
|
|
10
|
+
Markdown conforme o agente low-ticket — 1. Capa (≤6 palavras); 2. Diagnóstico (5 motivos);
|
|
11
|
+
3. Solução (5–7 passos); 4. Exemplos (3 casos); 5. Checklist; 6. Bônus.
|
|
12
|
+
- **`POST /api/builder/generate-assets`:** 3 Order Bumps (checkout +R$21,90 · upsell +R$197 ·
|
|
13
|
+
parceiro 50/50) + **Quiz com IA** (5 perguntas) — rota própria conforme imagem 4.
|
|
14
|
+
- **`POST /api/builder/generate-landing`:** landing page de vendas com as **12 seções
|
|
15
|
+
validadas**, salva em `paginas` com preview em `/pages/{id}`.
|
|
16
|
+
- **Wizard com 4 passos:** (1) mineração + parceiro; (2) **seleção de formato** (E-book/
|
|
17
|
+
Planilha/Mini-curso/Desafio/Workshop) + estrutura + assets + preço R$27–97; (3) conteúdo
|
|
18
|
+
completo + **PDF** + landing page; (4) **12 anúncios + 10 pins** + **Publicar no GitHub +
|
|
19
|
+
Cloudflare** (reaproveita o motor da Fase 4).
|
|
20
|
+
- `generate-ads` agora devolve também os 10 pins; `generate-pdf` usa o conteúdo completo
|
|
21
|
+
quando disponível (antes renderia só o esqueleto).
|
|
22
|
+
- **Salvamento completo:** `salvar` grava anúncios + pins e **cria o registro em `produtos`**
|
|
23
|
+
(status aprovado, vinculado via `infoprodutos.produto_id`) — AdsPages e AdsCreator passam
|
|
24
|
+
a consumir o produto do builder automaticamente.
|
|
25
|
+
|
|
6
26
|
## 0.1.13 — 2026-10-31
|
|
7
27
|
|
|
8
28
|
- **Fase 10 — Product Builder Wizard (motor de criação de ofertas).**
|
package/app/api/builder.py
CHANGED
|
@@ -17,6 +17,17 @@ class EstruturaBody(BaseModel):
|
|
|
17
17
|
oferta: dict
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class ProdutoBody(BaseModel):
|
|
21
|
+
oferta: dict
|
|
22
|
+
estrutura: dict = {}
|
|
23
|
+
formato: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AssetsBody(BaseModel):
|
|
27
|
+
oferta: dict
|
|
28
|
+
preco: float = 47
|
|
29
|
+
|
|
30
|
+
|
|
20
31
|
class AnunciosBody(BaseModel):
|
|
21
32
|
produto: str
|
|
22
33
|
promessa: str = ""
|
|
@@ -24,17 +35,25 @@ class AnunciosBody(BaseModel):
|
|
|
24
35
|
link: str = ""
|
|
25
36
|
|
|
26
37
|
|
|
38
|
+
class LandingBody(BaseModel):
|
|
39
|
+
oferta: dict
|
|
40
|
+
estrutura: dict = {}
|
|
41
|
+
produto_md: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
27
44
|
class PdfBody(BaseModel):
|
|
28
45
|
oferta: dict
|
|
29
|
-
estrutura: dict
|
|
46
|
+
estrutura: dict = {}
|
|
30
47
|
preco: float
|
|
31
48
|
parceiro: str = ""
|
|
32
49
|
modo: str = "rapido"
|
|
50
|
+
produto_md: str = ""
|
|
33
51
|
|
|
34
52
|
|
|
35
53
|
class SalvarBody(BaseModel):
|
|
36
54
|
record_id: int
|
|
37
|
-
anuncios: list
|
|
55
|
+
anuncios: list = []
|
|
56
|
+
pins: list = []
|
|
38
57
|
|
|
39
58
|
|
|
40
59
|
def _erro(exc: LLMError) -> HTTPException:
|
|
@@ -57,6 +76,33 @@ def generate_structure(body: EstruturaBody):
|
|
|
57
76
|
raise _erro(exc)
|
|
58
77
|
|
|
59
78
|
|
|
79
|
+
@router.post("/generate-product")
|
|
80
|
+
def generate_product(body: ProdutoBody):
|
|
81
|
+
"""Conteúdo completo do produto em Markdown (agente low-ticket)."""
|
|
82
|
+
try:
|
|
83
|
+
return builder.gerar_produto(body.oferta, body.estrutura, body.formato)
|
|
84
|
+
except LLMError as exc:
|
|
85
|
+
raise _erro(exc)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.post("/generate-assets")
|
|
89
|
+
def generate_assets(body: AssetsBody):
|
|
90
|
+
"""3 Order Bumps (checkout/upsell/parceiro) + Quiz com IA."""
|
|
91
|
+
try:
|
|
92
|
+
return builder.gerar_assets(body.oferta, body.preco)
|
|
93
|
+
except LLMError as exc:
|
|
94
|
+
raise _erro(exc)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.post("/generate-landing")
|
|
98
|
+
def generate_landing(body: LandingBody):
|
|
99
|
+
"""Landing page de vendas (12 seções) para o produto do wizard."""
|
|
100
|
+
try:
|
|
101
|
+
return builder.gerar_landing(body.oferta, body.estrutura, body.produto_md)
|
|
102
|
+
except LLMError as exc:
|
|
103
|
+
raise _erro(exc)
|
|
104
|
+
|
|
105
|
+
|
|
60
106
|
@router.post("/generate-ads")
|
|
61
107
|
def generate_ads(body: AnunciosBody):
|
|
62
108
|
try:
|
|
@@ -76,6 +122,6 @@ def generate_pdf(body: PdfBody):
|
|
|
76
122
|
@router.post("/salvar")
|
|
77
123
|
def salvar(body: SalvarBody):
|
|
78
124
|
try:
|
|
79
|
-
return builder.salvar(body.anuncios, body.record_id)
|
|
125
|
+
return builder.salvar(body.anuncios, body.record_id, body.pins)
|
|
80
126
|
except (LLMError, ValueError) as exc:
|
|
81
127
|
raise HTTPException(status_code=400, detail=str(exc))
|
package/app/main.py
CHANGED
|
@@ -15,7 +15,7 @@ from .services.database import ensure_db, get_db, now_str, row_to_dict
|
|
|
15
15
|
from .services.llm import LLMError, analyze_product, list_providers
|
|
16
16
|
from .services.store import PRODMAKER_HOME, STORAGE, STATE, ensure_state_dir, get_master_code, get_settings, save_settings
|
|
17
17
|
|
|
18
|
-
APP_VERSION = "0.1.
|
|
18
|
+
APP_VERSION = "0.1.14"
|
|
19
19
|
COOKIE_NAME = "prodmaker_session"
|
|
20
20
|
SESSION_DAYS = 30
|
|
21
21
|
MASTER_ALIAS = "admin"
|
|
@@ -264,7 +264,7 @@ def health():
|
|
|
264
264
|
|
|
265
265
|
@app.get("/api/version")
|
|
266
266
|
def version():
|
|
267
|
-
return {"version": APP_VERSION, "canonical": "0.1.
|
|
267
|
+
return {"version": APP_VERSION, "canonical": "0.1.14"}
|
|
268
268
|
|
|
269
269
|
|
|
270
270
|
@app.get("/api/settings")
|
package/app/services/builder.py
CHANGED
|
@@ -94,6 +94,56 @@ def gerar_estrutura(oferta: dict) -> dict:
|
|
|
94
94
|
return {"estrutura": estrutura, "provider": result["provider"]}
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def gerar_produto(oferta: dict, estrutura: dict, formato: str = "") -> dict:
|
|
98
|
+
"""Gera o CONTEÚDO COMPLETO do produto em Markdown (estrutura obrigatória do agente .md)."""
|
|
99
|
+
if not oferta or not (oferta.get("promessa") or "").strip():
|
|
100
|
+
raise LLMError("Selecione uma oferta antes de gerar o conteúdo do produto.")
|
|
101
|
+
formato = (formato or oferta.get("formato") or "E-book").strip()
|
|
102
|
+
prompt = _fill(
|
|
103
|
+
_template("builder-produto.md", "Gere o conteudo completo do produto {OFERTA} em Markdown."),
|
|
104
|
+
{
|
|
105
|
+
"OFERTA": oferta.get("nome") or "",
|
|
106
|
+
"FORMATO": formato,
|
|
107
|
+
"PROMESSA": oferta.get("promessa") or "",
|
|
108
|
+
"NICHO": oferta.get("publico") or (oferta.get("dores") or [""])[0],
|
|
109
|
+
"AVATAR": oferta.get("publico") or "",
|
|
110
|
+
"PRECO": oferta.get("preco_sugerido") or 47,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
result = generate(
|
|
114
|
+
prompt,
|
|
115
|
+
system="Você é um especialista em produtos digitais práticos. Escreva o produto completo em Markdown, sem comentários.",
|
|
116
|
+
max_tokens=12000,
|
|
117
|
+
)
|
|
118
|
+
md_text = (result["text"] or "").strip()
|
|
119
|
+
if len(md_text) < 400 or "##" not in md_text:
|
|
120
|
+
raise LLMError("O LLM não retornou o conteúdo completo do produto — tente novamente.")
|
|
121
|
+
return {"markdown": md_text, "provider": result["provider"]}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def gerar_assets(oferta: dict, preco: float = 47) -> dict:
|
|
125
|
+
"""Gera 3 Order Bumps (checkout/upsell/parceiro 50-50) + Quiz com IA."""
|
|
126
|
+
if not oferta or not (oferta.get("promessa") or "").strip():
|
|
127
|
+
raise LLMError("Selecione uma oferta antes de gerar os assets.")
|
|
128
|
+
prompt = _fill(
|
|
129
|
+
_template("builder-assets.md", "Gere order bumps e quiz para {OFERTA} em JSON."),
|
|
130
|
+
{
|
|
131
|
+
"OFERTA": oferta.get("nome") or "",
|
|
132
|
+
"PROMESSA": oferta.get("promessa") or "",
|
|
133
|
+
"PRECO": preco or oferta.get("preco_sugerido") or 47,
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
result = generate(prompt, system="Você é um estrategista de monetização. Responda SOMENTE JSON válido.", max_tokens=5000)
|
|
137
|
+
try:
|
|
138
|
+
parsed = _parse_json(result["text"])
|
|
139
|
+
except json.JSONDecodeError as exc:
|
|
140
|
+
raise LLMError(f"JSON do LLM inválido: {exc}") from exc
|
|
141
|
+
bumps = parsed.get("order_bumps") or []
|
|
142
|
+
if len(bumps) < 3:
|
|
143
|
+
raise LLMError(f"O LLM retornou {len(bumps)} order bumps (esperado 3) — tente novamente.")
|
|
144
|
+
return {"order_bumps": bumps[:3], "quiz": parsed.get("quiz") or {}, "provider": result["provider"]}
|
|
145
|
+
|
|
146
|
+
|
|
97
147
|
def gerar_anuncios(produto: str, promessa: str, nicho: str = "", link: str = "") -> dict:
|
|
98
148
|
if not produto.strip():
|
|
99
149
|
raise LLMError("Informe o nome do produto para gerar os anúncios.")
|
|
@@ -101,15 +151,16 @@ def gerar_anuncios(produto: str, promessa: str, nicho: str = "", link: str = "")
|
|
|
101
151
|
_template("builder-anuncios.md", "Gere 12 anuncios para {PRODUTO} em JSON com chave 'anuncios'."),
|
|
102
152
|
{"PRODUTO": produto, "PROMESSA": promessa, "NICHO": nicho, "LINK": link},
|
|
103
153
|
)
|
|
104
|
-
result = generate(prompt, system="Você é um copywriter de direct response. Responda SOMENTE JSON válido.", max_tokens=
|
|
154
|
+
result = generate(prompt, system="Você é um copywriter de direct response e especialista em Pinterest. Responda SOMENTE JSON válido.", max_tokens=6000)
|
|
105
155
|
try:
|
|
106
156
|
parsed = _parse_json(result["text"])
|
|
107
157
|
except json.JSONDecodeError as exc:
|
|
108
158
|
raise LLMError(f"JSON do LLM inválido: {exc}") from exc
|
|
109
159
|
anuncios = [item for item in (parsed.get("anuncios") or []) if isinstance(item, dict)]
|
|
160
|
+
pins = [item for item in (parsed.get("pins") or []) if isinstance(item, dict)]
|
|
110
161
|
if len(anuncios) < 12:
|
|
111
162
|
raise LLMError(f"O LLM retornou {len(anuncios)} anúncios (esperado 12) — tente novamente.")
|
|
112
|
-
return {"anuncios": anuncios[:12], "provider": result["provider"]}
|
|
163
|
+
return {"anuncios": anuncios[:12], "pins": pins[:10], "provider": result["provider"]}
|
|
113
164
|
|
|
114
165
|
|
|
115
166
|
def _imagem_prompts(oferta: dict, estrutura: dict) -> list[str]:
|
|
@@ -154,14 +205,48 @@ def _ensure_columns() -> None:
|
|
|
154
205
|
add_column_if_missing(conn, "infoprodutos", "oferta_json TEXT NOT NULL DEFAULT ''", "oferta_json")
|
|
155
206
|
add_column_if_missing(conn, "infoprodutos", "estrutura_json TEXT NOT NULL DEFAULT ''", "estrutura_json")
|
|
156
207
|
add_column_if_missing(conn, "infoprodutos", "anuncios_json TEXT NOT NULL DEFAULT ''", "anuncios_json")
|
|
208
|
+
add_column_if_missing(conn, "infoprodutos", "pins_json TEXT NOT NULL DEFAULT ''", "pins_json")
|
|
157
209
|
add_column_if_missing(conn, "infoprodutos", "preco REAL NOT NULL DEFAULT 0", "preco")
|
|
158
210
|
add_column_if_missing(conn, "infoprodutos", "parceiro TEXT NOT NULL DEFAULT ''", "parceiro")
|
|
211
|
+
add_column_if_missing(conn, "infoprodutos", "produto_id INTEGER", "produto_id")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def gerar_landing(oferta: dict, estrutura: dict, produto_md: str = "") -> dict:
|
|
215
|
+
"""Gera a landing page de vendas (12 seções) via motor de páginas e salva em paginas."""
|
|
216
|
+
from . import pages
|
|
217
|
+
|
|
218
|
+
form = {
|
|
219
|
+
"titulo": oferta.get("nome") or "Produto",
|
|
220
|
+
"avatar": oferta.get("publico") or "",
|
|
221
|
+
"resultado": oferta.get("promessa") or "",
|
|
222
|
+
"dor": (oferta.get("dores") or [""])[0],
|
|
223
|
+
"afiliado_link": oferta.get("checkout_link") or "",
|
|
224
|
+
"checkout_link": oferta.get("checkout_link") or "",
|
|
225
|
+
"prompt": (produto_md or "")[:3000],
|
|
226
|
+
}
|
|
227
|
+
produto_ctx = {"id": None, "nome": oferta.get("nome") or "Produto", "link_afiliado": oferta.get("checkout_link") or ""}
|
|
228
|
+
resultado = pages.gerar_pagina("vendas", produto_ctx, form)
|
|
229
|
+
timestamp = now_str()
|
|
230
|
+
with get_db() as conn:
|
|
231
|
+
cursor = conn.execute(
|
|
232
|
+
"INSERT INTO paginas (titulo, tipo, produto_id, idioma, html_path, status, created_at, updated_at) VALUES (?, 'vendas', NULL, ?, '', 'rascunho', ?, ?)",
|
|
233
|
+
((oferta.get("nome") or "Produto")[:120], oferta.get("idioma") or "Portugues", timestamp, timestamp),
|
|
234
|
+
)
|
|
235
|
+
page_id = cursor.lastrowid
|
|
236
|
+
pages.salvar_html(page_id, resultado["html"])
|
|
237
|
+
with get_db() as conn:
|
|
238
|
+
conn.execute("UPDATE paginas SET html_path = ? WHERE id = ?", (f"pages/{page_id}/index.html", page_id))
|
|
239
|
+
row = conn.execute("SELECT * FROM paginas WHERE id = ?", (page_id,)).fetchone()
|
|
240
|
+
record = row_to_dict(row)
|
|
241
|
+
record["preview_url"] = f"/pages/{page_id}"
|
|
242
|
+
record["provider"] = resultado["provider"]
|
|
243
|
+
return record
|
|
159
244
|
|
|
160
245
|
|
|
161
|
-
def gerar_pdf(oferta: dict, estrutura: dict, preco: float, parceiro: str = "", modo: str = "rapido") -> dict:
|
|
162
|
-
"""Gera o PDF do produto + prompts de imagem
|
|
246
|
+
def gerar_pdf(oferta: dict, estrutura: dict, preco: float, parceiro: str = "", modo: str = "rapido", produto_md: str = "") -> dict:
|
|
247
|
+
"""Gera o PDF do produto (conteúdo completo quando disponível) + prompts de imagem."""
|
|
163
248
|
_ensure_columns()
|
|
164
|
-
md_text = _markdown_produto(oferta, estrutura, preco, parceiro)
|
|
249
|
+
md_text = (produto_md or "").strip() or _markdown_produto(oferta, estrutura, preco, parceiro)
|
|
165
250
|
prompt_imagens = _imagem_prompts(oferta, estrutura)
|
|
166
251
|
timestamp = now_str()
|
|
167
252
|
with get_db() as conn:
|
|
@@ -204,14 +289,45 @@ def gerar_pdf(oferta: dict, estrutura: dict, preco: float, parceiro: str = "", m
|
|
|
204
289
|
return record
|
|
205
290
|
|
|
206
291
|
|
|
207
|
-
def
|
|
292
|
+
def _criar_produto_para_consumo(record_id: int) -> dict:
|
|
293
|
+
"""Cria (ou reusa) o registro em `produtos` para AdsPages/AdsCreator consumirem."""
|
|
294
|
+
row = _get_infoproduto(record_id)
|
|
295
|
+
oferta = json.loads(row.get("oferta_json") or "{}")
|
|
296
|
+
nome = (oferta.get("nome") or row["titulo"] or "Produto")[:160]
|
|
297
|
+
with get_db() as conn:
|
|
298
|
+
existente = None
|
|
299
|
+
if row.get("produto_id"):
|
|
300
|
+
existente = conn.execute("SELECT id FROM produtos WHERE id = ?", (row["produto_id"],)).fetchone()
|
|
301
|
+
if not existente:
|
|
302
|
+
mesmo_nome = conn.execute("SELECT id FROM produtos WHERE nome = ? ORDER BY id DESC LIMIT 1", (nome,)).fetchone()
|
|
303
|
+
if mesmo_nome:
|
|
304
|
+
produto_id = mesmo_nome["id"]
|
|
305
|
+
else:
|
|
306
|
+
cursor = conn.execute(
|
|
307
|
+
"INSERT INTO produtos (nome, plataforma, link_afiliado, comissao, preco, nicho, status, created_at, updated_at) VALUES (?, 'Outro', '', 0, ?, ?, 'aprovado', ?, ?)",
|
|
308
|
+
(nome, float(row.get("preco") or oferta.get("preco_sugerido") or 47), (oferta.get("publico") or "")[:120], now_str(), now_str()),
|
|
309
|
+
)
|
|
310
|
+
produto_id = cursor.lastrowid
|
|
311
|
+
conn.execute("UPDATE infoprodutos SET produto_id = ? WHERE id = ?", (produto_id, record_id))
|
|
312
|
+
return {"produto_id": produto_id, "nome": nome}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _get_infoproduto(record_id: int) -> dict:
|
|
316
|
+
with get_db() as conn:
|
|
317
|
+
row = conn.execute("SELECT * FROM infoprodutos WHERE id = ?", (record_id,)).fetchone()
|
|
318
|
+
if not row:
|
|
319
|
+
raise LLMError("Infoproduto não encontrado.")
|
|
320
|
+
return row_to_dict(row)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def salvar(anuncios: list, record_id: int, pins: list | None = None) -> dict:
|
|
208
324
|
_ensure_columns()
|
|
209
325
|
with get_db() as conn:
|
|
210
326
|
conn.execute(
|
|
211
|
-
"UPDATE infoprodutos SET anuncios_json = ? WHERE id = ?",
|
|
212
|
-
(json.dumps(anuncios[:12], ensure_ascii=False), record_id),
|
|
327
|
+
"UPDATE infoprodutos SET anuncios_json = ?, pins_json = ? WHERE id = ?",
|
|
328
|
+
(json.dumps((anuncios or [])[:12], ensure_ascii=False), json.dumps((pins or [])[:10], ensure_ascii=False), record_id),
|
|
213
329
|
)
|
|
214
|
-
|
|
215
|
-
record =
|
|
330
|
+
produto = _criar_produto_para_consumo(record_id)
|
|
331
|
+
record = _get_infoproduto(record_id)
|
|
216
332
|
oferta = json.loads(record.get("oferta_json") or "{}")
|
|
217
|
-
return {**record, "oferta": oferta}
|
|
333
|
+
return {**record, "oferta": oferta, "produto": produto}
|