Razka Bot commited on
Commit
d4b0885
·
1 Parent(s): d88b0a5

Replace Space with OpenClaw affiliate ops panel

Browse files
Files changed (3) hide show
  1. README.md +9 -4
  2. requirements.txt +2 -7
  3. server.py +175 -1667
README.md CHANGED
@@ -1,9 +1,14 @@
1
  ---
2
- title: Openclaw Neo Bot
3
- emoji: 🦅
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
- app_port: 7860
8
  pinned: false
9
- ---
 
 
 
 
 
 
 
1
  ---
2
+ title: OpenClaw Neo Bot
3
+ emoji:
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
 
7
  pinned: false
8
+ ---
9
+
10
+ OpenClaw Affiliate Ops panel connected to Supabase.
11
+
12
+ Set these Space Secrets:
13
+ - `SUPABASE_URL`
14
+ - `SUPABASE_ANON_KEY`
requirements.txt CHANGED
@@ -1,7 +1,2 @@
1
- flask
2
- flask-cors
3
- playwright
4
- requests
5
- beautifulsoup4
6
- python-dotenv
7
- supabase
 
1
+ flask==3.0.3
2
+ requests==2.32.3
 
 
 
 
 
server.py CHANGED
@@ -1,1695 +1,203 @@
1
- """
2
- Openclaw Server — Versi Neo dengan Dashboard Premium
3
- """
4
-
5
  import os
6
- import re
7
- import json
8
  import time
9
- import types
10
- import requests
11
- import importlib
12
- import threading
13
- from datetime import datetime
14
  from flask import Flask, request, jsonify, render_template_string
15
- from flask_cors import CORS
16
- from bs4 import BeautifulSoup
17
- from dotenv import load_dotenv
18
- from supabase import create_client
19
 
20
- load_dotenv()
21
  app = Flask(__name__)
22
- CORS(app)
23
-
24
- # =================== CONFIG ===================
25
-
26
- SUPABASE_URL = os.environ.get("SUPABASE_URL")
27
- SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
28
-
29
- supabase = None
30
- if SUPABASE_URL and SUPABASE_KEY:
31
- try:
32
- supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
33
- print("✅ Supabase connected")
34
- except Exception as e:
35
- print(f"⚠️ Supabase error: {e}")
36
-
37
- WORKSPACE = "/tmp/openclaw"
38
- MEMORY_FILE = f"{WORKSPACE}/memory.md"
39
- LOG_FILE = f"{WORKSPACE}/log.md"
40
- SKILLS_DIR = f"{WORKSPACE}/skills"
41
- TASKS_FILE = f"{WORKSPACE}/tasks.json"
42
- BOTS_DIR = f"{WORKSPACE}/bots"
43
- MCP_DIR = f"{WORKSPACE}/mcp"
44
-
45
- for d in [WORKSPACE, SKILLS_DIR, BOTS_DIR, MCP_DIR]:
46
- os.makedirs(d, exist_ok=True)
47
-
48
- for f, default in [
49
- (MEMORY_FILE, "# Memory Openclaw\n\n"),
50
- (LOG_FILE, "# Log Aktivitas\n\n"),
51
- (TASKS_FILE, "[]"),
52
- ]:
53
- if not os.path.exists(f):
54
- with open(f, "w") as fp:
55
- fp.write(default)
56
-
57
- # =================== LOGGING ===================
58
-
59
- def tulis_log(msg):
60
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
61
- line = f"[{ts}] {msg}\n"
62
- with open(LOG_FILE, "a") as f:
63
- f.write(line)
64
- print(line.strip())
65
-
66
- def baca_log():
67
- with open(LOG_FILE, "r") as f:
68
- return f.read()[-4000:]
69
-
70
- # =================== DYNAMIC MCP LOADER ===================
71
-
72
- _mcp_registry = {}
73
-
74
- def load_mcp(name):
75
- path = f"{MCP_DIR}/{name}.py"
76
- if not os.path.exists(path):
77
- if supabase:
78
- try:
79
- res = supabase.table("mcps").select("*").eq("name", name).execute()
80
- if res.data:
81
- with open(path, "w") as f:
82
- f.write(res.data[0]["code"])
83
- else:
84
- return None
85
- except Exception as e:
86
- tulis_log(f"MCP load error: {e}")
87
- return None
88
- else:
89
- return None
90
-
91
- try:
92
- spec = importlib.util.spec_from_file_location(name, path)
93
- mod = importlib.util.module_from_spec(spec)
94
- spec.loader.exec_module(mod)
95
- _mcp_registry[name] = mod
96
- tulis_log(f"✅ MCP '{name}' loaded")
97
- return mod
98
- except Exception as e:
99
- tulis_log(f"❌ MCP '{name}' load error: {e}")
100
- return None
101
-
102
- def get_mcp(name):
103
- if name in _mcp_registry:
104
- return _mcp_registry[name]
105
- return load_mcp(name)
106
-
107
- def install_mcp(name, code, description=""):
108
- try:
109
- path = f"{MCP_DIR}/{name}.py"
110
- with open(path, "w") as f:
111
- f.write(code)
112
- spec = importlib.util.spec_from_file_location(name, path)
113
- mod = importlib.util.module_from_spec(spec)
114
- spec.loader.exec_module(mod)
115
- _mcp_registry[name] = mod
116
- if supabase:
117
- try:
118
- supabase.table("mcps").upsert({
119
- "name": name, "code": code, "description": description
120
- }).execute()
121
- except Exception:
122
- pass
123
- tulis_log(f"✅ MCP '{name}' installed")
124
- return True, f"✅ MCP '{name}' berhasil diinstall!"
125
- except Exception as e:
126
- tulis_log(f"❌ MCP '{name}' install error: {e}")
127
- return False, f"❌ MCP '{name}' gagal: {str(e)}"
128
-
129
- def list_mcps():
130
- mcps = []
131
- for fname in os.listdir(MCP_DIR):
132
- if fname.endswith(".py"):
133
- mcps.append(fname.replace(".py", ""))
134
- return mcps
135
-
136
- def load_all_mcps():
137
- count = 0
138
- for fname in os.listdir(MCP_DIR):
139
- if fname.endswith(".py"):
140
- name = fname.replace(".py", "")
141
- if name not in _mcp_registry:
142
- load_mcp(name)
143
- count += 1
144
- if supabase:
145
- try:
146
- res = supabase.table("mcps").select("name, code").execute()
147
- if res.data:
148
- for row in res.data:
149
- name = row["name"]
150
- path = f"{MCP_DIR}/{name}.py"
151
- if not os.path.exists(path):
152
- with open(path, "w") as f:
153
- f.write(row["code"])
154
- load_mcp(name)
155
- count += 1
156
- except Exception as e:
157
- tulis_log(f"Load MCPs Supabase error: {e}")
158
- if count:
159
- tulis_log(f"✅ {count} MCPs loaded")
160
-
161
- def call_mcp_function(mcp_name, func_name, args):
162
- mod = get_mcp(mcp_name)
163
- if not mod:
164
- return f"❌ MCP '{mcp_name}' belum diinstall."
165
- func = getattr(mod, func_name, None)
166
- if not func:
167
- return f"❌ Fungsi '{func_name}' tidak ada di MCP '{mcp_name}'"
168
- try:
169
- if isinstance(args, dict):
170
- return func(**args)
171
- return func(args)
172
- except Exception as e:
173
- return f"❌ Error MCP {mcp_name}.{func_name}: {str(e)}"
174
-
175
- # =================== MULTI-PROVIDER LLM ===================
176
-
177
- LLM_PROVIDERS = [
178
- {
179
- "name": "groq",
180
- "env": "GROQ_API_KEY",
181
- "url": "https://api.groq.com/openai/v1/chat/completions",
182
- "models": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant"],
183
- "headers": {},
184
- },
185
- {
186
- "name": "groq_reasoning",
187
- "env": "GROQ_API_KEY",
188
- "url": "https://api.groq.com/openai/v1/chat/completions",
189
- "models": ["qwq-32b", "deepseek-r1-distill-llama-70b"],
190
- "headers": {},
191
- "reasoning": True,
192
- },
193
- {
194
- "name": "gemini",
195
- "env": "GEMINI_API_KEY",
196
- "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
197
- "models": ["gemini-2.0-flash", "gemini-2.0-flash-thinking-exp"],
198
- "headers": {},
199
- },
200
- {
201
- "name": "openrouter",
202
- "env": "OPENROUTER_API_KEY",
203
- "url": "https://openrouter.ai/api/v1/chat/completions",
204
- "models": ["deepseek/deepseek-r1:free", "meta-llama/llama-3.3-70b-instruct:free", "qwen/qwq-32b:free"],
205
- "headers": {
206
- "HTTP-Referer": "https://huggingface.co/spaces/bakulkrupuk2025/openclaw-neo-bot",
207
- "X-Title": "OpenClaw"
208
- },
209
- },
210
- {
211
- "name": "cerebras",
212
- "env": "CEREBRAS_API_KEY",
213
- "url": "https://api.cerebras.ai/v1/chat/completions",
214
- "models": ["llama-3.3-70b", "llama3.1-8b"],
215
- "headers": {},
216
- },
217
- {
218
- "name": "together",
219
- "env": "TOGETHER_API_KEY",
220
- "url": "https://api.together.xyz/v1/chat/completions",
221
- "models": ["meta-llama/Llama-3.3-70B-Instruct-Turbo-Free", "deepseek-ai/DeepSeek-R1-Distill-Llama-70B-free"],
222
- "headers": {},
223
- },
224
- {
225
- "name": "mistral",
226
- "env": "MISTRAL_API_KEY",
227
- "url": "https://api.mistral.ai/v1/chat/completions",
228
- "models": ["mistral-small-latest"],
229
- "headers": {},
230
- },
231
- {
232
- "name": "cohere",
233
- "env": "COHERE_API_KEY",
234
- "url": "https://api.cohere.com/compatibility/v1/chat/completions",
235
- "models": ["command-r-plus"],
236
- "headers": {},
237
- },
238
- ]
239
-
240
- _cooldown = {}
241
-
242
- def _cooldown_set(name):
243
- _cooldown[name] = time.time() + 60
244
-
245
- def _cooldown_ok(name):
246
- if name in _cooldown:
247
- if time.time() < _cooldown[name]:
248
- return False
249
- del _cooldown[name]
250
- return True
251
-
252
- def get_active_providers(reasoning=False):
253
- result = []
254
- for p in LLM_PROVIDERS:
255
- if reasoning and not p.get("reasoning"):
256
- continue
257
- base_env = p["env"]
258
- for i in range(1, 6):
259
- env_name = base_env if i == 1 else f"{base_env}_{i}"
260
- key = os.environ.get(env_name, "")
261
- if not key:
262
- continue
263
- slot = p["name"] if i == 1 else f"{p['name']}_{i}"
264
- if not _cooldown_ok(slot):
265
- continue
266
- entry = dict(p)
267
- entry["name"] = slot
268
- entry["api_key"] = key
269
- result.append(entry)
270
- return result
271
-
272
- REASONING_WORDS = [
273
- "analisis","strategi","kenapa","why","jelaskan","explain",
274
- "bagaimana","compare","bandingkan","evaluasi","pertimbangkan",
275
- "rekomendasi","solusi","rencana","planning","pendapat","pikir",
276
- "perbedaan","keuntungan","kekurangan","review","audit","riset mendalam"
277
- ]
278
-
279
- def needs_reasoning(text):
280
- t = text.lower()
281
- return len(text) > 100 or any(w in t for w in REASONING_WORDS)
282
-
283
- def call_llm(messages, tools=None, max_tokens=2048, temperature=0.5, reasoning=False):
284
- providers = get_active_providers(reasoning=reasoning)
285
- if not providers:
286
- providers = get_active_providers(reasoning=False)
287
- if not providers:
288
- raise Exception("❌ Tidak ada API key aktif. Ketik 'cek setup' untuk panduan.")
289
-
290
- for p in providers:
291
- for model in p["models"]:
292
- try:
293
- headers = {
294
- "Content-Type": "application/json",
295
- "Authorization": f"Bearer {p['api_key']}",
296
- **p["headers"]
297
- }
298
- payload = {
299
- "model": model,
300
- "messages": messages,
301
- "max_tokens": max_tokens,
302
- "temperature": temperature,
303
- }
304
- if tools and not reasoning:
305
- payload["tools"] = tools
306
- payload["tool_choice"] = "auto"
307
-
308
- r = requests.post(p["url"], headers=headers, json=payload, timeout=45)
309
- if r.status_code == 200:
310
- tulis_log(f"{'🧠' if reasoning else '✅'} LLM: {p['name']}/{model}")
311
- return r.json(), p["name"]
312
- elif r.status_code in (429, 503):
313
- _cooldown_set(p["name"])
314
- break
315
- except requests.Timeout:
316
- continue
317
- except Exception as e:
318
- tulis_log(f"LLM error {p['name']}: {e}")
319
-
320
- raise Exception("❌ Semua provider gagal sementara.")
321
-
322
- # =================== MEMORY ===================
323
-
324
- def baca_memory():
325
- with open(MEMORY_FILE) as f:
326
- return f.read()
327
-
328
- def tulis_memory(info):
329
- with open(MEMORY_FILE, "a") as f:
330
- f.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M')}] {info}\n")
331
- if supabase:
332
- try:
333
- supabase.table("memory").insert({"info": info}).execute()
334
- except Exception:
335
- pass
336
-
337
- # =================== SKILLS ===================
338
-
339
- def load_skills():
340
- skills = {}
341
- for name in os.listdir(SKILLS_DIR):
342
- path = f"{SKILLS_DIR}/{name}/SKILL.md"
343
- if os.path.exists(path):
344
- with open(path) as f:
345
- skills[name] = f.read()
346
- return skills
347
-
348
- def skills_for_prompt():
349
- skills = load_skills()
350
- if not skills:
351
- return ""
352
- lines = ["\n## Skills Tersedia:"]
353
- for name, content in skills.items():
354
- desc = ""
355
- for line in content.split("\n"):
356
- if "description:" in line:
357
- desc = line.replace("description:", "").strip()
358
- break
359
- lines.append(f"- **{name}**: {desc}")
360
- return "\n".join(lines)
361
-
362
- def install_skill(name, content):
363
- skill_dir = f"{SKILLS_DIR}/{name}"
364
- os.makedirs(skill_dir, exist_ok=True)
365
- with open(f"{skill_dir}/SKILL.md", "w") as f:
366
- f.write(content)
367
- if supabase:
368
- try:
369
- supabase.table("skills").upsert({"name": name, "content": content}).execute()
370
- except Exception:
371
- pass
372
- return f"✅ Skill '{name}' installed!"
373
-
374
- def load_skills_from_supabase():
375
- if not supabase:
376
- return
377
- try:
378
- res = supabase.table("skills").select("*").execute()
379
- for row in (res.data or []):
380
- d = f"{SKILLS_DIR}/{row['name']}"
381
- os.makedirs(d, exist_ok=True)
382
- with open(f"{d}/SKILL.md", "w") as f:
383
- f.write(row["content"])
384
- if res.data:
385
- tulis_log(f"✅ {len(res.data)} skills dari Supabase")
386
- except Exception as e:
387
- tulis_log(f"Skills Supabase error: {e}")
388
-
389
- # =================== TASKS ===================
390
-
391
- def baca_tasks():
392
- with open(TASKS_FILE) as f:
393
- return json.load(f)
394
-
395
- def simpan_tasks(tasks):
396
- with open(TASKS_FILE, "w") as f:
397
- json.dump(tasks, f, indent=2, ensure_ascii=False)
398
 
