| |
| """ |
| 从远程 WorkBuddy 实例提取 Token。 |
| |
| 支持两种模式: |
| 1. 本地模式:自动检测本机 WorkBuddy |
| 2. 远程模式:连接远程 Mac 的 CDP 端口 |
| |
| 用法: |
| # 本机自动检测 |
| python extract_token_remote.py |
| |
| # 远程 Mac(需要先在目标机器启动 WorkBuddy 调试模式) |
| python extract_token_remote.py --host 192.168.1.100 --port 9222 --save |
| |
| 前置条件(远程 Mac): |
| /Applications/WorkBuddy.app/Contents/MacOS/Electron --remote-debugging-port=9222 |
| |
| 注意:可能需要配置防火墙允许 9222 端口入站。 |
| """ |
|
|
| import argparse |
| import asyncio |
| import json |
| import sys |
| import time |
| from pathlib import Path |
|
|
| if sys.platform == "win32": |
| asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) |
|
|
| try: |
| import httpx |
| except ImportError: |
| sys.exit("请先安装依赖: pip install httpx") |
|
|
| try: |
| import websockets |
| except ImportError: |
| sys.exit("请先安装依赖: pip install websockets") |
|
|
|
|
| async def extract(cdp_host: str = "127.0.0.1", cdp_port: int = 9222) -> dict | None: |
| base = f"http://{cdp_host}:{cdp_port}" |
|
|
| print(f"正在连接 CDP ({base})...") |
|
|
| async with httpx.AsyncClient() as client: |
| try: |
| resp = await client.get(f"{base}/json", timeout=10) |
| except httpx.ConnectError as e: |
| print(f"❌ 无法连接 CDP: {e}") |
| print(f"\n请确保远程 Mac 已启动 WorkBuddy 调试模式:") |
| print(f" /Applications/WorkBuddy.app/Contents/MacOS/Electron --remote-debugging-port={cdp_port}") |
| return None |
| targets = resp.json() |
|
|
| ws_url = None |
| for t in targets: |
| if t.get("type") == "page" and "workbench" in t.get("url", ""): |
| ws_url = t.get("webSocketDebuggerUrl") |
| if ws_url: |
| if cdp_host != "127.0.0.1" and cdp_host != "localhost": |
| ws_url = ws_url.replace("127.0.0.1", cdp_host) |
| print(f"✅ 找到 Workbench 页面") |
| break |
|
|
| if not ws_url: |
| for t in targets: |
| if t.get("type") == "page": |
| ws_url = t.get("webSocketDebuggerUrl") |
| if ws_url: |
| if cdp_host != "127.0.0.1" and cdp_host != "localhost": |
| ws_url = ws_url.replace("127.0.0.1", cdp_host) |
| print(f"⚠️ 未找到 Workbench,找到通用页面") |
| break |
|
|
| if not ws_url: |
| print("❌ 未找到可用的 CDP 页面目标") |
| return None |
|
|
| print(f"正在通过 WebSocket 提取 Token...") |
|
|
| async with websockets.connect(ws_url, ping_timeout=30) as ws: |
| cmd = { |
| "id": 1, |
| "method": "Runtime.evaluate", |
| "params": { |
| "expression": """ |
| (async () => { |
| try { |
| const s = await window.vscode.ipcRenderer.invoke( |
| 'vscode:genie:auth:getSession' |
| ); |
| return JSON.stringify(s); |
| } catch(e) { |
| return JSON.stringify({error: e.message}); |
| } |
| })() |
| """, |
| "awaitPromise": True, |
| "returnByValue": True, |
| }, |
| } |
| await ws.send(json.dumps(cmd)) |
| result = json.loads(await asyncio.wait_for(ws.recv(), timeout=30)) |
|
|
| value = result.get("result", {}).get("result", {}).get("value", "") |
| if not value: |
| print("❌ CDP 返回为空") |
| return None |
|
|
| session = json.loads(value) |
| if session.get("error"): |
| print(f"❌ CDP 错误: {session['error']}") |
| return None |
|
|
| return session |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="从 WorkBuddy 提取 Token(支持远程)", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| 示例: |
| # 本机自动检测 |
| python extract_token_remote.py |
| |
| # 远程 Mac |
| python extract_token_remote.py --host 192.168.1.100 --save |
| |
| # 自定义端口 |
| python extract_token_remote.py --host 192.168.1.100 --port 9223 --save |
| """ |
| ) |
| parser.add_argument( |
| "--host", "-H", |
| default="127.0.0.1", |
| help="CDP 主机地址 (默认: 127.0.0.1)" |
| ) |
| parser.add_argument( |
| "--port", "-p", |
| type=int, |
| default=9222, |
| help="CDP 端口 (默认: 9222)" |
| ) |
| parser.add_argument( |
| "--save", "-s", |
| action="store_true", |
| help="保存到 data/token.json" |
| ) |
| parser.add_argument( |
| "--output", "-o", |
| help="保存到指定文件 (默认: data/token.json)" |
| ) |
| args = parser.parse_args() |
|
|
| is_remote = args.host not in ("127.0.0.1", "localhost") |
|
|
| if is_remote: |
| print(f"\n🔗 远程模式") |
| print(f" 目标: {args.host}:{args.port}") |
| print(f" 注意: 确保目标 Mac 已启动 WorkBuddy 调试模式\n") |
| else: |
| print(f"\n🔗 本地模式\n") |
|
|
| session = asyncio.run(extract(args.host, args.port)) |
| if not session: |
| sys.exit(1) |
|
|
| auth = session.get("auth", session) |
| access_token = auth.get("accessToken", "") |
| refresh_token = auth.get("refreshToken", "") |
|
|
| if not access_token: |
| print("❌ 未获取到 accessToken") |
| sys.exit(1) |
|
|
| print(f"\n✅ Token 提取成功!") |
| print(f" accessToken: {access_token[:40]}... (len={len(access_token)})") |
| print(f" refreshToken: {refresh_token[:40] if refresh_token else 'N/A'}... " |
| f"(len={len(refresh_token) if refresh_token else 0})") |
|
|
| if args.save or args.output: |
| import time as time_module |
| out_path = Path(args.output) if args.output else Path(__file__).parent / "data" / "token.json" |
| out_path.parent.mkdir(parents=True, exist_ok=True) |
| out_path.write_text(json.dumps({ |
| "access_token": access_token, |
| "refresh_token": refresh_token, |
| "saved_at": time_module.strftime("%Y-%m-%d %H:%M:%S"), |
| "source_host": args.host if is_remote else "localhost", |
| "source_port": args.port, |
| }, indent=2)) |
| print(f"\n💾 已保存到: {out_path}") |
|
|
| if is_remote: |
| print(f"\n📋 接下来在本地 Mac 上:") |
| print(f" 1. 将生成的 data/token.json 复制到本项目") |
| print(f" 2. 启动代理: python server.py") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|