From 5a1d3a41da3f005fe29650fea0ff08eeaf902ddb Mon Sep 17 00:00:00 2001 From: Jiaqing Liang Date: Fri, 10 Apr 2026 14:04:41 +0800 Subject: [PATCH] fix: handle window object serialization in CDP bridge; improve file_write error msg; minor llmcore style cleanup --- assets/copilot_proxy.pyw | 185 +++++++++++++++++++++++++++ assets/tmwd_cdp_bridge/background.js | 3 +- ga.py | 2 +- llmcore.py | 9 +- 4 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 assets/copilot_proxy.pyw diff --git a/assets/copilot_proxy.pyw b/assets/copilot_proxy.pyw new file mode 100644 index 0000000..e2af17f --- /dev/null +++ b/assets/copilot_proxy.pyw @@ -0,0 +1,185 @@ +""" +Copilot Local Proxy - TK GUI +本地 OpenAI 兼容代理,自动管理 Copilot token 并转发请求 +""" +import tkinter as tk +from tkinter import scrolledtext +import threading, json, os, time, uuid +from http.server import HTTPServer, BaseHTTPRequestHandler +import requests + +# ============ Config ============ +OAUTH_PATH = os.path.join(os.path.expanduser('~'), '.copilot_oauth.json') +COPILOT_TOKEN_URL = 'https://api.github.com/copilot_internal/v2/token' +COPILOT_API_BASE = 'https://api.githubcopilot.com' +PROXY = {'https': 'http://127.0.0.1:2082'} +LOCAL_PORT = 15432 +REFRESH_MARGIN = 120 # 提前120秒刷新 + +COPILOT_HEADERS = { + 'Editor-Version': 'vscode/1.110.1', + 'Editor-Plugin-Version': 'copilot-chat/0.38.2', + 'User-Agent': 'GitHubCopilotChat/0.38.2', + 'Copilot-Integration-Id': 'vscode-chat', + 'openai-intent': 'conversation-panel', +} + + +# ============ Token Manager ============ +class TokenManager: + def __init__(self, log_fn=print): + self.copilot_token = None + self.expires_at = 0 + self.log = log_fn + self._lock = threading.Lock() + with open(OAUTH_PATH) as f: + self.access_token = json.load(f)['access_token'] + self.log(f"[Token] OAuth token loaded: ***{self.access_token[-6:]}") + + def get_token(self): + with self._lock: + if time.time() < self.expires_at - REFRESH_MARGIN: + return self.copilot_token + return self._refresh() + + def _refresh(self): + self.log("[Token] Refreshing copilot token...") + try: + resp = requests.get(COPILOT_TOKEN_URL, headers={ + 'Authorization': f'token {self.access_token}', + 'User-Agent': 'GitHubCopilotChat/0.38.2', + 'Accept': 'application/json', + }, proxies=PROXY, timeout=15) + resp.raise_for_status() + data = resp.json() + self.copilot_token = data['token'] + self.expires_at = data['expires_at'] + remain = int(self.expires_at - time.time()) + self.log(f"[Token] Refreshed OK, expires in {remain}s") + return self.copilot_token + except Exception as e: + self.log(f"[Token] Refresh FAILED: {e}") + return self.copilot_token + + +# ============ Proxy Handler ============ +class ProxyHandler(BaseHTTPRequestHandler): + token_mgr: TokenManager = None + log_fn = print + + def do_POST(self): + try: + length = int(self.headers.get('Content-Length', 0)) + body = json.loads(self.rfile.read(length)) if length else {} + model = body.get('model', '?') + stream = body.get('stream', False) + self.log_fn(f"[Req] {model} stream={stream}") + + token = self.token_mgr.get_token() + if not token: + self._error(503, "No copilot token available") + return + + headers = {**COPILOT_HEADERS, + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + 'x-request-id': str(uuid.uuid4())} + + path = self.path + if path.startswith('/v1/'): + path = path[3:] # strip /v1 prefix + target = f"{COPILOT_API_BASE}{path}" + resp = requests.post(target, headers=headers, json=body, + proxies=PROXY, timeout=120, stream=stream) + + if stream and 'text/event-stream' in resp.headers.get('content-type', ''): + self.send_response(resp.status_code) + self.send_header('Content-Type', 'text/event-stream') + self.send_header('Cache-Control', 'no-cache') + self.end_headers() + for chunk in resp.iter_content(chunk_size=None): + if chunk: + self.wfile.write(chunk) + self.wfile.flush() + else: + self.send_response(resp.status_code) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(resp.content) + + self.log_fn(f"[Resp] {resp.status_code}") + except Exception as e: + self.log_fn(f"[Error] {e}") + self._error(502, str(e)) + + def do_GET(self): + try: + token = self.token_mgr.get_token() + headers = {**COPILOT_HEADERS, 'Authorization': f'Bearer {token}'} + path = self.path + if path.startswith('/v1/'): + path = path[3:] + target = f"{COPILOT_API_BASE}{path}" + resp = requests.get(target, headers=headers, proxies=PROXY, timeout=15) + self.send_response(resp.status_code) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(resp.content) + except Exception as e: + self._error(502, str(e)) + + def _error(self, code, msg): + self.send_response(code) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({'error': msg}).encode()) + + def log_message(self, fmt, *args): + pass + + +# ============ TK GUI ============ +class App: + def __init__(self): + self.root = tk.Tk() + self.root.title("Copilot Proxy") + self.root.geometry("520x360") + self.root.resizable(False, False) + + frm = tk.Frame(self.root) + frm.pack(fill='x', padx=8, pady=4) + self.status_var = tk.StringVar(value="Starting...") + tk.Label(frm, textvariable=self.status_var, fg='blue', anchor='w').pack(side='left') + tk.Label(frm, text=f":{LOCAL_PORT}", fg='gray').pack(side='right') + + self.log_area = scrolledtext.ScrolledText( + self.root, height=18, state='disabled', font=('Consolas', 9)) + self.log_area.pack(fill='both', expand=True, padx=8, pady=4) + + self.token_mgr = TokenManager(log_fn=self.log) + threading.Thread(target=self._run_server, daemon=True).start() + + def log(self, msg): + ts = time.strftime('%H:%M:%S') + def _append(): + self.log_area.config(state='normal') + self.log_area.insert('end', f"[{ts}] {msg}\n") + self.log_area.see('end') + self.log_area.config(state='disabled') + self.root.after(0, _append) + + def _run_server(self): + ProxyHandler.token_mgr = self.token_mgr + ProxyHandler.log_fn = self.log + server = HTTPServer(('127.0.0.1', LOCAL_PORT), ProxyHandler) + self.log(f"[Server] Listening on http://127.0.0.1:{LOCAL_PORT}") + self.root.after(0, lambda: self.status_var.set(f"Running 127.0.0.1:{LOCAL_PORT}")) + self.token_mgr.get_token() + server.serve_forever() + + def run(self): + self.root.mainloop() + + +if __name__ == '__main__': + App().run() diff --git a/assets/tmwd_cdp_bridge/background.js b/assets/tmwd_cdp_bridge/background.js index 4a0c87f..8e070ae 100644 --- a/assets/tmwd_cdp_bridge/background.js +++ b/assets/tmwd_cdp_bridge/background.js @@ -135,6 +135,7 @@ function buildExecScript(code, errorHandler) { return `(async () => { function smartProcessResult(result) { if (result === null || result === undefined || typeof result !== 'object') return result; + try { if (result.window === result && result.document) return '[Window: ' + (result.location?.href || 'about:blank') + ']'; } catch(_){} if (typeof jQuery !== 'undefined' && result instanceof jQuery) { const elements = []; for (let i = 0; i < result.length; i++) { if (result[i] && result[i].nodeType === 1) elements.push(result[i].outerHTML); } return elements; } @@ -149,7 +150,7 @@ function buildExecScript(code, errorHandler) { for (let i = 0; i < length; i++) { const elem = result[i]; if (elem && elem.nodeType === 1) elements.push(elem.outerHTML); } return elements; } } - try { return JSON.parse(JSON.stringify(result, function(key, value) { if (typeof value === 'object' && value !== null) { if (value.nodeType === 1) return value.outerHTML; if (value === window || value === document) return '[Object]'; } return value; })); } catch (e) { return '[无法序列化: ' + e.message + ']'; } + try { return JSON.parse(JSON.stringify(result, function(key, value) { if (typeof value === 'object' && value !== null) { if (value.nodeType === 1) return value.outerHTML; if (value === window || value === document) return '[Object]'; try { if (value.window === value && value.document) return '[Window]'; } catch(_){} } return value; })); } catch (e) { return '[无法序列化: ' + e.message + ']'; } } try { const jsCode = ${JSON.stringify(code)}.trim(); diff --git a/ga.py b/ga.py index c2d2dbe..9cef05f 100644 --- a/ga.py +++ b/ga.py @@ -383,7 +383,7 @@ class GenericAgentHandler(BaseHandler): blocks = extract_robust_content(response.content) if not blocks: yield f"[Status] ❌ 失败: 未在回复中找到代码块内容\n" - return StepOutcome({"status": "error", "msg": "No content found, if you want a blank, you should use code_run"}, next_prompt="\n") + return StepOutcome({"status": "error", "msg": "No content found. Put content inside ... tags in your reply body and call file_write."}, next_prompt="\n") try: new_content = expand_file_refs(blocks, base_dir=self.cwd) if mode == "prepend": diff --git a/llmcore.py b/llmcore.py index a7bcdde..8443df1 100644 --- a/llmcore.py +++ b/llmcore.py @@ -413,13 +413,10 @@ def _msgs_claude2oai(messages): src = b.get("source") or {} if src.get("type") == "base64" and src.get("data"): text_parts.append({"type": "image_url", "image_url": {"url": f"data:{src.get('media_type', 'image/png')};base64,{src.get('data', '')}"}}) - elif b.get("type") == "image_url": - text_parts.append(b) - elif b.get("type") == "text": - text_parts.append({"type": "text", "text": b.get("text", "")}) + elif b.get("type") == "image_url": text_parts.append(b) + elif b.get("type") == "text": text_parts.append({"type": "text", "text": b.get("text", "")}) if text_parts: result.append({"role": "user", "content": text_parts}) - else: - result.append(msg) + else: result.append(msg) return result