From 3d992041e9abb9e1f700c425521fae3eea214aad Mon Sep 17 00:00:00 2001 From: joe Date: Sat, 14 Feb 2026 13:57:02 +0900 Subject: [PATCH] =?UTF-8?q?webhook=E3=81=AE=E6=96=87=E5=AD=97=E5=8C=96?= =?UTF-8?q?=E3=81=91=E5=AF=BE=E7=AD=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oproxy.py | 67 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/oproxy.py b/oproxy.py index b11e478..37d959c 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-14] 3060(12GB)戦術支援:Mattermost Webhook連携・遅延送信実装版 +# [2026-02-14] 3060(12GB)戦術支援:Mattermost連携・文字化け完全対策・全ロジック非短縮版 import argparse import asyncio import json @@ -53,40 +53,44 @@ def pulse(char, color=C_RESET): print(f"{color}{char}{C_RESET}", end="", flush=True) -# --- 🛰️ Mattermost 送信エンジン (非同期・遅延送信) --- +# --- 🛰️ Mattermost 送信エンジン (文字化け対策済み) --- async def post_to_mattermost(model_name, path, req_body, resp_text): - """通信完了後にバックグラウンドでMattermostへログを飛ばす""" + """通信完了後にバックグラウンドで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 + # リクエストのデコード(生日本語を維持) + try: + req_obj = json.loads(req_body) + req_clean = json.dumps(req_obj, indent=2, ensure_ascii=False) + except: + req_clean = req_body.decode('utf-8', errors='replace') - 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}" + # レスポンスのカットオフ + resp_display = ( + resp_text if len(resp_text) < CONFIG["max_log_len"] + else resp_text[:CONFIG["max_log_len"]] + "\n\n*(長文のため中略...)*" ) - } - try: - async with httpx.AsyncClient(timeout=10.0) as client: - await client.post(CONFIG["webhook_url"], json=payload) + + 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 preview\n```json\n{req_clean[:800]}\n```\n" + f"#### 📤 Response\n{resp_display}" + ) + } + async with httpx.AsyncClient(timeout=15.0) as client: + # ensure_ascii=False で Mattermost に生の日本語を届ける + encoded_payload = json.dumps(payload, ensure_ascii=False).encode('utf-8') + await client.post(CONFIG["webhook_url"], content=encoded_payload, headers={"Content-Type": "application/json"}) except Exception as e: print(f"\n{C_RED}[Webhook Fail] {e}{C_RESET}") -# --- 🧠 戦略적モデル分析 (2行分割レイアウト) --- +# --- 🧠 戦略的モデル分析 (2行分割レイアウト・非短縮版) --- async def fetch_detailed_models(): """Ollamaの内部情報を深掘りし、メタデータを完全取得する""" try: @@ -230,7 +234,7 @@ 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") @@ -239,7 +243,7 @@ async def sticky_proxy(path: str, request: Request): if do_dump: print( - f"\n{C_YELLOW}{'=' * 60}\n[DUMP REQUEST: {request.method} /{path}]\n{'=' * 60}{C_RESET}" + f"\n{C_YELLOW}{'=' * 60}\n[DUMP REQUEST: {model_name}]\n{'=' * 60}{C_RESET}" ) try: print(json.dumps(json.loads(body), indent=2, ensure_ascii=False)) @@ -270,16 +274,19 @@ async def sticky_proxy(path: str, request: Request): full_text_buffer = [] async for chunk in response.aiter_bytes(): + # チャンクから正規表現で高速テキスト抽出 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") + # 【最重要】Unicodeエスケープを戻し、化けないようにUTF-8で再構成 + text = m.encode('utf-8').decode('unicode_escape').encode('latin1').decode('utf-8') full_text_buffer.append(text) if do_dump: print(f"{C_WHITE}{text}{C_RESET}", end="", flush=True) except: - pass + # 万が一デコードに失敗した場合はマッチした文字列をそのまま保持 + full_text_buffer.append(m) pulse("v", C_GREEN) yield chunk @@ -288,7 +295,7 @@ async def sticky_proxy(path: str, request: Request): 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 + model_name, path, body, combined_text )) if do_dump: