Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/basharbhassan336699-cell_cobweaverclaw.json

1 line
635 KiB
JSON

{"repo": "basharbhassan336699-cell/cobweaverclaw", "files": {"core/gateway_server.py": {"1ff842735aa56b544736ac2208ae57106366857d": {"t": 1785745460.7660506, "n": 43499, "h": "662254c0b81ebba2", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\",\n attachments: list = None, web_tool: dict = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\n\n attachments: [{name,type,size,data(base64)}] — تُحفظ في ~/.cobweaverclaw/uploads\n ويُحقن نص الملفات النصية في رسالة النموذج.\n web_tool: {\"enabled\":bool,\"mode\":\"auto|stealth|crawl|browser\"} — تفعيل 🌐 لهذه الجلسة.\n \"\"\"\n load_env_into_os()\n # موافقة/رفض تثبيت حزمة معلّقة (لا نشغّل pip دون إذن)\n try:\n from tools.pkg_guard import handle_command as _pkg_cmd\n _reply = _pkg_cmd(message)\n if _reply is not None:\n return {\"reply\": _reply, \"model_used\": \"system\", \"tools\": []}\n except Exception:\n pass\n # تفعيل/إيقاف أداة ذكاء الويب لهذه الرسالة (يفلترها model_router)\n _wt = web_tool or {}\n if _wt.get(\"enabled\"):\n os.environ[\"WEB_TOOL_ENABLED\"] = \"1\"\n os.environ[\"WEB_TOOL_MODE\"] = _wt.get(\"mode\", \"auto\") or \"auto\"\n else:\n os.environ[\"WEB_TOOL_ENABLED\"] = \"0\"\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n # المرفقات: نصية → محتواها قبل الرسالة؛ صور → base64 للنموذج مباشرة\n model_message = message\n memory_message = message\n chat_images = []\n if attachments:\n from core.files_api import save_attachments\n pre, fresh = [], []\n for a in attachments:\n (pre if (a.get(\"content\") is not None or a.get(\"image\")) and a.get(\"saved_name\")\n else fresh).append(a)\n saved = (save_attachments(fresh) if fresh else []) + pre\n text_parts, names = [], []\n for r in saved:\n names.append(r.get(\"name\", \"ملف\"))\n if r.get(\"image\"):\n if r.get(\"data\"):\n chat_images.append({\"mime\": r.get(\"type\") or r.get(\"mime_type\")\n or \"image/png\", \"data\": r[\"data\"]})\n else:\n text_parts.append(f\"[صورة مرفقة: {r.get('name')} — محفوظة في \"\n f\"{r.get('path', 'uploads')}]\")\n else:\n content = (r.get(\"content\") or r.get(\"text\") or \"\")[:12000]\n if content:\n text_parts.append(f\"محتوى الملف [{r.get('name')}]:\\n\\n{content}\")\n elif r.get(\"error\") or r.get(\"extract_error\"):\n text_parts.append(f\"[الملف {r.get('name')} — \"\n f\"{r.get('error') or r.get('extract_error')}]\")\n if text_parts:\n model_message = \"\\n\\n---\\n\\n\".join(text_parts) + \"\\n\\n---\\n\" + message\n joined = \"، \".join(names)\n memory_message = (message + f\"\\n[📎 مرفقات: {joined}]\") if message else f\"[📎 مرفقات: {joined}]\"\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, memory_message)\n if chat_images:\n router._pending_images = chat_images # صور المرفقات (OpenAI vision format)\n out = await router.complete_meta(model_message, ctx, lang, force_model=model or None)\n await mem.save(sid, memory_message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"agentrouter\": \"AGENTROUTER_API_KEY\",\n \"moonshot\": \"MOONSHOT_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"Claude Sonnet 4.5 (Free)\", \"Claude Haiku 4.5 (Free)\", \"GLM 5 (Free)\",\n \"DeepSeek 3.2 (Free)\", \"Mistral Large (Free)\", \"Claude Sonnet 5\",\n \"Claude Opus 4.8\", \"Claude Fable 5\", \"GPT-5.5\", \"Kimi K2.7 Code\",\n \"DeepSeek V4 Pro\", \"Qwen 3.7 Max\", \"MiMo V2.5 Pro\", \"MiniMax M3\"]},\n {\"id\": \"agentrouter\", \"name\": \"AgentRouter\", \"env\": \"AGENTROUTER_API_KEY\",\n \"login\": \"https://agentrouter.org\", \"color\": \"#10B981\", \"letter\": \"A\",\n \"models\": [\"GLM-4.6 (Free)\", \"GLM-4.5 Air (Free)\", \"DeepSeek-V2 Lite (Free)\",\n \"Qwen2-7B-Instruct (Free)\", \"Mistral-7B-Instruct (Free)\", \"Claude Sonnet 4.5\",\n \"Claude Opus 4.5\", \"claude-opus-4-6\", \"GPT-4o\", \"GPT-5\", \"Gemini 3 Pro\",\n \"DeepSeek R1\", \"deepseek-v3.2\", \"Qwen3 Coder 480B\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"),\n body.get(\"attachments\"), body.get(\"web_tool\"))\n self._send_json(out)\n elif path == \"/api/files/upload\":\n from core.files_api import upload_files\n body_out, code = upload_files(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/upload\":\n from core.skills_api import upload_skill\n body_out, code = upload_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/create\":\n from core.skills_api import create_skill\n body_out, code = create_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/trades\":\n self._send_json(dash.trades_log(body))\n elif path == \"/api/trades/close\":\n self._send_json(dash.trades_close(body))\n elif path == \"/api/trades/simulate\":\n self._send_json(dash.simulate_entry(body))\n elif path == \"/api/trades/regime\":\n self._send_json(dash.regime_status(body))\n elif path == \"/api/trades/manipulation\":\n self._send_json(dash.manipulation_alerts(body))\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n elif path == \"/api/simcore/platform/connect\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n else:\n from simcore.platforms import connect_platform\n body_out, code = connect_platform(**(body or {})), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/website/connect\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n else:\n from simcore.platforms import connect_website\n body_out, code = connect_website(**(body or {})), 200\n self._send_json(body_out, code)\n elif path.startswith(\"/api/simcore/\"):\n action = path.rsplit(\"/\", 1)[-1]\n if action not in (\"probe\", \"run\", \"feedback\"):\n self._send_json({\"error\": \"not_found\"}, 404)\n return\n # v2 حي → توجيه إليه؛ وإلا النواة المدمجة (نفس العقود)\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"v2\")\n else:\n from simcore import api as simcore_api\n fn = {\"probe\": simcore_api.probe_source,\n \"run\": simcore_api.run_cycle,\n \"feedback\": simcore_api.record_feedback}[action]\n body_out, code = fn(body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"embedded\")\n # بعد كل دورة تحليل ناجحة: تغذية ذاكرة الوكيل + تنبيهات تيليجرام\n if action == \"run\" and code == 200:\n try:\n dash.simcore_sync_decisions()\n except Exception:\n pass\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/files/delete/\"):\n from core.files_api import delete_file\n fname = urllib.parse.unquote(parsed.path[len(\"/api/files/delete/\"):])\n body_out, code = delete_file(fname)\n self._send_json(body_out, code)\n elif parsed.path.startswith(\"/api/simcore/platform/delete/\"):\n name = urllib.parse.unquote(parsed.path[len(\"/api/simcore/platform/delete/\"):])\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"DELETE\", parsed.path)\n else:\n from simcore.platforms import delete_platform\n body_out, code = delete_platform(name), 200\n self._send_json(body_out, code)\n elif parsed.path.startswith(\"/api/skills/\"):\n from core.skills_api import delete_skill\n import urllib.parse as _up\n name = _up.unquote(parsed.path[len(\"/api/skills/\"):])\n body_out, code = delete_skill(name)\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n from core.skills_api import list_user_skills\n self._send_json({\"skills\": list_skills(),\n \"user_skills\": list_user_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/trades/regime\":\n self._send_json(dash.regime_status())\n elif path == \"/api/trades/manipulation\":\n self._send_json(dash.manipulation_alerts())\n elif path == \"/api/trades/emergency\":\n self._send_json(dash.emergency_status())\n elif path == \"/api/trades/emergency/report\":\n self._send_json(dash.emergency_report())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n elif path == \"/api/files/list\":\n from core.files_api import list_files\n body_out, code = list_files()\n self._send_json(body_out, code)\n elif path.startswith(\"/api/files/download/\"):\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/download/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.send_header(\"Content-Disposition\",\n \"attachment; filename*=UTF-8''\" + urllib.parse.quote(name))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/view/\"):\n # مثل التنزيل لكن للعرض داخل المتصفح (صور/PDF في العارض)\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/view/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/extract/\"):\n from core.files_api import extract_for\n fname = urllib.parse.unquote(path[len(\"/api/files/extract/\"):])\n body_out, code = extract_for(fname)\n self._send_json(body_out, code)\n elif path == \"/api/simcore/v2/status\":\n self._send_json(dash.simcore_v2_status())\n elif path == \"/api/simcore/platform/list\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", path)\n else:\n from simcore.platforms import list_platforms\n body_out, code = list_platforms(), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/website/list\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", path)\n else:\n from simcore.platforms import list_websites\n body_out, code = list_websites(), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/domains\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", \"/api/simcore/domains\")\n else:\n from simcore.api import get_domains\n body_out, code = get_domains()\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "7deb43ac330993b4fc0cc278f1d58bebc257aa79": {"t": 1785753504.912413, "n": 43499, "h": "662254c0b81ebba2", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\",\n attachments: list = None, web_tool: dict = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\n\n attachments: [{name,type,size,data(base64)}] — تُحفظ في ~/.cobweaverclaw/uploads\n ويُحقن نص الملفات النصية في رسالة النموذج.\n web_tool: {\"enabled\":bool,\"mode\":\"auto|stealth|crawl|browser\"} — تفعيل 🌐 لهذه الجلسة.\n \"\"\"\n load_env_into_os()\n # موافقة/رفض تثبيت حزمة معلّقة (لا نشغّل pip دون إذن)\n try:\n from tools.pkg_guard import handle_command as _pkg_cmd\n _reply = _pkg_cmd(message)\n if _reply is not None:\n return {\"reply\": _reply, \"model_used\": \"system\", \"tools\": []}\n except Exception:\n pass\n # تفعيل/إيقاف أداة ذكاء الويب لهذه الرسالة (يفلترها model_router)\n _wt = web_tool or {}\n if _wt.get(\"enabled\"):\n os.environ[\"WEB_TOOL_ENABLED\"] = \"1\"\n os.environ[\"WEB_TOOL_MODE\"] = _wt.get(\"mode\", \"auto\") or \"auto\"\n else:\n os.environ[\"WEB_TOOL_ENABLED\"] = \"0\"\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n # المرفقات: نصية → محتواها قبل الرسالة؛ صور → base64 للنموذج مباشرة\n model_message = message\n memory_message = message\n chat_images = []\n if attachments:\n from core.files_api import save_attachments\n pre, fresh = [], []\n for a in attachments:\n (pre if (a.get(\"content\") is not None or a.get(\"image\")) and a.get(\"saved_name\")\n else fresh).append(a)\n saved = (save_attachments(fresh) if fresh else []) + pre\n text_parts, names = [], []\n for r in saved:\n names.append(r.get(\"name\", \"ملف\"))\n if r.get(\"image\"):\n if r.get(\"data\"):\n chat_images.append({\"mime\": r.get(\"type\") or r.get(\"mime_type\")\n or \"image/png\", \"data\": r[\"data\"]})\n else:\n text_parts.append(f\"[صورة مرفقة: {r.get('name')} — محفوظة في \"\n f\"{r.get('path', 'uploads')}]\")\n else:\n content = (r.get(\"content\") or r.get(\"text\") or \"\")[:12000]\n if content:\n text_parts.append(f\"محتوى الملف [{r.get('name')}]:\\n\\n{content}\")\n elif r.get(\"error\") or r.get(\"extract_error\"):\n text_parts.append(f\"[الملف {r.get('name')} — \"\n f\"{r.get('error') or r.get('extract_error')}]\")\n if text_parts:\n model_message = \"\\n\\n---\\n\\n\".join(text_parts) + \"\\n\\n---\\n\" + message\n joined = \"، \".join(names)\n memory_message = (message + f\"\\n[📎 مرفقات: {joined}]\") if message else f\"[📎 مرفقات: {joined}]\"\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, memory_message)\n if chat_images:\n router._pending_images = chat_images # صور المرفقات (OpenAI vision format)\n out = await router.complete_meta(model_message, ctx, lang, force_model=model or None)\n await mem.save(sid, memory_message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"agentrouter\": \"AGENTROUTER_API_KEY\",\n \"moonshot\": \"MOONSHOT_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"Claude Sonnet 4.5 (Free)\", \"Claude Haiku 4.5 (Free)\", \"GLM 5 (Free)\",\n \"DeepSeek 3.2 (Free)\", \"Mistral Large (Free)\", \"Claude Sonnet 5\",\n \"Claude Opus 4.8\", \"Claude Fable 5\", \"GPT-5.5\", \"Kimi K2.7 Code\",\n \"DeepSeek V4 Pro\", \"Qwen 3.7 Max\", \"MiMo V2.5 Pro\", \"MiniMax M3\"]},\n {\"id\": \"agentrouter\", \"name\": \"AgentRouter\", \"env\": \"AGENTROUTER_API_KEY\",\n \"login\": \"https://agentrouter.org\", \"color\": \"#10B981\", \"letter\": \"A\",\n \"models\": [\"GLM-4.6 (Free)\", \"GLM-4.5 Air (Free)\", \"DeepSeek-V2 Lite (Free)\",\n \"Qwen2-7B-Instruct (Free)\", \"Mistral-7B-Instruct (Free)\", \"Claude Sonnet 4.5\",\n \"Claude Opus 4.5\", \"claude-opus-4-6\", \"GPT-4o\", \"GPT-5\", \"Gemini 3 Pro\",\n \"DeepSeek R1\", \"deepseek-v3.2\", \"Qwen3 Coder 480B\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"),\n body.get(\"attachments\"), body.get(\"web_tool\"))\n self._send_json(out)\n elif path == \"/api/files/upload\":\n from core.files_api import upload_files\n body_out, code = upload_files(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/upload\":\n from core.skills_api import upload_skill\n body_out, code = upload_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/create\":\n from core.skills_api import create_skill\n body_out, code = create_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/trades\":\n self._send_json(dash.trades_log(body))\n elif path == \"/api/trades/close\":\n self._send_json(dash.trades_close(body))\n elif path == \"/api/trades/simulate\":\n self._send_json(dash.simulate_entry(body))\n elif path == \"/api/trades/regime\":\n self._send_json(dash.regime_status(body))\n elif path == \"/api/trades/manipulation\":\n self._send_json(dash.manipulation_alerts(body))\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n elif path == \"/api/simcore/platform/connect\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n else:\n from simcore.platforms import connect_platform\n body_out, code = connect_platform(**(body or {})), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/website/connect\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n else:\n from simcore.platforms import connect_website\n body_out, code = connect_website(**(body or {})), 200\n self._send_json(body_out, code)\n elif path.startswith(\"/api/simcore/\"):\n action = path.rsplit(\"/\", 1)[-1]\n if action not in (\"probe\", \"run\", \"feedback\"):\n self._send_json({\"error\": \"not_found\"}, 404)\n return\n # v2 حي → توجيه إليه؛ وإلا النواة المدمجة (نفس العقود)\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"v2\")\n else:\n from simcore import api as simcore_api\n fn = {\"probe\": simcore_api.probe_source,\n \"run\": simcore_api.run_cycle,\n \"feedback\": simcore_api.record_feedback}[action]\n body_out, code = fn(body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"embedded\")\n # بعد كل دورة تحليل ناجحة: تغذية ذاكرة الوكيل + تنبيهات تيليجرام\n if action == \"run\" and code == 200:\n try:\n dash.simcore_sync_decisions()\n except Exception:\n pass\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/files/delete/\"):\n from core.files_api import delete_file\n fname = urllib.parse.unquote(parsed.path[len(\"/api/files/delete/\"):])\n body_out, code = delete_file(fname)\n self._send_json(body_out, code)\n elif parsed.path.startswith(\"/api/simcore/platform/delete/\"):\n name = urllib.parse.unquote(parsed.path[len(\"/api/simcore/platform/delete/\"):])\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"DELETE\", parsed.path)\n else:\n from simcore.platforms import delete_platform\n body_out, code = delete_platform(name), 200\n self._send_json(body_out, code)\n elif parsed.path.startswith(\"/api/skills/\"):\n from core.skills_api import delete_skill\n import urllib.parse as _up\n name = _up.unquote(parsed.path[len(\"/api/skills/\"):])\n body_out, code = delete_skill(name)\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n from core.skills_api import list_user_skills\n self._send_json({\"skills\": list_skills(),\n \"user_skills\": list_user_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/trades/regime\":\n self._send_json(dash.regime_status())\n elif path == \"/api/trades/manipulation\":\n self._send_json(dash.manipulation_alerts())\n elif path == \"/api/trades/emergency\":\n self._send_json(dash.emergency_status())\n elif path == \"/api/trades/emergency/report\":\n self._send_json(dash.emergency_report())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n elif path == \"/api/files/list\":\n from core.files_api import list_files\n body_out, code = list_files()\n self._send_json(body_out, code)\n elif path.startswith(\"/api/files/download/\"):\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/download/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.send_header(\"Content-Disposition\",\n \"attachment; filename*=UTF-8''\" + urllib.parse.quote(name))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/view/\"):\n # مثل التنزيل لكن للعرض داخل المتصفح (صور/PDF في العارض)\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/view/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/extract/\"):\n from core.files_api import extract_for\n fname = urllib.parse.unquote(path[len(\"/api/files/extract/\"):])\n body_out, code = extract_for(fname)\n self._send_json(body_out, code)\n elif path == \"/api/simcore/v2/status\":\n self._send_json(dash.simcore_v2_status())\n elif path == \"/api/simcore/platform/list\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", path)\n else:\n from simcore.platforms import list_platforms\n body_out, code = list_platforms(), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/website/list\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", path)\n else:\n from simcore.platforms import list_websites\n body_out, code = list_websites(), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/domains\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", \"/api/simcore/domains\")\n else:\n from simcore.api import get_domains\n body_out, code = get_domains()\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "bea02d50b336e4204893dc6fa6277fd6a3590a07": {"t": 1785753507.552036, "n": 41802, "h": "f2ef3285a58304a3", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\",\n attachments: list = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\n\n attachments: [{name,type,size,data(base64)}] — تُحفظ في ~/.cobweaverclaw/uploads\n ويُحقن نص الملفات النصية في رسالة النموذج.\n \"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n # المرفقات: نصية → محتواها قبل الرسالة؛ صور → base64 للنموذج مباشرة\n model_message = message\n memory_message = message\n chat_images = []\n if attachments:\n from core.files_api import save_attachments\n pre, fresh = [], []\n for a in attachments:\n (pre if (a.get(\"content\") is not None or a.get(\"image\")) and a.get(\"saved_name\")\n else fresh).append(a)\n saved = (save_attachments(fresh) if fresh else []) + pre\n text_parts, names = [], []\n for r in saved:\n names.append(r.get(\"name\", \"ملف\"))\n if r.get(\"image\"):\n if r.get(\"data\"):\n chat_images.append({\"mime\": r.get(\"type\") or r.get(\"mime_type\")\n or \"image/png\", \"data\": r[\"data\"]})\n else:\n text_parts.append(f\"[صورة مرفقة: {r.get('name')} — محفوظة في \"\n f\"{r.get('path', 'uploads')}]\")\n else:\n content = (r.get(\"content\") or r.get(\"text\") or \"\")[:12000]\n if content:\n text_parts.append(f\"محتوى الملف [{r.get('name')}]:\\n\\n{content}\")\n elif r.get(\"error\") or r.get(\"extract_error\"):\n text_parts.append(f\"[الملف {r.get('name')} — \"\n f\"{r.get('error') or r.get('extract_error')}]\")\n if text_parts:\n model_message = \"\\n\\n---\\n\\n\".join(text_parts) + \"\\n\\n---\\n\" + message\n joined = \"، \".join(names)\n memory_message = (message + f\"\\n[📎 مرفقات: {joined}]\") if message else f\"[📎 مرفقات: {joined}]\"\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, memory_message)\n if chat_images:\n router._pending_images = chat_images # صور المرفقات (OpenAI vision format)\n out = await router.complete_meta(model_message, ctx, lang, force_model=model or None)\n await mem.save(sid, memory_message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"agentrouter\": \"AGENTROUTER_API_KEY\",\n \"moonshot\": \"MOONSHOT_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"Claude Sonnet 4.5 (Free)\", \"Claude Haiku 4.5 (Free)\", \"GLM 5 (Free)\",\n \"DeepSeek 3.2 (Free)\", \"Mistral Large (Free)\", \"Claude Sonnet 5\",\n \"Claude Opus 4.8\", \"Claude Fable 5\", \"GPT-5.5\", \"Kimi K2.7 Code\",\n \"DeepSeek V4 Pro\", \"Qwen 3.7 Max\", \"MiMo V2.5 Pro\", \"MiniMax M3\"]},\n {\"id\": \"agentrouter\", \"name\": \"AgentRouter\", \"env\": \"AGENTROUTER_API_KEY\",\n \"login\": \"https://agentrouter.org\", \"color\": \"#10B981\", \"letter\": \"A\",\n \"models\": [\"GLM-4.6 (Free)\", \"GLM-4.5 Air (Free)\", \"DeepSeek-V2 Lite (Free)\",\n \"Qwen2-7B-Instruct (Free)\", \"Mistral-7B-Instruct (Free)\", \"Claude Sonnet 4.5\",\n \"Claude Opus 4.5\", \"claude-opus-4-6\", \"GPT-4o\", \"GPT-5\", \"Gemini 3 Pro\",\n \"DeepSeek R1\", \"deepseek-v3.2\", \"Qwen3 Coder 480B\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"),\n body.get(\"attachments\"))\n self._send_json(out)\n elif path == \"/api/files/upload\":\n from core.files_api import upload_files\n body_out, code = upload_files(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/upload\":\n from core.skills_api import upload_skill\n body_out, code = upload_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/create\":\n from core.skills_api import create_skill\n body_out, code = create_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n elif path == \"/api/simcore/platform/connect\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n else:\n from simcore.platforms import connect_platform\n body_out, code = connect_platform(**(body or {})), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/website/connect\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n else:\n from simcore.platforms import connect_website\n body_out, code = connect_website(**(body or {})), 200\n self._send_json(body_out, code)\n elif path.startswith(\"/api/simcore/\"):\n action = path.rsplit(\"/\", 1)[-1]\n if action not in (\"probe\", \"run\", \"feedback\"):\n self._send_json({\"error\": \"not_found\"}, 404)\n return\n # v2 حي → توجيه إليه؛ وإلا النواة المدمجة (نفس العقود)\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"v2\")\n else:\n from simcore import api as simcore_api\n fn = {\"probe\": simcore_api.probe_source,\n \"run\": simcore_api.run_cycle,\n \"feedback\": simcore_api.record_feedback}[action]\n body_out, code = fn(body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"embedded\")\n # بعد كل دورة تحليل ناجحة: تغذية ذاكرة الوكيل + تنبيهات تيليجرام\n if action == \"run\" and code == 200:\n try:\n dash.simcore_sync_decisions()\n except Exception:\n pass\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/files/delete/\"):\n from core.files_api import delete_file\n fname = urllib.parse.unquote(parsed.path[len(\"/api/files/delete/\"):])\n body_out, code = delete_file(fname)\n self._send_json(body_out, code)\n elif parsed.path.startswith(\"/api/simcore/platform/delete/\"):\n name = urllib.parse.unquote(parsed.path[len(\"/api/simcore/platform/delete/\"):])\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"DELETE\", parsed.path)\n else:\n from simcore.platforms import delete_platform\n body_out, code = delete_platform(name), 200\n self._send_json(body_out, code)\n elif parsed.path.startswith(\"/api/skills/\"):\n from core.skills_api import delete_skill\n import urllib.parse as _up\n name = _up.unquote(parsed.path[len(\"/api/skills/\"):])\n body_out, code = delete_skill(name)\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n from core.skills_api import list_user_skills\n self._send_json({\"skills\": list_skills(),\n \"user_skills\": list_user_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n elif path == \"/api/files/list\":\n from core.files_api import list_files\n body_out, code = list_files()\n self._send_json(body_out, code)\n elif path.startswith(\"/api/files/download/\"):\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/download/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.send_header(\"Content-Disposition\",\n \"attachment; filename*=UTF-8''\" + urllib.parse.quote(name))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/view/\"):\n # مثل التنزيل لكن للعرض داخل المتصفح (صور/PDF في العارض)\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/view/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/extract/\"):\n from core.files_api import extract_for\n fname = urllib.parse.unquote(path[len(\"/api/files/extract/\"):])\n body_out, code = extract_for(fname)\n self._send_json(body_out, code)\n elif path == \"/api/simcore/v2/status\":\n self._send_json(dash.simcore_v2_status())\n elif path == \"/api/simcore/platform/list\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", path)\n else:\n from simcore.platforms import list_platforms\n body_out, code = list_platforms(), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/website/list\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", path)\n else:\n from simcore.platforms import list_websites\n body_out, code = list_websites(), 200\n self._send_json(body_out, code)\n elif path == \"/api/simcore/domains\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", \"/api/simcore/domains\")\n else:\n from simcore.api import get_domains\n body_out, code = get_domains()\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "0d702ab54304e821f4ff06169b97d5e9bf4269c9": {"t": 1785753510.1407993, "n": 39786, "h": "cbd759e2a5c5293e", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\",\n attachments: list = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\n\n attachments: [{name,type,size,data(base64)}] — تُحفظ في ~/.cobweaverclaw/uploads\n ويُحقن نص الملفات النصية في رسالة النموذج.\n \"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n # المرفقات: نصية → محتواها قبل الرسالة؛ صور → base64 للنموذج مباشرة\n model_message = message\n memory_message = message\n chat_images = []\n if attachments:\n from core.files_api import save_attachments\n pre, fresh = [], []\n for a in attachments:\n (pre if (a.get(\"content\") is not None or a.get(\"image\")) and a.get(\"saved_name\")\n else fresh).append(a)\n saved = (save_attachments(fresh) if fresh else []) + pre\n text_parts, names = [], []\n for r in saved:\n names.append(r.get(\"name\", \"ملف\"))\n if r.get(\"image\"):\n if r.get(\"data\"):\n chat_images.append({\"mime\": r.get(\"type\") or r.get(\"mime_type\")\n or \"image/png\", \"data\": r[\"data\"]})\n else:\n text_parts.append(f\"[صورة مرفقة: {r.get('name')} — محفوظة في \"\n f\"{r.get('path', 'uploads')}]\")\n else:\n content = (r.get(\"content\") or r.get(\"text\") or \"\")[:12000]\n if content:\n text_parts.append(f\"محتوى الملف [{r.get('name')}]:\\n\\n{content}\")\n elif r.get(\"error\") or r.get(\"extract_error\"):\n text_parts.append(f\"[الملف {r.get('name')} — \"\n f\"{r.get('error') or r.get('extract_error')}]\")\n if text_parts:\n model_message = \"\\n\\n---\\n\\n\".join(text_parts) + \"\\n\\n---\\n\" + message\n joined = \"، \".join(names)\n memory_message = (message + f\"\\n[📎 مرفقات: {joined}]\") if message else f\"[📎 مرفقات: {joined}]\"\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, memory_message)\n if chat_images:\n router._pending_images = chat_images # صور المرفقات (OpenAI vision format)\n out = await router.complete_meta(model_message, ctx, lang, force_model=model or None)\n await mem.save(sid, memory_message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"agentrouter\": \"AGENTROUTER_API_KEY\",\n \"moonshot\": \"MOONSHOT_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"Claude Sonnet 4.5 (Free)\", \"Claude Haiku 4.5 (Free)\", \"GLM 5 (Free)\",\n \"DeepSeek 3.2 (Free)\", \"Mistral Large (Free)\", \"Claude Sonnet 5\",\n \"Claude Opus 4.8\", \"Claude Fable 5\", \"GPT-5.5\", \"Kimi K2.7 Code\",\n \"DeepSeek V4 Pro\", \"Qwen 3.7 Max\", \"MiMo V2.5 Pro\", \"MiniMax M3\"]},\n {\"id\": \"agentrouter\", \"name\": \"AgentRouter\", \"env\": \"AGENTROUTER_API_KEY\",\n \"login\": \"https://agentrouter.org\", \"color\": \"#10B981\", \"letter\": \"A\",\n \"models\": [\"GLM-4.6 (Free)\", \"GLM-4.5 Air (Free)\", \"DeepSeek-V2 Lite (Free)\",\n \"Qwen2-7B-Instruct (Free)\", \"Mistral-7B-Instruct (Free)\", \"Claude Sonnet 4.5\",\n \"Claude Opus 4.5\", \"claude-opus-4-6\", \"GPT-4o\", \"GPT-5\", \"Gemini 3 Pro\",\n \"DeepSeek R1\", \"deepseek-v3.2\", \"Qwen3 Coder 480B\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"),\n body.get(\"attachments\"))\n self._send_json(out)\n elif path == \"/api/files/upload\":\n from core.files_api import upload_files\n body_out, code = upload_files(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/upload\":\n from core.skills_api import upload_skill\n body_out, code = upload_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/create\":\n from core.skills_api import create_skill\n body_out, code = create_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n elif path.startswith(\"/api/simcore/\"):\n action = path.rsplit(\"/\", 1)[-1]\n if action not in (\"probe\", \"run\", \"feedback\"):\n self._send_json({\"error\": \"not_found\"}, 404)\n return\n # v2 حي → توجيه إليه؛ وإلا النواة المدمجة (نفس العقود)\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"v2\")\n else:\n from simcore import api as simcore_api\n fn = {\"probe\": simcore_api.probe_source,\n \"run\": simcore_api.run_cycle,\n \"feedback\": simcore_api.record_feedback}[action]\n body_out, code = fn(body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"embedded\")\n # بعد كل دورة تحليل ناجحة: تغذية ذاكرة الوكيل + تنبيهات تيليجرام\n if action == \"run\" and code == 200:\n try:\n dash.simcore_sync_decisions()\n except Exception:\n pass\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/files/delete/\"):\n from core.files_api import delete_file\n fname = urllib.parse.unquote(parsed.path[len(\"/api/files/delete/\"):])\n body_out, code = delete_file(fname)\n self._send_json(body_out, code)\n elif parsed.path.startswith(\"/api/skills/\"):\n from core.skills_api import delete_skill\n import urllib.parse as _up\n name = _up.unquote(parsed.path[len(\"/api/skills/\"):])\n body_out, code = delete_skill(name)\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n from core.skills_api import list_user_skills\n self._send_json({\"skills\": list_skills(),\n \"user_skills\": list_user_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n elif path == \"/api/files/list\":\n from core.files_api import list_files\n body_out, code = list_files()\n self._send_json(body_out, code)\n elif path.startswith(\"/api/files/download/\"):\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/download/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.send_header(\"Content-Disposition\",\n \"attachment; filename*=UTF-8''\" + urllib.parse.quote(name))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/view/\"):\n # مثل التنزيل لكن للعرض داخل المتصفح (صور/PDF في العارض)\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/view/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/extract/\"):\n from core.files_api import extract_for\n fname = urllib.parse.unquote(path[len(\"/api/files/extract/\"):])\n body_out, code = extract_for(fname)\n self._send_json(body_out, code)\n elif path == \"/api/simcore/v2/status\":\n self._send_json(dash.simcore_v2_status())\n elif path == \"/api/simcore/domains\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", \"/api/simcore/domains\")\n else:\n from simcore.api import get_domains\n body_out, code = get_domains()\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "8b8492d46dde684c3dc2b62a9314a50373a6e819": {"t": 1785753511.6956766, "n": 38962, "h": "2871429ba88cab2b", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\",\n attachments: list = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\n\n attachments: [{name,type,size,data(base64)}] — تُحفظ في ~/.cobweaverclaw/uploads\n ويُحقن نص الملفات النصية في رسالة النموذج.\n \"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n # المرفقات: نصية → محتواها قبل الرسالة؛ صور → base64 للنموذج مباشرة\n model_message = message\n memory_message = message\n chat_images = []\n if attachments:\n from core.files_api import save_attachments\n pre, fresh = [], []\n for a in attachments:\n (pre if (a.get(\"content\") is not None or a.get(\"image\")) and a.get(\"saved_name\")\n else fresh).append(a)\n saved = (save_attachments(fresh) if fresh else []) + pre\n text_parts, names = [], []\n for r in saved:\n names.append(r.get(\"name\", \"ملف\"))\n if r.get(\"image\"):\n if r.get(\"data\"):\n chat_images.append({\"mime\": r.get(\"type\") or r.get(\"mime_type\")\n or \"image/png\", \"data\": r[\"data\"]})\n else:\n text_parts.append(f\"[صورة مرفقة: {r.get('name')} — محفوظة في \"\n f\"{r.get('path', 'uploads')}]\")\n else:\n content = (r.get(\"content\") or r.get(\"text\") or \"\")[:12000]\n if content:\n text_parts.append(f\"محتوى الملف [{r.get('name')}]:\\n\\n{content}\")\n elif r.get(\"error\") or r.get(\"extract_error\"):\n text_parts.append(f\"[الملف {r.get('name')} — \"\n f\"{r.get('error') or r.get('extract_error')}]\")\n if text_parts:\n model_message = \"\\n\\n---\\n\\n\".join(text_parts) + \"\\n\\n---\\n\" + message\n joined = \"، \".join(names)\n memory_message = (message + f\"\\n[📎 مرفقات: {joined}]\") if message else f\"[📎 مرفقات: {joined}]\"\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, memory_message)\n if chat_images:\n router._pending_images = chat_images # صور المرفقات (OpenAI vision format)\n out = await router.complete_meta(model_message, ctx, lang, force_model=model or None)\n await mem.save(sid, memory_message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"gpt-4o-mini\", \"gpt-4o\", \"claude-3.5-sonnet\", \"deepseek-chat\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"),\n body.get(\"attachments\"))\n self._send_json(out)\n elif path == \"/api/files/upload\":\n from core.files_api import upload_files\n body_out, code = upload_files(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/upload\":\n from core.skills_api import upload_skill\n body_out, code = upload_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/create\":\n from core.skills_api import create_skill\n body_out, code = create_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n elif path.startswith(\"/api/simcore/\"):\n action = path.rsplit(\"/\", 1)[-1]\n if action not in (\"probe\", \"run\", \"feedback\"):\n self._send_json({\"error\": \"not_found\"}, 404)\n return\n # v2 حي → توجيه إليه؛ وإلا النواة المدمجة (نفس العقود)\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"v2\")\n else:\n from simcore import api as simcore_api\n fn = {\"probe\": simcore_api.probe_source,\n \"run\": simcore_api.run_cycle,\n \"feedback\": simcore_api.record_feedback}[action]\n body_out, code = fn(body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"embedded\")\n # بعد كل دورة تحليل ناجحة: تغذية ذاكرة الوكيل + تنبيهات تيليجرام\n if action == \"run\" and code == 200:\n try:\n dash.simcore_sync_decisions()\n except Exception:\n pass\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/files/delete/\"):\n from core.files_api import delete_file\n fname = urllib.parse.unquote(parsed.path[len(\"/api/files/delete/\"):])\n body_out, code = delete_file(fname)\n self._send_json(body_out, code)\n elif parsed.path.startswith(\"/api/skills/\"):\n from core.skills_api import delete_skill\n import urllib.parse as _up\n name = _up.unquote(parsed.path[len(\"/api/skills/\"):])\n body_out, code = delete_skill(name)\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n from core.skills_api import list_user_skills\n self._send_json({\"skills\": list_skills(),\n \"user_skills\": list_user_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n elif path == \"/api/files/list\":\n from core.files_api import list_files\n body_out, code = list_files()\n self._send_json(body_out, code)\n elif path.startswith(\"/api/files/download/\"):\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/download/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.send_header(\"Content-Disposition\",\n \"attachment; filename*=UTF-8''\" + urllib.parse.quote(name))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/view/\"):\n # مثل التنزيل لكن للعرض داخل المتصفح (صور/PDF في العارض)\n from core.files_api import file_bytes\n fname = urllib.parse.unquote(path[len(\"/api/files/view/\"):])\n res = file_bytes(fname)\n if res is None:\n self._send_json({\"error\": \"الملف غير موجود\"}, 404)\n else:\n blob, mime, name = res\n self.send_response(200)\n self.send_header(\"Content-Type\", mime)\n self.send_header(\"Content-Length\", str(len(blob)))\n self.end_headers()\n self.wfile.write(blob)\n elif path.startswith(\"/api/files/extract/\"):\n from core.files_api import extract_for\n fname = urllib.parse.unquote(path[len(\"/api/files/extract/\"):])\n body_out, code = extract_for(fname)\n self._send_json(body_out, code)\n elif path == \"/api/simcore/v2/status\":\n self._send_json(dash.simcore_v2_status())\n elif path == \"/api/simcore/domains\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", \"/api/simcore/domains\")\n else:\n from simcore.api import get_domains\n body_out, code = get_domains()\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "9056909e30ceaa1bcd3a7db5ad3966d02c9b00ec": {"t": 1785753516.482237, "n": 35352, "h": "ba2f8210cc7694cd", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\",\n attachments: list = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\n\n attachments: [{name,type,size,data(base64)}] — تُحفظ في ~/.cobweaverclaw/uploads\n ويُحقن نص الملفات النصية في رسالة النموذج.\n \"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n # المرفقات: حفظ + كتلة سياق للنموذج + ملاحظة موجزة للذاكرة\n model_message = message\n memory_message = message\n if attachments:\n from core.files_api import save_attachments, build_context_block\n saved = save_attachments(attachments)\n model_message = message + build_context_block(saved)\n names = \"، \".join(r[\"name\"] for r in saved)\n memory_message = (message + f\"\\n[📎 مرفقات: {names}]\") if message else f\"[📎 مرفقات: {names}]\"\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, memory_message)\n out = await router.complete_meta(model_message, ctx, lang, force_model=model or None)\n await mem.save(sid, memory_message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"gpt-4o-mini\", \"gpt-4o\", \"claude-3.5-sonnet\", \"deepseek-chat\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"),\n body.get(\"attachments\"))\n self._send_json(out)\n elif path == \"/api/files/upload\":\n from core.files_api import upload_files\n body_out, code = upload_files(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/upload\":\n from core.skills_api import upload_skill\n body_out, code = upload_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/skills/create\":\n from core.skills_api import create_skill\n body_out, code = create_skill(body)\n self._send_json(body_out, code)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n elif path.startswith(\"/api/simcore/\"):\n action = path.rsplit(\"/\", 1)[-1]\n if action not in (\"probe\", \"run\", \"feedback\"):\n self._send_json({\"error\": \"not_found\"}, 404)\n return\n # v2 حي → توجيه إليه؛ وإلا النواة المدمجة (نفس العقود)\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"v2\")\n else:\n from simcore import api as simcore_api\n fn = {\"probe\": simcore_api.probe_source,\n \"run\": simcore_api.run_cycle,\n \"feedback\": simcore_api.record_feedback}[action]\n body_out, code = fn(body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"embedded\")\n # بعد كل دورة تحليل ناجحة: تغذية ذاكرة الوكيل + تنبيهات تيليجرام\n if action == \"run\" and code == 200:\n try:\n dash.simcore_sync_decisions()\n except Exception:\n pass\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/skills/\"):\n from core.skills_api import delete_skill\n import urllib.parse as _up\n name = _up.unquote(parsed.path[len(\"/api/skills/\"):])\n body_out, code = delete_skill(name)\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n from core.skills_api import list_user_skills\n self._send_json({\"skills\": list_skills(),\n \"user_skills\": list_user_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n elif path == \"/api/simcore/v2/status\":\n self._send_json(dash.simcore_v2_status())\n elif path == \"/api/simcore/domains\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", \"/api/simcore/domains\")\n else:\n from simcore.api import get_domains\n body_out, code = get_domains()\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "69aef6ad4d7f5454d2d4e3d596bc12acec20256d": {"t": 1785753517.8389769, "n": 33559, "h": "9c4b973353557901", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\") -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(sid, message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"gpt-4o-mini\", \"gpt-4o\", \"claude-3.5-sonnet\", \"deepseek-chat\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"))\n self._send_json(out)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n elif path.startswith(\"/api/simcore/\"):\n action = path.rsplit(\"/\", 1)[-1]\n if action not in (\"probe\", \"run\", \"feedback\"):\n self._send_json({\"error\": \"not_found\"}, 404)\n return\n # v2 حي → توجيه إليه؛ وإلا النواة المدمجة (نفس العقود)\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"POST\", path, body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"v2\")\n else:\n from simcore import api as simcore_api\n fn = {\"probe\": simcore_api.probe_source,\n \"run\": simcore_api.run_cycle,\n \"feedback\": simcore_api.record_feedback}[action]\n body_out, code = fn(body)\n if isinstance(body_out, dict):\n body_out.setdefault(\"engine\", \"embedded\")\n # بعد كل دورة تحليل ناجحة: تغذية ذاكرة الوكيل + تنبيهات تيليجرام\n if action == \"run\" and code == 200:\n try:\n dash.simcore_sync_decisions()\n except Exception:\n pass\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n elif path == \"/api/simcore/v2/status\":\n self._send_json(dash.simcore_v2_status())\n elif path == \"/api/simcore/domains\":\n if dash.simcore_v2_alive():\n body_out, code = dash.simcore_v2_forward(\"GET\", \"/api/simcore/domains\")\n else:\n from simcore.api import get_domains\n body_out, code = get_domains()\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "0de0456bc2ab91b526a6e7790cc229abbf5f8123": {"t": 1785753519.4827826, "n": 32514, "h": "1e460ef2246bfd8e", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\") -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(sid, message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"gpt-4o-mini\", \"gpt-4o\", \"claude-3.5-sonnet\", \"deepseek-chat\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"))\n self._send_json(out)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n elif path.startswith(\"/api/simcore/\"):\n from simcore import api as simcore_api\n action = path.rsplit(\"/\", 1)[-1]\n fn = {\"probe\": simcore_api.probe_source,\n \"run\": simcore_api.run_cycle,\n \"feedback\": simcore_api.record_feedback}.get(action)\n if fn is None:\n self._send_json({\"error\": \"not_found\"}, 404)\n else:\n body_out, code = fn(body)\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n elif path == \"/api/simcore/domains\":\n from simcore.api import get_domains\n body_out, code = get_domains()\n self._send_json(body_out, code)\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "124f3ba201c94d41fd2b0b7c3da9c9f4adfec544": {"t": 1785753521.0951881, "n": 31763, "h": "ab34a39f1d773a39", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# لوحة v2 — بيانات التبويبات الجديدة (سجلات/استخدام/ذاكرة موزونة/dreaming/تداول/workboard/إقران)\nimport sys as _sys\nif BASE_DIR not in _sys.path:\n _sys.path.insert(0, BASE_DIR)\nfrom core import dashboard_api as dash\ndash.install_log_handler()\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\") -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(sid, message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"gpt-4o-mini\", \"gpt-4o\", \"claude-3.5-sonnet\", \"deepseek-chat\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"))\n self._send_json(out)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/dreaming/run\":\n self._send_json(dash.dreaming_run())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_add(body.get(\"text\", \"\")))\n elif path == \"/api/workboard/move\":\n self._send_json(dash.workboard_move(body.get(\"id\", 0),\n body.get(\"direction\", \"next\")))\n elif path == \"/api/workboard/clear-done\":\n self._send_json(dash.workboard_clear_done())\n elif path == \"/api/pairing/approve\":\n self._send_json(dash.pairing_approve(body.get(\"id\", 0)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n # ── لوحة v2 ──────────────────────────────────\n elif parsed.path.startswith(\"/api/memory/\"):\n mid = parsed.path.rsplit(\"/\", 1)[-1]\n if mid.isdigit():\n self._send_json(dash.memory_delete(int(mid)))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n elif parsed.path.startswith(\"/api/pairing/approved/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_unpair(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n elif parsed.path.startswith(\"/api/pairing/\"):\n pid = parsed.path.rsplit(\"/\", 1)[-1]\n self._send_json(dash.pairing_reject(int(pid)) if pid.isdigit()\n else {\"error\": \"not_found\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n # ── لوحة v2 ──────────────────────────────────\n elif path == \"/api/memory/list\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.memory_list(qs.get(\"context\", [\"general\"])[0]))\n elif path == \"/api/dreaming/status\":\n self._send_json(dash.dreaming_status())\n elif path == \"/api/dreaming/diary\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.dreaming_diary(int(qs.get(\"limit\", [\"50\"])[0] or 50)))\n elif path == \"/api/dreaming/weekly-summary\":\n self._send_json(dash.dreaming_weekly_summary())\n elif path == \"/api/trades\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.trades_list(qs.get(\"outcome\", [\"\"])[0]))\n elif path == \"/api/trades/patterns\":\n self._send_json(dash.trades_patterns())\n elif path == \"/api/workboard\":\n self._send_json(dash.workboard_list())\n elif path == \"/api/pairing/pending\":\n self._send_json(dash.pairing_pending())\n elif path == \"/api/pairing/approved\":\n self._send_json(dash.pairing_approved())\n elif path == \"/api/usage\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.usage_report(int(qs.get(\"days\", [\"7\"])[0] or 7)))\n elif path in (\"/api/logs\", \"/api/logs/stream\"):\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(dash.logs_list(\n limit=int(qs.get(\"limit\", [\"50\"])[0] or 50),\n since=int(qs.get(\"since\", [\"0\"])[0] or 0),\n level=qs.get(\"level\", [\"\"])[0]))\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "9bf7e3716deefb78d7bddd1ab5c26d07fa620f4a": {"t": 1785753522.7353249, "n": 28143, "h": "c0519a9c9dd556c9", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع ��احتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\") -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(sid, message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n from memory.learning.dreaming import light_dreaming; light_dreaming()\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"gpt-4o-mini\", \"gpt-4o\", \"claude-3.5-sonnet\", \"deepseek-chat\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"))\n self._send_json(out)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "be8c00399051f046f360c0ab654fe552af48b9f3": {"t": 1785753524.0779996, "n": 28069, "h": "aa58edd87f1f80cc", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\") -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(sid, message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\n# ── AI Models catalog (قسم نماذج الذكاء — تسجيل الدخول بالحساب/التوكين) ──\nMODELS_CATALOG = [\n {\"id\": \"anthropic\", \"name\": \"Claude (Anthropic)\", \"env\": \"ANTHROPIC_API_KEY\",\n \"login\": \"https://console.anthropic.com/settings/keys\", \"color\": \"#D97757\", \"letter\": \"C\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\"]},\n {\"id\": \"openai\", \"name\": \"ChatGPT (OpenAI)\", \"env\": \"OPENAI_API_KEY\",\n \"login\": \"https://platform.openai.com/api-keys\", \"color\": \"#10A37F\", \"letter\": \"O\",\n \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"o1-mini\"]},\n {\"id\": \"deepseek\", \"name\": \"DeepSeek\", \"env\": \"DEEPSEEK_API_KEY\",\n \"login\": \"https://platform.deepseek.com/api_keys\", \"color\": \"#4D6BFE\", \"letter\": \"D\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]},\n {\"id\": \"moonshot\", \"name\": \"Kimi (Moonshot)\", \"env\": \"MOONSHOT_API_KEY\",\n \"login\": \"https://platform.moonshot.ai/console/api-keys\", \"color\": \"#111111\", \"letter\": \"K\",\n \"models\": [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]},\n {\"id\": \"zai\", \"name\": \"GLM (Z.AI)\", \"env\": \"ZAI_API_KEY\",\n \"login\": \"https://z.ai/manage-apikey/apikey-list\", \"color\": \"#2E7CF6\", \"letter\": \"G\",\n \"models\": [\"glm-4.6\", \"glm-4.5\", \"glm-4.5-air\", \"glm-4-flash\"]},\n {\"id\": \"gemini\", \"name\": \"Gemini (Google)\", \"env\": \"GEMINI_API_KEY\",\n \"login\": \"https://aistudio.google.com/app/apikey\", \"color\": \"#4285F4\", \"letter\": \"G\",\n \"models\": [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"]},\n {\"id\": \"groq\", \"name\": \"Groq\", \"env\": \"GROQ_API_KEY\",\n \"login\": \"https://console.groq.com/keys\", \"color\": \"#F55036\", \"letter\": \"q\",\n \"models\": [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"]},\n {\"id\": \"mistral\", \"name\": \"Mistral AI\", \"env\": \"MISTRAL_API_KEY\",\n \"login\": \"https://console.mistral.ai/api-keys\", \"color\": \"#FF7000\", \"letter\": \"M\",\n \"models\": [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"]},\n {\"id\": \"xai\", \"name\": \"Grok (xAI)\", \"env\": \"XAI_API_KEY\",\n \"login\": \"https://console.x.ai\", \"color\": \"#1A1A1A\", \"letter\": \"X\",\n \"models\": [\"grok-2\", \"grok-2-mini\"]},\n {\"id\": \"openrouter\", \"name\": \"OpenRouter\", \"env\": \"OPENROUTER_API_KEY\",\n \"login\": \"https://openrouter.ai/keys\", \"color\": \"#6467F2\", \"letter\": \"R\",\n \"models\": [\"meta-llama/llama-3.3-70b\", \"qwen/qwen-2.5-72b\", \"mistralai/mistral-7b\"]},\n {\"id\": \"nararouter\", \"name\": \"NaraRouter\", \"env\": \"NARAROUTER_API_KEY\",\n \"login\": \"https://router.bynara.id\", \"color\": \"#8B5CF6\", \"letter\": \"N\",\n \"models\": [\"gpt-4o-mini\", \"gpt-4o\", \"claude-3.5-sonnet\", \"deepseek-chat\"]},\n {\"id\": \"cloudflare\", \"name\": \"Cloudflare AI\", \"env\": \"CLOUDFLARE_API_TOKEN\",\n \"login\": \"https://dash.cloudflare.com/login\", \"color\": \"#F38020\", \"letter\": \"C\",\n \"models\": [\"@cf/meta/llama-3.1-8b-instruct\", \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"]},\n]\n\n\ndef models_catalog() -> dict:\n \"\"\"قائمة مزوّدي نماذج الذكاء + حالة الاتصال (متصل إن وُجد التوكين).\"\"\"\n load_env_into_os()\n cfg = load_config()\n primary = cfg.get(\"brain\", {}).get(\"primary\", \"\")\n out = []\n for p in MODELS_CATALOG:\n out.append({**p,\n \"connected\": bool(os.environ.get(p[\"env\"])),\n \"active_model\": primary.split(\"/\", 1)[1] if primary.startswith(p[\"id\"] + \"/\") else \"\"})\n return {\"providers\": out}\n\n\ndef connect_model(body: dict) -> dict:\n \"\"\"يربط حساب مزوّد بالتوكين، ويختار نموذجه (تسجيل دخول عبر الحساب).\"\"\"\n prov = (body.get(\"provider\") or \"\").strip()\n token = (body.get(\"token\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n account = (body.get(\"account\") or \"\").strip() # لـ Cloudflare\n cat = {p[\"id\"]: p for p in MODELS_CATALOG}\n if prov not in cat:\n return {\"ok\": False, \"error\": \"unknown provider\"}\n if token:\n save_env_keys({cat[prov][\"env\"]: token})\n if prov == \"cloudflare\" and account:\n save_env_keys({\"CLOUDFLARE_ACCOUNT_ID\": account})\n if model:\n save_config({\"brain\": {\"primary\": f\"{prov}/{model}\"}})\n load_env_into_os()\n return {\"ok\": True, \"connected\": bool(os.environ.get(cat[prov][\"env\"])),\n \"primary\": f\"{prov}/{model}\" if model else \"\"}\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"))\n self._send_json(out)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/models/connect\":\n self._send_json(connect_model(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n elif path == \"/api/models/catalog\":\n self._send_json(models_catalog())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "0113c299c5552fe959cbbe7d383e37cf9d905ac6": {"t": 1785753525.8405428, "n": 23628, "h": "2b463d9bc959aaef", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\") -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(sid, message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\ndef add_custom_provider(body: dict) -> dict:\n \"\"\"يضيف مزوّد/نموذج ذكاء مخصّص (مستقلّ عن المفاتيح الثابتة).\"\"\"\n name = (body.get(\"name\") or \"\").strip()\n base = (body.get(\"base_url\") or \"\").strip()\n model = (body.get(\"model\") or \"\").strip()\n key = (body.get(\"key\") or \"\").strip()\n if not name or not base or not model:\n return {\"ok\": False, \"error\": \"name/base_url/model required\"}\n cid = \"\".join(ch for ch in name.lower().replace(\" \", \"\") if ch.isalnum()) or \"custom\"\n env = cid.upper() + \"_API_KEY\"\n cfg = load_config()\n providers = [p for p in cfg.get(\"custom_providers\", []) if (p.get(\"id\") or \"\") != cid]\n providers.append({\"id\": cid, \"name\": name, \"base_url\": base, \"model\": model, \"key_env\": env})\n save_config({\"custom_providers\": providers})\n if key:\n save_env_keys({env: key})\n return {\"ok\": True, \"id\": cid, \"value\": f\"{cid}/{model}\", \"label\": name}\n\n\ndef list_providers() -> dict:\n \"\"\"يُعيد المزوّدين المخصّصين كخيارات نماذج للوحة (تُضاف للقائمة الثابتة).\"\"\"\n cfg = load_config()\n custom = [{\"value\": f\"{p.get('id')}/{p.get('model')}\", \"label\": p.get(\"name\", p.get(\"id\"))}\n for p in cfg.get(\"custom_providers\", []) if p.get(\"id\") and p.get(\"model\")]\n return {\"custom\": custom}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"))\n self._send_json(out)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/providers/add\":\n self._send_json(add_custom_provider(body))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n elif path == \"/api/providers\":\n self._send_json(list_providers())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "90754cb38803d4e7e3698c31dfbab29504c8f2ea": {"t": 1785753527.1750817, "n": 22155, "h": "13002c924d0d25c5", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None, session: str = \"dashboard\") -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n sid = session or \"dashboard\"\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(sid, message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(sid, message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(sid)\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef _mem():\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n return SQLiteStore(dict(load_config().get(\"memory\", {})))\n\n\ndef conversations_list() -> dict:\n return {\"conversations\": _mem().list_conversations()}\n\n\ndef conversation_get(conv_id: str) -> dict:\n return {\"id\": conv_id, \"messages\": _mem().conversation_messages(conv_id or \"dashboard\")}\n\n\ndef conversation_new() -> dict:\n import time\n return {\"id\": f\"conv:{int(time.time())}\"}\n\n\n# ── telegram as a chat gateway ───────────────────────────────\ndef telegram_info() -> dict:\n \"\"\"اسم البوت وحالته (للوحة).\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n res = d[\"result\"]\n return {\"ok\": True, \"username\": res.get(\"username\", \"\"),\n \"name\": res.get(\"first_name\", \"Bot\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef telegram_messages() -> dict:\n \"\"\"محادثة تيليجرام المخزّنة (تظهر في قسم محادثة تيليجرام باللوحة).\"\"\"\n return {\"messages\": _mem().conversation_messages(\"telegram\", limit=100)}\n\n\ndef telegram_send(message: str) -> dict:\n \"\"\"يرسل رسالة من اللوحة إلى تيليجرام، ويحفظها في سجل المحادثة.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from tools.agent_tools import send_telegram\n res = send_telegram(message)\n ok = res.startswith(\"✅\")\n if ok:\n try:\n asyncio.run(_mem().save(\"telegram\", f\"(من اللوحة) {message}\", \"📤 أُرسلت إلى تيليجرام\", \"ar\"))\n except Exception:\n pass\n return {\"ok\": ok, \"detail\": res}\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"),\n body.get(\"session\", \"dashboard\"))\n self._send_json(out)\n elif path == \"/api/conversations\":\n self._send_json(conversation_new())\n elif path == \"/api/telegram/send\":\n self._send_json(telegram_send(body.get(\"message\", \"\")))\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n elif path == \"/api/conversations\":\n self._send_json(conversations_list())\n elif path == \"/api/conversation\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(conversation_get(qs.get(\"id\", [\"dashboard\"])[0]))\n elif path == \"/api/telegram/info\":\n self._send_json(telegram_info())\n elif path == \"/api/telegram/messages\":\n self._send_json(telegram_messages())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "006b3fda0443669d756ca3c9bbc857d6fbf4035c": {"t": 1785753528.661908, "n": 19467, "h": "ceab918b92b96558", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(\"dashboard\", message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(\"dashboard\", message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(\"dashboard\")\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n \"nararouter\": \"NARAROUTER_API_KEY\",\n \"cloudflare\": \"CLOUDFLARE_API_TOKEN\",\n \"cloudflare_account\": \"CLOUDFLARE_ACCOUNT_ID\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"))\n self._send_json(out)\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "5a24539a444bad1c4498c785d30a1c10446445c7": {"t": 1785753530.1114225, "n": 19334, "h": "62344f5f8cf4e1c6", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(\"dashboard\", message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(\"dashboard\", message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(\"dashboard\")\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef _telegram_token() -> str:\n \"\"\"يجلب توكين تيليجرام من كل المواضع الممكنة (env / .env / config).\"\"\"\n load_env_into_os()\n cfg = load_config()\n return (os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\")\n or cfg.get(\"interfaces\", {}).get(\"telegram\", {}).get(\"token\", \"\")\n or cfg.get(\"channels\", {}).get(\"telegram\", {}).get(\"token\", \"\"))\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n token = _telegram_token()\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"))\n self._send_json(out)\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "c42dc2b60227ee62ea341df150caf3475c0ff603": {"t": 1785753531.4188306, "n": 19120, "h": "62789b585e44887b", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(\"dashboard\", message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(\"dashboard\", message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(\"dashboard\")\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n load_env_into_os()\n cfg = load_config()\n tg = cfg.get(\"interfaces\", {}).get(\"telegram\", {})\n token = os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\") or tg.get(\"token\", \"\")\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\n# اسم المزوّد → متغيّر البيئة الخاص بمفتاحه\nPROVIDER_KEY_ENV = {\n \"deepseek\": \"DEEPSEEK_API_KEY\", \"zai\": \"ZAI_API_KEY\",\n \"anthropic\": \"ANTHROPIC_API_KEY\", \"openai\": \"OPENAI_API_KEY\",\n \"groq\": \"GROQ_API_KEY\", \"xai\": \"XAI_API_KEY\",\n \"mistral\": \"MISTRAL_API_KEY\", \"gemini\": \"GEMINI_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n}\n\n\ndef save_provider_keys(keys: dict):\n \"\"\"يحوّل {provider: secret} إلى متغيّرات بيئة ويحفظها في .env.\"\"\"\n env_vars = {}\n for prov, secret in (keys or {}).items():\n if secret and prov in PROVIDER_KEY_ENV:\n env_vars[PROVIDER_KEY_ENV[prov]] = secret\n elif secret and prov.endswith(\"_API_KEY\"): # توافق: قد يأتي اسم env مباشرة\n env_vars[prov] = secret\n if env_vars:\n save_env_keys(env_vars)\n\n\ndef memory_search(query: str) -> dict:\n \"\"\"يبحث في طبقات الذاكرة (الحقائق + الحلقات + السجل) عن نص.\"\"\"\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n cfg = load_config()\n mem = SQLiteStore(dict(cfg.get(\"memory\", {})))\n q = (query or \"\").strip().lower()\n results = []\n if not q:\n return {\"results\": []}\n try:\n # Core Knowledge\n for k, v in mem.conn.execute(\"SELECT key, value FROM facts\").fetchall():\n if q in str(k).lower() or q in str(v).lower():\n results.append({\"text\": f\"🧠 {k}: {v}\", \"layer\": \"core\"})\n # Episodic\n for r in mem.conn.execute(\"SELECT topic, key_facts, outcome FROM episodes\").fetchall():\n blob = f\"{r['topic']} {r['key_facts']} {r['outcome']}\"\n if q in blob.lower():\n results.append({\"text\": f\"📚 {r['topic']}: {r['key_facts']}\", \"layer\": \"episodic\"})\n # Working / history\n for r in mem.conn.execute(\n \"SELECT message, response FROM history ORDER BY id DESC LIMIT 200\").fetchall():\n if q in str(r['message']).lower() or q in str(r['response']).lower():\n results.append({\"text\": f\"💬 {r['message']} → {r['response'][:80]}\", \"layer\": \"working\"})\n except Exception as e:\n return {\"results\": [], \"error\": str(e)[:100]}\n return {\"results\": results[:50]}\n\n\ndef create_backup() -> dict:\n \"\"\"ينشئ نسخة احتياطية كاملة ويُعيد اسم الملف.\"\"\"\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n out = AccountManager().create_backup()\n return {\"file\": os.path.basename(out), \"path\": out}\n\n\ndef restart_agent():\n \"\"\"يعيد تشغيل عملية البوابة (re-exec) بعد مهلة قصيرة.\"\"\"\n import sys, threading, time\n def _go():\n time.sleep(0.6)\n try:\n os.execv(sys.executable, [sys.executable] + sys.argv)\n except Exception:\n os._exit(0)\n threading.Thread(target=_go, daemon=True).start()\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard + معرّف الحساب.\"\"\"\n cfg = load_config()\n try:\n import sys\n sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n from account_manager import AccountManager\n cfg = dict(cfg)\n cfg[\"account\"] = AccountManager().get_account_id()\n except Exception:\n pass\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"))\n self._send_json(out)\n elif path == \"/api/config\":\n # المفاتيح تُكتب لـ .env (لا تُحفظ نصاً في config.yaml)\n save_env_keys(body.pop(\"_keys\", {}) or {}) # توافق قديم\n save_provider_keys(body.pop(\"keys\", {}) or {}) # {provider: secret}\n if body:\n save_config(body)\n self._send_json({\"status\": \"saved\"})\n elif path == \"/api/backup\":\n self._send_json(create_backup())\n elif path == \"/api/restart\":\n restart_agent()\n self._send_json({\"status\": \"restarting\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/memory/search\":\n qs = urllib.parse.parse_qs(parsed.query)\n self._send_json(memory_search(qs.get(\"q\", [\"\"])[0]))\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "45a0dd37e859fb5192ec854354ba36540c985f06": {"t": 1785753532.8453064, "n": 15579, "h": "908d2ea6705bf28c", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(\"dashboard\", message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(\"dashboard\", message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(\"dashboard\")\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef telegram_test() -> dict:\n \"\"\"يختبر اتصال بوت تيليجرام المُعدّ عبر getMe.\"\"\"\n load_env_into_os()\n cfg = load_config()\n tg = cfg.get(\"interfaces\", {}).get(\"telegram\", {})\n token = os.environ.get(\"TELEGRAM_BOT_TOKEN\", \"\") or tg.get(\"token\", \"\")\n if not token:\n return {\"ok\": False, \"error\": \"no_token\"}\n import urllib.request\n try:\n with urllib.request.urlopen(\n f\"https://api.telegram.org/bot{token}/getMe\", timeout=8) as r:\n d = json.loads(r.read())\n if d.get(\"ok\"):\n return {\"ok\": True, \"username\": d[\"result\"].get(\"username\", \"\")}\n return {\"ok\": False, \"error\": \"invalid_token\"}\n except Exception as e:\n return {\"ok\": False, \"error\": str(e)[:80]}\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard مع إخفاء المفاتيح الحساسة.\"\"\"\n cfg = load_config()\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"))\n self._send_json(out)\n elif path == \"/api/config\":\n # تحديث المفاتيح يُكتب لـ .env، الباقي لـ config.yaml\n keys = body.pop(\"_keys\", {}) or {}\n if keys:\n save_env_keys(keys)\n cfg = save_config(body) if body else load_config()\n self._send_json({\"status\": \"saved\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n elif path == \"/api/channels/telegram/test\":\n self._send_json(telegram_test())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "16e2651f1d60b1de8013eccceac6525b39d46a2d": {"t": 1785753534.0964131, "n": 14721, "h": "484b49b226b5d6c9", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport glob\nimport asyncio\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\nCONFIG_DIR = Path(os.path.expanduser(\"~/.cobweaverclaw\"))\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# ── config + env helpers (المجلد الآمن فقط) ──────────────────\ndef load_config() -> dict:\n \"\"\"يحمّل config.yaml من المجلد الآمن ثم مجلد المشروع كاحتياط.\"\"\"\n import yaml\n for p in [CONFIG_DIR / \"config.yaml\", Path(BASE_DIR) / \"config.yaml\"]:\n if p.exists():\n try:\n return yaml.safe_load(p.read_text()) or {}\n except Exception:\n return {}\n return {}\n\n\ndef _deep_merge(base: dict, updates: dict) -> dict:\n for k, v in (updates or {}).items():\n if isinstance(v, dict) and isinstance(base.get(k), dict):\n _deep_merge(base[k], v)\n else:\n base[k] = v\n return base\n\n\ndef save_config(updates: dict) -> dict:\n \"\"\"يدمج التحديثات مع config الحالي ويحفظه في المجلد الآمن فقط.\"\"\"\n import yaml\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n cfg = load_config()\n _deep_merge(cfg, updates)\n (CONFIG_DIR / \"config.yaml\").write_text(\n yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False))\n return cfg\n\n\ndef load_env_into_os():\n \"\"\"يحمّل مفاتيح .env من المجلد الآمن إلى البيئة (لاستدعاء النماذج).\"\"\"\n env_path = CONFIG_DIR / \".env\"\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n line = line.strip()\n if line and \"=\" in line and not line.startswith(\"#\"):\n k, v = line.split(\"=\", 1)\n os.environ.setdefault(k.strip(), v.strip())\n\n\ndef save_env_keys(env_vars: dict):\n \"\"\"يحفظ/يحدّث مفاتيح .env في المجلد الآمن (0600).\"\"\"\n CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n env_path = CONFIG_DIR / \".env\"\n existing = {}\n if env_path.exists():\n for line in env_path.read_text().splitlines():\n if \"=\" in line and not line.startswith(\"#\"):\n existing[line.split(\"=\", 1)[0]] = line\n for k, v in env_vars.items():\n if v:\n existing[k] = f\"{k}={v}\"\n os.environ[k] = v\n env_path.write_text(\"\\n\".join(existing.values()) + \"\\n\")\n try:\n os.chmod(env_path, 0o600)\n except Exception:\n pass\n\n\n# ── chat: build memory context + call model ──────────────────\ndef run_chat(message: str, model: str = None) -> dict:\n \"\"\"ينفّذ محادثة: يحمّل المفاتيح + الذاكرة، يستدعي النموذج، يحفظ التبادل.\"\"\"\n load_env_into_os()\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n from brain.model_router import ModelRouter\n\n mem_cfg = dict(cfg.get(\"memory\", {}))\n mem_cfg.setdefault(\"memory_size\", cfg.get(\"agent\", {}).get(\"memory_size\", 20))\n mem = SQLiteStore(mem_cfg)\n router = ModelRouter(cfg)\n lang = \"ar\" if any(c in message for c in \"ابتثجحخدذرزسشصضطظعغفقكلمنهوي\") else \"en\"\n\n async def _go():\n ctx = await mem.get_context(\"dashboard\", message)\n out = await router.complete_meta(message, ctx, lang, force_model=model or None)\n await mem.save(\"dashboard\", message, out.get(\"reply\", \"\"), lang)\n try:\n await mem.maybe_summarize(\"dashboard\")\n except Exception:\n pass\n return out\n\n return asyncio.run(_go())\n\n\ndef memory_stats() -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).stats()\n\n\ndef memory_clear(layer: str) -> dict:\n cfg = load_config()\n import sys\n sys.path.insert(0, BASE_DIR)\n from memory.sqlite_store import SQLiteStore\n mem_cfg = dict(cfg.get(\"memory\", {}))\n return SQLiteStore(mem_cfg).clear(layer or \"working\")\n\n\ndef list_skills() -> list:\n \"\"\"يسرد مهارات مجلد skills/ مع الوصف من docstring وحالة التفعيل.\"\"\"\n cfg = load_config()\n enabled = cfg.get(\"skills\", {}).get(\"enabled\", True)\n out = []\n for f in sorted(glob.glob(os.path.join(BASE_DIR, \"skills\", \"*.py\"))):\n base = os.path.basename(f)[:-3]\n if base.startswith(\"_\") or base in (\"base\",):\n continue\n desc = \"\"\n try:\n import ast\n desc = ast.get_docstring(ast.parse(open(f, encoding=\"utf-8\").read())) or \"\"\n except Exception:\n pass\n out.append({\"name\": base, \"description\": desc.strip().split(\"\\n\")[0][:120],\n \"enabled\": bool(enabled)})\n return out\n\n\ndef channels_status() -> dict:\n \"\"\"حالة قنوات الاتصال من config.\"\"\"\n cfg = load_config().get(\"interfaces\", {})\n def mask(tok):\n return (\"•\" * 6 + tok[-4:]) if tok and len(tok) > 4 else (\"مضبوط\" if tok else \"\")\n tg = cfg.get(\"telegram\", {})\n return {\n \"telegram\": {\"enabled\": bool(tg.get(\"enabled\")),\n \"token_masked\": mask(tg.get(\"token\", \"\")),\n \"status\": \"active\" if tg.get(\"enabled\") and tg.get(\"token\") else \"inactive\"},\n \"discord\": {\"status\": \"coming_soon\"},\n \"slack\": {\"status\": \"coming_soon\"},\n \"whatsapp\": {\"status\": \"coming_soon\"},\n \"signal\": {\"status\": \"coming_soon\"},\n }\n\n\ndef config_public() -> dict:\n \"\"\"config كامل للعرض في الـ dashboard مع إخفاء المفاتيح الحساسة.\"\"\"\n cfg = load_config()\n return cfg\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = False # localhost يدخل بلا توكين افتراضياً\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _read_body(self) -> dict:\n \"\"\"يقرأ جسم الطلب JSON.\"\"\"\n try:\n length = int(self.headers.get(\"Content-Length\", 0))\n if length:\n return json.loads(self.rfile.read(length).decode(\"utf-8\") or \"{}\")\n except Exception:\n pass\n return {}\n\n def do_POST(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n path = parsed.path\n try:\n if path == \"/api/chat\":\n out = run_chat(body.get(\"message\", \"\"), body.get(\"model\"))\n self._send_json(out)\n elif path == \"/api/config\":\n # تحديث المفاتيح يُكتب لـ .env، الباقي لـ config.yaml\n keys = body.pop(\"_keys\", {}) or {}\n if keys:\n save_env_keys(keys)\n cfg = save_config(body) if body else load_config()\n self._send_json({\"status\": \"saved\"})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def do_DELETE(self):\n parsed = urllib.parse.urlparse(self.path)\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n body = self._read_body()\n try:\n if parsed.path == \"/api/memory/clear\":\n self._send_json({\"status\": \"cleared\",\n **memory_clear(body.get(\"layer\", \"working\"))})\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API (GET) بعد التحقق.\"\"\"\n try:\n if path == \"/api/status\":\n self._send_json({\"agent\": \"running\", \"platform\": \"android\", \"version\": \"0.1.0\"})\n elif path == \"/api/account\":\n import sys; sys.path.insert(0, os.path.dirname(__file__))\n from account_manager import AccountManager\n self._send_json(AccountManager().get_or_create_account())\n elif path == \"/api/config\":\n self._send_json(config_public())\n elif path == \"/api/memory/stats\":\n self._send_json(memory_stats())\n elif path == \"/api/skills\":\n self._send_json({\"skills\": list_skills()})\n elif path == \"/api/channels\":\n self._send_json(channels_status())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n except Exception as e:\n self._send_json({\"error\": str(e)}, 500)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "4ff1554e7521b35f85775b29b067aec95741160b": {"t": 1785753535.7425041, "n": 7053, "h": "c7e1ed6f3a219ab3", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = True # عند False: localhost يدخل بلا توكين\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n # منع التخزين المؤقت — يضمن تحميل أحدث نسخة دائماً\n self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate, max-age=0\")\n self.send_header(\"Pragma\", \"no-cache\")\n self.send_header(\"Expires\", \"0\")\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API بعد التحقق.\"\"\"\n if path == \"/api/status\":\n import platform\n self._send_json({\n \"agent\": \"running\",\n \"platform\": platform.system().lower(),\n \"version\": \"0.1.0\",\n })\n elif path == \"/api/account\":\n import sys, os; sys.path.insert(0, os.path.dirname(__file__)); from account_manager import AccountManager\n am = AccountManager()\n self._send_json(am.get_or_create_account())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "7cebf44a9b20653a05d73f067c861f067f4a45a3": {"t": 1785753537.4307566, "n": 6838, "h": "6adc015137220268", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n require_token = True # عند False: localhost يدخل بلا توكين\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين، أو يسمح مباشرة إن كان الوضع بلا توكين.\"\"\"\n # وضع بلا توكين: الوصول المحلي (localhost) مسموح مباشرة\n if not self.require_token:\n client = self.client_address[0] if self.client_address else \"\"\n if client in (\"127.0.0.1\", \"::1\", \"localhost\"):\n return True\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML (التوكين يُتحقق منه عبر JS من الـ hash)\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API بعد التحقق.\"\"\"\n if path == \"/api/status\":\n import platform\n self._send_json({\n \"agent\": \"running\",\n \"platform\": platform.system().lower(),\n \"version\": \"0.1.0\",\n })\n elif path == \"/api/account\":\n import sys, os; sys.path.insert(0, os.path.dirname(__file__)); from account_manager import AccountManager\n am = AccountManager()\n self._send_json(am.get_or_create_account())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\",\n require_token: bool = True):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n require_token=False → localhost يدخل بلا توكين (أسهل، آمن محلياً فقط)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n # على الشبكة المفتوحة، التوكين إجباري دائماً (أمان)\n if host == \"0.0.0.0\":\n require_token = True\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n GatewayHandler.require_token = require_token\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n if not require_token:\n print(f\" افتح في المتصفح مباشرة (بلا توكين):\")\n print(f\" http://{host}:{port}/\")\n print(f\"\\n 🔓 localhost بلا توكين — الدخول مباشر\")\n else:\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "60364942246e22723ae03d98253198e7459f8fd7": {"t": 1785753538.6426299, "n": 5997, "h": "2a3b4cecabdfb4c3", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين في الـ query أو الـ header.\"\"\"\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML (التوكين يُتحقق منه عبر JS من الـ hash)\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API بعد التحقق.\"\"\"\n if path == \"/api/status\":\n import platform\n self._send_json({\n \"agent\": \"running\",\n \"platform\": platform.system().lower(),\n \"version\": \"0.1.0\",\n })\n elif path == \"/api/account\":\n import sys, os; sys.path.insert(0, os.path.dirname(__file__)); from account_manager import AccountManager\n am = AccountManager()\n self._send_json(am.get_or_create_account())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\"):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n # خادم يضبط SO_REUSEADDR *قبل* الربط — يمنع خطأ \"Address already in use\"\n class ReusableServer(socketserver.TCPServer):\n allow_reuse_address = True # يُطبَّق قبل bind تلقائياً\n\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n except OSError as e:\n if e.errno in (98, 48): # 98=Linux, 48=Mac: المنفذ مشغول\n print(f\" ⚠️ المنفذ {port} مشغول — غالباً بوابة قديمة ما زالت تعمل.\\n\")\n print(f\" الحل:\")\n print(f\" 1) أغلق البوابة القديمة: python main.py stop\")\n print(f\" 2) أو أغلق نوافذ Termux الأخرى التي تشغّل gateway\")\n print(f\" 3) ثم أعد: python main.py gateway\\n\")\n # محاولة أخيرة: إيقاف العملية القديمة تلقائياً\n _try_free_port(port)\n try:\n httpd = ReusableServer((host, port), GatewayHandler)\n print(f\" ✅ تم تحرير المنفذ تلقائياً — البوابة تعمل الآن.\\n\")\n except OSError:\n print(f\" ❌ تعذّر تحرير المنفذ تلقائياً. استخدم: python main.py stop\")\n return\n else:\n raise\n\n with httpd:\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n httpd.shutdown()\n\n\ndef _try_free_port(port: int):\n \"\"\"يحاول إيقاف أي عملية تشغّل البوابة على هذا المنفذ (Termux/Linux).\"\"\"\n import subprocess\n import signal\n try:\n # ابحث عن PID المستخدم للمنفذ عبر /proc (يعمل في Termux بلا أدوات إضافية)\n my_pid = os.getpid()\n for pid in os.listdir(\"/proc\"):\n if not pid.isdigit() or int(pid) == my_pid:\n continue\n try:\n cmdline = open(f\"/proc/{pid}/cmdline\", \"rb\").read().decode(\"utf-8\", \"ignore\")\n # عملية python تشغّل gateway (وليست هذه العملية)\n if \"python\" in cmdline and \"gateway\" in cmdline:\n os.kill(int(pid), signal.SIGTERM)\n except Exception:\n continue\n import time\n time.sleep(1) # مهلة لتحرير المنفذ\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "71a815dc29c65200d32ff51fe6d27f28142b3120": {"t": 1785753539.927921, "n": 4121, "h": "b58284c9cf5c2c53", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين في الـ query أو الـ header.\"\"\"\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML (التوكين يُتحقق منه عبر JS من الـ hash)\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API بعد التحقق.\"\"\"\n if path == \"/api/status\":\n import platform\n self._send_json({\n \"agent\": \"running\",\n \"platform\": platform.system().lower(),\n \"version\": \"0.1.0\",\n })\n elif path == \"/api/account\":\n import sys, os; sys.path.insert(0, os.path.dirname(__file__)); from account_manager import AccountManager\n am = AccountManager()\n self._send_json(am.get_or_create_account())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 7878,\n config_dir: str = None, dashboard_html: str = \"\"):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n with socketserver.TCPServer((host, port), GatewayHandler) as httpd:\n httpd.allow_reuse_address = True\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "2d41af0f5da86ab7d72acebc4b5135c277577ea5": {"t": 1785753541.139587, "n": 4121, "h": "e125151a0572acbd", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين في الـ query أو الـ header.\"\"\"\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML (التوكين يُتحقق منه عبر JS من الـ hash)\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API بعد التحقق.\"\"\"\n if path == \"/api/status\":\n import platform\n self._send_json({\n \"agent\": \"running\",\n \"platform\": platform.system().lower(),\n \"version\": \"0.1.0\",\n })\n elif path == \"/api/account\":\n import sys, os; sys.path.insert(0, os.path.dirname(__file__)); from account_manager import AccountManager\n am = AccountManager()\n self._send_json(am.get_or_create_account())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 8787,\n config_dir: str = None, dashboard_html: str = \"\"):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n with socketserver.TCPServer((host, port), GatewayHandler) as httpd:\n httpd.allow_reuse_address = True\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}, "340dc12f5dfb863cb9a3111e3032e45388e2bcd5": {"t": 1785753542.407744, "n": 4058, "h": "400ff87db760cc4a", "c": "\"\"\"\nGateway Server — بوابة التحكم عبر المتصفح.\n- localhost افتراضياً (127.0.0.1) — أأمن\n- توكين إجباري لكل طلب\n- يمكن فتحه للشبكة المحلية بأمر صريح (--host 0.0.0.0)\nيعمل بمكتبة http.server القياسية — لا اعتماديات خارجية.\n\"\"\"\nimport os\nimport json\nimport http.server\nimport socketserver\nimport urllib.parse\nfrom pathlib import Path\nfrom gateway_auth import GatewayAuth\n\n\nclass GatewayHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"يعالج طلبات البوابة مع التحقق من التوكين.\"\"\"\n\n auth = None # تُحقن من الخارج\n dashboard_html = \"\" # محتوى HTML\n\n def log_message(self, fmt, *args):\n pass # كتم سجل الطلبات الافتراضي\n\n def _check_auth(self) -> bool:\n \"\"\"يتحقق من التوكين في الـ query أو الـ header.\"\"\"\n parsed = urllib.parse.urlparse(self.path)\n qs = urllib.parse.parse_qs(parsed.query)\n token = (qs.get(\"token\", [None])[0]\n or self.headers.get(\"X-Gateway-Token\"))\n return self.auth.verify(token) if token else False\n\n def _send_json(self, data: dict, status: int = 200):\n body = json.dumps(data, ensure_ascii=False).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n\n def do_GET(self):\n parsed = urllib.parse.urlparse(self.path)\n path = parsed.path\n\n # الصفحة الرئيسية تُقدّم HTML (التوكين يُتحقق منه عبر JS من الـ hash)\n if path == \"/\" or path == \"/index.html\":\n body = self.dashboard_html.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n return\n\n # كل الـ API endpoints تتطلب توكيناً صحيحاً\n if path.startswith(\"/api/\"):\n if not self._check_auth():\n self._send_json({\"error\": \"unauthorized\"}, 401)\n return\n self._handle_api(path, parsed)\n return\n\n self.send_response(404)\n self.end_headers()\n\n def _handle_api(self, path: str, parsed):\n \"\"\"معالجة نقاط الـ API بعد التحقق.\"\"\"\n if path == \"/api/status\":\n import platform\n self._send_json({\n \"agent\": \"running\",\n \"platform\": platform.system().lower(),\n \"version\": \"0.1.0\",\n })\n elif path == \"/api/account\":\n from account_manager import AccountManager\n am = AccountManager()\n self._send_json(am.get_or_create_account())\n else:\n self._send_json({\"error\": \"not_found\"}, 404)\n\n\ndef run_gateway(host: str = \"127.0.0.1\", port: int = 8787,\n config_dir: str = None, dashboard_html: str = \"\"):\n \"\"\"\n يشغّل بوابة التحكم.\n host=127.0.0.1 → localhost فقط (افتراضي، أأمن)\n host=0.0.0.0 → الشبكة المحلية (يحتاج توكيناً، يُفعّل بأمر صريح)\n \"\"\"\n auth = GatewayAuth(config_dir)\n token = auth.get_or_create_token()\n\n GatewayHandler.auth = auth\n GatewayHandler.dashboard_html = dashboard_html\n\n url = f\"http://{host if host != '0.0.0.0' else '<your-ip>'}:{port}/#token={token}\"\n\n print(f\"\\n🕷️ CoBWeaverClaw Gateway\")\n print(f\" الرابط (مع التوكين):\")\n print(f\" {url}\")\n if host == \"0.0.0.0\":\n print(f\"\\n ⚠️ مفتوح على الشبكة المحلية — التوكين إجباري للحماية\")\n else:\n print(f\"\\n 🔒 localhost فقط — آمن، لا أحد على الشبكة يصله\")\n print(f\"\\n لإيقاف البوابة: Ctrl+C\\n\")\n\n with socketserver.TCPServer((host, port), GatewayHandler) as httpd:\n httpd.allow_reuse_address = True\n try:\n httpd.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n تم إيقاف البوابة.\")\n\n\nif __name__ == \"__main__\":\n import sys\n host = \"0.0.0.0\" if \"--network\" in sys.argv else \"127.0.0.1\"\n run_gateway(host=host)\n"}}}}