399
- def tambah_task(judul, deskripsi):
400
- tasks = baca_tasks()
401
- task = {"id": len(tasks)+1, "judul": judul, "deskripsi": deskripsi,
402
- "status": "pending", "dibuat": datetime.now().strftime("%Y-%m-%d %H:%M"), "selesai": None}
403
- tasks.append(task)
404
- simpan_tasks(tasks)
405
- if supabase:
406
- try:
407
- supabase.table("tasks").insert(task).execute()
408
- except Exception:
409
- pass
410
- return task
411
-
412
- def update_task(task_id, status):
413
- tasks = baca_tasks()
414
- for t in tasks:
415
- if t["id"] == int(task_id):
416
- t["status"] = status
417
- if status == "selesai":
418
- t["selesai"] = datetime.now().strftime("%Y-%m-%d %H:%M")
419
- simpan_tasks(tasks)
420
-
421
- # =================== CONFIGS ===================
422
-
423
- CONFIGURABLE_KEYS = {
424
- "GROQ_API_KEY": {"label": "Groq API Key", "url": "https://console.groq.com"},
425
- "GEMINI_API_KEY": {"label": "Gemini API Key", "url": "https://aistudio.google.com"},
426
- "OPENROUTER_API_KEY": {"label": "OpenRouter API Key", "url": "https://openrouter.ai"},
427
- "CEREBRAS_API_KEY": {"label": "Cerebras API Key", "url": "https://cloud.cerebras.ai"},
428
- "TOGETHER_API_KEY": {"label": "Together AI Key", "url": "https://api.together.ai"},
429
- "MISTRAL_API_KEY": {"label": "Mistral API Key", "url": "https://console.mistral.ai"},
430
- "BRAVE_API_KEY": {"label": "Brave Search Key", "url": "https://api.search.brave.com"},
431
- "GOOGLE_TOKEN": {"label": "Google OAuth Token", "url": ""},
432
- "GITHUB_TOKEN": {"label": "GitHub Token", "url": "https://github.com/settings/tokens"},
433
- "YOUTUBE_API_KEY": {"label": "YouTube API Key", "url": "https://console.cloud.google.com"},
434
- "TELEGRAM_BOT_TOKEN": {"label": "Telegram Bot Token", "url": "https://t.me/BotFather"},
435
- "N8N_URL": {"label": "N8N URL", "url": "https://n8n.cloud"},
436
- "N8N_API_KEY": {"label": "N8N API Key", "url": "https://n8n.cloud"},
437
- "SUI_PRIVATE_KEY": {"label": "SUI Wallet Key", "url": "https://suiwallet.com"},
438
- "BINANCE_API_KEY": {"label": "Binance API Key", "url": "https://www.binance.com"},
439
  }
440
 
441
- def save_config(key, value):
442
- os.environ[key] = value
443
- if supabase:
444
- try:
445
- supabase.table("configs").upsert({"key": key, "value": value}).execute()
446
- except Exception:
447
- pass
448
- tulis_log(f"✅ Config '{key}' disimpan")
449
-
450
- def load_configs():
451
- if not supabase:
452
- return
453
- try:
454
- res = supabase.table("configs").select("*").execute()
455
- for row in (res.data or []):
456
- os.environ[row["key"]] = row["value"]
457
- if res.data:
458
- tulis_log(f"✅ {len(res.data)} configs loaded")
459
- except Exception as e:
460
- tulis_log(f"Configs error: {e}")
461
-
462
- def detect_api_key(text):
463
- text = text.strip()
464
- patterns = {
465
- "GROQ_API_KEY": ["gsk_"],
466
- "GEMINI_API_KEY": ["AIza"],
467
- "OPENROUTER_API_KEY": ["sk-or-"],
468
- "CEREBRAS_API_KEY": ["csk-"],
469
- "GITHUB_TOKEN": ["ghp_", "github_pat_"],
470
- "TELEGRAM_BOT_TOKEN": [":AAF", ":AAE", ":AAH"],
471
- }
472
- for key, prefixes in patterns.items():
473
- for prefix in prefixes:
474
- if text.lower().startswith(prefix.lower()):
475
- return key, text
476
- return None, None
477
-
478
- # =================== BOTS ===================
479
-
480
- _bots = {}
481
-
482
- def load_bots():
483
- if supabase:
484
- try:
485
- res = supabase.table("bots").select("*").execute()
486
- for row in (res.data or []):
487
- _bots[row["slug"]] = row
488
- if res.data:
489
- tulis_log(f"✅ {len(res.data)} bots loaded")
490
- except Exception:
491
- pass
492
-
493
- def save_bot(slug, name, description, system_prompt, warna="#0369a1"):
494
- slug = slug.lower().replace(" ", "-")
495
- html = f"""<!DOCTYPE html><html><head><title>{name}</title>
496
- <meta name="viewport" content="width=device-width,initial-scale=1">
497
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
498
- <style>*{{box-sizing:border-box;margin:0;padding:0}}body{{font-family:'Space Grotesk',sans-serif;background:#060a15;color:#e2e8f0;height:100vh;display:flex;justify-content:center;align-items:center}}.c{{width:96%;max-width:700px;height:95vh;background:rgba(10,18,40,0.95);border-radius:20px;display:flex;flex-direction:column;overflow:hidden;border:1px solid rgba(99,179,237,0.15);box-shadow:0 0 80px rgba(56,189,248,0.06)}}.h{{background:linear-gradient(135deg,{warna},{warna}cc);padding:15px 20px;display:flex;align-items:center;gap:12px}}.h h1{{font-size:16px;font-weight:700;letter-spacing:1.5px}}.back{{margin-left:auto;font-size:11px;color:rgba(255,255,255,0.7);text-decoration:none;padding:5px 12px;border:1px solid rgba(255,255,255,0.25);border-radius:8px;backdrop-filter:blur(4px)}}.msgs{{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px}}.msg{{padding:12px 16px;border-radius:14px;max-width:88%;word-wrap:break-word;white-space:pre-wrap;line-height:1.65;font-size:13.5px}}.user{{align-self:flex-end;background:linear-gradient(135deg,#1e40af,#1d4ed8)}}.bot{{align-self:flex-start;background:rgba(15,32,64,0.8);border:1px solid rgba(99,179,237,0.12)}}.ia{{padding:12px;background:rgba(2,8,23,0.8);display:flex;gap:8px;border-top:1px solid rgba(30,58,95,0.5)}}textarea{{flex:1;padding:11px 14px;border-radius:10px;border:1px solid rgba(30,58,95,0.6);background:rgba(10,18,40,0.8);color:white;outline:none;font-size:13px;resize:none;height:44px;font-family:inherit}}button{{background:linear-gradient(135deg,{warna},{warna}cc);border:none;padding:0 18px;color:white;border-radius:10px;cursor:pointer;font-weight:700;font-size:13px}}</style></head>
499
- <body><div class="c"><div class="h"><span style="font-size:22px">🤖</span><h1>{name.upper()}</h1><a href="/" class="back">← Openclaw</a></div>
500
- <div id="cb" class="msgs"><div class="msg bot">👋 Halo! Aku <strong>{name}</strong>. {description}</div></div>
501
- <div class="ia"><textarea id="inp" placeholder="Ketik pesan..." onkeypress="if(event.key==='Enter'&&!event.shiftKey){{event.preventDefault();send()}}"></textarea><button id="btn" onclick="send()">Kirim</button></div></div>
502
- <script>async function send(){{const inp=document.getElementById('inp'),btn=document.getElementById('btn'),msg=inp.value.trim();if(!msg)return;add('user',msg);inp.value='';btn.disabled=true;const th=Object.assign(document.createElement('div'),{{className:'msg bot',textContent:'⏳ Memproses...'}});document.getElementById('cb').appendChild(th);try{{const r=await fetch('/bots/{slug}/chat',{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify({{message:msg}})}});const d=await r.json();th.remove();add('bot',d.balasan);}}catch(e){{th.remove();add('bot','❌ '+e.message);}}btn.disabled=false;inp.focus();}}function add(c,t){{const cb=document.getElementById('cb'),d=Object.assign(document.createElement('div'),{{className:'msg '+c,textContent:t}});cb.appendChild(d);cb.scrollTop=cb.scrollHeight;}}</script></body></html>"""
503
-
504
- bot = {"slug": slug, "name": name, "description": description,
505
- "html": html, "system_prompt": system_prompt}
506
- _bots[slug] = bot
507
- if supabase:
508
- try:
509
- supabase.table("bots").upsert(bot).execute()
510
- except Exception:
511
- pass
512
- return f"✅ Bot '{name}' dibuat! Akses di: /bots/{slug}"
513
-
514
- # =================== CORE TOOLS ===================
515
-
516
- def tool_web_search(query):
517
- brave = os.environ.get("BRAVE_API_KEY", "")
518
- if brave:
519
- try:
520
- r = requests.get(
521
- "https://api.search.brave.com/res/v1/web/search",
522
- params={"q": query, "count": 5},
523
- headers={"Accept": "application/json", "X-Subscription-Token": brave},
524
- timeout=10
525
- )
526
- items = r.json().get("web", {}).get("results", [])
527
- return "\n\n".join([f"• {i['title']}\n {i.get('description','')}\n {i['url']}" for i in items[:5]])
528
- except Exception:
529
- pass
530
- try:
531
- r = requests.get(
532
- f"https://html.duckduckgo.com/html/?q={requests.utils.quote(query)}",
533
- headers={"User-Agent": "Mozilla/5.0"}, timeout=10
534
- )
535
- soup = BeautifulSoup(r.text, "html.parser")
536
- results = []
537
- for t, s in zip(soup.select(".result__title")[:5], soup.select(".result__snippet")[:5]):
538
- results.append(f"• {t.get_text(strip=True)}: {s.get_text(strip=True)}")
539
- return "\n".join(results) or "Tidak ada hasil."
540
- except Exception as e:
541
- return f"Search gagal: {e}"
542
-
543
- def tool_baca_url(url):
544
- try:
545
- r = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=15)
546
- soup = BeautifulSoup(r.text, "html.parser")
547
- for tag in soup(["script", "style", "nav", "footer"]):
548
- tag.decompose()
549
- return soup.get_text(separator="\n", strip=True)[:5000]
550
- except Exception as e:
551
- return f"Gagal: {e}"
552
-
553
- def tool_baca_file(path):
554
- try:
555
- full = f"{WORKSPACE}/{path}" if not path.startswith("/") else path
556
- with open(full) as f:
557
- return f.read()[:4000]
558
- except Exception as e:
559
- return f"Gagal: {e}"
560
-
561
- def tool_tulis_file(path, content):
562
- try:
563
- full = f"{WORKSPACE}/{path}" if not path.startswith("/") else path
564
- os.makedirs(os.path.dirname(full) or WORKSPACE, exist_ok=True)
565
- with open(full, "w") as f:
566
- f.write(content)
567
- return f"✅ File disimpan: {path}"
568
- except Exception as e:
569
- return f"Gagal: {e}"
570
-
571
- def tool_pip_install(package):
572
- try:
573
- import subprocess
574
- r = subprocess.run(
575
- ["pip", "install", package, "-q", "--break-system-packages"],
576
- capture_output=True, text=True, timeout=60
577
- )
578
- if r.returncode == 0:
579
- return f"✅ {package} berhasil diinstall!"
580
- return f"❌ Gagal install {package}: {r.stderr[:300]}"
581
- except Exception as e:
582
- return f"❌ Error: {e}"
583
-
584
- # =================== RESPONSE CLEANER ===================
585
-
586
- def bersihkan_balasan(teks):
587
- """
588
- Hapus log teknis, kode internal, dan noise dari response AI
589
- sebelum dikirim ke user.
590
- """
591
- if not teks:
592
- return teks
593
-
594
- lines = teks.split('\n')
595
- clean = []
596
- skip_block = False
597
-
598
- for line in lines:
599
- # Skip baris yang terlihat seperti log teknis
600
- if re.match(r'^\[.*?\]\s*(✅|❌|⚠️|🔧)', line):
601
- continue
602
- if re.match(r'^(DEBUG|INFO|WARNING|ERROR|CRITICAL):', line):
603
- continue
604
- # Skip traceback Python
605
- if 'Traceback (most recent call last):' in line:
606
- skip_block = True
607
- if skip_block:
608
- if line.strip() == '' and len(clean) > 0:
609
- skip_block = False
610
- continue
611
- # Skip baris dengan path file Python
612
- if re.match(r'^\s+File ".*\.py", line \d+', line):
613
- continue
614
- clean.append(line)
615
-
616
- result = '\n'.join(clean).strip()
617
- # Hapus multiple blank lines berturut-turut
618
- result = re.sub(r'\n{3,}', '\n\n', result)
619
- return result
620
-
621
- # =================== TOOLS DEFINITION ===================
622
-
623
- TOOLS = [
624
- {"type": "function", "function": {
625
- "name": "web_search",
626
- "description": "Cari informasi terbaru di internet.",
627
- "parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
628
- }},
629
- {"type": "function", "function": {
630
- "name": "baca_url",
631
- "description": "Baca konten dari URL/website.",
632
- "parameters": {"type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"]}
633
- }},
634
- {"type": "function", "function": {
635
- "name": "baca_file",
636
- "description": "Baca isi file dari workspace.",
637
- "parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}
638
- }},
639
- {"type": "function", "function": {
640
- "name": "tulis_file",
641
- "description": "Tulis/simpan file ke workspace.",
642
- "parameters": {"type": "object", "properties": {
643
- "path": {"type": "string"}, "content": {"type": "string"}
644
- }, "required": ["path", "content"]}
645
- }},
646
- {"type": "function", "function": {
647
- "name": "pip_install",
648
- "description": "Install Python package yang dibutuhkan MCP baru.",
649
- "parameters": {"type": "object", "properties": {"package": {"type": "string"}}, "required": ["package"]}
650
- }},
651
- {"type": "function", "function": {
652
- "name": "simpan_memory",
653
- "description": "Simpan info penting ke memory permanen.",
654
- "parameters": {"type": "object", "properties": {"info": {"type": "string"}}, "required": ["info"]}
655
- }},
656
- {"type": "function", "function": {
657
- "name": "lihat_memory",
658
- "description": "Lihat semua memory.",
659
- "parameters": {"type": "object", "properties": {}}
660
- }},
661
- {"type": "function", "function": {
662
- "name": "buat_task",
663
- "description": "Buat task baru.",
664
- "parameters": {"type": "object", "properties": {
665
- "judul": {"type": "string"}, "deskripsi": {"type": "string"}
666
- }, "required": ["judul", "deskripsi"]}
667
- }},
668
- {"type": "function", "function": {
669
- "name": "lihat_tasks",
670
- "description": "Lihat semua task.",
671
- "parameters": {"type": "object", "properties": {}}
672
- }},
673
- {"type": "function", "function": {
674
- "name": "selesaikan_task",
675
- "description": "Tandai task selesai.",
676
- "parameters": {"type": "object", "properties": {"task_id": {"type": "string"}}, "required": ["task_id"]}
677
- }},
678
- {"type": "function", "function": {
679
- "name": "install_skill",
680
- "description": "Install skill baru dari konten SKILL.md.",
681
- "parameters": {"type": "object", "properties": {
682
- "name": {"type": "string"}, "content": {"type": "string"}
683
- }, "required": ["name", "content"]}
684
- }},
685
- {"type": "function", "function": {
686
- "name": "lihat_skills",
687
- "description": "Lihat semua skill yang terinstall.",
688
- "parameters": {"type": "object", "properties": {}}
689
- }},
690
- {"type": "function", "function": {
691
- "name": "install_mcp",
692
- "description": "Install MCP baru.",
693
- "parameters": {"type": "object", "properties": {
694
- "name": {"type": "string"},
695
- "code": {"type": "string"},
696
- "description": {"type": "string"}
697
- }, "required": ["name", "code"]}
698
- }},
699
- {"type": "function", "function": {
700
- "name": "lihat_mcps",
701
- "description": "Lihat semua MCP yang terinstall.",
702
- "parameters": {"type": "object", "properties": {}}
703
- }},
704
- {"type": "function", "function": {
705
- "name": "panggil_mcp",
706
- "description": "Panggil fungsi dari MCP yang sudah terinstall.",
707
- "parameters": {"type": "object", "properties": {
708
- "mcp_name": {"type": "string"},
709
- "func_name": {"type": "string"},
710
- "args": {"type": "object"}
711
- }, "required": ["mcp_name", "func_name"]}
712
- }},
713
- {"type": "function", "function": {
714
- "name": "cek_setup",
715
- "description": "Cek status semua API key.",
716
- "parameters": {"type": "object", "properties": {}}
717
- }},
718
- {"type": "function", "function": {
719
- "name": "simpan_key",
720
- "description": "Simpan API key.",
721
- "parameters": {"type": "object", "properties": {
722
- "key_name": {"type": "string"}, "value": {"type": "string"}
723
- }, "required": ["key_name", "value"]}
724
- }},
725
- {"type": "function", "function": {
726
- "name": "buat_bot",
727
- "description": "Buat bot baru dengan dashboard sendiri.",
728
- "parameters": {"type": "object", "properties": {
729
- "slug": {"type": "string"}, "name": {"type": "string"},
730
- "description": {"type": "string"}, "system_prompt": {"type": "string"},
731
- "warna": {"type": "string", "default": "#0369a1"}
732
- }, "required": ["slug", "name", "description", "system_prompt"]}
733
- }},
734
- {"type": "function", "function": {
735
- "name": "lihat_bots",
736
- "description": "Lihat semua bot yang sudah dibuat.",
737
- "parameters": {"type": "object", "properties": {}}
738
- }},
739
- {"type": "function", "function": {
740
- "name": "cek_provider",
741
- "description": "Cek status semua AI provider.",
742
- "parameters": {"type": "object", "properties": {}}
743
- }},
744
- ]
745
-
746
- # =================== TOOL EXECUTOR ===================
747
-
748
- def run_tool(name, args):
749
- if name == "web_search": return tool_web_search(args.get("query",""))
750
- elif name == "baca_url": return tool_baca_url(args.get("url",""))
751
- elif name == "baca_file": return tool_baca_file(args.get("path",""))
752
- elif name == "tulis_file": return tool_tulis_file(args.get("path",""), args.get("content",""))
753
- elif name == "pip_install": return tool_pip_install(args.get("package",""))
754
- elif name == "simpan_memory":
755
- tulis_memory(args.get("info",""))
756
- return "✅ Memory disimpan"
757
- elif name == "lihat_memory": return baca_memory()
758
- elif name == "buat_task":
759
- t = tambah_task(args.get("judul",""), args.get("deskripsi",""))
760
- return f"✅ Task #{t['id']} dibuat"
761
- elif name == "lihat_tasks":
762
- tasks = baca_tasks()
763
- if not tasks: return "Belum ada task."
764
- return "\n".join([f"{'✅' if t['status']=='selesai' else '⏳'} #{t['id']} {t['judul']} [{t['status']}]" for t in tasks])
765
- elif name == "selesaikan_task":
766
- update_task(args.get("task_id","0"), "selesai")
767
- return "✅ Task selesai"
768
- elif name == "install_skill": return install_skill(args.get("name",""), args.get("content",""))
769
- elif name == "lihat_skills":
770
- skills = load_skills()
771
- if not skills: return "Belum ada skill."
772
- return "\n".join([f"⚡ {n}" for n in skills.keys()])
773
- elif name == "install_mcp":
774
- ok, msg = install_mcp(args.get("name",""), args.get("code",""), args.get("description",""))
775
- return msg
776
- elif name == "lihat_mcps":
777
- mcps = list_mcps()
778
- return "\n".join([f"🔌 {m}" for m in mcps]) if mcps else "Belum ada MCP terinstall."
779
- elif name == "panggil_mcp":
780
- return call_mcp_function(args.get("mcp_name",""), args.get("func_name",""), args.get("args",{}))
781
- elif name == "cek_setup":
782
- ada = [f"✅ {v['label']}" for k,v in CONFIGURABLE_KEYS.items() if os.environ.get(k)]
783
- blm = [f"❌ {v['label']} — {v['url']}" for k,v in CONFIGURABLE_KEYS.items() if not os.environ.get(k)]
784
- return "📊 STATUS SETUP\n\n" + "\n".join(ada) + "\n\n❌ BELUM:\n" + "\n".join(blm[:5])
785
- elif name == "simpan_key":
786
- save_config(args.get("key_name",""), args.get("value",""))
787
- return f"✅ {args.get('key_name')} disimpan!"
788
- elif name == "buat_bot":
789
- return save_bot(args.get("slug",""), args.get("name",""), args.get("description",""),
790
- args.get("system_prompt",""), args.get("warna","#0369a1"))
791
- elif name == "lihat_bots":
792
- return "\n".join([f"🤖 {b['name']} → /bots/{b['slug']}" for b in _bots.values()]) or "Belum ada bot."
793
- elif name == "cek_provider":
794
- providers = get_active_providers()
795
- return "\n".join([f"✅ {p['name']}" for p in providers]) or "❌ Tidak ada provider aktif."
796
- return f"❓ Tool '{name}' tidak dikenal."
797
-
798
- # =================== AGENT ===================
799
-
800
- _histories = {}
801
-
802
- def run_agent(pesan, history=None):
803
- if history is None:
804
- history = []
805
-
806
- memory = baca_memory()[-1500:]
807
- skills = skills_for_prompt()
808
- mcps_list = list_mcps()
809
- mcp_info = f"\n\n## MCPs Terinstall: {', '.join(mcps_list)}" if mcps_list else "\n\n## MCPs: Belum ada."
810
- pending = [t for t in baca_tasks() if t["status"] == "pending"]
811
- task_info = f"\n\n## {len(pending)} Task Pending" if pending else ""
812
- mode = needs_reasoning(pesan)
813
-
814
- reasoning_ctx = ""
815
- if mode:
816
- try:
817
- r_msgs = [
818
- {"role": "system", "content": "Kamu sistem reasoning internal. Analisis mendalam pertanyaan ini. Jawab dalam bahasa Indonesia."},
819
- {"role": "user", "content": pesan}
820
- ]
821
- r_data, r_prov = call_llm(r_msgs, reasoning=True, max_tokens=3000, temperature=0.6)
822
- raw = r_data["choices"][0]["message"].get("content", "")
823
- match = re.search(r'</think>(.*)', raw, re.DOTALL)
824
- reasoning_ctx = match.group(1).strip() if match else raw[:1500]
825
- tulis_log(f"🧠 Reasoning done [{r_prov}]")
826
- except Exception as e:
827
- tulis_log(f"Reasoning skip: {e}")
828
-
829
- system = f"""Kamu adalah Openclaw — AI agent cerdas, ramah, dan proaktif.
830
-
831
- ## ATURAN PENTING:
832
- - JANGAN tampilkan kode Python, log teknis, traceback, atau path file ke user
833
- - JANGAN tampilkan proses internal seperti "[2024-01-01] ✅ MCP loaded"
834
- - Sampaikan hasil dengan bahasa natural yang mudah dimengerti
835
- - Jawab seperti asisten yang cerdas dan helpful, bukan seperti terminal/console
836
- - Jika ada error teknis, sampaikan dengan bahasa sederhana tanpa kode error
837
 
838
- ## Cara Berpikir:
839
- 1. Pahami apa yang benar-benar dibutuhkan user
840
- 2. Analisis konteks dan informasi yang tersedia
841
- 3. Pertimbangkan opsi terbaik
842
- 4. Eksekusi dengan tools yang tepat
843
- 5. Laporkan hasil dengan bahasa yang bersih dan jelas
844
 
845
- ## Arsitektur:
846
- - Server = manager ringan, semua MCP di luar
847
- - MCPs = modul terpisah di /tmp/openclaw/mcp/
848
- - Skills = instruksi di /tmp/openclaw/skills/
849
- - Semua tersimpan permanen di Supabase
850
 
851
- ## Memory:
852
- {memory}
853
- {task_info}
854
- {mcp_info}
855
- {skills}
856
-
857
- ## Aturan Operasional:
858
- 1. Task baru → buat_task dulu
859
- 2. Info terbaru → web_search dulu
860
- 3. Info penting → simpan_memory
861
- 4. MCP belum ada → install SEKALI saja
862
- 5. API key di-paste user → langsung simpan_key
863
- 6. LANGSUNG eksekusi, jangan banyak tanya
864
- 7. Jawab bahasa yang sama dengan user (Indonesia/English)
865
- 8. Jika install MCP → test langsung setelahnya
866
-
867
- ## Mode: {"🧠 DEEP REASONING" if mode else "⚡ FAST"}"""
868
-
869
- messages = [{"role": "system", "content": system}]
870
- for h in history[-10:]:
871
- messages.append(h)
872
-
873
- user_content = f"{pesan}\n\n[Konteks reasoning]:\n{reasoning_ctx}" if reasoning_ctx else pesan
874
- messages.append({"role": "user", "content": user_content})
875
-
876
- tools_used = []
877
- for _ in range(6):
878
- try:
879
- data, provider = call_llm(messages, tools=TOOLS, max_tokens=2048)
880
- choice = data["choices"][0]
881
- msg = choice["message"]
882
- reason = choice["finish_reason"]
883
- messages.append(msg)
884
-
885
- if reason == "tool_calls" and msg.get("tool_calls"):
886
- for tc in msg["tool_calls"]:
887
- tname = tc["function"]["name"]
888
- try:
889
- targs = json.loads(tc["function"]["arguments"])
890
- except Exception:
891
- targs = {}
892
- tools_used.append(tname)
893
- tulis_log(f"Tool: {tname}")
894
- hasil = run_tool(tname, targs)
895
- messages.append({"role": "tool", "tool_call_id": tc["id"], "content": str(hasil)})
896
- else:
897
- balasan = msg.get("content", "...")
898
- # Bersihkan log teknis sebelum kirim ke user
899
- balasan = bersihkan_balasan(balasan)
900
- new_history = history + [
901
- {"role": "user", "content": pesan},
902
- {"role": "assistant", "content": balasan}
903
- ]
904
- tulis_log(f"Chat [{provider}]: {pesan[:50]}")
905
- return balasan, tools_used, new_history, mode
906
-
907
- except Exception as e:
908
- tulis_log(f"Agent error: {e}")
909
- return f"❌ {str(e)}", tools_used, history, mode
910
-
911
- return "Selesai.", tools_used, history, mode
912
-
913
- # =================== HTML DASHBOARD NEO ===================
914
-
915
- HTML = """<!DOCTYPE html>
916
- <html lang="id">
917
- <head>
918
- <meta charset="UTF-8">
919
- <title>Openclaw Neo</title>
920
- <meta name="viewport" content="width=device-width,initial-scale=1">
921
- <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
922
- <style>
923
- :root {
924
- --bg: #04080f;
925
- --surface: #080f1e;
926
- --surface2: #0c1628;
927
- --border: rgba(56,189,248,0.1);
928
- --border-bright: rgba(56,189,248,0.25);
929
- --accent: #38bdf8;
930
- --accent2: #818cf8;
931
- --text: #e2e8f0;
932
- --text-muted: #64748b;
933
- --user-bg: linear-gradient(135deg,#1e3a8a,#1d4ed8);
934
- --bot-bg: rgba(12,22,40,0.9);
935
- --glow: 0 0 60px rgba(56,189,248,0.06);
936
- }
937
- * { box-sizing: border-box; margin: 0; padding: 0; }
938
- html, body { height: 100%; overflow: hidden; }
939
- body {
940
- font-family: 'DM Sans', sans-serif;
941
- background: var(--bg);
942
- color: var(--text);
943
- display: flex;
944
- justify-content: center;
945
- align-items: center;
946
- }
947
-
948
- /* Animated background */
949
- body::before {
950
- content: '';
951
- position: fixed;
952
- top: -50%;
953
- left: -50%;
954
- width: 200%;
955
- height: 200%;
956
- background: radial-gradient(ellipse at 20% 50%, rgba(56,189,248,0.03) 0%, transparent 50%),
957
- radial-gradient(ellipse at 80% 20%, rgba(129,140,248,0.03) 0%, transparent 50%);
958
- animation: bgShift 12s ease-in-out infinite alternate;
959
- pointer-events: none;
960
- z-index: 0;
961
- }
962
- @keyframes bgShift {
963
- from { transform: translate(0,0); }
964
- to { transform: translate(2%,1%); }
965
- }
966
-
967
- .app {
968
- position: relative;
969
- z-index: 1;
970
- width: 96%;
971
- max-width: 660px;
972
- height: 97vh;
973
- display: flex;
974
- flex-direction: column;
975
- background: var(--surface);
976
- border-radius: 22px;
977
- overflow: hidden;
978
- border: 1px solid var(--border);
979
- box-shadow: var(--glow), inset 0 1px 0 rgba(255,255,255,0.04);
980
- }
981
-
982
- /* ── Header ── */
983
- .header {
984
- padding: 14px 18px;
985
- background: linear-gradient(135deg, rgba(3,105,161,0.8), rgba(29,78,216,0.6));
986
- border-bottom: 1px solid var(--border);
987
- display: flex;
988
- align-items: center;
989
- gap: 12px;
990
- backdrop-filter: blur(20px);
991
- flex-shrink: 0;
992
- }
993
- .logo {
994
- width: 36px;
995
- height: 36px;
996
- background: rgba(255,255,255,0.1);
997
- border-radius: 10px;
998
- display: flex;
999
- align-items: center;
1000
- justify-content: center;
1001
- font-size: 20px;
1002
- border: 1px solid rgba(255,255,255,0.15);
1003
- }
1004
- .header-title { flex: 1; }
1005
- .header-title h1 {
1006
- font-family: 'Syne', sans-serif;
1007
- font-size: 16px;
1008
- font-weight: 800;
1009
- letter-spacing: 2px;
1010
- background: linear-gradient(90deg, #fff, #38bdf8);
1011
- -webkit-background-clip: text;
1012
- -webkit-text-fill-color: transparent;
1013
- }
1014
- .header-title p {
1015
- font-size: 10px;
1016
- color: rgba(255,255,255,0.45);
1017
- margin-top: 1px;
1018
- letter-spacing: 0.5px;
1019
- }
1020
- .header-badges { display: flex; gap: 6px; }
1021
- .hbadge {
1022
- font-size: 9px;
1023
- font-weight: 600;
1024
- letter-spacing: 0.8px;
1025
- padding: 3px 9px;
1026
- border-radius: 20px;
1027
- border: 1px solid;
1028
- }
1029
- .hbadge.auto { color: #38bdf8; border-color: rgba(56,189,248,0.4); }
1030
- .hbadge.db { color: #34d399; border-color: rgba(52,211,153,0.4); }
1031
-
1032
- /* ── Tabs ── */
1033
- .tabs {
1034
- display: flex;
1035
- background: rgba(4,8,15,0.6);
1036
- border-bottom: 1px solid var(--border);
1037
- flex-shrink: 0;
1038
- overflow-x: auto;
1039
- scrollbar-width: none;
1040
- }
1041
- .tabs::-webkit-scrollbar { display: none; }
1042
- .tab {
1043
- flex: 1;
1044
- min-width: 60px;
1045
- padding: 10px 6px;
1046
- text-align: center;
1047
- font-size: 10px;
1048
- font-weight: 600;
1049
- letter-spacing: 0.3px;
1050
- cursor: pointer;
1051
- color: var(--text-muted);
1052
- border-bottom: 2px solid transparent;
1053
- transition: all 0.2s;
1054
- white-space: nowrap;
1055
- }
1056
- .tab:hover { color: #94a3b8; }
1057
- .tab.on {
1058
- color: var(--accent);
1059
- border-bottom-color: var(--accent);
1060
- background: rgba(56,189,248,0.04);
1061
- }
1062
- .tab-icon { font-size: 13px; display: block; margin-bottom: 2px; }
1063
-
1064
- /* ── Panels ── */
1065
- .panel { display: none; flex: 1; flex-direction: column; overflow: hidden; min-height: 0; }
1066
- .panel.on { display: flex; }
1067
-
1068
- /* ── Chat ── */
1069
- .messages {
1070
- flex: 1;
1071
- overflow-y: auto;
1072
- padding: 16px;
1073
- display: flex;
1074
- flex-direction: column;
1075
- gap: 10px;
1076
- scroll-behavior: smooth;
1077
- }
1078
- .messages::-webkit-scrollbar { width: 3px; }
1079
- .messages::-webkit-scrollbar-track { background: transparent; }
1080
- .messages::-webkit-scrollbar-thumb { background: rgba(56,189,248,0.2); border-radius: 2px; }
1081
-
1082
- .msg {
1083
- padding: 12px 16px;
1084
- border-radius: 16px;
1085
- max-width: 88%;
1086
- word-wrap: break-word;
1087
- white-space: pre-wrap;
1088
- line-height: 1.65;
1089
- font-size: 13.5px;
1090
- animation: msgIn 0.25s ease;
1091
- }
1092
- @keyframes msgIn {
1093
- from { opacity: 0; transform: translateY(6px); }
1094
- to { opacity: 1; transform: translateY(0); }
1095
- }
1096
- .msg.user {
1097
- align-self: flex-end;
1098
- background: var(--user-bg);
1099
- border-bottom-right-radius: 4px;
1100
- box-shadow: 0 4px 20px rgba(29,78,216,0.3);
1101
- }
1102
- .msg.bot {
1103
- align-self: flex-start;
1104
- background: var(--bot-bg);
1105
- border: 1px solid var(--border);
1106
- border-bottom-left-radius: 4px;
1107
- }
1108
- .msg.bot.thinking {
1109
- color: var(--text-muted);
1110
- font-style: italic;
1111
- border-style: dashed;
1112
- }
1113
-
1114
- /* Tool/Mode badges */
1115
- .meta-badge {
1116
- align-self: flex-start;
1117
- font-size: 10px;
1118
- font-weight: 600;
1119
- padding: 3px 10px;
1120
- border-radius: 20px;
1121
- margin-bottom: -4px;
1122
- letter-spacing: 0.3px;
1123
- animation: msgIn 0.2s ease;
1124
- }
1125
- .meta-badge.tool { background: rgba(56,189,248,0.08); color: #38bdf8; border: 1px solid rgba(56,189,248,0.2); }
1126
- .meta-badge.reasoning { background: rgba(129,140,248,0.08); color: #818cf8; border: 1px solid rgba(129,140,248,0.2); }
1127
-
1128
- /* Typing dots */
1129
- .dots span {
1130
- display: inline-block;
1131
- width: 5px; height: 5px;
1132
- background: var(--accent);
1133
- border-radius: 50%;
1134
- margin: 0 1.5px;
1135
- animation: dot 1.2s infinite;
1136
- }
1137
- .dots span:nth-child(2) { animation-delay: 0.2s; }
1138
- .dots span:nth-child(3) { animation-delay: 0.4s; }
1139
- @keyframes dot {
1140
- 0%,60%,100% { transform: translateY(0); opacity: 0.4; }
1141
- 30% { transform: translateY(-5px); opacity: 1; }
1142
- }
1143
-
1144
- /* ── Input Area ── */
1145
- .input-area {
1146
- padding: 12px 14px;
1147
- background: rgba(4,8,15,0.7);
1148
- border-top: 1px solid var(--border);
1149
- display: flex;
1150
- gap: 9px;
1151
- align-items: flex-end;
1152
- backdrop-filter: blur(10px);
1153
- flex-shrink: 0;
1154
- }
1155
- .input-wrap { flex: 1; position: relative; }
1156
- textarea {
1157
- width: 100%;
1158
- padding: 11px 14px;
1159
- background: var(--surface2);
1160
- border: 1px solid var(--border);
1161
- border-radius: 12px;
1162
- color: var(--text);
1163
- font-family: 'DM Sans', sans-serif;
1164
- font-size: 13.5px;
1165
- line-height: 1.5;
1166
- resize: none;
1167
- outline: none;
1168
- min-height: 44px;
1169
- max-height: 120px;
1170
- transition: border-color 0.2s;
1171
- }
1172
- textarea:focus { border-color: var(--border-bright); }
1173
- textarea::placeholder { color: var(--text-muted); }
1174
- .send-btn {
1175
- width: 44px;
1176
- height: 44px;
1177
- background: linear-gradient(135deg, #0369a1, #1d4ed8);
1178
- border: none;
1179
- border-radius: 12px;
1180
- color: white;
1181
- cursor: pointer;
1182
- display: flex;
1183
- align-items: center;
1184
- justify-content: center;
1185
- font-size: 17px;
1186
- transition: all 0.2s;
1187
- flex-shrink: 0;
1188
- box-shadow: 0 4px 15px rgba(29,78,216,0.3);
1189
- }
1190
- .send-btn:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(29,78,216,0.45); }
1191
- .send-btn:disabled { background: var(--surface2); box-shadow: none; transform: none; cursor: not-allowed; }
1192
-
1193
- /* ── Side Panels ── */
1194
- .side-panel {
1195
- flex: 1;
1196
- overflow-y: auto;
1197
- padding: 16px;
1198
- }
1199
- .side-panel::-webkit-scrollbar { width: 3px; }
1200
- .side-panel::-webkit-scrollbar-thumb { background: rgba(56,189,248,0.2); border-radius: 2px; }
1201
-
1202
- .panel-header {
1203
- font-family: 'Syne', sans-serif;
1204
- font-size: 11px;
1205
- font-weight: 700;
1206
- letter-spacing: 1.5px;
1207
- color: var(--text-muted);
1208
- text-transform: uppercase;
1209
- margin-bottom: 12px;
1210
- }
1211
-
1212
- .item-card {
1213
- background: var(--surface2);
1214
- border: 1px solid var(--border);
1215
- border-radius: 12px;
1216
- padding: 12px 14px;
1217
- margin-bottom: 8px;
1218
- transition: border-color 0.2s;
1219
- }
1220
- .item-card:hover { border-color: var(--border-bright); }
1221
- .item-card .item-name {
1222
- font-size: 13px;
1223
- font-weight: 600;
1224
- color: var(--accent);
1225
- margin-bottom: 3px;
1226
- display: flex;
1227
- align-items: center;
1228
- gap: 6px;
1229
- }
1230
- .item-card .item-meta {
1231
- font-size: 11px;
1232
- color: var(--text-muted);
1233
- }
1234
-
1235
- .empty-state {
1236
- text-align: center;
1237
- padding: 40px 20px;
1238
- color: var(--text-muted);
1239
- font-size: 13px;
1240
- }
1241
- .empty-state .empty-icon { font-size: 32px; margin-bottom: 10px; }
1242
-
1243
- .status-dot {
1244
- width: 6px; height: 6px;
1245
- background: #34d399;
1246
- border-radius: 50%;
1247
- display: inline-block;
1248
- box-shadow: 0 0 6px #34d399;
1249
- }
1250
-
1251
- /* Memory/Log panel */
1252
- .log-view {
1253
- flex: 1;
1254
- overflow-y: auto;
1255
- padding: 14px;
1256
- font-family: 'DM Mono', 'Courier New', monospace;
1257
- font-size: 11px;
1258
- color: #475569;
1259
- line-height: 1.8;
1260
- white-space: pre-wrap;
1261
- }
1262
- .log-view::-webkit-scrollbar { width: 3px; }
1263
- .log-view::-webkit-scrollbar-thumb { background: rgba(56,189,248,0.15); }
1264
-
1265
- /* Tasks */
1266
- .task-card {
1267
- background: var(--surface2);
1268
- border: 1px solid var(--border);
1269
- border-radius: 12px;
1270
- padding: 12px 14px;
1271
- margin-bottom: 8px;
1272
- display: flex;
1273
- align-items: flex-start;
1274
- gap: 10px;
1275
- }
1276
- .task-status {
1277
- width: 18px; height: 18px;
1278
- border-radius: 50%;
1279
- border: 2px solid;
1280
- flex-shrink: 0;
1281
- margin-top: 1px;
1282
- display: flex;
1283
- align-items: center;
1284
- justify-content: center;
1285
- font-size: 9px;
1286
- }
1287
- .task-status.done { border-color: #34d399; color: #34d399; }
1288
- .task-status.pending { border-color: #f59e0b; color: #f59e0b; }
1289
- .task-title { font-size: 13px; font-weight: 600; margin-bottom: 2px; }
1290
- .task-desc { font-size: 11px; color: var(--text-muted); }
1291
-
1292
- /* Responsive */
1293
- @media (max-width: 480px) {
1294
- .app { border-radius: 14px; height: 100vh; width: 100%; }
1295
- .tab { padding: 9px 4px; font-size: 9px; }
1296
- }
1297
- </style>
1298
- </head>
1299
- <body>
1300
- <div class="app">
1301
-
1302
- <!-- Header -->
1303
- <div class="header">
1304
- <div class="logo">🦅</div>
1305
- <div class="header-title">
1306
- <h1>OPENCLAW</h1>
1307
- <p>Autonomous AI Agent</p>
1308
- </div>
1309
- <div class="header-badges">
1310
- <span class="hbadge auto">AUTONOMOUS</span>
1311
- <span class="hbadge db">SUPABASE</span>
1312
- </div>
1313
- </div>
1314
-
1315
- <!-- Tabs -->
1316
- <div class="tabs">
1317
- <div class="tab on" onclick="switchTab('chat',this)">
1318
- <span class="tab-icon">💬</span>Chat
1319
- </div>
1320
- <div class="tab" onclick="switchTab('mcp',this)">
1321
- <span class="tab-icon">🔌</span>MCP
1322
- </div>
1323
- <div class="tab" onclick="switchTab('skills',this)">
1324
- <span class="tab-icon">⚡</span>Skills
1325
- </div>
1326
- <div class="tab" onclick="switchTab('tasks',this)">
1327
- <span class="tab-icon">✅</span>Tasks
1328
- </div>
1329
- <div class="tab" onclick="switchTab('memory',this)">
1330
- <span class="tab-icon">🧠</span>Memory
1331
- </div>
1332
- <div class="tab" onclick="switchTab('bots',this)">
1333
- <span class="tab-icon">🤖</span>Bots
1334
- </div>
1335
- </div>
1336
-
1337
- <!-- Chat Panel -->
1338
- <div id="p-chat" class="panel on">
1339
- <div id="messages" class="messages">
1340
- <div class="msg bot">👋 <strong>Openclaw siap!</strong>
1341
-
1342
- Aku bisa bantu kamu:
1343
- • Ketik <strong>"konek Google"</strong> → login Google otomatis
1344
- • Ketik <strong>"install MCP YouTube"</strong> → AI install sendiri
1345
- • Ketik <strong>"cek setup"</strong> → lihat status API key
1346
- • Paste API key di sini → langsung tersimpan
1347
-
1348
- Perintahkan apa saja, AI yang urus semuanya! 🚀</div>
1349
- </div>
1350
- <div class="input-area">
1351
- <div class="input-wrap">
1352
- <textarea id="inp" placeholder="Perintahkan sesuatu..." rows="1"
1353
- onkeypress="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send()}"
1354
- oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,120)+'px'">
1355
- </textarea>
1356
- </div>
1357
- <button class="send-btn" id="btn" onclick="send()">➤</button>
1358
- </div>
1359
- </div>
1360
-
1361
- <!-- MCP Panel -->
1362
- <div id="p-mcp" class="panel">
1363
- <div class="side-panel">
1364
- <div class="panel-header">Modul MCP Terinstall</div>
1365
- <div id="mcp-list"></div>
1366
- </div>
1367
- </div>
1368
-
1369
- <!-- Skills Panel -->
1370
- <div id="p-skills" class="panel">
1371
- <div class="side-panel">
1372
- <div class="panel-header">Skills Aktif</div>
1373
- <div id="skills-list"></div>
1374
- </div>
1375
- </div>
1376
-
1377
- <!-- Tasks Panel -->
1378
- <div id="p-tasks" class="panel">
1379
- <div class="side-panel">
1380
- <div class="panel-header">Daftar Task</div>
1381
- <div id="tasks-list"></div>
1382
- </div>
1383
- </div>
1384
-
1385
- <!-- Memory Panel -->
1386
- <div id="p-memory" class="panel">
1387
- <div class="panel-header" style="padding:14px 14px 0">Catatan Memory</div>
1388
- <div id="memory-content" class="log-view">Memuat...</div>
1389
- </div>
1390
-
1391
- <!-- Bots Panel -->
1392
- <div id="p-bots" class="panel">
1393
- <div class="side-panel">
1394
- <div class="panel-header">Bot Dibuat</div>
1395
- <div id="bots-list"></div>
1396
- </div>
1397
- </div>
1398
-
1399
- </div>
1400
-
1401
- <script>
1402
- var chatHistory = [];
1403
-
1404
- function switchTab(t, el) {
1405
- document.querySelectorAll('.tab').forEach(e => e.classList.remove('on'));
1406
- document.querySelectorAll('.panel').forEach(e => e.classList.remove('on'));
1407
- if (el) el.classList.add('on');
1408
- var p = document.getElementById('p-' + t);
1409
- if (p) p.classList.add('on');
1410
-
1411
- if (t === 'mcp') loadMcps();
1412
- if (t === 'skills') loadSkills();
1413
- if (t === 'tasks') loadTasks();
1414
- if (t === 'memory') loadMemory();
1415
- if (t === 'bots') loadBots();
1416
- }
1417
-
1418
- async function fetchApi(url) {
1419
- try {
1420
- var r = await fetch(url);
1421
- return await r.json();
1422
- } catch(e) {
1423
- return null;
1424
- }
1425
- }
1426
-
1427
- function emptyState(icon, text) {
1428
- return `<div class="empty-state"><div class="empty-icon">${icon}</div>${text}</div>`;
1429
- }
1430
-
1431
- async function loadMcps() {
1432
- var el = document.getElementById('mcp-list');
1433
- var d = await fetchApi('/api/mcps');
1434
- if (!d || !d.items || d.items.length === 0) {
1435
- el.innerHTML = emptyState('🔌', 'Belum ada MCP terinstall.<br><small>Ketik "install MCP YouTube" di chat</small>');
1436
- return;
1437
- }
1438
- el.innerHTML = d.items.map(m => `
1439
- <div class="item-card">
1440
- <div class="item-name"><span class="status-dot"></span>${m}</div>
1441
- <div class="item-meta">Modul Python aktif</div>
1442
- </div>`).join('');
1443
- }
1444
-
1445
- async function loadSkills() {
1446
- var el = document.getElementById('skills-list');
1447
- var d = await fetchApi('/api/skills');
1448
- if (!d || !d.items || d.items.length === 0) {
1449
- el.innerHTML = emptyState('⚡', 'Belum ada skill terinstall.');
1450
- return;
1451
- }
1452
- el.innerHTML = d.items.map(s => `
1453
- <div class="item-card">
1454
- <div class="item-name">⚡ ${s}</div>
1455
- <div class="item-meta">Skill aktif</div>
1456
- </div>`).join('');
1457
- }
1458
-
1459
- async function loadTasks() {
1460
- var el = document.getElementById('tasks-list');
1461
- var d = await fetchApi('/api/tasks');
1462
- if (!d || !d.items || d.items.length === 0) {
1463
- el.innerHTML = emptyState('✅', 'Belum ada task.<br><small>Ketik "buat task ..." di chat</small>');
1464
- return;
1465
- }
1466
- el.innerHTML = d.items.map(t => `
1467
- <div class="task-card">
1468
- <div class="task-status ${t.status === 'selesai' ? 'done' : 'pending'}">
1469
- ${t.status === 'selesai' ? '✓' : ''}
1470
- </div>
1471
- <div>
1472
- <div class="task-title">#${t.id} ${t.judul}</div>
1473
- <div class="task-desc">${t.deskripsi || ''} · ${t.status}</div>
1474
- </div>
1475
- </div>`).join('');
1476
- }
1477
-
1478
- async function loadMemory() {
1479
- var el = document.getElementById('memory-content');
1480
- var d = await fetchApi('/api/memory');
1481
- el.textContent = (d && d.data) ? d.data : 'Memory kosong.';
1482
- el.scrollTop = el.scrollHeight;
1483
- }
1484
-
1485
- async function loadBots() {
1486
- var el = document.getElementById('bots-list');
1487
- var d = await fetchApi('/api/bots');
1488
- if (!d || !d.items || d.items.length === 0) {
1489
- el.innerHTML = emptyState('🤖', 'Belum ada bot.<br><small>Ketik "buat bot ..." di chat</small>');
1490
- return;
1491
- }
1492
- el.innerHTML = d.items.map(b => `
1493
- <div class="item-card">
1494
- <div class="item-name">🤖 ${b.name}</div>
1495
- <div class="item-meta"><a href="/bots/${b.slug}" target="_blank" style="color:var(--accent)">Buka /bots/${b.slug}</a></div>
1496
- </div>`).join('');
1497
- }
1498
-
1499
- function addMsg(cls, text, extra) {
1500
- var cb = document.getElementById('messages');
1501
- var d = document.createElement('div');
1502
- d.className = 'msg ' + cls;
1503
- if (extra === 'thinking') {
1504
- d.className += ' thinking';
1505
- d.innerHTML = 'Berpikir <span class="dots"><span></span><span></span><span></span></span>';
1506
- } else {
1507
- d.textContent = text;
1508
- }
1509
- cb.appendChild(d);
1510
- cb.scrollTop = cb.scrollHeight;
1511
- return d;
1512
- }
1513
-
1514
- function addMeta(cls, text) {
1515
- var cb = document.getElementById('messages');
1516
- var d = document.createElement('div');
1517
- d.className = 'meta-badge ' + cls;
1518
- d.textContent = text;
1519
- cb.appendChild(d);
1520
- cb.scrollTop = cb.scrollHeight;
1521
- }
1522
-
1523
- async function send() {
1524
- var inp = document.getElementById('inp');
1525
- var btn = document.getElementById('btn');
1526
- var msg = inp.value.trim();
1527
- if (!msg) return;
1528
-
1529
- addMsg('user', msg);
1530
- inp.value = '';
1531
- inp.style.height = 'auto';
1532
- btn.disabled = true;
1533
-
1534
- var thinking = addMsg('bot', '', 'thinking');
1535
-
1536
- try {
1537
- var r = await fetch('/chat', {
1538
- method: 'POST',
1539
- headers: {'Content-Type': 'application/json'},
1540
- body: JSON.stringify({message: msg, history: chatHistory})
1541
- });
1542
- var d = await r.json();
1543
- thinking.remove();
1544
-
1545
- if (d.tools_used && d.tools_used.length > 0) {
1546
- addMeta('tool', '🔧 ' + d.tools_used.join(' → '));
1547
- }
1548
- if (d.mode) {
1549
- addMeta('reasoning', '🧠 Deep Reasoning aktif');
1550
- }
1551
-
1552
- addMsg('bot', d.balasan || '...');
1553
- if (Array.isArray(d.history)) {
1554
- chatHistory = d.history.slice(-20);
1555
- }
1556
- } catch(e) {
1557
- thinking.remove();
1558
- addMsg('bot', '❌ Koneksi bermasalah: ' + e.message);
1559
  }
1560
- btn.disabled = false;
1561
- inp.focus();
1562
- }
1563
- </script>
1564
- </body>
1565
- </html>"""
1566
-
1567
- # =================== FLASK ROUTES ===================
1568
-
1569
- @app.route("/")
1570
- def index():
1571
- return render_template_string(HTML)
1572
-
1573
- @app.route("/chat", methods=["POST"])
1574
- def chat():
1575
- data = request.json or {}
1576
- pesan = data.get("message", "").strip()
1577
- raw_history = data.get("history", [])
1578
- history = raw_history if isinstance(raw_history, list) else []
1579
-
1580
- if not pesan:
1581
- return jsonify({"balasan": "Pesan kosong.", "tools_used": [], "history": history, "mode": False})
1582
-
1583
- key_name, key_val = detect_api_key(pesan)
1584
- if key_name:
1585
- save_config(key_name, key_val)
1586
- return jsonify({
1587
- "balasan": f"✅ API key berhasil disimpan! Fitur terkait sekarang aktif.",
1588
- "tools_used": ["simpan_key"],
1589
- "history": history,
1590
- "mode": False
1591
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1592
 
1593
  try:
1594
- balasan, tools_used, new_history, mode = run_agent(pesan, history)
1595
- return jsonify({
1596
- "balasan": balasan,
1597
- "tools_used": tools_used,
1598
- "history": new_history[-20:],
1599
- "mode": mode
1600
- })
1601
  except Exception as e:
1602
- tulis_log(f"Chat error: {e}")
1603
- return jsonify({"balasan": f"❌ {str(e)}", "tools_used": [], "history": history, "mode": False})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1604
 
1605
- @app.route("/bots/<slug>")
1606
- def bot_page(slug):
1607
- bot = _bots.get(slug)
1608
- if not bot and supabase:
1609
- try:
1610
- res = supabase.table("bots").select("*").eq("slug", slug).execute()
1611
- if res.data:
1612
- bot = res.data[0]
1613
- _bots[slug] = bot
1614
- except Exception:
1615
- pass
1616
- if not bot:
1617
- return f"<h1>Bot '{slug}' tidak ditemukan.</h1>", 404
1618
- return bot["html"]
1619
 
1620
- @app.route("/bots/<slug>/chat", methods=["POST"])
1621
- def bot_chat(slug):
1622
- bot = _bots.get(slug)
1623
- if not bot:
1624
- return jsonify({"balasan": "Bot tidak ditemukan."}), 404
1625
- data = request.json or {}
1626
- pesan = data.get("message", "").strip()
1627
- if not pesan:
1628
- return jsonify({"balasan": "Pesan kosong."})
1629
- messages = [
1630
- {"role": "system", "content": bot.get("system_prompt", "Kamu adalah asisten AI yang membantu.")},
1631
- {"role": "user", "content": pesan}
1632
- ]
1633
  try:
1634
- resp, _ = call_llm(messages, max_tokens=1024)
1635
- balasan = resp["choices"][0]["message"].get("content", "...")
1636
- return jsonify({"balasan": balasan})
1637
  except Exception as e:
1638
- return jsonify({"balasan": f" {str(e)}"})
1639
-
1640
- # ── API Endpoints (format JSON baru dengan items array) ──
1641
-
1642
- @app.route("/api/mcps")
1643
- def api_mcps():
1644
- mcps = list_mcps()
1645
- return jsonify({"items": mcps, "count": len(mcps)})
1646
-
1647
- @app.route("/api/skills")
1648
- def api_skills():
1649
- skills = list(load_skills().keys())
1650
- return jsonify({"items": skills, "count": len(skills)})
 
 
 
 
 
 
 
 
 
 
 
 
 
1651
 
1652
- @app.route("/api/tasks")
1653
- def api_tasks():
1654
- tasks = baca_tasks()
1655
- return jsonify({"items": tasks, "count": len(tasks)})
1656
 
1657
- @app.route("/api/memory")
1658
- def api_memory():
1659
- return jsonify({"data": baca_memory()})
1660
 
1661
- @app.route("/api/bots")
1662
- def api_bots():
1663
- bots = [{"name": b["name"], "slug": b["slug"]} for b in _bots.values()]
1664
- return jsonify({"items": bots, "count": len(bots)})
1665
 
1666
- @app.route("/api/log")
1667
- def api_log():
1668
- return jsonify({"data": baca_log()})
 
 
 
 
 
 
 
1669
 
1670
- @app.route("/api/status")
1671
- def api_status():
1672
- providers = get_active_providers()
1673
- return jsonify({
1674
- "status": "ok",
1675
- "providers": len(providers),
1676
- "mcps": len(list_mcps()),
1677
- "skills": len(load_skills()),
1678
- "bots": len(_bots),
1679
- "supabase": supabase is not None
1680
- })
1681
 
1682
- # =================== STARTUP ===================
 
 
1683
 
1684
- def startup():
1685
- tulis_log("🦅 Openclaw Neo starting...")
1686
- load_configs()
1687
- load_skills_from_supabase()
1688
- load_all_mcps()
1689
- load_bots()
1690
- tulis_log("✅ Openclaw Neo ready!")
1691
 
1692
  if __name__ == "__main__":
1693
- startup()
1694
- port = int(os.environ.get("PORT", 7860))
1695
- app.run(host="0.0.0.0", port=port, debug=False)
 
 
 
 
 
1
  import os
 
 
2
  import time
3
+ from datetime import datetime, timezone
 
 
 
 
4
  from flask import Flask, request, jsonify, render_template_string
5
+ import requests
 
 
 
6
 
 
7
  app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "").rstrip("/")
10
+ SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY", "")
11
+
12
+ NEXT_AGENT = {
13
+ "trend_hunter": "offer_scout",
14
+ "offer_scout": "content_strategist",
15
+ "content_strategist": "seo_writer",
16
+ "seo_writer": "creative_generator",
17
+ "creative_generator": "publisher",
18
+ "publisher": "tracker",
19
+ "tracker": "analytics",
20
+ "analytics": "cro_optimizer",
21
+ "cro_optimizer": "compliance_guard",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ def now_iso():
26
+ return datetime.now(timezone.utc).isoformat()
 
 
 
 
27
 
 
 
 
 
 
28
 
29
+ def sb(path, method="GET", params=None, body=None):
30
+ if not SUPABASE_URL or not SUPABASE_ANON_KEY:
31
+ raise RuntimeError("Missing SUPABASE_URL / SUPABASE_ANON_KEY")
32
+ url = f"{SUPABASE_URL}/rest/v1/{path}"
33
+ headers = {
34
+ "apikey": SUPABASE_ANON_KEY,
35
+ "Authorization": f"Bearer {SUPABASE_ANON_KEY}",
36
+ "Content-Type": "application/json",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  }
38
+ if method in ("POST", "PATCH", "DELETE"):
39
+ headers["Prefer"] = "return=representation"
40
+ r = requests.request(method, url, headers=headers, params=params, json=body, timeout=25)
41
+ if r.status_code >= 400:
42
+ raise RuntimeError(f"Supabase {r.status_code}: {r.text[:250]}")
43
+ if not r.text:
44
+ return None
45
+ return r.json()
46
+
47
+
48
+ def run_agent(agent_code, input_data):
49
+ prev = (input_data or {}).get("prev_output", {})
50
+ if agent_code == "trend_hunter":
51
+ niche = (input_data or {}).get("niche", "affiliate marketing")
52
+ base = ["best", "review", "promo", "diskon", "vs"]
53
+ return {
54
+ "niche": niche,
55
+ "keywords": [f"{niche} {b}" for b in base],
56
+ "search_intent": "commercial+transactional",
57
+ }
58
+ if agent_code == "offer_scout":
59
+ kws = prev.get("keywords", [])
60
+ return {"offers": [{"keyword": k, "offer_id": f"offer_{i+1}", "score": 80 - i * 7} for i, k in enumerate(kws[:3])]}
61
+ if agent_code == "content_strategist":
62
+ offers = prev.get("offers", [])
63
+ return {
64
+ "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)],
65
+ "affiliate_link": (input_data or {}).get("affiliate_link"),
66
+ "short_url": (input_data or {}).get("short_url"),
67
+ }
68
+ if agent_code == "seo_writer":
69
+ title = prev.get("content_plan", [{}])[0].get("title_angle", "Panduan affiliate")
70
+ slug = "-".join([x for x in "".join([c.lower() if c.isalnum() else " " for c in title]).split() if x])
71
+ return {"title": title, "slug": slug, "article_html": f"<h1>{title}</h1><p>Draft siap publish.</p>"}
72
+ if agent_code == "creative_generator":
73
+ t = prev.get("title", "Promo affiliate")
74
+ return {"creatives": [{"channel": "facebook", "hook": f"{t} - cek sekarang"}, {"channel": "instagram", "hook": f"🔥 {t}"}]}
75
+ if agent_code == "publisher":
76
+ return {"publish_refs": [{"channel": "blog", "ref": f"post_{int(time.time())}"}], "short_url": (input_data or {}).get("short_url")}
77
+ if agent_code == "tracker":
78
+ return {"tracking_map": {"utm_source": "hf-space", "utm_medium": "affiliate", "utm_campaign": f"campaign_{int(time.time())}"}}
79
+ if agent_code == "analytics":
80
+ return {"kpi_snapshot": {"ctr": 0.07, "cvr": 0.02, "epc": 0.4}}
81
+ if agent_code == "cro_optimizer":
82
+ return {"experiments": [{"name": "headline-variant-a", "priority": "high"}]}
83
+ if agent_code == "compliance_guard":
84
+ return {"compliance_status": "pass-with-notes", "fixes": ["Tambahkan disclosure affiliate"]}
85
+ raise RuntimeError(f"No runner for {agent_code}")
86
+
87
+
88
+ def run_once():
89
+ task_rows = sb("aff_tasks", params={"status": "eq.queued", "order": "scheduled_at.asc", "limit": 1}) or []
90
+ if not task_rows:
91
+ return {"ok": True, "message": "No queued tasks"}
92
+
93
+ t = task_rows[0]
94
+ task_id = t["id"]
95
+ attempts = int(t.get("attempts") or 0) + 1
96
+
97
+ sb(f"aff_tasks?id=eq.{task_id}", method="PATCH", body={"status": "running", "started_at": now_iso(), "attempts": attempts})
98
 
99
  try:
100
+ output = run_agent(t["agent_code"], t.get("input") or {})
101
+ sb(f"aff_tasks?id=eq.{task_id}", method="PATCH", body={"status": "done", "output": output, "finished_at": now_iso()})
102
+ next_agent = NEXT_AGENT.get(t["agent_code"])
103
+ if next_agent:
104
+ sb("aff_tasks", method="POST", body={"agent_code": next_agent, "status": "queued", "input": {"prev_agent": t["agent_code"], "prev_output": output}})
105
+ return {"ok": True, "task_id": task_id, "agent": t["agent_code"], "status": "done"}
 
106
  except Exception as e:
107
+ sb(f"aff_tasks?id=eq.{task_id}", method="PATCH", body={"status": "error", "error": str(e), "finished_at": now_iso()})
108
+ return {"ok": False, "task_id": task_id, "agent": t["agent_code"], "error": str(e)}
109
+
110
+
111
+ def stats_today():
112
+ start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
113
+ rows = sb("aff_tasks", params={"select": "status,agent_code,created_at", "created_at": f"gte.{start}", "limit": 1000}) or []
114
+ agg = {"queued": 0, "running": 0, "done": 0, "error": 0}
115
+ by_agent = {}
116
+ for r in rows:
117
+ st = r.get("status", "queued")
118
+ agg[st] = agg.get(st, 0) + 1
119
+ a = r.get("agent_code", "unknown")
120
+ by_agent[a] = by_agent.get(a, 0) + 1
121
+ return {"total": len(rows), "agg": agg, "by_agent": by_agent}
122
+
123
+
124
+ PAGE = """
125
+ <!doctype html><html><head><meta charset='utf-8'><title>OpenClaw Affiliate Ops</title>
126
+ <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>
127
+ </head><body>
128
+ <h2>⚡ OpenClaw Affiliate Ops (HF Space)</h2>
129
+ <p>Supabase: {{supabase_ok}}</p>
130
+ <form method='post' action='/enqueue'>
131
+ <label>Affiliate Link</label><input name='affiliate_link' required>
132
+ <label>Short URL (optional)</label><input name='short_url'>
133
+ <button type='submit'>Enqueue Campaign</button>
134
+ </form>
135
+ <p>
136
+ <a href='/run-once'><button>Run Once</button></a>
137
+ <a href='/run-batch?count=5'><button>Run Batch x5</button></a>
138
+ <a href='/stats'><button>Refresh Stats (JSON)</button></a>
139
+ </p>
140
+ <pre>{{stats}}</pre>
141
+ </body></html>
142
+ """
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ @app.get("/")
146
+ def home():
 
 
 
 
 
 
 
 
 
 
 
147
  try:
148
+ s = stats_today()
149
+ return render_template_string(PAGE, supabase_ok="connected", stats=s)
 
150
  except Exception as e:
151
+ return render_template_string(PAGE, supabase_ok=f"error: {e}", stats={})
152
+
153
+
154
+ @app.get("/healthz")
155
+ def healthz():
156
+ return jsonify({"ok": True, "time": now_iso()})
157
+
158
+
159
+ @app.post("/enqueue")
160
+ def enqueue():
161
+ link = request.form.get("affiliate_link") or (request.json or {}).get("affiliate_link")
162
+ short = request.form.get("short_url") or (request.json or {}).get("short_url")
163
+ if not link:
164
+ return jsonify({"ok": False, "error": "affiliate_link required"}), 400
165
+ row = sb("aff_tasks", method="POST", body={
166
+ "agent_code": "content_strategist",
167
+ "status": "queued",
168
+ "input": {
169
+ "niche": "web hosting",
170
+ "offer": "Hostinger",
171
+ "affiliate_link": link,
172
+ "short_url": short,
173
+ "source": "hf-space"
174
+ }
175
+ })
176
+ return jsonify({"ok": True, "task": row[0] if isinstance(row, list) and row else row})
177
 
 
 
 
 
178
 
179
+ @app.get("/run-once")
180
+ def run_once_route():
181
+ return jsonify(run_once())
182
 
 
 
 
 
183
 
184
+ @app.get("/run-batch")
185
+ def run_batch():
186
+ count = int(request.args.get("count", 5))
187
+ out = []
188
+ for _ in range(max(1, min(count, 20))):
189
+ r = run_once()
190
+ out.append(r)
191
+ if r.get("message") == "No queued tasks":
192
+ break
193
+ return jsonify({"ok": True, "results": out})
194
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
+ @app.get("/stats")
197
+ def stats_route():
198
+ return jsonify(stats_today())
199
 
 
 
 
 
 
 
 
200
 
201
  if __name__ == "__main__":
202
+ port = int(os.getenv("PORT", "7860"))
203
+ app.run(host="0.0.0.0", port=port)