openclaw-neo-bot / server.py
Razka Bot
Replace Space with OpenClaw affiliate ops panel
d4b0885
Raw
History Blame
8.13 kB
import os
import time
from datetime import datetime, timezone
from flask import Flask, request, jsonify, render_template_string
import requests
app = Flask(__name__)
SUPABASE_URL = os.getenv("SUPABASE_URL", "").rstrip("/")
SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY", "")
NEXT_AGENT = {
"trend_hunter": "offer_scout",
"offer_scout": "content_strategist",
"content_strategist": "seo_writer",
"seo_writer": "creative_generator",
"creative_generator": "publisher",
"publisher": "tracker",
"tracker": "analytics",
"analytics": "cro_optimizer",
"cro_optimizer": "compliance_guard",
}
def now_iso():
return datetime.now(timezone.utc).isoformat()
def sb(path, method="GET", params=None, body=None):
if not SUPABASE_URL or not SUPABASE_ANON_KEY:
raise RuntimeError("Missing SUPABASE_URL / SUPABASE_ANON_KEY")
url = f"{SUPABASE_URL}/rest/v1/{path}"
headers = {
"apikey": SUPABASE_ANON_KEY,
"Authorization": f"Bearer {SUPABASE_ANON_KEY}",
"Content-Type": "application/json",
}
if method in ("POST", "PATCH", "DELETE"):
headers["Prefer"] = "return=representation"
r = requests.request(method, url, headers=headers, params=params, json=body, timeout=25)
if r.status_code >= 400:
raise RuntimeError(f"Supabase {r.status_code}: {r.text[:250]}")
if not r.text:
return None
return r.json()
def run_agent(agent_code, input_data):
prev = (input_data or {}).get("prev_output", {})
if agent_code == "trend_hunter":
niche = (input_data or {}).get("niche", "affiliate marketing")
base = ["best", "review", "promo", "diskon", "vs"]
return {
"niche": niche,
"keywords": [f"{niche} {b}" for b in base],
"search_intent": "commercial+transactional",
}
if agent_code == "offer_scout":
kws = prev.get("keywords", [])
return {"offers": [{"keyword": k, "offer_id": f"offer_{i+1}", "score": 80 - i * 7} for i, k in enumerate(kws[:3])]}
if agent_code == "content_strategist":
offers = prev.get("offers", [])
return {
"content_plan": [{"stage": "BOFU" if i == 0 else ("MOFU" if i == 1 else "TOFU"), "title_angle": f"Solusi terbaik untuk {o.get('keyword','offer')}"} for i, o in enumerate(offers)],
"affiliate_link": (input_data or {}).get("affiliate_link"),
"short_url": (input_data or {}).get("short_url"),
}
if agent_code == "seo_writer":
title = prev.get("content_plan", [{}])[0].get("title_angle", "Panduan affiliate")
slug = "-".join([x for x in "".join([c.lower() if c.isalnum() else " " for c in title]).split() if x])
return {"title": title, "slug": slug, "article_html": f"<h1>{title}</h1><p>Draft siap publish.</p>"}
if agent_code == "creative_generator":
t = prev.get("title", "Promo affiliate")
return {"creatives": [{"channel": "facebook", "hook": f"{t} - cek sekarang"}, {"channel": "instagram", "hook": f"🔥 {t}"}]}
if agent_code == "publisher":
return {"publish_refs": [{"channel": "blog", "ref": f"post_{int(time.time())}"}], "short_url": (input_data or {}).get("short_url")}
if agent_code == "tracker":
return {"tracking_map": {"utm_source": "hf-space", "utm_medium": "affiliate", "utm_campaign": f"campaign_{int(time.time())}"}}
if agent_code == "analytics":
return {"kpi_snapshot": {"ctr": 0.07, "cvr": 0.02, "epc": 0.4}}
if agent_code == "cro_optimizer":
return {"experiments": [{"name": "headline-variant-a", "priority": "high"}]}
if agent_code == "compliance_guard":
return {"compliance_status": "pass-with-notes", "fixes": ["Tambahkan disclosure affiliate"]}
raise RuntimeError(f"No runner for {agent_code}")
def run_once():
task_rows = sb("aff_tasks", params={"status": "eq.queued", "order": "scheduled_at.asc", "limit": 1}) or []
if not task_rows:
return {"ok": True, "message": "No queued tasks"}
t = task_rows[0]
task_id = t["id"]
attempts = int(t.get("attempts") or 0) + 1
sb(f"aff_tasks?id=eq.{task_id}", method="PATCH", body={"status": "running", "started_at": now_iso(), "attempts": attempts})
try:
output = run_agent(t["agent_code"], t.get("input") or {})
sb(f"aff_tasks?id=eq.{task_id}", method="PATCH", body={"status": "done", "output": output, "finished_at": now_iso()})
next_agent = NEXT_AGENT.get(t["agent_code"])
if next_agent:
sb("aff_tasks", method="POST", body={"agent_code": next_agent, "status": "queued", "input": {"prev_agent": t["agent_code"], "prev_output": output}})
return {"ok": True, "task_id": task_id, "agent": t["agent_code"], "status": "done"}
except Exception as e:
sb(f"aff_tasks?id=eq.{task_id}", method="PATCH", body={"status": "error", "error": str(e), "finished_at": now_iso()})
return {"ok": False, "task_id": task_id, "agent": t["agent_code"], "error": str(e)}
def stats_today():
start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
rows = sb("aff_tasks", params={"select": "status,agent_code,created_at", "created_at": f"gte.{start}", "limit": 1000}) or []
agg = {"queued": 0, "running": 0, "done": 0, "error": 0}
by_agent = {}
for r in rows:
st = r.get("status", "queued")
agg[st] = agg.get(st, 0) + 1
a = r.get("agent_code", "unknown")
by_agent[a] = by_agent.get(a, 0) + 1
return {"total": len(rows), "agg": agg, "by_agent": by_agent}
PAGE = """
<!doctype html><html><head><meta charset='utf-8'><title>OpenClaw Affiliate Ops</title>
<style>body{font-family:Arial,sans-serif;max-width:900px;margin:20px auto;padding:0 12px}button{padding:8px 12px;margin:4px}input{padding:8px;width:100%}pre{background:#111;color:#0f0;padding:12px;border-radius:8px;white-space:pre-wrap}</style>
</head><body>
<h2>⚡ OpenClaw Affiliate Ops (HF Space)</h2>
<p>Supabase: {{supabase_ok}}</p>
<form method='post' action='/enqueue'>
<label>Affiliate Link</label><input name='affiliate_link' required>
<label>Short URL (optional)</label><input name='short_url'>
<button type='submit'>Enqueue Campaign</button>
</form>
<p>
<a href='/run-once'><button>Run Once</button></a>
<a href='/run-batch?count=5'><button>Run Batch x5</button></a>
<a href='/stats'><button>Refresh Stats (JSON)</button></a>
</p>
<pre>{{stats}}</pre>
</body></html>
"""
@app.get("/")
def home():
try:
s = stats_today()
return render_template_string(PAGE, supabase_ok="connected", stats=s)
except Exception as e:
return render_template_string(PAGE, supabase_ok=f"error: {e}", stats={})
@app.get("/healthz")
def healthz():
return jsonify({"ok": True, "time": now_iso()})
@app.post("/enqueue")
def enqueue():
link = request.form.get("affiliate_link") or (request.json or {}).get("affiliate_link")
short = request.form.get("short_url") or (request.json or {}).get("short_url")
if not link:
return jsonify({"ok": False, "error": "affiliate_link required"}), 400
row = sb("aff_tasks", method="POST", body={
"agent_code": "content_strategist",
"status": "queued",
"input": {
"niche": "web hosting",
"offer": "Hostinger",
"affiliate_link": link,
"short_url": short,
"source": "hf-space"
}
})
return jsonify({"ok": True, "task": row[0] if isinstance(row, list) and row else row})
@app.get("/run-once")
def run_once_route():
return jsonify(run_once())
@app.get("/run-batch")
def run_batch():
count = int(request.args.get("count", 5))
out = []
for _ in range(max(1, min(count, 20))):
r = run_once()
out.append(r)
if r.get("message") == "No queued tasks":
break
return jsonify({"ok": True, "results": out})
@app.get("/stats")
def stats_route():
return jsonify(stats_today())
if __name__ == "__main__":
port = int(os.getenv("PORT", "7860"))
app.run(host="0.0.0.0", port=port)