gchat/gemini_cli.py
2026-02-12 16:08:15 +09:00

189 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import os
import re
import shlex
from datetime import datetime
from playwright.async_api import async_playwright
# [2026-02-11] Version: 1.9.3-Mad-Auto-Heal-Full
# [2025-07-04] Embed: Version info
# [2025-06-15] Format: Markdown Standard
class MadGeminiAgent:
def __init__(self, context, page):
self.context = context
self.page = page
self.input_selector = "div[role='textbox']"
self.container_name = "sandbox_container"
self.yolo_mode = False
self.max_iterations = 10
async def execute_in_sandbox(self, script):
"""コンテナへの電気信号送信"""
docker_cmd = f"docker exec -e OLLAMA_HOST=host.docker.internal -w /workspace {self.container_name} bash -c {shlex.quote(script)}"
proc = await asyncio.create_subprocess_shell(
docker_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
return stdout.decode('utf-8', errors='replace'), stderr.decode('utf-8', errors='replace')
async def wait_for_gemini_stable(self):
"""Geminiの打鍵が止まるまで粘り強く監視"""
print("🧠 Gemini思考中", end="", flush=True)
last_text = ""
stable_count = 0
for _ in range(120):
responses = await self.page.query_selector_all(".model-response-text")
if responses:
current_text = await responses[-1].inner_text()
if current_text == last_text and len(current_text) > 0:
stable_count += 1
else:
stable_count = 0
last_text = current_text
if stable_count >= 3:
await asyncio.sleep(1)
break
print(".", end="", flush=True)
await asyncio.sleep(1)
print(" 完了!")
def auto_heal_command(self, code):
"""
【外科手術】Geminiが壊したBash/C結合構文を物理的に修理する
"""
# 1. 'cat << 'EOF' > file.c#include' のように # が密着している場合、改行を挿入
code = re.sub(r"(cat <<\s*['\"]?EOF['\"]?\s*>\s*[^\s]+\.[a-z0-9]+)#", r"\1\n#", code)
# 2. 'EOF#' や 'EOFgcc' のように終端文字が次行とくっついている場合を分離
code = re.sub(r"EOF#", "EOF\n#", code)
code = re.sub(r"EOFgcc", "EOF\ngcc", code)
code = re.sub(r"EOF/", "EOF\n/", code)
# 3. その他、極端なワンライナー化の形跡があれば修正
# (必要に応じてここへ正規表現を追加)
return code
async def extract_commands(self, response_element):
"""HTMLから実行可能なコマンドを抽出し、Auto-Healを適用"""
code_elements = await response_element.query_selector_all("code")
results = []
for el in code_elements:
code_text = (await el.inner_text()).strip()
# 単語のみのタグは無視
if "\n" not in code_text and " " not in code_text:
continue
# --- Auto-Heal 適用 ---
code_text = self.auto_heal_command(code_text)
# 言語ラベルの除去
lines = code_text.split('\n')
if lines and lines[0].lower() in ['bash', 'sh', 'shell', 'c', 'python', 'cpp']:
code_text = '\n'.join(lines[1:])
if code_text.strip():
results.append(code_text.strip())
return results
async def send_feedback(self, text):
"""Geminiへのフィードバック送信"""
try:
target = self.page.locator(self.input_selector)
await target.wait_for(state="visible", timeout=10000)
await target.scroll_into_view_if_needed()
await target.click()
await target.fill(text)
await self.page.keyboard.press("Enter")
return True
except Exception as e:
print(f"\n⚠️ 通信障害: {e}")
return False
async def run_loop(self):
print(f"\n[{datetime.now().strftime('%H:%M:%S')}] 🧪 実験室オンラインHeal-Engine 1.0)。")
while True:
mode_str = "☢️ YOLO" if self.yolo_mode else "🛡️ NORMAL"
user_input = input(f"\n🧬 Master [{mode_str}] > ").strip()
if not user_input: continue
if user_input.lower() in ["exit", "quit"]: break
if user_input.lower() == "yolo on":
self.yolo_mode = True
print("☢️ YOLOモード自動修復・自動実行を開始。")
continue
elif user_input.lower() == "yolo off":
self.yolo_mode = False
print("🛡️ 安全装置:再起動。")
continue
await self.send_feedback(user_input)
iteration = 0
while iteration < self.max_iterations:
iteration += 1
await self.wait_for_gemini_stable()
responses = await self.page.query_selector_all(".model-response-text")
if not responses: break
commands = await self.extract_commands(responses[-1])
if not commands:
raw_text = await responses[-1].inner_text()
print(f"🤖 Gemini: {raw_text[:80].replace(chr(10), ' ')}...")
break
print(f"\n⚠️ {len(commands)}件の命令を確認。")
all_results = ""
for i, code in enumerate(commands):
if self.yolo_mode:
print(f"🚀 [YOLO] Auto-executing {i+1}/{len(commands)}...")
ans = 'y'
else:
print(f"\n--- 🧪 実行案 [{i+1}/{len(commands)}] ---\n{code}\n----------------")
ans = input("▶️ 実行許可? (y/n/skip): ").lower()
if ans == 'y':
print("📡 信号送信中...")
out, err = await self.execute_in_sandbox(code)
print(f"\n┏━━━━━━━━━━━━ 実行結果 ━━━━━━━━━━━━┓")
print(f"┃ [STDOUT]\n{out if out else '(空)'}")
if err:
print(f"┃ [STDERR]\n{err}")
print(f"┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
all_results += f"Command: {code}\nResult: {out}\nError: {err}\n"
elif ans == 'skip': continue
else: break
if all_results:
print(f"🔄 フィードバック送信中... ({iteration}/{self.max_iterations})")
feedback = f"実行結果だ。修正が必要ならコードを出せ。成功なら次のステップへ。\n{all_results}"
if not await self.send_feedback(feedback): break
else:
break
async def main():
async with async_playwright() as p:
user_data_dir = os.path.expanduser("~/dev/gchat/gemini_profile")
context = await p.chromium.launch_persistent_context(
user_data_dir, headless=False,
args=["--disable-blink-features=AutomationControlled", "--no-sandbox"]
)
page = context.pages[0]
await page.goto("https://gemini.google.com/app")
agent = MadGeminiAgent(context, page)
await agent.run_loop()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n☢️ 実験室を封鎖。")