File size: 6,547 Bytes
845178e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#!/usr/bin/env python3
"""
从远程 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()