diff --git a/oproxy.py b/oproxy.py index 7de8656..b11e478 100644 --- a/oproxy.py +++ b/oproxy.py @@ -1,5 +1,5 @@ # バージョン情報: Python 3.12+ / FastAPI 0.115.0 / uvicorn 0.30.0 / httpx 0.28.0 -# [2026-02-10] 3060(12GB)戦術支援:全機能完全統合・非短縮版 +# [2026-02-14] 3060(12GB)戦術支援:Mattermost Webhook連携・遅延送信実装版 import argparse import asyncio import json @@ -35,6 +35,10 @@ CONFIG = { "models_cache": [], "loop": None, "vram_total": 12.0, + # --- Mattermost 連携設定 --- + "webhook_url": "https://mm.ka.sugeee.com/hooks/ctjisw6ugjg85nhddo9ox9zt4c", + "enable_webhook": True, + "max_log_len": 4000 } # 高速抽出用正規表現 @@ -49,7 +53,40 @@ def pulse(char, color=C_RESET): print(f"{color}{char}{C_RESET}", end="", flush=True) -# --- 🧠 戦略的モデル分析 (2行分割レイアウト) --- +# --- 🛰️ Mattermost 送信エンジン (非同期・遅延送信) --- +async def post_to_mattermost(model_name, path, req_body, resp_text): + """通信完了後にバックグラウンドでMattermostへログを飛ばす""" + if not CONFIG["webhook_url"] or not CONFIG["enable_webhook"]: + return + try: + req_obj = json.loads(req_body) + req_clean = json.dumps(req_obj, indent=2, ensure_ascii=False) + except: + req_clean = req_body + + resp_display = ( + resp_text if len(resp_text) < CONFIG["max_log_len"] + else resp_text[:CONFIG["max_log_len"]] + "\n\n*(Truncated due to length)*" + ) + + payload = { + "username": f"oproxy:{model_name}", + "icon_url": "https://ollama.com/public/ollama.png", + "text": ( + f"### 🛡️ LLM Traffic Log: `{path}`\n" + f"- **Model:** `{model_name}` | **Time:** {datetime.now().strftime('%H:%M:%S')}\n" + f"#### 📥 Request\n```json\n{req_clean[:800]}\n```\n" + f"#### 📤 Response\n{resp_display}" + ) + } + try: + async with httpx.AsyncClient(timeout=10.0) as client: + await client.post(CONFIG["webhook_url"], json=payload) + except Exception as e: + print(f"\n{C_RED}[Webhook Fail] {e}{C_RESET}") + + +# --- 🧠 戦略적モデル分析 (2行分割レイアウト) --- async def fetch_detailed_models(): """Ollamaの内部情報を深掘りし、メタデータを完全取得する""" try: @@ -192,6 +229,13 @@ async def sticky_proxy(path: str, request: Request): print(f"{get_ts()} {C_WHITE}/{path: <10}{C_RESET} ", end="", flush=True) body = await request.body() + + # モデル名の特定 + model_name = "unknown" + try: + model_name = json.loads(body).get("model", "unknown") + except: + pass if do_dump: print( @@ -223,26 +267,30 @@ async def sticky_proxy(path: str, request: Request): headers=headers, ) as response: pulse("v", C_GREEN) - + + full_text_buffer = [] async for chunk in response.aiter_bytes(): - if do_dump: - # チャンクから正規表現で高速テキスト抽出(リアルタイム表示) - raw_data = chunk.decode(errors="ignore") - matches = RE_CONTENT.findall(raw_data) - for m in matches: - try: - text = m.encode().decode( - "unicode_escape", errors="ignore" - ) - print( - f"{C_WHITE}{text}{C_RESET}", end="", flush=True - ) - except: - pass + raw_data = chunk.decode(errors="ignore") + matches = RE_CONTENT.findall(raw_data) + for m in matches: + try: + text = m.encode().decode("unicode_escape", errors="ignore") + full_text_buffer.append(text) + if do_dump: + print(f"{C_WHITE}{text}{C_RESET}", end="", flush=True) + except: + pass pulse("v", C_GREEN) yield chunk + # 通信完了後に非同期タスクとしてWebhook送信 + if full_text_buffer: + combined_text = "".join(full_text_buffer) + asyncio.create_task(post_to_mattermost( + model_name, path, body.decode(errors='ignore'), combined_text + )) + if do_dump: print(f"\n{C_GREEN}{'=' * 60}{C_RESET}") pulse("*", C_YELLOW) @@ -300,4 +348,4 @@ if __name__ == "__main__": # メインスレッドでUvicorn、サブスレッドで入力待ち threading.Thread(target=input_handler, daemon=True).start() - uvicorn.run(app, host="127.0.0.1", port=args.local, log_level="error") + uvicorn.run(app, host="127.0.0.1", port=args.local, log_level="error") \ No newline at end of file