Spaces:
Running
Running
VinOS Agent Claude Opus 4.6 commited on
Commit Β·
6144942
1
Parent(s): 42a1221
Phase 0+6+7: Quick Wins, SEO-to-Revenue Pipeline, Self-Optimization
Browse filesPhase 0: Fix _handleWrite/_handleResearch crash bugs, wire /costs,
/scout, /landing commands, create link-in-bio landing page.
Phase 6: CTA auto-injection in WordPress articles with UTM tracking,
/seo2offer one-command pipeline (scoutβofferβarticleβpublishβindex),
post-publish offer suggestion.
Phase 7: A/B variant model per offer, /offer ab and /offer variants
commands, smart evaluation (auto-variant at 48h, pause at 72h, scale
winners), click tracking at /api/track, failure pattern learning from
retired offers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- .claude/claude.md +10 -0
- CHANGELOG.md +41 -0
- public/landing.html +85 -0
- server.js +272 -0
- skills/sales_engine.js +169 -26
- skills/set_telegram_menu.js +5 -1
- skills/wordpress_publisher.js +19 -1
.claude/claude.md
CHANGED
|
@@ -8,3 +8,13 @@ Your job is to maintain, expand, and execute the VinOS local stack using Node.js
|
|
| 8 |
2. Use `skills/api_caller.js` for external integrations.
|
| 9 |
3. Update `public/index.html` and `public/app.js` when adding new dashboard features.
|
| 10 |
4. If a route is missing, add it to `server.js`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
2. Use `skills/api_caller.js` for external integrations.
|
| 9 |
3. Update `public/index.html` and `public/app.js` when adding new dashboard features.
|
| 10 |
4. If a route is missing, add it to `server.js`.
|
| 11 |
+
|
| 12 |
+
## Deployment Protocol:
|
| 13 |
+
After every commit that completes a phase or significant feature:
|
| 14 |
+
1. **Push to HF Spaces** β `git push hf master:main` (deploys to live `AIgoose/vinos-engine`)
|
| 15 |
+
2. **Send Telegram deploy notification** β Run `node -e "require('./skills/hf_deployer').notifyDeploy()"` after push. This generates AI release notes from the commits and sends a notification to Telegram with what's new.
|
| 16 |
+
3. **Update CHANGELOG.md** β Add a dated entry at the top with: phase name, commit hash, what changed (files created/modified, new commands, new endpoints, new dashboards)
|
| 17 |
+
4. **Verify deployment** β Boot test (`node -e "require('./server.js')"`) or check HF Space logs
|
| 18 |
+
5. Never leave unpushed commits β if code is committed, it should be deployed unless explicitly told otherwise
|
| 19 |
+
|
| 20 |
+
Alternative: Use `hfDeployer.syncCode()` which handles push + notification together (used by `/sync` Telegram command).
|
CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
| 2 |
|
| 3 |
---
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
## Phase 5 β Autonomous Traffic-to-Sales Engine
|
| 6 |
**Date:** 2026-03-27 | **Commit:** `f30cee0`
|
| 7 |
|
|
|
|
| 2 |
|
| 3 |
---
|
| 4 |
|
| 5 |
+
## Phase 0 + 6 + 7 β Quick Wins, SEO-to-Revenue, Self-Optimization
|
| 6 |
+
**Date:** 2026-03-27
|
| 7 |
+
|
| 8 |
+
### Phase 0: Quick Wins + Bug Fix
|
| 9 |
+
- **Fixed `_handleWrite` crash** β Defined missing `_handleWrite()` and `_handleResearch()` functions. The `/write` flow no longer crashes when user replies with context.
|
| 10 |
+
- **`/costs` command** β Shows API spend breakdown by model from `cost_tracker.getCosts()`
|
| 11 |
+
- **`/scout [keyword]` command** β Market viability scout (competition, volume, angles)
|
| 12 |
+
- **`/landing` page** β Public link-in-bio page at `/landing` auto-renders active offers with Mayar CTA buttons
|
| 13 |
+
- **`/landing` command** β Returns the public URL for sharing
|
| 14 |
+
- Telegram menu updated with `/costs`, `/scout`, `/seo2offer`, `/landing`
|
| 15 |
+
|
| 16 |
+
### Phase 6: SEO-to-Revenue Pipeline
|
| 17 |
+
- **CTA injection in WordPress publisher** β Articles auto-inject a styled CTA block when a matching offer exists, with UTM tracking (`utm_source=seo&utm_medium=article&utm_campaign=...`)
|
| 18 |
+
- **UTM support in `injectOfferLink()`** β Returns `utmUrl` alongside `url` for attribution tracking across SEO articles vs social posts
|
| 19 |
+
- **`/seo2offer [keyword]` command** β One-command full pipeline: scout β create offer β write SEO article (with auto-CTA) β publish to WordPress β Google index
|
| 20 |
+
- **Post-publish offer suggestion** β After successful WP publish, if no matching offer exists, Telegram suggests `/offer [topic]`
|
| 21 |
+
|
| 22 |
+
### Phase 7: Self-Optimization & A/B Testing
|
| 23 |
+
- **Offer variants model** β Each offer now supports a `variants[]` array with per-variant stats (impressions, clicks, sales, isActive)
|
| 24 |
+
- **`createVariant(offerId)`** β AI generates alternative copy/angle, informed by failure patterns
|
| 25 |
+
- **Smart evaluation** β Replaces binary pause/scale: 48h no sales + <2 variants β auto-create variant; 72h still 0 β pause; 3+ sales/24h β scale winner + pause loser variants
|
| 26 |
+
- **`/offer ab [id]` command** β Create A/B test variant for any offer
|
| 27 |
+
- **`/offer variants [id]` command** β View all variants with conversion rates
|
| 28 |
+
- **Click tracking** β `GET /api/track?offer=X&variant=Y` increments counters and redirects to Mayar
|
| 29 |
+
- **Failure pattern learning** β `analyzeFailurePatterns()` extracts patterns from retired offers via LLM, stores in `failure_patterns`, future offer creation avoids repeating mistakes
|
| 30 |
+
|
| 31 |
+
#### NEW FILES
|
| 32 |
+
| File | Purpose |
|
| 33 |
+
|------|---------|
|
| 34 |
+
| `public/landing.html` | Link-in-bio page β dark theme, auto-renders active offers from `/api/landing` |
|
| 35 |
+
|
| 36 |
+
#### MODIFIED FILES
|
| 37 |
+
| File | Changes |
|
| 38 |
+
|------|---------|
|
| 39 |
+
| `server.js` | `_handleWrite()`, `_handleResearch()` functions, `/costs`, `/scout`, `/landing`, `/seo2offer` commands, `/api/track` click tracking, `/api/landing` endpoint, `/offer ab`, `/offer variants` |
|
| 40 |
+
| `skills/sales_engine.js` | `createVariant()`, `getVariants()`, `trackClick()`, `analyzeFailurePatterns()`, smart `evaluateOffers()`, UTM-aware `injectOfferLink()` |
|
| 41 |
+
| `skills/wordpress_publisher.js` | CTA block injection with offer matching before `finalContent` assembly |
|
| 42 |
+
| `skills/set_telegram_menu.js` | Added `/costs`, `/scout`, `/seo2offer`, `/landing` |
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
## Phase 5 β Autonomous Traffic-to-Sales Engine
|
| 47 |
**Date:** 2026-03-27 | **Commit:** `f30cee0`
|
| 48 |
|
public/landing.html
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>VinOS β Digital Products</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 11 |
+
:root {
|
| 12 |
+
--bg: #090c12; --surface: #111622; --surface2: #161e2e; --border: #1e2d45;
|
| 13 |
+
--accent: #00e5ff; --accent2: #7c3aed; --green: #22d3a0; --orange: #f59e0b;
|
| 14 |
+
--text: #e2e8f0; --muted: #64748b;
|
| 15 |
+
}
|
| 16 |
+
body { font-family: 'Space Grotesk', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; padding: 20px; }
|
| 17 |
+
.container { max-width: 600px; margin: 0 auto; }
|
| 18 |
+
.brand { text-align: center; padding: 40px 0 30px; }
|
| 19 |
+
.brand h1 { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg, var(--accent), var(--green)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
| 20 |
+
.brand p { color: var(--muted); margin-top: 8px; font-size: 0.9rem; }
|
| 21 |
+
.offers-grid { display: flex; flex-direction: column; gap: 16px; margin-bottom: 40px; }
|
| 22 |
+
.offer-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 24px; transition: transform 0.2s, box-shadow 0.2s; }
|
| 23 |
+
.offer-card:hover { transform: translateY(-2px); box-shadow: 0 0 30px rgba(0, 229, 255, 0.08); }
|
| 24 |
+
.offer-card .pillar { color: var(--accent); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 600; margin-bottom: 8px; }
|
| 25 |
+
.offer-card h2 { font-size: 1.2rem; font-weight: 600; margin-bottom: 8px; }
|
| 26 |
+
.offer-card .desc { color: var(--muted); font-size: 0.85rem; margin-bottom: 16px; line-height: 1.5; }
|
| 27 |
+
.offer-card .price { font-size: 1.4rem; font-weight: 700; color: var(--green); margin-bottom: 16px; }
|
| 28 |
+
.offer-card .cta { display: inline-block; padding: 12px 28px; background: linear-gradient(135deg, var(--accent), var(--green)); color: #000; font-weight: 700; font-size: 0.9rem; border-radius: 10px; text-decoration: none; transition: opacity 0.2s; }
|
| 29 |
+
.offer-card .cta:hover { opacity: 0.9; }
|
| 30 |
+
.empty { text-align: center; color: var(--muted); padding: 60px 20px; font-style: italic; }
|
| 31 |
+
.footer { text-align: center; padding: 30px 0; border-top: 1px solid var(--border); }
|
| 32 |
+
.footer p { color: var(--muted); font-size: 0.75rem; }
|
| 33 |
+
.footer .links { margin-top: 12px; display: flex; justify-content: center; gap: 16px; }
|
| 34 |
+
.footer .links a { color: var(--accent); text-decoration: none; font-size: 0.8rem; }
|
| 35 |
+
.footer .links a:hover { text-decoration: underline; }
|
| 36 |
+
</style>
|
| 37 |
+
</head>
|
| 38 |
+
<body>
|
| 39 |
+
<div class="container">
|
| 40 |
+
<div class="brand">
|
| 41 |
+
<h1>VinOS</h1>
|
| 42 |
+
<p>AI-Powered Digital Products</p>
|
| 43 |
+
</div>
|
| 44 |
+
<div class="offers-grid" id="offersGrid">
|
| 45 |
+
<div class="empty">Loading offers...</div>
|
| 46 |
+
</div>
|
| 47 |
+
<div class="footer">
|
| 48 |
+
<p>Powered by VinOS Engine</p>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
<script>
|
| 52 |
+
const esc = s => { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
|
| 53 |
+
const fmt = n => `Rp ${(n || 0).toLocaleString('id-ID')}`;
|
| 54 |
+
|
| 55 |
+
async function loadOffers() {
|
| 56 |
+
try {
|
| 57 |
+
const res = await fetch('/api/landing');
|
| 58 |
+
const data = await res.json();
|
| 59 |
+
const grid = document.getElementById('offersGrid');
|
| 60 |
+
|
| 61 |
+
if (!data.success || !data.offers || data.offers.length === 0) {
|
| 62 |
+
grid.innerHTML = '<div class="empty">No offers available right now. Check back soon!</div>';
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
let html = '';
|
| 67 |
+
for (const o of data.offers) {
|
| 68 |
+
html += `<div class="offer-card">
|
| 69 |
+
${o.pillar ? `<div class="pillar">${esc(o.pillar)}</div>` : ''}
|
| 70 |
+
<h2>${esc(o.title)}</h2>
|
| 71 |
+
${o.description ? `<div class="desc">${esc(o.description)}</div>` : ''}
|
| 72 |
+
<div class="price">${fmt(o.priceIDR)}</div>
|
| 73 |
+
${o.paymentUrl ? `<a class="cta" href="${esc(o.paymentUrl)}" target="_blank">Get It Now →</a>` : '<span class="desc">Coming soon</span>'}
|
| 74 |
+
</div>`;
|
| 75 |
+
}
|
| 76 |
+
grid.innerHTML = html;
|
| 77 |
+
} catch (e) {
|
| 78 |
+
document.getElementById('offersGrid').innerHTML = '<div class="empty">Unable to load offers.</div>';
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
loadOffers();
|
| 83 |
+
</script>
|
| 84 |
+
</body>
|
| 85 |
+
</html>
|
server.js
CHANGED
|
@@ -57,6 +57,8 @@ const analyticsEngine = require('./analytics-engine');
|
|
| 57 |
const viralFlow = require('./skills/viral_flow');
|
| 58 |
const salesEngine = require('./skills/sales_engine');
|
| 59 |
const mayarClient = require('./skills/mayar_client');
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
|
|
@@ -80,6 +82,21 @@ app.get('/revenue', (req, res) => {
|
|
| 80 |
res.sendFile(path.join(__dirname, 'public', 'revenue-dashboard.html'));
|
| 81 |
});
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
// ============================================
|
| 84 |
// SALES ENGINE API
|
| 85 |
// ============================================
|
|
@@ -136,6 +153,19 @@ app.post('/api/sales/webhook/mayar', async (req, res) => {
|
|
| 136 |
}
|
| 137 |
});
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
// ============================================
|
| 140 |
// VIRAL CONTENT ENGINE API
|
| 141 |
// ============================================
|
|
@@ -1048,6 +1078,131 @@ async function processOrchestratorIntent(chatId, intent, userText, params) {
|
|
| 1048 |
}
|
| 1049 |
}
|
| 1050 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1051 |
// Telegram Webhook
|
| 1052 |
app.post('/api/telegram-webhook', async (req, res) => {
|
| 1053 |
res.status(200).send({ status: 'received' });
|
|
@@ -1727,14 +1882,22 @@ This engine researches any URL or topic and generates content with your options.
|
|
| 1727 |
`π <b>SEO ENGINE</b>\n` +
|
| 1728 |
`/seo [keyword] β Architect SEO Pillar Strategy\n` +
|
| 1729 |
`/write [siteId] [size] [topic] β Publish SEO Article\n` +
|
|
|
|
| 1730 |
`/index [url] β Submit to Google API\n\n` +
|
| 1731 |
`π° <b>SALES ENGINE</b>\n` +
|
| 1732 |
`/offer β List active offers with payment links\n` +
|
| 1733 |
`/offer [topic] β Create offer (AI copy β Mayar link)\n` +
|
| 1734 |
`/offer pause [id] β Pause underperforming offer\n` +
|
|
|
|
|
|
|
| 1735 |
`/offer board β Pipeline stage counts\n` +
|
|
|
|
| 1736 |
`/revenue β Revenue summary\n` +
|
| 1737 |
`/revenue today β Today's sales only\n\n` +
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1738 |
`π <b>CRM MANAGER</b>\n` +
|
| 1739 |
`/updates β View Trello project statuses\n` +
|
| 1740 |
`/move [4-char-ID] [new list] β Update card status`;
|
|
@@ -1818,6 +1981,65 @@ This engine researches any URL or topic and generates content with your options.
|
|
| 1818 |
await apiCaller.sendTelegramMessage(chatId, `β Image generation failed: ${gashResult.error || 'All providers exhausted'}`);
|
| 1819 |
}
|
| 1820 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1821 |
} else if (userText.toLowerCase().startsWith('/seo')) {
|
| 1822 |
const keyword = userText.split(' ').slice(1).join(' ');
|
| 1823 |
if (!keyword) {
|
|
@@ -2084,6 +2306,27 @@ This engine researches any URL or topic and generates content with your options.
|
|
| 2084 |
const emoji = { active: 'π’', pending_link: 'π‘', pending_validation: 'π΅', paused: 'βΈοΈ', retired: 'π΄' };
|
| 2085 |
const lines = stages.filter(s => counts[s]).map(s => `${emoji[s] || 'β’'} ${s}: ${counts[s]}`).join('\n');
|
| 2086 |
await apiCaller.sendTelegramMessage(chatId, `π <b>Offer Pipeline</b>\n\n${lines || 'No offers yet.'}\n\nTotal: ${db.offers.length}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2087 |
} else if (args.toLowerCase().startsWith('eval')) {
|
| 2088 |
// /offer eval β run evaluation
|
| 2089 |
await apiCaller.sendTelegramMessage(chatId, 'π <i>Evaluating offers...</i>');
|
|
@@ -2136,6 +2379,35 @@ This engine researches any URL or topic and generates content with your options.
|
|
| 2136 |
);
|
| 2137 |
}
|
| 2138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2139 |
} else {
|
| 2140 |
await handleVinIntent(chatId, from, userText);
|
| 2141 |
}
|
|
|
|
| 57 |
const viralFlow = require('./skills/viral_flow');
|
| 58 |
const salesEngine = require('./skills/sales_engine');
|
| 59 |
const mayarClient = require('./skills/mayar_client');
|
| 60 |
+
const costTracker = require('./skills/cost_tracker');
|
| 61 |
+
const scoutAgent = require('./skills/scout_agent');
|
| 62 |
|
| 63 |
|
| 64 |
|
|
|
|
| 82 |
res.sendFile(path.join(__dirname, 'public', 'revenue-dashboard.html'));
|
| 83 |
});
|
| 84 |
|
| 85 |
+
app.get('/landing', (req, res) => {
|
| 86 |
+
res.sendFile(path.join(__dirname, 'public', 'landing.html'));
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
app.get('/api/landing', (req, res) => {
|
| 90 |
+
try {
|
| 91 |
+
const dash = salesEngine.getDashboardData();
|
| 92 |
+
const activeOffers = dash.activeOffers.map(o => ({
|
| 93 |
+
title: o.title, description: o.description || '',
|
| 94 |
+
priceIDR: o.priceIDR, paymentUrl: o.paymentUrl, pillar: o.pillar || ''
|
| 95 |
+
}));
|
| 96 |
+
res.json({ success: true, offers: activeOffers });
|
| 97 |
+
} catch (e) { res.json({ success: true, offers: [] }); }
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
// ============================================
|
| 101 |
// SALES ENGINE API
|
| 102 |
// ============================================
|
|
|
|
| 153 |
}
|
| 154 |
});
|
| 155 |
|
| 156 |
+
// Click tracking redirect (for A/B variant attribution)
|
| 157 |
+
app.get('/api/track', (req, res) => {
|
| 158 |
+
const { offer, variant, action } = req.query;
|
| 159 |
+
if (!offer) return res.status(400).json({ error: 'Missing offer param' });
|
| 160 |
+
const redirectUrl = salesEngine.trackClick(offer, variant || 'v0');
|
| 161 |
+
if (redirectUrl) {
|
| 162 |
+
const utm = `${redirectUrl.includes('?') ? '&' : '?'}utm_source=track&utm_medium=cta&utm_campaign=${encodeURIComponent(offer)}`;
|
| 163 |
+
res.redirect(302, redirectUrl + utm);
|
| 164 |
+
} else {
|
| 165 |
+
res.status(404).json({ error: 'Offer not found' });
|
| 166 |
+
}
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
// ============================================
|
| 170 |
// VIRAL CONTENT ENGINE API
|
| 171 |
// ============================================
|
|
|
|
| 1078 |
}
|
| 1079 |
}
|
| 1080 |
|
| 1081 |
+
// --- Helper: handle /write context reply ---
|
| 1082 |
+
async function _handleWrite(chatId, pending, userContext) {
|
| 1083 |
+
const { targetSiteId, topic, size, pov, intent, tone } = pending;
|
| 1084 |
+
|
| 1085 |
+
const initRes = await apiCaller.sendTelegramMessage(chatId, `π <b>SEO Engine Pipeline</b>\n[β β‘β‘β‘β‘β‘] 16% AI Architecting: "${topic}"...`);
|
| 1086 |
+
const msgId = initRes.messageId;
|
| 1087 |
+
|
| 1088 |
+
try {
|
| 1089 |
+
const articleRes = await seoWriter.generateArticle(size, pov, intent, tone, topic, userContext);
|
| 1090 |
+
if (!articleRes.success) {
|
| 1091 |
+
if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `β οΈ <b>Pipeline Failed</b>\nβ Writer Error: ${articleRes.error}`);
|
| 1092 |
+
return;
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β‘β‘β‘β‘] 33% QC: Scoring EEAT Quality...`);
|
| 1096 |
+
const scoreRes = await seoWriter.scoreEEAT(articleRes.html);
|
| 1097 |
+
|
| 1098 |
+
if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β‘β‘β‘] 50% Image Engine: Generating ${articleRes.imagePlaceholders?.length || 0} AI Images...`);
|
| 1099 |
+
|
| 1100 |
+
if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β‘β‘] 66% Publishing: Uploading to [${targetSiteId}]...`);
|
| 1101 |
+
const pubRes = await wordpressPublisher.publishPost(articleRes, targetSiteId);
|
| 1102 |
+
|
| 1103 |
+
if (!pubRes.success) {
|
| 1104 |
+
if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `β οΈ <b>Pipeline Failed</b>\nβ WordPress Error: ${pubRes.error}`);
|
| 1105 |
+
return;
|
| 1106 |
+
}
|
| 1107 |
+
|
| 1108 |
+
if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β β‘] 83% SEO Indexing: Google Search Console...`);
|
| 1109 |
+
const idxRes = await googleIndexer.submitUrl(pubRes.url);
|
| 1110 |
+
|
| 1111 |
+
if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β β ] 100% CRM Sync: Trello Workflow...`);
|
| 1112 |
+
const trelloRes = await trelloManager.logSeoPost(topic, targetSiteId || 'Default', pubRes.url, scoreRes.score || 'N/A');
|
| 1113 |
+
|
| 1114 |
+
let finalMsg = `π <b>Pipeline Complete!</b>\n\n`;
|
| 1115 |
+
finalMsg += `<b>π Article:</b> ${pubRes.url}\n`;
|
| 1116 |
+
if (scoreRes.success) finalMsg += `<b>π§ EEAT Score:</b> ${scoreRes.score}/100\n`;
|
| 1117 |
+
if (idxRes.success) finalMsg += `<b>π Google:</b> Indexed π’\n`;
|
| 1118 |
+
else finalMsg += `<b>π Google:</b> Pending π‘ (${idxRes.error})\n`;
|
| 1119 |
+
if (trelloRes.success) finalMsg += `<b>π Trello:</b> <a href="${trelloRes.url}">View Card</a> π\n`;
|
| 1120 |
+
else finalMsg += `<b>π Trello:</b> Failed π΄\n`;
|
| 1121 |
+
|
| 1122 |
+
// Check for matching sales offer β suggest creating one if none
|
| 1123 |
+
try {
|
| 1124 |
+
const offerLink = salesEngine.injectOfferLink(topic);
|
| 1125 |
+
if (offerLink) {
|
| 1126 |
+
finalMsg += `\nπ° <b>Offer CTA injected:</b> ${offerLink}\n`;
|
| 1127 |
+
} else {
|
| 1128 |
+
finalMsg += `\nπ‘ No matching offer for "${topic}". Create one? <code>/offer ${topic}</code>\n`;
|
| 1129 |
+
}
|
| 1130 |
+
} catch (e) { /* non-critical */ }
|
| 1131 |
+
|
| 1132 |
+
await apiCaller.sendTelegramMessage(chatId, finalMsg);
|
| 1133 |
+
|
| 1134 |
+
// Auto-push SEO publish metadata to HF
|
| 1135 |
+
try {
|
| 1136 |
+
await hfStorage.saveRecord('seo', `seo_${Date.now()}`, {
|
| 1137 |
+
title: `SEO: ${topic}`,
|
| 1138 |
+
timestamp: new Date().toISOString(),
|
| 1139 |
+
source_url: pubRes.url,
|
| 1140 |
+
niche: 'SEO'
|
| 1141 |
+
}, `# Published: ${topic}\n\n- URL: ${pubRes.url}\n- Site: ${targetSiteId || 'Default'}\n- EEAT Score: ${scoreRes.score || 'N/A'}\n- Google Indexed: ${idxRes.success}\n- Trello: ${trelloRes.success ? trelloRes.url : 'N/A'}`);
|
| 1142 |
+
} catch (e) {
|
| 1143 |
+
console.error('[HF Auto] SEO log failed:', e.message);
|
| 1144 |
+
}
|
| 1145 |
+
} catch (err) {
|
| 1146 |
+
console.error('[_handleWrite] Error:', err.message);
|
| 1147 |
+
await apiCaller.sendTelegramMessage(chatId, `β οΈ <b>Write Pipeline Error:</b> ${err.message}`);
|
| 1148 |
+
}
|
| 1149 |
+
}
|
| 1150 |
+
|
| 1151 |
+
// --- Helper: handle /research context reply ---
|
| 1152 |
+
async function _handleResearch(chatId, url, angle) {
|
| 1153 |
+
await apiCaller.sendTelegramMessage(chatId, `π <b>Research Auto-Pilot</b>\nAnalyzing URL with focus: <i>${angle || 'General'}</i>...`);
|
| 1154 |
+
|
| 1155 |
+
const researchModule = require('./research');
|
| 1156 |
+
const aiContentMod = require('./ai-content');
|
| 1157 |
+
const imageGenMod = require('./image-gen');
|
| 1158 |
+
const schedulerMod = require('./scheduler');
|
| 1159 |
+
|
| 1160 |
+
try {
|
| 1161 |
+
const scrapeRes = await researchModule.scrapePost(url, angle);
|
| 1162 |
+
if (!scrapeRes.success) {
|
| 1163 |
+
await apiCaller.sendTelegramMessage(chatId, `β Scrape failed: ${scrapeRes.error}`);
|
| 1164 |
+
return;
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
await apiCaller.sendTelegramMessage(chatId, `π§ Scrape successful! Remixing content...`);
|
| 1168 |
+
const remixRes = await aiContentMod.remix(scrapeRes.data);
|
| 1169 |
+
if (!remixRes.success) {
|
| 1170 |
+
await apiCaller.sendTelegramMessage(chatId, `β AI Remix failed: ${remixRes.error}`);
|
| 1171 |
+
return;
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
const v1 = remixRes.data.variant_1;
|
| 1175 |
+
await apiCaller.sendTelegramMessage(chatId, `π¨ Generating high-conversion visual...`);
|
| 1176 |
+
const imgRes = await imageGenMod.generateAndUpload(v1.visual_prompt, 'res_v1');
|
| 1177 |
+
if (imgRes.success) v1.mediaUrl = imgRes.publicUrl;
|
| 1178 |
+
|
| 1179 |
+
const draftId = `res_${Date.now().toString().slice(-6)}`;
|
| 1180 |
+
const draftPost = {
|
| 1181 |
+
id: draftId, status: 'draft', created: new Date().toISOString(),
|
| 1182 |
+
input: url, platform: scrapeRes.data.platform || 'web',
|
| 1183 |
+
sourceData: scrapeRes.data, type: 'standard',
|
| 1184 |
+
v1, v2: remixRes.data.variant_2, v3: remixRes.data.variant_3
|
| 1185 |
+
};
|
| 1186 |
+
schedulerMod.config.posts.push(draftPost);
|
| 1187 |
+
schedulerMod.saveDB();
|
| 1188 |
+
|
| 1189 |
+
if (imgRes.success && v1.mediaUrl) {
|
| 1190 |
+
const FormData = require('form-data');
|
| 1191 |
+
const fForm = new FormData();
|
| 1192 |
+
fForm.append('chat_id', chatId);
|
| 1193 |
+
fForm.append('photo', v1.mediaUrl.startsWith('/') ? fs.createReadStream(path.join(__dirname, 'public', v1.mediaUrl)) : v1.mediaUrl);
|
| 1194 |
+
fForm.append('caption', `β
<b>Research Draft Ready!</b>\n\n${v1.ig_en.substring(0, 800)}...\n\nReview in Dashboard.`);
|
| 1195 |
+
fForm.append('parse_mode', 'HTML');
|
| 1196 |
+
await apiCaller.axiosIPv4.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendPhoto`, fForm, { headers: fForm.getHeaders() });
|
| 1197 |
+
} else {
|
| 1198 |
+
await apiCaller.sendTelegramMessage(chatId, `β
<b>Research Draft Ready!</b>\n\n${v1.ig_en.substring(0, 800)}...\n\nReview in Dashboard.`);
|
| 1199 |
+
}
|
| 1200 |
+
} catch (err) {
|
| 1201 |
+
console.error('[_handleResearch] Error:', err.message);
|
| 1202 |
+
await apiCaller.sendTelegramMessage(chatId, `β οΈ <b>Research Error:</b> ${err.message}`);
|
| 1203 |
+
}
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
// Telegram Webhook
|
| 1207 |
app.post('/api/telegram-webhook', async (req, res) => {
|
| 1208 |
res.status(200).send({ status: 'received' });
|
|
|
|
| 1882 |
`π <b>SEO ENGINE</b>\n` +
|
| 1883 |
`/seo [keyword] β Architect SEO Pillar Strategy\n` +
|
| 1884 |
`/write [siteId] [size] [topic] β Publish SEO Article\n` +
|
| 1885 |
+
`/seo2offer [keyword] β Full pipeline: scout β offer β article β publish β index\n` +
|
| 1886 |
`/index [url] β Submit to Google API\n\n` +
|
| 1887 |
`π° <b>SALES ENGINE</b>\n` +
|
| 1888 |
`/offer β List active offers with payment links\n` +
|
| 1889 |
`/offer [topic] β Create offer (AI copy β Mayar link)\n` +
|
| 1890 |
`/offer pause [id] β Pause underperforming offer\n` +
|
| 1891 |
+
`/offer ab [id] β Create A/B test variant\n` +
|
| 1892 |
+
`/offer variants [id] β View variant stats\n` +
|
| 1893 |
`/offer board β Pipeline stage counts\n` +
|
| 1894 |
+
`/offer eval β Run offer evaluation\n` +
|
| 1895 |
`/revenue β Revenue summary\n` +
|
| 1896 |
`/revenue today β Today's sales only\n\n` +
|
| 1897 |
+
`π <b>RESEARCH</b>\n` +
|
| 1898 |
+
`/scout [keyword] β Market viability scout\n` +
|
| 1899 |
+
`/costs β API spend breakdown\n` +
|
| 1900 |
+
`/landing β Your link-in-bio page\n\n` +
|
| 1901 |
`π <b>CRM MANAGER</b>\n` +
|
| 1902 |
`/updates β View Trello project statuses\n` +
|
| 1903 |
`/move [4-char-ID] [new list] β Update card status`;
|
|
|
|
| 1981 |
await apiCaller.sendTelegramMessage(chatId, `β Image generation failed: ${gashResult.error || 'All providers exhausted'}`);
|
| 1982 |
}
|
| 1983 |
}
|
| 1984 |
+
} else if (userText.toLowerCase().startsWith('/seo2offer')) {
|
| 1985 |
+
const keyword = userText.split(' ').slice(1).join(' ').trim();
|
| 1986 |
+
if (!keyword) {
|
| 1987 |
+
await apiCaller.sendTelegramMessage(chatId, `β Usage: <code>/seo2offer [keyword]</code>\nFull pipeline: scout β offer β article β publish β index`);
|
| 1988 |
+
return;
|
| 1989 |
+
}
|
| 1990 |
+
await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β‘β‘β‘β‘β‘] Scouting: "${keyword}"...`);
|
| 1991 |
+
|
| 1992 |
+
// 1. Scout market viability
|
| 1993 |
+
let scoutResult;
|
| 1994 |
+
try { scoutResult = await scoutAgent.runScout(keyword); } catch (e) { scoutResult = 'Scout unavailable'; }
|
| 1995 |
+
|
| 1996 |
+
await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β β‘β‘β‘β‘] Creating offer + Mayar link...`);
|
| 1997 |
+
|
| 1998 |
+
// 2. Create offer
|
| 1999 |
+
const offerResult = await salesEngine.createOfferFromTopic(keyword, 49000);
|
| 2000 |
+
let offerMsg = '';
|
| 2001 |
+
if (offerResult.success) {
|
| 2002 |
+
offerMsg = `β
Offer: ${offerResult.offer.title}\nπ³ ${offerResult.offer.paymentUrl || 'Pending'}`;
|
| 2003 |
+
} else {
|
| 2004 |
+
offerMsg = `β οΈ Offer: ${offerResult.verdict || 'skipped'} (${offerResult.reason || offerResult.error || ''})`;
|
| 2005 |
+
}
|
| 2006 |
+
|
| 2007 |
+
await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β β β‘β‘β‘] Writing SEO article...`);
|
| 2008 |
+
|
| 2009 |
+
// 3. Write article
|
| 2010 |
+
let sites = {};
|
| 2011 |
+
try { sites = JSON.parse(process.env.WP_SITES_JSON || "{}"); } catch(e){}
|
| 2012 |
+
const siteKeys = Object.keys(sites);
|
| 2013 |
+
const targetSite = siteKeys.length > 0 ? siteKeys[0] : null;
|
| 2014 |
+
|
| 2015 |
+
const articleRes = await seoWriter.generateArticle('medium', '3rd person', 'Educational', 'Authoritative', keyword, '');
|
| 2016 |
+
if (!articleRes.success) {
|
| 2017 |
+
await apiCaller.sendTelegramMessage(chatId, `β οΈ Article failed: ${articleRes.error}\n\n${offerMsg}\n\nπ΅οΈ <b>Scout:</b>\n${scoutResult}`);
|
| 2018 |
+
return;
|
| 2019 |
+
}
|
| 2020 |
+
|
| 2021 |
+
await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β β β β‘β‘] Publishing to WordPress (CTA auto-injected)...`);
|
| 2022 |
+
|
| 2023 |
+
// 4. Publish (CTA injection happens inside publisher automatically)
|
| 2024 |
+
const pubRes = await wordpressPublisher.publishPost(articleRes, targetSite);
|
| 2025 |
+
if (!pubRes.success) {
|
| 2026 |
+
await apiCaller.sendTelegramMessage(chatId, `β οΈ Publish failed: ${pubRes.error}\n\n${offerMsg}\n\nπ΅οΈ <b>Scout:</b>\n${scoutResult}`);
|
| 2027 |
+
return;
|
| 2028 |
+
}
|
| 2029 |
+
|
| 2030 |
+
await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β β β β β‘] Indexing on Google...`);
|
| 2031 |
+
|
| 2032 |
+
// 5. Index
|
| 2033 |
+
const idxRes = await googleIndexer.submitUrl(pubRes.url);
|
| 2034 |
+
|
| 2035 |
+
// 6. Final report
|
| 2036 |
+
let finalMsg = `π― <b>SEO-to-Revenue Complete!</b>\n\n`;
|
| 2037 |
+
finalMsg += `π <b>Article:</b> ${pubRes.url}\n`;
|
| 2038 |
+
finalMsg += `π <b>Google:</b> ${idxRes.success ? 'Indexed π’' : 'Pending π‘'}\n`;
|
| 2039 |
+
finalMsg += `${offerMsg}\n\n`;
|
| 2040 |
+
finalMsg += `π΅οΈ <b>Scout Report:</b>\n${typeof scoutResult === 'string' ? scoutResult.substring(0, 500) : 'N/A'}`;
|
| 2041 |
+
await apiCaller.sendTelegramMessage(chatId, finalMsg);
|
| 2042 |
+
|
| 2043 |
} else if (userText.toLowerCase().startsWith('/seo')) {
|
| 2044 |
const keyword = userText.split(' ').slice(1).join(' ');
|
| 2045 |
if (!keyword) {
|
|
|
|
| 2306 |
const emoji = { active: 'π’', pending_link: 'π‘', pending_validation: 'π΅', paused: 'βΈοΈ', retired: 'π΄' };
|
| 2307 |
const lines = stages.filter(s => counts[s]).map(s => `${emoji[s] || 'β’'} ${s}: ${counts[s]}`).join('\n');
|
| 2308 |
await apiCaller.sendTelegramMessage(chatId, `π <b>Offer Pipeline</b>\n\n${lines || 'No offers yet.'}\n\nTotal: ${db.offers.length}`);
|
| 2309 |
+
} else if (args.toLowerCase().startsWith('ab ')) {
|
| 2310 |
+
// /offer ab [id] β create A/B variant
|
| 2311 |
+
const offerId = args.split(' ')[1];
|
| 2312 |
+
await apiCaller.sendTelegramMessage(chatId, `π§ͺ <i>Creating variant for ${offerId}...</i>`);
|
| 2313 |
+
const result = await salesEngine.createVariant(offerId);
|
| 2314 |
+
if (!result.success) {
|
| 2315 |
+
await apiCaller.sendTelegramMessage(chatId, `β ${result.error}`);
|
| 2316 |
+
}
|
| 2317 |
+
} else if (args.toLowerCase().startsWith('variants ')) {
|
| 2318 |
+
// /offer variants [id] β show all variants
|
| 2319 |
+
const offerId = args.split(' ')[1];
|
| 2320 |
+
const result = salesEngine.getVariants(offerId);
|
| 2321 |
+
if (!result.success) {
|
| 2322 |
+
await apiCaller.sendTelegramMessage(chatId, `β ${result.error}`);
|
| 2323 |
+
} else {
|
| 2324 |
+
const lines = result.variants.map(v => {
|
| 2325 |
+
const convRate = v.clicks > 0 ? ((v.sales / v.clicks) * 100).toFixed(1) : '0.0';
|
| 2326 |
+
return `π <b>${v.id}:</b> ${v.title}\n π ${v.sales} sales | ${v.clicks} clicks | ${convRate}% conv | ${v.isActive ? 'π’ Active' : 'π΄ Paused'}`;
|
| 2327 |
+
}).join('\n\n');
|
| 2328 |
+
await apiCaller.sendTelegramMessage(chatId, `π§ͺ <b>Variants: ${result.offerTitle || offerId}</b>\n\n${lines}`);
|
| 2329 |
+
}
|
| 2330 |
} else if (args.toLowerCase().startsWith('eval')) {
|
| 2331 |
// /offer eval β run evaluation
|
| 2332 |
await apiCaller.sendTelegramMessage(chatId, 'π <i>Evaluating offers...</i>');
|
|
|
|
| 2379 |
);
|
| 2380 |
}
|
| 2381 |
|
| 2382 |
+
} else if (userText.toLowerCase().startsWith('/costs')) {
|
| 2383 |
+
const costs = costTracker.getCosts();
|
| 2384 |
+
let msg = `πΈ <b>API Cost Tracker</b>\n\n`;
|
| 2385 |
+
msg += `π <b>Total Spend:</b> $${(costs.total || 0).toFixed(4)}\n\n`;
|
| 2386 |
+
const models = Object.entries(costs.by_model || {}).sort((a, b) => b[1] - a[1]);
|
| 2387 |
+
if (models.length > 0) {
|
| 2388 |
+
msg += `<b>By Model:</b>\n`;
|
| 2389 |
+
for (const [model, cost] of models) {
|
| 2390 |
+
msg += `β’ ${model}: $${cost.toFixed(4)}\n`;
|
| 2391 |
+
}
|
| 2392 |
+
} else {
|
| 2393 |
+
msg += `<i>No costs tracked yet.</i>`;
|
| 2394 |
+
}
|
| 2395 |
+
await apiCaller.sendTelegramMessage(chatId, msg);
|
| 2396 |
+
|
| 2397 |
+
} else if (userText.toLowerCase().startsWith('/scout')) {
|
| 2398 |
+
const keyword = userText.split(' ').slice(1).join(' ').trim();
|
| 2399 |
+
if (!keyword) {
|
| 2400 |
+
await apiCaller.sendTelegramMessage(chatId, `β Usage: <code>/scout [keyword]</code>\nExample: /scout AI productivity tools`);
|
| 2401 |
+
return;
|
| 2402 |
+
}
|
| 2403 |
+
await apiCaller.sendTelegramMessage(chatId, `π <b>Scouting:</b> "${keyword}"...\nAnalyzing competition, volume, and angles...`);
|
| 2404 |
+
const result = await scoutAgent.runScout(keyword);
|
| 2405 |
+
await apiCaller.sendTelegramMessage(chatId, `π΅οΈ <b>Scout Report: ${keyword}</b>\n\n${result}`);
|
| 2406 |
+
|
| 2407 |
+
} else if (userText.toLowerCase().startsWith('/landing')) {
|
| 2408 |
+
const publicUrl = process.env.PUBLIC_URL || `https://${process.env.SPACE_ID || 'aigoose-vinos-engine'}.hf.space`;
|
| 2409 |
+
await apiCaller.sendTelegramMessage(chatId, `π <b>Your Landing Page:</b>\n${publicUrl}/landing\n\nShare this link as your link-in-bio!`);
|
| 2410 |
+
|
| 2411 |
} else {
|
| 2412 |
await handleVinIntent(chatId, from, userText);
|
| 2413 |
}
|
skills/sales_engine.js
CHANGED
|
@@ -233,53 +233,189 @@ module.exports = {
|
|
| 233 |
return { success: true, transaction: txn, offer };
|
| 234 |
},
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
async evaluateOffers() {
|
| 237 |
const db = readSalesDB();
|
| 238 |
const now = Date.now();
|
| 239 |
let actions = [];
|
| 240 |
|
| 241 |
for (const offer of db.offers) {
|
| 242 |
-
if (offer.status !== 'active')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
const ageHours = (now - new Date(offer.created).getTime()) / 3600000;
|
| 245 |
const salesLast24h = db.transactions.filter(t =>
|
| 246 |
t.offerId === offer.id && (now - new Date(t.ts).getTime()) < 86400000
|
| 247 |
).length;
|
| 248 |
|
|
|
|
|
|
|
| 249 |
if (salesLast24h >= 3) {
|
| 250 |
// Winner! Scale it
|
| 251 |
actions.push({ offerId: offer.id, action: 'scale', salesLast24h });
|
| 252 |
await this.scaleWinner(offer.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
} else if (ageHours >= 72 && offer.totalSales === 0) {
|
| 254 |
-
//
|
| 255 |
offer.status = 'paused';
|
| 256 |
actions.push({ offerId: offer.id, action: 'pause', reason: 'no_sales_72h' });
|
| 257 |
}
|
| 258 |
-
|
| 259 |
-
// Re-check pending_validation offers
|
| 260 |
-
if (offer.status === 'pending_validation' && offer.nextCheck && new Date(offer.nextCheck) <= new Date()) {
|
| 261 |
-
try {
|
| 262 |
-
const trendValidator = require('./trend_validator');
|
| 263 |
-
const recheck = await trendValidator.validateTopic(offer.topic);
|
| 264 |
-
if (recheck.verdict === 'go') {
|
| 265 |
-
// Promote to active offer creation
|
| 266 |
-
offer.status = 'retired'; // retire the pending one
|
| 267 |
-
await this.createOfferFromTopic(offer.topic, offer.priceIDR, offer.sourcePostIds[0], offer.sourcePillar);
|
| 268 |
-
actions.push({ offerId: offer.id, action: 'promoted_from_wait' });
|
| 269 |
-
} else if (recheck.verdict === 'skip') {
|
| 270 |
-
offer.status = 'retired';
|
| 271 |
-
actions.push({ offerId: offer.id, action: 'retired_after_recheck' });
|
| 272 |
-
} else {
|
| 273 |
-
offer.nextCheck = new Date(Date.now() + 48 * 3600000).toISOString();
|
| 274 |
-
}
|
| 275 |
-
} catch (e) { /* skip recheck on error */ }
|
| 276 |
-
}
|
| 277 |
}
|
| 278 |
|
| 279 |
writeSalesDB(db);
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
if (actions.length > 0) {
|
| 282 |
-
const summary = actions.map(a => `β’ ${a.offerId}: ${a.action}`).join('\n');
|
| 283 |
await apiCaller.sendTelegramMessage(process.env.TELEGRAM_CHAT_ID,
|
| 284 |
`π <b>Offer Evaluation Complete</b>\n\n${summary}`
|
| 285 |
);
|
|
@@ -389,12 +525,19 @@ module.exports = {
|
|
| 389 |
};
|
| 390 |
},
|
| 391 |
|
| 392 |
-
injectOfferLink(topic) {
|
| 393 |
const db = readSalesDB();
|
|
|
|
| 394 |
const matching = db.offers
|
| 395 |
.filter(o => o.status === 'active' && o.paymentUrl)
|
| 396 |
-
.find(o =>
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
}
|
| 400 |
};
|
|
|
|
| 233 |
return { success: true, transaction: txn, offer };
|
| 234 |
},
|
| 235 |
|
| 236 |
+
async createVariant(offerId) {
|
| 237 |
+
const db = readSalesDB();
|
| 238 |
+
const offer = db.offers.find(o => o.id === offerId);
|
| 239 |
+
if (!offer) return { success: false, error: 'Offer not found' };
|
| 240 |
+
|
| 241 |
+
// Initialize variants array if not present (backward compat)
|
| 242 |
+
if (!offer.variants) {
|
| 243 |
+
offer.variants = [{
|
| 244 |
+
id: 'v0', title: offer.title, description: offer.description,
|
| 245 |
+
priceIDR: offer.priceIDR, benefits: offer.benefits || [],
|
| 246 |
+
impressions: 0, clicks: 0, sales: offer.totalSales || 0, isActive: true
|
| 247 |
+
}];
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
const existingStats = offer.variants.map(v =>
|
| 251 |
+
`Variant ${v.id}: "${v.title}" β ${v.sales} sales, ${v.clicks} clicks`
|
| 252 |
+
).join('\n');
|
| 253 |
+
|
| 254 |
+
// Load failure patterns for context
|
| 255 |
+
const mainDb = memory.readDB();
|
| 256 |
+
const failureHints = (mainDb.failure_patterns || []).slice(0, 3).map(p => `- ${p}`).join('\n');
|
| 257 |
+
|
| 258 |
+
const prompt = [
|
| 259 |
+
{ role: 'system', content: 'You are an A/B testing expert for digital products. Create a DIFFERENT angle/hook than existing variants. Output JSON only.' },
|
| 260 |
+
{ role: 'user', content: `Topic: "${offer.topic}"\nPrice: Rp ${offer.priceIDR.toLocaleString()}\n\nExisting variants:\n${existingStats}\n\n${failureHints ? `Known failure patterns to avoid:\n${failureHints}\n\n` : ''}Create variant ${offer.variants.length} with a different angle, hook, or value proposition.\nJSON: { "title": string, "description": string, "benefits": [string, string, string] }` }
|
| 261 |
+
];
|
| 262 |
+
|
| 263 |
+
const aiResult = await apiCaller.callOpenRouter(prompt);
|
| 264 |
+
let variantData = { title: `${offer.topic} (V${offer.variants.length})`, description: offer.description, benefits: [] };
|
| 265 |
+
if (aiResult.success) {
|
| 266 |
+
try {
|
| 267 |
+
const parsed = JSON.parse(aiResult.data.replace(/```json?\n?|```/g, '').trim());
|
| 268 |
+
variantData = { ...variantData, ...parsed };
|
| 269 |
+
} catch (e) { /* use default */ }
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
const variant = {
|
| 273 |
+
id: `v${offer.variants.length}`,
|
| 274 |
+
title: variantData.title,
|
| 275 |
+
description: variantData.description,
|
| 276 |
+
priceIDR: offer.priceIDR,
|
| 277 |
+
benefits: variantData.benefits || [],
|
| 278 |
+
impressions: 0, clicks: 0, sales: 0, isActive: true
|
| 279 |
+
};
|
| 280 |
+
offer.variants.push(variant);
|
| 281 |
+
writeSalesDB(db);
|
| 282 |
+
|
| 283 |
+
await apiCaller.sendTelegramMessage(process.env.TELEGRAM_CHAT_ID,
|
| 284 |
+
`π§ͺ <b>New Variant Created!</b>\n\n` +
|
| 285 |
+
`π¦ Offer: ${offer.title}\n` +
|
| 286 |
+
`π Variant ${variant.id}: "${variant.title}"\n` +
|
| 287 |
+
`π ${variant.description}\n\n` +
|
| 288 |
+
`Total variants: ${offer.variants.length}`
|
| 289 |
+
);
|
| 290 |
+
|
| 291 |
+
return { success: true, variant, offer };
|
| 292 |
+
},
|
| 293 |
+
|
| 294 |
+
getVariants(offerId) {
|
| 295 |
+
const db = readSalesDB();
|
| 296 |
+
const offer = db.offers.find(o => o.id === offerId);
|
| 297 |
+
if (!offer) return { success: false, error: 'Offer not found' };
|
| 298 |
+
if (!offer.variants || offer.variants.length === 0) {
|
| 299 |
+
return { success: true, variants: [{ id: 'v0', title: offer.title, sales: offer.totalSales, clicks: 0, impressions: 0, isActive: true }] };
|
| 300 |
+
}
|
| 301 |
+
return { success: true, variants: offer.variants, offerTitle: offer.title };
|
| 302 |
+
},
|
| 303 |
+
|
| 304 |
+
trackClick(offerId, variantId) {
|
| 305 |
+
const db = readSalesDB();
|
| 306 |
+
const offer = db.offers.find(o => o.id === offerId);
|
| 307 |
+
if (!offer) return null;
|
| 308 |
+
if (offer.variants) {
|
| 309 |
+
const variant = offer.variants.find(v => v.id === variantId);
|
| 310 |
+
if (variant) { variant.clicks = (variant.clicks || 0) + 1; variant.impressions = (variant.impressions || 0) + 1; }
|
| 311 |
+
}
|
| 312 |
+
writeSalesDB(db);
|
| 313 |
+
return offer.paymentUrl;
|
| 314 |
+
},
|
| 315 |
+
|
| 316 |
+
async analyzeFailurePatterns() {
|
| 317 |
+
const db = readSalesDB();
|
| 318 |
+
const retired = db.offers.filter(o => o.status === 'retired' || o.status === 'paused');
|
| 319 |
+
if (retired.length < 3) return { success: false, reason: 'Need at least 3 retired/paused offers to analyze' };
|
| 320 |
+
|
| 321 |
+
const retiredSummary = retired.map(o =>
|
| 322 |
+
`- "${o.title}" (${o.topic}): ${o.totalSales} sales, Rp ${o.priceIDR}, pillar: ${o.sourcePillar || 'general'}, trend: ${o.trendScore || 'N/A'}`
|
| 323 |
+
).join('\n');
|
| 324 |
+
|
| 325 |
+
const prompt = [
|
| 326 |
+
{ role: 'system', content: 'You are a sales strategist analyzing failed digital product offers. Extract 3-5 actionable patterns. Be specific about what to avoid. Output as a JSON array of strings.' },
|
| 327 |
+
{ role: 'user', content: `Retired/paused offers:\n${retiredSummary}\n\nExtract failure patterns as JSON array: ["pattern 1", "pattern 2", ...]` }
|
| 328 |
+
];
|
| 329 |
+
|
| 330 |
+
const aiResult = await apiCaller.callOpenRouter(prompt);
|
| 331 |
+
let patterns = [];
|
| 332 |
+
if (aiResult.success) {
|
| 333 |
+
try {
|
| 334 |
+
patterns = JSON.parse(aiResult.data.replace(/```json?\n?|```/g, '').trim());
|
| 335 |
+
} catch (e) { patterns = [aiResult.data.substring(0, 200)]; }
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
const mainDb = memory.readDB();
|
| 339 |
+
mainDb.failure_patterns = patterns;
|
| 340 |
+
memory.writeDB(mainDb);
|
| 341 |
+
|
| 342 |
+
return { success: true, patterns };
|
| 343 |
+
},
|
| 344 |
+
|
| 345 |
async evaluateOffers() {
|
| 346 |
const db = readSalesDB();
|
| 347 |
const now = Date.now();
|
| 348 |
let actions = [];
|
| 349 |
|
| 350 |
for (const offer of db.offers) {
|
| 351 |
+
if (offer.status !== 'active') {
|
| 352 |
+
// Re-check pending_validation offers
|
| 353 |
+
if (offer.status === 'pending_validation' && offer.nextCheck && new Date(offer.nextCheck) <= new Date()) {
|
| 354 |
+
try {
|
| 355 |
+
const trendValidator = require('./trend_validator');
|
| 356 |
+
const recheck = await trendValidator.validateTopic(offer.topic);
|
| 357 |
+
if (recheck.verdict === 'go') {
|
| 358 |
+
offer.status = 'retired';
|
| 359 |
+
await this.createOfferFromTopic(offer.topic, offer.priceIDR, offer.sourcePostIds[0], offer.sourcePillar);
|
| 360 |
+
actions.push({ offerId: offer.id, action: 'promoted_from_wait' });
|
| 361 |
+
} else if (recheck.verdict === 'skip') {
|
| 362 |
+
offer.status = 'retired';
|
| 363 |
+
actions.push({ offerId: offer.id, action: 'retired_after_recheck' });
|
| 364 |
+
} else {
|
| 365 |
+
offer.nextCheck = new Date(Date.now() + 48 * 3600000).toISOString();
|
| 366 |
+
}
|
| 367 |
+
} catch (e) { /* skip recheck on error */ }
|
| 368 |
+
}
|
| 369 |
+
continue;
|
| 370 |
+
}
|
| 371 |
|
| 372 |
const ageHours = (now - new Date(offer.created).getTime()) / 3600000;
|
| 373 |
const salesLast24h = db.transactions.filter(t =>
|
| 374 |
t.offerId === offer.id && (now - new Date(t.ts).getTime()) < 86400000
|
| 375 |
).length;
|
| 376 |
|
| 377 |
+
const variantCount = (offer.variants || []).length;
|
| 378 |
+
|
| 379 |
if (salesLast24h >= 3) {
|
| 380 |
// Winner! Scale it
|
| 381 |
actions.push({ offerId: offer.id, action: 'scale', salesLast24h });
|
| 382 |
await this.scaleWinner(offer.id);
|
| 383 |
+
|
| 384 |
+
// If variants exist, pause losers
|
| 385 |
+
if (offer.variants && offer.variants.length > 1) {
|
| 386 |
+
const sorted = [...offer.variants].sort((a, b) => (b.sales || 0) - (a.sales || 0));
|
| 387 |
+
for (let i = 1; i < sorted.length; i++) {
|
| 388 |
+
sorted[i].isActive = false;
|
| 389 |
+
}
|
| 390 |
+
actions.push({ offerId: offer.id, action: 'variant_loser_paused', winner: sorted[0].id });
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
} else if (ageHours >= 48 && offer.totalSales === 0 && variantCount < 2) {
|
| 394 |
+
// Stale 48h, no sales, < 2 variants β auto-create variant instead of waiting
|
| 395 |
+
try {
|
| 396 |
+
await this.createVariant(offer.id);
|
| 397 |
+
actions.push({ offerId: offer.id, action: 'auto_variant_created', reason: 'no_sales_48h' });
|
| 398 |
+
} catch (e) {
|
| 399 |
+
actions.push({ offerId: offer.id, action: 'variant_create_failed', error: e.message });
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
} else if (ageHours >= 72 && offer.totalSales === 0) {
|
| 403 |
+
// 72h, still 0 sales even with variants β pause
|
| 404 |
offer.status = 'paused';
|
| 405 |
actions.push({ offerId: offer.id, action: 'pause', reason: 'no_sales_72h' });
|
| 406 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
}
|
| 408 |
|
| 409 |
writeSalesDB(db);
|
| 410 |
|
| 411 |
+
// Analyze failure patterns if enough retired offers
|
| 412 |
+
const retiredCount = db.offers.filter(o => o.status === 'retired' || o.status === 'paused').length;
|
| 413 |
+
if (retiredCount >= 3) {
|
| 414 |
+
try { await this.analyzeFailurePatterns(); } catch (e) { /* non-critical */ }
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
if (actions.length > 0) {
|
| 418 |
+
const summary = actions.map(a => `β’ ${a.offerId}: ${a.action}${a.reason ? ` (${a.reason})` : ''}`).join('\n');
|
| 419 |
await apiCaller.sendTelegramMessage(process.env.TELEGRAM_CHAT_ID,
|
| 420 |
`π <b>Offer Evaluation Complete</b>\n\n${summary}`
|
| 421 |
);
|
|
|
|
| 525 |
};
|
| 526 |
},
|
| 527 |
|
| 528 |
+
injectOfferLink(topic, source = 'social') {
|
| 529 |
const db = readSalesDB();
|
| 530 |
+
const topicLower = topic.toLowerCase();
|
| 531 |
const matching = db.offers
|
| 532 |
.filter(o => o.status === 'active' && o.paymentUrl)
|
| 533 |
+
.find(o => {
|
| 534 |
+
const offerWords = o.topic.toLowerCase().split(/\s+/);
|
| 535 |
+
const topicWords = topicLower.split(/\s+/);
|
| 536 |
+
return offerWords.some(w => w.length > 3 && topicLower.includes(w)) ||
|
| 537 |
+
topicWords.some(w => w.length > 3 && o.topic.toLowerCase().includes(w));
|
| 538 |
+
});
|
| 539 |
+
if (!matching) return null;
|
| 540 |
+
const utm = `?utm_source=${source}&utm_medium=${source === 'seo' ? 'article' : 'post'}&utm_campaign=${encodeURIComponent(topic.replace(/\s+/g, '-').toLowerCase())}`;
|
| 541 |
+
return { url: matching.paymentUrl, utmUrl: matching.paymentUrl + utm, title: matching.title, priceIDR: matching.priceIDR, offerId: matching.id };
|
| 542 |
}
|
| 543 |
};
|
skills/set_telegram_menu.js
CHANGED
|
@@ -36,7 +36,11 @@ const setCommands = async () => {
|
|
| 36 |
{ command: 'pulseconfig', description: 'βοΈ Configure Daily Pulse (v2.0)' },
|
| 37 |
{ command: 'viral', description: 'π¦ Viral Content Engine (v1.0)' },
|
| 38 |
{ command: 'offer', description: 'π° Sales Offers (create/list/pause)' },
|
| 39 |
-
{ command: 'revenue', description: 'π Revenue Dashboard & Summary' }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
];
|
| 41 |
|
| 42 |
try {
|
|
|
|
| 36 |
{ command: 'pulseconfig', description: 'βοΈ Configure Daily Pulse (v2.0)' },
|
| 37 |
{ command: 'viral', description: 'π¦ Viral Content Engine (v1.0)' },
|
| 38 |
{ command: 'offer', description: 'π° Sales Offers (create/list/pause)' },
|
| 39 |
+
{ command: 'revenue', description: 'π Revenue Dashboard & Summary' },
|
| 40 |
+
{ command: 'costs', description: 'πΈ API Spend Breakdown' },
|
| 41 |
+
{ command: 'scout', description: 'π Market Viability Scout' },
|
| 42 |
+
{ command: 'seo2offer', description: 'π― Full SEO-to-Revenue Pipeline' },
|
| 43 |
+
{ command: 'landing', description: 'π Link-in-Bio Page' }
|
| 44 |
];
|
| 45 |
|
| 46 |
try {
|
skills/wordpress_publisher.js
CHANGED
|
@@ -66,7 +66,25 @@ class WordPressPublisher {
|
|
| 66 |
// Also handle any leftover Pollinations <img> tags (from older articles)
|
| 67 |
htmlWithMedia = htmlWithMedia.replace(/<img[^>]+src="https:\/\/image\.pollinations\.ai[^"]*"[^>]*>/g, '');
|
| 68 |
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
// Handle Tags
|
| 72 |
let tagIds = [];
|
|
|
|
| 66 |
// Also handle any leftover Pollinations <img> tags (from older articles)
|
| 67 |
htmlWithMedia = htmlWithMedia.replace(/<img[^>]+src="https:\/\/image\.pollinations\.ai[^"]*"[^>]*>/g, '');
|
| 68 |
|
| 69 |
+
// CTA injection β match active sales offer to article keyword
|
| 70 |
+
let ctaBlock = '';
|
| 71 |
+
try {
|
| 72 |
+
const salesEngine = require('./sales_engine');
|
| 73 |
+
const keyword = articleData.meta?.keyword || articleData.meta?.title || '';
|
| 74 |
+
const offerMatch = salesEngine.injectOfferLink(keyword, 'seo');
|
| 75 |
+
if (offerMatch) {
|
| 76 |
+
ctaBlock = `\n<div class="vinos-cta" style="margin:32px 0;padding:24px;background:#f8f9fa;border-left:4px solid #22d3a0;border-radius:8px;">` +
|
| 77 |
+
`<h3 style="margin:0 0 8px;font-size:1.2rem;">${offerMatch.title}</h3>` +
|
| 78 |
+
`<p style="margin:0 0 12px;color:#666;">Rp ${(offerMatch.priceIDR || 0).toLocaleString('id-ID')}</p>` +
|
| 79 |
+
`<a href="${offerMatch.utmUrl}" target="_blank" rel="noopener" style="display:inline-block;padding:10px 24px;background:#22d3a0;color:#fff;text-decoration:none;border-radius:6px;font-weight:bold;">Get It Now →</a>` +
|
| 80 |
+
`</div>`;
|
| 81 |
+
console.log(`[WP Publisher] CTA injected for offer: ${offerMatch.title}`);
|
| 82 |
+
}
|
| 83 |
+
} catch (e) {
|
| 84 |
+
console.error('[WP Publisher] CTA injection skipped:', e.message);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
const finalContent = `${htmlWithMedia}${ctaBlock}\n\n<!-- SEO Schema (Generated by VinOS) -->\n${articleData.schema}`;
|
| 88 |
|
| 89 |
// Handle Tags
|
| 90 |
let tagIds = [];
|