From df6645adccd1f8e75d9391ed1290241c705f07cf Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Thu, 12 Feb 2026 21:25:15 +0800 Subject: [PATCH] fix: improve JSON parsing error handling with bad_json mechanism --- ...64d56503-36b1-4f61-8f06-cfc8adaca535.vsidx | Bin 0 -> 172365 bytes .vs/GenericAgent/v17/.wsuo | Bin 0 -> 15360 bytes .vs/GenericAgent/v17/DocumentLayout.json | 12 + .vs/ProjectSettings.json | 3 + .vs/VSWorkspaceState.json | 6 + .vs/slnx.sqlite | Bin 0 -> 90112 bytes agent_loop.py | 2 + agentmain.py | 7 +- ga.py | 16 +- restore_commit.txt | 2964 +++++++++++++++++ sidercall.py | 14 +- 11 files changed, 3000 insertions(+), 24 deletions(-) create mode 100644 .vs/GenericAgent/FileContentIndex/64d56503-36b1-4f61-8f06-cfc8adaca535.vsidx create mode 100644 .vs/GenericAgent/v17/.wsuo create mode 100644 .vs/GenericAgent/v17/DocumentLayout.json create mode 100644 .vs/ProjectSettings.json create mode 100644 .vs/VSWorkspaceState.json create mode 100644 .vs/slnx.sqlite create mode 100644 restore_commit.txt diff --git a/.vs/GenericAgent/FileContentIndex/64d56503-36b1-4f61-8f06-cfc8adaca535.vsidx b/.vs/GenericAgent/FileContentIndex/64d56503-36b1-4f61-8f06-cfc8adaca535.vsidx new file mode 100644 index 0000000000000000000000000000000000000000..f5a6b2c30dbdbe30ae7e28144f7734282992677d GIT binary patch literal 172365 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

iNBmnfW?}+XYnxBDXuRLLu?H(-70*Ew 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

5?|o1?x#eb8hu*y>CtDD3V%x6 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 Z1b_e#00KY&2mk>f00e*l5O|RU{tq!ptJMGi literal 0 HcmV?d00001 diff --git a/agent_loop.py b/agent_loop.py index 673dd3e..b7d1e77 100644 --- a/agent_loop.py +++ b/agent_loop.py @@ -24,6 +24,8 @@ class BaseHandler: ret = yield from try_call_generator(getattr(self, method_name), args, response) _ = yield from try_call_generator(self.tool_after_callback, tool_name, args, response, ret) return ret + elif tool_name == 'bad_json': + return StepOutcome(None, next_prompt=args.get('msg', 'bad_json'), should_exit=False) else: yield f"❌ 未知工具: {tool_name}\n" return StepOutcome(None, next_prompt=f"未知工具 {tool_name}", should_exit=False) diff --git a/agentmain.py b/agentmain.py index 3d537f0..62f91ac 100644 --- a/agentmain.py +++ b/agentmain.py @@ -30,15 +30,14 @@ class GeneraticAgent: if not os.path.exists('temp'): os.makedirs('temp') from sidercall import sider_cookie, oai_configs, claude_configs llm_sessions = [] + for cfg in claude_configs.values(): + llm_sessions += [ClaudeSession(api_key=cfg['apikey'], api_base=cfg['apibase'], model=cfg['model'])] if sider_cookie: llm_sessions += [SiderLLMSession(default_model=x) for x in \ ["gemini-3.0-flash", "claude-haiku-4.5", "kimi-k2"]] for cfg in oai_configs.values(): llm_sessions += [LLMSession(api_key=cfg['apikey'], api_base=cfg['apibase'], model=cfg['model'])] - for cfg in claude_configs.values(): - llm_sessions += [ClaudeSession(api_key=cfg['apikey'], api_base=cfg['apibase'], model=cfg['model'])] if len(llm_sessions) > 0: - llmclient = ToolClient(llm_sessions, auto_save_tokens=True) - self.llmclient = llmclient + self.llmclient = ToolClient(llm_sessions, auto_save_tokens=True) else: self.llmclient = None self.lock = threading.Lock() diff --git a/ga.py b/ga.py index 0c35b89..f62368b 100644 --- a/ga.py +++ b/ga.py @@ -84,20 +84,10 @@ def code_run(code, code_type="python", timeout=60, cwd=None, code_cwd=None, stop def ask_user(question: str, candidates: list = None): + """question: 向用户提出的问题。candidates: 可选的候选项列表。需要保证should_exit为True """ - 构造一个中断请求。 - question: 向用户提出的问题。 - candidates: 可选的候选项列表。 - 需要保证should_exit为True - """ - return { - "status": "INTERRUPT", - "intent": "HUMAN_INTERVENTION", - "data": { - "question": question, - "candidates": candidates or [] - } - } + return {"status": "INTERRUPT", "intent": "HUMAN_INTERVENTION", + "data": {"question": question, "candidates": candidates or []}} from simphtml import execute_js_rich, get_html diff --git a/restore_commit.txt b/restore_commit.txt new file mode 100644 index 0000000..556d1bf --- /dev/null +++ b/restore_commit.txt @@ -0,0 +1,2964 @@ +commit 9b20ca82972ec66622193846801630fd356ce231 +Author: Liang Jiaqing +Date: Fri Jan 16 23:50:19 2026 +0800 + + fix: restore files removed by mistake and keep zip ignored + +diff --git a/README.md b/README.md +new file mode 100644 +index 0000000..4b399e9 +--- /dev/null ++++ b/README.md +@@ -0,0 +1,52 @@ ++# pc-agent-loop ++ ++pc-agent-loop 是一个**极致简约**的 PC 级自主 AI Agent 框架。它通过不到 100 行的核心代码和约 200 行的工具实现,构筑了把整个pc给它(浏览器、终端、文件系统)的物理级自动化能力。 ++ ++## 🚀 核心特性 ++ ++- **极简设计**: 仅由 **7 个基本工具** 和一个高效的 **Agentic Loop** 构成,拒绝过度设计。 ++- **自主代码执行 (Code Execution)**: 能够根据任务需求自主编写并运行 Python 或 PowerShell 脚本,直接操控系统资源。 ++- **深度 Web 自动化 (Advanced Web Automation)**: ++ - **语义化扫描**: 自动清洗 HTML 内容,将复杂的 DOM 转化为 AI 易读的结构。 ++ - **JS 注入执行**: 在浏览器上下文中执行自定义 JavaScript,实现精准点击、滚动或数据抓取。 ++ - **TMWebDriver**: 支持通过 Tampermonkey 实现的持久化会话驱动。 ++- **精准文件编辑 (Smart File Patching)**: 并非盲目覆盖,而是支持通过 `file_patch` 以代码块匹配方式进行精确修改。 ++- **人机协作模式 (Human-in-the-loop)**: 在遇到验证码、关键权限或模糊决策时,主动请求用户介入。 ++ ++## 📂 项目结构 ++ ++- `agent_loop.py`: **核心引擎**,负责“感知-思考-行动”的自主循环逻辑。 ++- `ga.py`: **工具箱**,定义了 7 大核心原子工具的具体实现。 ++- `agentapp.py`: 基于 Streamlit 构建的轻量化交互式 Web 界面。 ++- `sidercall.py`: LLM 通信层,支持流式输出与 API 调用。 ++- `TMWebDriver.py`: 浏览器驱动模块(需配合 Tampermonkey 脚本使用)。 ++ ++## 🛠️ 快速开始 ++ ++### 1. 环境准备 ++- 安装 Python 3.8+。 ++- (可选)若需网页自动化,请在浏览器中安装 **Tampermonkey** 插件并导入本项目提供的对应脚本。 ++ ++### 2. 安装依赖 ++缺啥装啥 ++ ++### 3. 启动应用 ++在项目根目录下执行: ++```bash ++python launch.pyw ++``` ++ ++## 🧩 7 大核心工具 ++ ++Agent 仅依靠以下 7 个原子工具的组合来完成复杂任务: ++ ++1. **`code_run`**: 针对 Windows 优化的双模态代码执行器(Python/PowerShell)。 ++2. **`web_scan`**: 获取网页清洗后的语义化 HTML 结构,支持多标签管理。 ++3. **`web_execute_js`**: 网页 JS 脚本注入,支持将结果存盘分析。 ++4. **`file_read`**: 分页式文件读取,支持行号定位。 ++5. **`file_write`**: 文件全量写入或追加。 ++6. **`file_patch`**: 基于源码块匹配的精准局部修改,确保缩进一致性。 ++7. **`ask_user`**: 关键节点请求人类干预。 ++ ++--- ++**⚠️ 警告**: 本 Agent 具备执行本地代码和控制操作系统的物理权限。请务必在受信任的环境中运行,并在运行前仔细检查 Agent 的执行意图。 +\ No newline at end of file +diff --git a/TMWebDriver.py b/TMWebDriver.py +new file mode 100644 +index 0000000..0f58e79 +--- /dev/null ++++ b/TMWebDriver.py +@@ -0,0 +1,285 @@ ++import json, threading, time, uuid, queue, socket, requests ++from typing import Dict, Any, Optional, List ++from simple_websocket_server import WebSocketServer, WebSocket ++from bs4 import BeautifulSoup ++import bottle, random ++from bottle import route, template, request, response ++ ++class Session: ++ def __init__(self, session_id, info, client=None): ++ self.id = session_id ++ self.info = info ++ self.connect_at = time.time() ++ self.disconnect_at = None ++ self.type = info.get('type', 'ws') ++ self.ws_client = client if self.type == 'ws' else None ++ self.http_queue = client if self.type == 'http' else None ++ @property ++ def url(self): return self.info.get('url', '') ++ def is_active(self): ++ return self.disconnect_at is None ++ def reconnect(self, client, info): ++ self.info = info ++ self.type = info.get('type', 'ws') ++ if self.type == 'ws': ++ self.ws_client = client ++ self.http_queue = None ++ elif self.type == 'http': ++ self.http_queue = client ++ self.connect_at = time.time() ++ self.disconnect_at = None ++ def mark_disconnected(self): ++ self.disconnect_at = time.time() ++ ++ ++class TMWebDriver: ++ def __init__(self, host: str = 'localhost', port: int = 18765): ++ self.host = host ++ self.port = port ++ self.sessions = {} ++ self.results = {} ++ ++ self.default_session_id = None ++ self.latest_session_id = None ++ self.last_cmd_time = 0 ++ self.is_remote = socket.socket().connect_ex((host, port+1)) == 0 ++ if not self.is_remote: ++ self.start_ws_server() ++ self.start_http_server() ++ else: ++ self.remote = f'http://{self.host}:{self.port+1}/link' ++ ++ def start_http_server(self): ++ self.app = app = bottle.Bottle() ++ ++ @app.route('/api/longpoll', method=['GET', 'POST']) ++ def long_poll(): ++ data = request.json ++ session_id = data.get('sessionId') ++ session_info = {'url': data.get('url'), 'title': data.get('title', ''), 'type': 'http'} ++ if session_id not in self.sessions: ++ session = Session(session_id, session_info, queue.Queue()) ++ print(f"Browser http connected: {session.url} (Session: {session_id})") ++ self.sessions[session_id] = session ++ session = self.sessions[session_id] ++ if session.type == 'http': msgQ = session.http_queue ++ else: return json.dumps({"id": "", "ret": "use ws"}) ++ try: return msgQ.get(timeout=5) ++ except queue.Empty: return json.dumps({"id": "", "ret": "next long-poll"}) ++ ++ @app.route('/api/result', method=['GET','POST']) ++ def result(): ++ data = request.json ++ if data.get('type') == 'result': ++ self.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])} ++ elif data.get('type') == 'error': ++ self.results[data.get('id')] = {'success': False, 'data': data.get('error')} ++ return 'ok' ++ ++ @app.route('/link', method=['GET','POST']) ++ def link(): ++ data = request.json ++ if data.get('cmd') == 'get_all_sessions': return json.dumps({'r': self.get_all_sessions()}, ensure_ascii=False) ++ if data.get('cmd') == 'find_session': ++ url_pattern = data.get('url_pattern', '') ++ return json.dumps({'r': self.find_session(url_pattern)}, ensure_ascii=False) ++ if data.get('cmd') == 'execute_js': ++ session_id = data.get('sessionId') ++ code = data.get('code') ++ timeout = float(data.get('timeout', 10.0)) ++ auto_switch_newtab = data.get('auto_switch_newtab', False) ++ try: ++ result = self.execute_js(code, timeout=timeout, session_id=session_id, auto_switch_newtab=auto_switch_newtab) ++ newTabs = result.get('newTabs', []) if isinstance(result, dict) else [] ++ return json.dumps({'result': result, 'newTabs': newTabs}, ensure_ascii=False) ++ except Exception as e: ++ return json.dumps({'error': str(e)}, ensure_ascii=False) ++ return 'ok' ++ ++ def run(): ++ import asyncio ++ loop = asyncio.new_event_loop() ++ asyncio.set_event_loop(loop) ++ bottle.run(app, host=self.host, port=self.port+1, server='tornado') ++ ++ http_thread = threading.Thread(target=run) ++ http_thread.daemon = True ++ http_thread.start() ++ ++ def clean_sessions(self): ++ sids = list(self.sessions.keys()) ++ for sid in sids: ++ session = self.sessions[sid] ++ if not session.is_active() and time.time() - session.disconnect_at > 600: ++ del self.sessions[sid] ++ ++ def start_ws_server(self) -> None: ++ driver = self ++ class JSExecutor(WebSocket): ++ def handle(self) -> None: ++ try: ++ data = json.loads(self.data) ++ if data.get('type') == 'ready': ++ session_id = data.get('sessionId') ++ session_info = {'url': data.get('url'), 'title': data.get('title', ''), ++ 'connected_at': time.time(), 'type': 'ws'} ++ driver._register_client(session_id, self, session_info) ++ elif data.get('type') in 'result': ++ driver.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])} ++ elif data.get('type') == 'error': ++ driver.results[data.get('id')] = {'success': False, 'data': data.get('error')} ++ except Exception as e: ++ print(f"Error handling message: {e}") ++ if hasattr(self, 'data'): print(self.data) ++ def connected(self): (f"New connection from {self.address}") ++ def handle_close(self): driver._unregister_client(self) ++ ++ self.server = WebSocketServer(self.host, self.port, JSExecutor) ++ server_thread = threading.Thread(target=self.server.serve_forever) ++ server_thread.daemon = True ++ server_thread.start() ++ print(f"WebSocket server running on ws://{self.host}:{self.port}") ++ ++ def _register_client(self, session_id: str, client: WebSocket, session_info) -> None: ++ is_new_session = session_id not in self.sessions ++ ++ if is_new_session: ++ session = Session(session_id, session_info, client) ++ self.sessions[session_id] = session ++ print(f"New tab connected: {session.url} (Session: {session_id})") ++ else: ++ session = self.sessions[session_id] ++ session.reconnect(client, session_info) ++ print(f"Tab reconnected: {session.url} (Session: {session_id})") ++ ++ self.latest_session_id = session_id ++ if self.default_session_id is None: ++ self.default_session_id = session_id ++ elif is_new_session: ++ if time.time() - self.last_cmd_time < 5.0: ++ print(f"检测到脚本触发的新窗口,自动切换焦点: {session_id}") ++ self.default_session_id = session_id ++ ++ ++ def _unregister_client(self, client: WebSocket) -> None: ++ for session in self.sessions.values(): ++ if session.ws_client == client: ++ session.mark_disconnected() ++ break ++ ++ def execute_js(self, code, timeout=10.0, session_id=None, auto_switch_newtab=False) -> Any: ++ if session_id is None: session_id = self.default_session_id ++ if self.is_remote: ++ print('remote_execute_js') ++ response = self._remote_cmd({"cmd": "execute_js", "sessionId": session_id, ++ "code": code, "timeout": str(timeout), ++ "auto_switch_newtab": auto_switch_newtab}) ++ if response.get('error'): raise Exception(response['error']) ++ if auto_switch_newtab and 'newTabs' in response: ++ newtabs = response.get('newTabs', []) ++ if len(newtabs) > 0: ++ new_session_id = newtabs[0]['sessionId'] ++ self.default_session_id = new_session_id ++ print(f"自动切换到新标签会话: {new_session_id}") ++ return response.get('result', None) ++ ++ session = self.sessions.get(session_id) ++ if not session or not session.is_active(): ++ time.sleep(3) ++ session = self.sessions.get(session_id) ++ if not session or not session.is_active(): ++ alive_sessions = [s for s in self.sessions.values() if s.is_active()] ++ if alive_sessions: ++ session = alive_sessions[0] ++ print(f"会话 {session_id} 未连接,自动切换到最新活动会话: {session.id}") ++ session_id = self.default_session_id = session.id ++ if not session or not session.is_active(): ++ breakpoint() ++ raise ValueError(f"会话ID {session_id} 未连接") ++ ++ tp = session.type ++ assert tp in ['ws', 'http'], f"Unsupported session type: {tp}" ++ exec_id = str(uuid.uuid4()) ++ payload = json.dumps({'id': exec_id, 'code': code, 'auto_switch_newtab': auto_switch_newtab}) ++ ++ if tp == 'ws': ++ session.ws_client.send_message(payload) ++ elif tp == 'http': ++ session.http_queue.put(payload) ++ ++ start_time = time.time() ++ self.clean_sessions() ++ hasjump = False ++ ++ while exec_id not in self.results: ++ time.sleep(0.1) ++ if tp == 'ws': ++ if not session.is_active(): hasjump = True ++ if hasjump and session.is_active(): ++ if not self.is_remote and auto_switch_newtab: self.last_cmd_time = time.time() ++ return {"result": f"Session {session_id} reloaded.", "closed":1} ++ if time.time() - start_time > timeout: ++ if tp == 'ws': ++ return {"result": f"No response data in {timeout}s"} ++ elif tp == 'http': ++ return {"result": f"Session {session_id} no response."} ++ ++ result = self.results.pop(exec_id) ++ if not result['success']: raise Exception(result['data']) ++ if not self.is_remote and auto_switch_newtab: ++ newtabs = result.get('newTabs', []) ++ if len(newtabs) > 0: ++ new_session_id = newtabs[0]['sessionId'] ++ self.default_session_id = new_session_id ++ print(f"自动切换到新标签会话: {new_session_id}") ++ elif not self.is_remote: ++ self.last_cmd_time = time.time() ++ return result['data'] ++ ++ def _remote_cmd(self, cmd): ++ resp = requests.post(self.remote, ++ headers={"Content-Type": "application/json"}, ++ json=cmd).json() ++ return resp ++ ++ def get_all_sessions(self): ++ if self.is_remote: ++ return self._remote_cmd({"cmd": "get_all_sessions"}).get('r', []) ++ return [{'id': session.id, **session.info} for session in self.sessions.values() ++ if session.is_active()] ++ ++ def get_session_dict(self): ++ return {session.id: session.url for session in self.sessions.values() if session.is_active()} ++ ++ def find_session(self, url_pattern: str): ++ if url_pattern == '': ++ session = self.sessions.get(self.latest_session_id) ++ return [(session.id, session.info)] if session else [] ++ matching_sessions = [] ++ for session in self.sessions.values(): ++ if not session.is_active(): continue ++ if 'url' in session.info and url_pattern in session.info['url']: ++ matching_sessions.append((session.id, session.info)) ++ return matching_sessions ++ ++ def set_session(self, url_pattern: str) -> bool: ++ if self.is_remote: ++ matched = self._remote_cmd({"cmd": "find_session", "url_pattern": url_pattern}).get('r', []) ++ else: ++ matched = self.find_session(url_pattern) ++ if not matched: return print(f"警告: 未找到URL包含 '{url_pattern}' 的会话") ++ if len(matched) > 1: print(f"警告: 找到多个URL包含 '{url_pattern}' 的会话,选择第一个") ++ self.last_cmd_time = 0 ++ self.default_session_id, info = matched[0] ++ print(f"成功设置默认会话: {self.default_session_id}: {info['url']}") ++ return self.default_session_id ++ ++ def jump(self, url, timeout=10): self.execute_js(f"window.location.href='{url}'", timeout=timeout) ++ def page_source(self): return self.execute_js("document.documentElement.outerHTML") ++ def body(self): return self.execute_js("document.body.outerHTML") ++ def newtab(self, url=None): ++ if url is None: url = "http://www.baidu.com/robots.txt" ++ return self.execute_js(f'GM_openInTab("{url}");', auto_switch_newtab=True) ++ ++if __name__ == "__main__": ++ driver = TMWebDriver(host='localhost', port=18765) +\ No newline at end of file +diff --git a/agent_loop.py b/agent_loop.py +new file mode 100644 +index 0000000..e30eecb +--- /dev/null ++++ b/agent_loop.py +@@ -0,0 +1,67 @@ ++import json ++from dataclasses import dataclass ++from typing import Any, Optional ++@dataclass ++class StepOutcome: ++ data: Any ++ next_prompt: Optional[str] = None ++ should_exit: bool = False ++ ++ ++def try_call_generator(func, *args, **kwargs): ++ ret = func(*args, **kwargs) ++ if hasattr(ret, '__iter__') and not isinstance(ret, (str, bytes, dict, list)): ++ ret = yield from ret ++ return ret ++ ++class BaseHandler: ++ def tool_before_callback(self, tool_name, args, content): pass ++ def tool_after_callback(self, tool_name, args, content): pass ++ def dispatch(self, tool_name, args, response): ++ method_name = f"do_{tool_name}" ++ if hasattr(self, method_name): ++ _ = yield from try_call_generator(self.tool_before_callback, tool_name, args, response) ++ ret = yield from try_call_generator(getattr(self, method_name), args, response) ++ _ = yield from try_call_generator(self.tool_after_callback, tool_name, args, response) ++ return ret ++ else: ++ yield f"❌ 未知工具: {tool_name}\n" ++ return StepOutcome(None, "未知工具", "ERROR") ++ ++def json_default(o): ++ if isinstance(o, set): return list(o) ++ return str(o) ++ ++def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, max_turns=15): ++ messages = [ ++ {"role": "system", "content": system_prompt}, ++ {"role": "user", "content": user_input} ++ ] ++ for turn in range(max_turns): ++ yield f"\n[🤖 LLM Thinking (Turn {turn+1})] ..." ++ response = client.chat(messages=messages, tools=tools_schema) ++ ++ if response.thinking: yield '' + response.thinking + '\n' ++ yield response.content ++ ++ if not response.tool_calls: ++ tool_name, args = 'no_tool', {} ++ else: ++ tool_call = response.tool_calls[0] ++ tool_name = tool_call.function.name ++ args = json.loads(tool_call.function.arguments) ++ ++ if tool_name == 'no_tool': pass ++ else: yield f"\n\n正在调用工具: {tool_name},参数: {args}\n" ++ outcome = yield from handler.dispatch(tool_name, args, response) ++ ++ if outcome.next_prompt is None: return {'result': 'CURRENT_TASK_DONE', 'data': outcome.data} ++ if outcome.should_exit: return {'result': 'EXITED', 'data': outcome.data} ++ ++ next_prompt = "" ++ if outcome.data is not None: ++ datastr = json.dumps(outcome.data, ensure_ascii=False, default=json_default) if type(outcome.data) in [dict, list] else str(outcome.data) ++ next_prompt += f"\n{datastr}\n\n\n" ++ next_prompt += outcome.next_prompt ++ messages = [{"role": "user", "content": next_prompt}] ++ return {'result': 'MAX_TURNS_EXCEEDED'} +\ No newline at end of file +diff --git a/agentapp.py b/agentapp.py +new file mode 100644 +index 0000000..915921d +--- /dev/null ++++ b/agentapp.py +@@ -0,0 +1,94 @@ ++import os, sys ++if sys.stdout is None: sys.stdout = open(os.devnull, "w") ++if sys.stderr is None: sys.stderr = open(os.devnull, "w") ++sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) ++ ++ ++import streamlit as st ++import time, json, re ++ ++with open('tools_schema.json', 'r', encoding='utf-8') as f: ++ TOOLS_SCHEMA = json.load(f) ++ ++ ++st.set_page_config(page_title="Cowork", layout="wide") ++ ++from sidercall import SiderLLMSession, LLMSession, ToolClient ++from agent_loop import agent_runner_loop, StepOutcome, BaseHandler ++ ++@st.cache_resource ++def init(): ++ mainllm = SiderLLMSession(multiturns=6) ++ llmclient = ToolClient(mainllm.ask, auto_save_tokens=True) ++ return llmclient ++ ++llmclient = init() ++ ++from ga import GenericAgentHandler ++ ++def get_system_prompt(): ++ with open('sys_prompt.txt', 'r', encoding='utf-8') as f: ++ return f.read() ++ ++if "last_goal" not in st.session_state: ++ st.session_state.last_goal = "" ++ ++def refine_user_goal(raw_query, last_goal): ++ """通过 LLM 提炼用户真实意图""" ++ if not last_goal: ++ return raw_query ++ ++ decide_prompt = f""" ++用户之前的目标是: "{last_goal}" ++用户现在输入了: "{raw_query}" ++ ++请判断: ++1. 如果用户提供补充信息、或者是接续之前的任务,请输出合并后的【最终目标】。 ++2. 如果用户只是指出之前做法有错而非变更目标,那么请输出原目标不做修改。 ++3. 如果用户开启了一个完全不相关的新话题,请直接输出用户现在的输入内容。 ++ ++请直接输出目标描述,不要包含任何多余的文字、解释或标点。 ++""" ++ try: ++ refined = llmclient.llm_func(decide_prompt).strip() ++ return refined if refined else raw_query ++ except: ++ return raw_query ++ ++def agent_backend_stream(raw_query): ++ final_goal = refine_user_goal(raw_query, st.session_state.last_goal) ++ ++ if final_goal != raw_query: ++ yield f"[Goal Refined] {final_goal}\n" ++ ++ sys_prompt = get_system_prompt() ++ handler = GenericAgentHandler(None, final_goal, './temp') ++ llmclient.last_tools = '' ++ ret = yield from agent_runner_loop(llmclient, ++ sys_prompt, raw_query, handler, ++ TOOLS_SCHEMA, max_turns=25) ++ st.session_state.last_goal = final_goal ++ return ret ++ ++st.title("🖥️ Cowork") ++ ++if "messages" not in st.session_state: ++ st.session_state.messages = [] ++ ++for msg in st.session_state.messages: ++ with st.chat_message(msg["role"]): ++ st.markdown(msg["content"]) ++ ++if prompt := st.chat_input("请输入指令"): ++ st.session_state.messages.append({"role": "user", "content": prompt}) ++ with st.chat_message("user"): ++ st.markdown(prompt) ++ ++ with st.chat_message("assistant"): ++ message_placeholder = st.empty() ++ full_response = "" ++ for chunk in agent_backend_stream(prompt): ++ full_response += chunk ++ message_placeholder.markdown(full_response + "▌") ++ message_placeholder.markdown(full_response) ++ st.session_state.messages.append({"role": "assistant", "content": full_response}) +\ No newline at end of file +diff --git a/ga.py b/ga.py +new file mode 100644 +index 0000000..446a7a3 +--- /dev/null ++++ b/ga.py +@@ -0,0 +1,379 @@ ++import sys, os, re ++import pyperclip ++import json, time ++from pathlib import Path ++import subprocess ++import tempfile ++if sys.stdout is None: sys.stdout = open(os.devnull, "w") ++if sys.stderr is None: sys.stderr = open(os.devnull, "w") ++sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) ++ ++from sidercall import LLMSession, ToolClient ++from agent_loop import BaseHandler, StepOutcome, agent_runner_loop ++ ++def code_run(code: str, code_type: str = "python", timeout: int = 60, cwd: str = None): ++ """ ++ 针对 Windows 优化的双模态执行器 ++ python: 运行复杂的 .py 脚本(文件模式) ++ powershell: 运行单行指令(命令模式) ++ 优先使用python,仅在必要系统操作时使用powershell。 ++ """ ++ # 统一路径处理 ++ preview = (code[:60].replace('\n', ' ') + '...') if len(code) > 60 else code.strip() ++ yield f"\n[Action] Running {code_type} in {os.path.basename(cwd)}: {preview}\n" ++ cwd = cwd or os.getcwd() ++ if code_type == "python": ++ # Python 依然建议走文件,因为模型生成的逻辑通常包含多行、import 和类定义 ++ tmp_file = tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode='w', encoding='utf-8') ++ tmp_file.write(code) ++ tmp_path = tmp_file.name ++ tmp_file.close() ++ cmd = ["python", "-u", tmp_path] ++ elif code_type == "powershell": ++ cmd = ["powershell", "-NoProfile", "-NonInteractive", "-Command", code] ++ tmp_path = None ++ else: ++ return {"status": "error", "msg": f"不支持的类型: {code_type}"} ++ print("code run output:") ++ startupinfo = None ++ if os.name == 'nt': ++ startupinfo = subprocess.STARTUPINFO() ++ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW ++ startupinfo.wShowWindow = 0 # SW_HIDE ++ full_stdout = [] ++ full_stderr = [] ++ try: ++ process = subprocess.Popen( ++ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ++ bufsize=0, cwd=cwd, startupinfo=startupinfo ++ ) ++ for line_bytes in iter(process.stdout.readline, b''): ++ try: ++ line = line_bytes.decode('utf-8') ++ except UnicodeDecodeError: ++ line = line_bytes.decode('gbk', errors='ignore') ++ print(line, end="") ++ full_stdout.append(line) ++ ++ stdout_rem, stderr_raw = process.communicate(timeout=timeout) ++ if stdout_rem: ++ try: rem_str = stdout_rem.decode('utf-8') ++ except UnicodeDecodeError: ++ rem_str = stdout_rem.decode('gbk', errors='ignore') ++ full_stdout.append(rem_str) ++ ++ if stderr_raw: ++ try: stderr_str = stderr_raw.decode('utf-8') ++ except UnicodeDecodeError: ++ stderr_str = stderr_raw.decode('gbk', errors='ignore') ++ full_stderr.append(stderr_str) ++ print(f"Error: {stderr_str}") ++ ++ status = "success" if process.returncode == 0 else "error" ++ stdout_str = "".join(full_stdout) ++ stderr_str = "".join(full_stderr) ++ status_icon = "✅" if process.returncode == 0 else "❌" ++ output_snippet = (stdout_str[:200] + '...') if len(stdout_str) > 200 else stdout_str ++ yield f"[Status] {status_icon} Exit Code: {process.returncode}\n[Stdout] {output_snippet}\n" ++ return { ++ "status": status, ++ "stdout": stdout_str[-2000:], ++ "stderr": stderr_str[-2000:], ++ "exit_code": process.returncode ++ } ++ except subprocess.TimeoutExpired: ++ return {"status": "error", "msg": "Timeout"} ++ except Exception as e: ++ return {"status": "error", "msg": str(e)} ++ finally: ++ if code_type == "python" and tmp_path and os.path.exists(tmp_path): os.remove(tmp_path) ++ ++ ++def ask_user(question: str, candidates: list = None): ++ """ ++ 构造一个中断请求。 ++ question: 向用户提出的问题。 ++ candidates: 可选的候选项列表。 ++ 需要保证should_exit为True ++ """ ++ return { ++ "status": "INTERRUPT", ++ "intent": "HUMAN_INTERVENTION", ++ "data": { ++ "question": question, ++ "candidates": candidates or [] ++ } ++ } ++ ++from web_tools import execute_js_rich, get_html ++ ++driver = None ++ ++def first_init_driver(): ++ global driver ++ from TMWebDriver import TMWebDriver ++ driver = TMWebDriver() ++ while True: ++ time.sleep(1) ++ sess = driver.get_all_sessions() ++ if len(sess) > 0: break ++ driver.newtab() ++ time.sleep(5) ++ ++def web_scan(focus_item="", switch_tab_id=None): ++ """ ++ 利用 get_html 获取清洗后的网页内容。 ++ focus_item: 语义过滤指令。如果用户在找特定内容(如“小米汽车”), ++ 算法会优先保留包含该关键词的列表项。 ++ switch_tab_id: 可选参数,如果提供,则在扫描前切换到该标签页。 ++ 应当多用execute_js,少全量观察html。 ++ """ ++ global driver ++ if driver is None: first_init_driver() ++ try: ++ tabs = [] ++ for sess in driver.get_all_sessions(): ++ sess.pop('connected_at', None) ++ sess.pop('type', None) ++ sess['url'] = sess.get('url', '')[:50] + ("..." if len(sess.get('url', '')) > 50 else "") ++ tabs.append(sess) ++ if switch_tab_id: driver.default_session_id = switch_tab_id ++ content = get_html(driver, cutlist=True, instruction=focus_item, maxchars=23000) ++ return { ++ "status": "success", ++ "metadata": { ++ "tabs_count": len(tabs), ++ "tabs": tabs, ++ "active_tab": driver.default_session_id ++ }, ++ "content": content ++ } ++ except Exception as e: ++ return {"status": "error", "msg": format_error(e)} ++ ++import traceback ++def format_error(e): ++ exc_type, exc_value, exc_traceback = sys.exc_info() ++ tb = traceback.extract_tb(exc_traceback) ++ if tb: ++ f = tb[-1] ++ fname = os.path.basename(f.filename) ++ return f"{exc_type.__name__}: {str(e)} @ {fname}:{f.lineno}, {f.name} -> `{f.line}`" ++ return f"{exc_type.__name__}: {str(e)}" ++ ++def web_execute_js(script: str): ++ """ ++ 执行 JS 脚本来控制浏览器,并捕获结果和页面变化。 ++ script: 要执行的 JavaScript 代码字符串。 ++ return { ++ "status": "failed" if error_msg else "success", ++ "js_return": result, ++ "error": error_msg, ++ "transients": transients, ++ "environment": { ++ "new_tab": new_tab, ++ "reloaded": reloaded ++ }, ++ "diff": diff_summary, ++ "suggestion": "" if is_significant_change else "页面无明显变化" ++ } ++ """ ++ global driver ++ if driver is None: first_init_driver() ++ try: ++ result = execute_js_rich(script, driver) ++ return result ++ except Exception as e: ++ return {"status": "error", "msg": format_error(e)} ++ ++def file_patch(path: str, old_content: str, new_content: str): ++ """ ++ 在文件中寻找唯一的 old_content 块并替换为 new_content。 ++ """ ++ path = str(Path(path).resolve()) ++ try: ++ if not os.path.exists(path): ++ return {"status": "error", "msg": "文件不存在"} ++ with open(path, 'r', encoding='utf-8') as f: ++ full_text = f.read() ++ # 检查唯一性 ++ count = full_text.count(old_content) ++ if count == 0: ++ return {"status": "error", "msg": "未找到匹配的旧文本块,请检查空格、缩进和换行是否完全一致。"} ++ if count > 1: ++ return {"status": "error", "msg": f"找到 {count} 处匹配,请提供更长的旧文本块以确保唯一性。"} ++ updated_text = full_text.replace(old_content, new_content) ++ with open(path, 'w', encoding='utf-8') as f: ++ f.write(updated_text) ++ return {"status": "success", "msg": "文件局部修改成功"} ++ except Exception as e: ++ return {"status": "error", "msg": str(e)} ++ ++def file_read(path, start=1, count=100, show_linenos=True): ++ try: ++ with open(path, 'r', encoding='utf-8', errors='replace') as f: ++ lines = f.readlines() ++ chunk = lines[start-1 : start-1+count] ++ if show_linenos: res = [f"{i+start}|{l[:200]}" for i, l in enumerate(chunk)] ++ else: res = [l for l in chunk] ++ return f"Total:{len(lines)} lines\n" + "".join(res) ++ except Exception as e: ++ return f"Error: {str(e)}" ++ ++class GenericAgentHandler(BaseHandler): ++ ''' ++ Generic Agent 工具库,包含多种工具的实现。工具函数自动加上了 do_ 前缀。实际工具名没有前缀。 ++ ''' ++ def __init__(self, parent, user_input, cwd): ++ self.parent = parent ++ self.user_input = user_input ++ self.plan = "" ++ self.focus = "" ++ self.cwd = cwd ++ ++ def _get_abs_path(self, path): ++ if not path: return "" ++ return os.path.abspath(os.path.join(self.cwd, path)) ++ ++ def do_code_run(self, args, response): ++ '''执行代码片段,有长度限制,不允许代码中放大量数据,如有需要应当通过文件读取进行。 ++ ''' ++ code_type = args.get("type", "python") ++ # 从 response.content 中提取代码块 ++ # 匹配 ```python ... ``` 或 ```powershell ... ``` ++ pattern = rf"```{code_type}\n(.*?)\n```" ++ # 也可以更通用一点,不分类型提取最后一个代码块:rf"```(?:{code_type})?\n(.*?)\n```" ++ matches = re.findall(pattern, response.content, re.DOTALL) ++ if not matches: ++ return StepOutcome(None, next_prompt=f"【系统错误】:你调用了 code_run,但未在回复中提供 ```{code_type} 代码块。请重新输出代码并附带工具调用。") ++ # 提取最后一个代码块(通常是模型修正后的最终逻辑) ++ code = matches[-1].strip() ++ timeout = args.get("timeout", 60) ++ cwd = args.get("cwd", self.cwd) ++ result = yield from code_run(code, code_type, timeout, cwd) ++ return StepOutcome(result, next_prompt=self._get_anchor_prompt()) ++ ++ def do_ask_user(self, args, response): ++ question = args.get("question", "请提供输入:") ++ candidates = args.get("candidates", []) ++ result = ask_user(question, candidates) ++ return StepOutcome(result, next_prompt="", should_exit=True) ++ ++ def do_web_scan(self, args, response): ++ '''focus_item仅用于在长列表中模糊搜寻相关item ++ 此工具也提供标签页查看和标签页切换功能。 ++ ''' ++ focus_item = args.get("focus_item", "") ++ switch_tab_id = args.get("switch_tab_id", None) ++ result = web_scan(focus_item, switch_tab_id=switch_tab_id) ++ content = result.pop("content", None) ++ yield f'\n{str(result)}\n' ++ next_prompt = f"```html\n{content}\n```" ++ return StepOutcome(result, next_prompt=next_prompt) ++ ++ def do_web_execute_js(self, args, response): ++ '''web情况下的优先使用工具,执行任何js达成对浏览器的*完全*控制。 ++ 支持将结果保存到文件供后续读取分析,但保存功能仅限即时读取,与await等异步操作不兼容。 ++ ''' ++ script = args.get("script", "") ++ save_to_file = args.get("save_to_file", "") ++ result = web_execute_js(script) ++ if save_to_file and "js_return" in result: ++ content = str(result["js_return"] or '') ++ abs_path = self._get_abs_path(save_to_file) ++ with open(abs_path, 'w', encoding='utf-8') as f: f.write(str(content)) ++ result["js_return"] = content[:200] + ("..." if len(content) > 200 else "") ++ result["js_return"] += f"\n\n[已保存以上内容到 {abs_path}]" ++ print("Web Execute JS Result:", result) ++ return StepOutcome(result, next_prompt=self._get_anchor_prompt()) ++ ++ def do_file_patch(self, args, response): ++ path = self._get_abs_path(args.get("path", "")) ++ yield f"\n[Action] Patching file: {path}\n" ++ old_content = args.get("old_content", "") ++ new_content = args.get("new_content", "") ++ result = file_patch(path, old_content, new_content) ++ yield str(result) + "\n" ++ return StepOutcome(result, next_prompt=self._get_anchor_prompt()) ++ ++ def do_file_write(self, args, response): ++ '''用于对整个文件的大量处理,精细修改要用file_patch。 ++ ''' ++ path = self._get_abs_path(args.get("path", "")) ++ mode = args.get("mode", "overwrite") ++ action_str = "Appending to" if mode == "append" else "Writing" ++ yield f"\n[Action] {action_str} file: {os.path.basename(path)}\n" ++ ++ def extract_intended_block(content): ++ start_marker = "```" ++ first_idx = content.find(start_marker) ++ last_idx = content.rfind(start_marker) ++ if first_idx == -1 or last_idx == -1 or first_idx == last_idx: ++ return None ++ header_end = content.find("\n", first_idx) ++ if header_end == -1 or header_end > last_idx: ++ return None ++ actual_content = content[header_end + 1 : last_idx].strip() ++ return actual_content ++ ++ blocks = extract_intended_block(response.content) ++ if not blocks: ++ yield f"[Status] ❌ 失败: 未在回复中找到代码块内容\n" ++ return StepOutcome({"status": "error", "msg": "No code block found in response"}, next_prompt="\n") ++ new_content = blocks ++ try: ++ write_mode = 'a' if mode == "append" else 'w' ++ final_content = ("\n" + new_content) if mode == "append" else new_content ++ with open(path, write_mode, encoding="utf-8") as f: ++ f.write(final_content) ++ yield f"[Status] ✅ {mode.capitalize()} 成功 ({len(new_content)} bytes)\n" ++ return StepOutcome({"status": "success"}, ++ next_prompt=f"\n提醒: {self.user_input}请继续执行下一步。\n") ++ except Exception as e: ++ yield f"[Status] ❌ 写入异常: {str(e)}\n" ++ return StepOutcome({"status": "error", "msg": str(e)}, next_prompt="\n") ++ ++ def do_file_read(self, args, response): ++ path = self._get_abs_path(args.get("path", "")) ++ yield f"\n[Action] Reading file: {path}\n" ++ start = args.get("start", 1) ++ count = args.get("count", 100) ++ show_linenos = args.get("show_linenos", True) ++ result = file_read(path, start, count, show_linenos) ++ return StepOutcome(result, next_prompt=self._get_anchor_prompt()) ++ ++ def do_update_plan(self, args, response): ++ ''' ++ 同步宏观任务进度与战略重心。 ++ 【设计意图】: ++ 1. 仅在任务涉及多步逻辑(如:先搜索、再重构、后测试)时进行初始拆解。 ++ 2. 仅在发生重大的方针变更时调用(例如:原定方案 A 物理不可行,需彻底转向方案 B)。 ++ 3. 严禁用于记录细微的调试步骤或代码纠错。 ++ 简单任务无需使用。 ++ ''' ++ new_plan = args.get("plan", "") ++ new_focus = args.get("focus", "") ++ if new_plan: self.plan = new_plan ++ if new_focus: self.focus = new_focus ++ yield f"\n[Info] Updated plan and focus.\n" ++ yield f"New Plan:\n{self.plan}\n\n" ++ yield f"New Focus:\n{self.focus}\n" ++ return StepOutcome({"status": "success"}, ++ next_prompt=self._get_anchor_prompt()) ++ ++ def do_no_tool(self, args, response): ++ '''这是一个特殊工具,由引擎自主调用,不要包含在TOOLS_SCHEMA里。 ++ ''' ++ yield "\n\n[Info] No tool called. Final response to user.\n" ++ return StepOutcome(response, next_prompt=None, should_exit=True) ++ ++ def _get_anchor_prompt(self): ++ prompt = f"\n提醒: \n{self.user_input}\n" ++ if self.plan: prompt += f"\n{self.plan}\n\n" ++ if self.focus: prompt += f"\n{self.focus}\n\n" ++ prompt += "\n请继续执行下一步。" ++ return prompt ++ ++ ++if __name__ == "__main__": ++ pass +\ No newline at end of file +diff --git a/launch.pyw b/launch.pyw +new file mode 100644 +index 0000000..4b91d6f +--- /dev/null ++++ b/launch.pyw +@@ -0,0 +1,48 @@ ++import webview ++import threading ++import subprocess ++import sys, time, os, ctypes ++import atexit ++ ++# === 配置区域 === ++WINDOW_WIDTH = 600 ++WINDOW_HEIGHT = 900 ++RIGHT_PADDING = 0 # 离屏幕右边缘的距离 ++TOP_PADDING = 300 # 离屏幕上边缘的距离 ++ ++def get_screen_width(): ++ try: ++ # GetSystemMetrics(0) 获取主屏幕宽度 ++ user32 = ctypes.windll.user32 ++ return user32.GetSystemMetrics(0) ++ except: ++ # 如果不是 Windows 或者出错了,返回一个兜底值 (比如 1920) ++ return 1920 ++ ++def start_streamlit(): ++ global proc ++ cmd = [ ++ sys.executable, "-m", "streamlit", "run", "agentapp.py", ++ "--server.port", "8501", ++ "--server.headless", "true", ++ "--theme.base", "dark" #以此默认开启暗黑模式,更有极客感 ++ ] ++ proc = subprocess.Popen(cmd) ++ atexit.register(proc.kill) ++ ++if __name__ == '__main__': ++ t = threading.Thread(target=start_streamlit, daemon=True) ++ t.start() ++ screen_width = get_screen_width() ++ x_pos = screen_width - WINDOW_WIDTH - RIGHT_PADDING ++ time.sleep(2) ++ webview.create_window( ++ title='GenericAgent', ++ url='http://localhost:8501', ++ width=WINDOW_WIDTH, ++ height=WINDOW_HEIGHT, ++ x=x_pos, y=TOP_PADDING, ++ resizable=True, ++ text_select=True ++ ) ++ webview.start() +\ No newline at end of file +diff --git a/ljq_web_driver.user.js b/ljq_web_driver.user.js +new file mode 100644 +index 0000000..01eb42b +--- /dev/null ++++ b/ljq_web_driver.user.js +@@ -0,0 +1,428 @@ ++// ==UserScript== ++// @name ljq_web_driver ++// @namespace http://tampermonkey.net/ ++// @version 0.2 ++// @description Execute JS via ljq_web_driver ++// @require https://code.jquery.com/jquery-3.6.0.min.js ++// @author You ++// @match *://*/* ++// @grant GM_setValue ++// @grant GM_getValue ++// @grant GM_xmlhttpRequest ++// @grant GM_openInTab ++// @grant unsafeWindow ++// @connect localhost ++// @run-at document-start ++// ==/UserScript== ++ ++ ++(function() { ++ 'use strict'; ++ const log_prefix = "ljq_driver: "; ++ ++ if (window.self !== window.top) { ++ console.log(log_prefix + '在iframe中不执行'); ++ return; ++ } ++ ++ const wsUrl = 'ws://localhost:18765'; ++ const httpUrl = 'http://localhost:18766/'; ++ ++ function isWebSocketServerAlive(callback) { ++ GM_xmlhttpRequest({ ++ method: 'GET', ++ url: 'http://localhost:18765/', ++ onload: () => callback(true), ++ onerror: () => callback(false) ++ }); ++ } ++ ++ let ws; ++ let sid = (window.name && window.name.startsWith('ljq_')) ? ++ window.name : window.sessionStorage.getItem('ljq_driver_sid'); ++ if (!sid) { ++ sid = `ljq_${Date.now().toString().slice(-2)}${Math.random().toString(36).slice(2, 4)}`; ++ window.sessionStorage.setItem('ljq_driver_sid', sid); ++ window.name = sid; ++ console.log(log_prefix + `创建新会话ID: ${sid}`); ++ } else { ++ if (window.name !== sid) window.name = sid; ++ console.log(log_prefix + `使用现有会话ID: ${sid}`); ++ } ++ ++ try { ++ GM_setValue('new_tab_report', { ++ url: window.location.href, ++ sessionId: sid, ++ ts: Date.now() ++ }); ++ } catch (e) {} ++ ++ // 保存会话ID ++ GM_setValue('sid', sid); ++ ++ // 获取或创建状态指示器 ++ function getIndicator() { ++ // 检查现有指示器 ++ let ind = document.getElementById('ljq-ind'); ++ ++ // 删除重复指示器 ++ const dups = document.querySelectorAll('[id="ljq-ind"]'); ++ if (dups.length > 1) { ++ for (let i = 1; i < dups.length; i++) { ++ dups[i].remove(); ++ } ++ ind = dups[0]; ++ } ++ ++ // 创建新指示器 ++ if (!ind && document.body) { ++ ind = document.createElement('div'); ++ ind.id = 'ljq-ind'; ++ ind.style.cssText = ` ++ position: fixed;bottom: 10px; ++ right: 10px;background-color: #f44336; ++ color: white;padding: 8px 12px; ++ border-radius: 6px;font-size: 14px; ++ font-weight: bold;z-index: 9999; ++ transition: background-color 0.3s; ++ cursor: pointer;box-shadow: 0 3px 6px rgba(0,0,0,0.25); ++ `; ++ ind.innerText = log_prefix + '正在连接...'; ++ ++ ind.addEventListener('click', () => alert(`会话ID: ${sid}\n当前URL: ${location.href}`)); ++ document.body.appendChild(ind); ++ } ++ ++ return ind; ++ } ++ ++ // 更新状态 ++ function updateStatus(status, msg) { ++ if (!document.body) return setTimeout(() => updateStatus(status, msg), 100); ++ ++ const ind = getIndicator(); ++ if (!ind) return; ++ ++ if (status === 'ok') { ++ ind.style.backgroundColor = '#4CAF50'; ++ ind.innerText = log_prefix + '连接成功'; ++ } else if (status === 'disc') { ++ ind.style.backgroundColor = '#f44336'; ++ ind.innerText = log_prefix + '连接断开'; ++ } else if (status === 'conn') { ++ ind.style.backgroundColor = '#2196F3'; ++ ind.innerText = log_prefix + '正在连接(HTTP)'; ++ } else if (status === 'err') { ++ ind.style.backgroundColor = '#FF9800'; ++ ind.innerText = log_prefix + `发生错误 (${msg})`; ++ } else if (status === 'exec') { ++ ind.style.backgroundColor = '#2196F3'; ++ ind.innerText = log_prefix + '正在执行指令...'; ++ } ++ } ++ ++ function handleError(id, error, errorSource) { ++ console.error(`${errorSource}错误:`, error); ++ updateStatus('err', error.message); ++ ++ const errorMessage = { ++ type: 'error', ++ id: id, ++ sessionId: sid, ++ error: { ++ name: error.name, ++ message: error.message, ++ stack: error.stack, ++ source: errorSource ++ } ++ }; ++ ++ if (typeof ws !== 'undefined' && ws && ws.readyState === WebSocket.OPEN) { ++ ws.send(JSON.stringify(errorMessage)); ++ } else { ++ GM_xmlhttpRequest({ ++ method: "POST", ++ url: httpUrl + "api/result", ++ headers: {"Content-Type": "application/json"}, ++ data: JSON.stringify(errorMessage), ++ onload: function(response) {console.log("错误信息已通过HTTP发送", response);}, ++ onerror: function(err) {console.error("发送错误信息失败", err);} ++ }); ++ } ++ } ++ ++ function smartProcessResult(result) { ++ // 处理 null 和原始类型 ++ if (result === null || result === undefined || typeof result !== 'object') { ++ return result; ++ } ++ ++ // 1. 处理 jQuery 对象 - 强制转换为HTML字符串数组 ++ 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; // 始终返回数组 ++ } ++ ++ // 2. 处理 NodeList 和 HTMLCollection ++ if (result instanceof NodeList || result instanceof HTMLCollection) { ++ const elements = []; ++ for (let i = 0; i < result.length; i++) { ++ if (result[i] && result[i].nodeType === 1) { ++ elements.push(result[i].outerHTML); ++ } ++ } ++ return elements; ++ } ++ ++ // 3. 处理单个 DOM 元素 ++ if (result.nodeType === 1) { ++ return result.outerHTML; ++ } ++ ++ // 4. 检查是否是具有数字索引和length属性的类数组对象 ++ if (!Array.isArray(result) && ++ typeof result === 'object' && ++ 'length' in result && ++ typeof result.length === 'number') { ++ ++ // 检查第一个元素是否是DOM节点 ++ const firstElement = result[0]; ++ if (firstElement && firstElement.nodeType === 1) { ++ const elements = []; ++ const length = Math.min(result.length, 100); ++ ++ for (let i = 0; i < length; i++) { ++ const elem = result[i]; ++ if (elem && elem.nodeType === 1) { ++ elements.push(elem.outerHTML); ++ } ++ } ++ ++ return elements; ++ } ++ } ++ ++ // 5. 处理普通对象和数组 - 使用标准序列化 ++ 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) { ++ console.error("序列化对象失败:", e); ++ return `[无法序列化的对象: ${e.message}]`; ++ } ++ } ++ ++ // 防止重复初始化 ++ if (window.ljq_init) return; ++ window.ljq_init = true; ++ ++ function connecthttp() { ++ if (window.use_ws) return; ++ updateStatus('conn'); ++ GM_xmlhttpRequest({ ++ method: "POST", ++ url: httpUrl + "api/longpoll", ++ headers: {"Content-Type": "application/json"}, ++ data: JSON.stringify({ ++ type: 'ready', ++ url: location.href, ++ sessionId: sid ++ }), ++ onload: function(resp) { ++ if (resp.status === 200) { ++ let data = JSON.parse(resp.responseText); ++ console.log(log_prefix + '接收到数据:', data); ++ if (data.id === "" && data.ret === "use ws") return; ++ if (data.id === "") return setTimeout(connecthttp, 100); ++ const response = executeCode(data); ++ ++ if (response.error) { ++ handleError(data.id, response.error, '执行代码'); ++ } else { ++ GM_xmlhttpRequest({ ++ method: "POST", ++ url: httpUrl + "api/result", ++ headers: {"Content-Type": "application/json"}, ++ data: JSON.stringify({ ++ type: 'result', ++ id: data.id, ++ sessionId: sid, ++ result: response.result ++ }) ++ }); ++ } ++ } else { ++ console.error(log_prefix + '请求失败,状态码:', resp.status); ++ updateStatus('err', '请求失败'); ++ } ++ setTimeout(connecthttp, 1000); ++ }, ++ onerror: function(err) { ++ console.error(log_prefix + '请求错误', err); ++ updateStatus('err', '请求失败'); ++ setTimeout(connecthttp, 5000); ++ }, ++ ontimeout: function() { ++ console.log(log_prefix + '请求超时'); ++ updateStatus('err', '请求超时'); ++ setTimeout(connecthttp, 5000); ++ } ++ }); ++ } ++ ++ function executeCode(data) { ++ let id = data.id || 'unknown'; // 获取 ID ++ let result; ++ ++ if (!data.code) { ++ console.log('收到非代码执行消息:', data); ++ return { error: '没有可执行的代码' }; ++ } ++ updateStatus('exec'); ++ ++ try { ++ const jsCode = data.code.trim(); ++ const lines = jsCode.split(/\r?\n/).filter(l => l.trim()); ++ const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : ''; ++ ++ if (lastLine.startsWith('return')) { ++ // 最后一行包含 return 语句,使用 Function 构造器 ++ result = (new Function(jsCode))(); ++ } else { ++ try { ++ result = eval(jsCode); ++ } catch (e) { ++ if (isIllegalReturnError(e)) { ++ result = (new Function(jsCode))(); ++ } else { ++ throw e; ++ } ++ } ++ } ++ const processedResult = smartProcessResult(result); ++ return { result: processedResult }; ++ ++ } catch (execError) { ++ return { error: execError }; // 返回错误信息 ++ } ++ } ++ ++ function isIllegalReturnError(e) { ++ return e instanceof SyntaxError && ( ++ /Illegal return statement/i.test(e.message) || // Chrome 常见 ++ /return not in function/i.test(e.message) || // Firefox 常见 ++ /Illegal 'return' statement/i.test(e.message) // 兼容旧文案 ++ ); ++ } ++ ++ function connect() { ++ ws = new WebSocket(wsUrl); ++ ++ ws.onopen = function() { ++ window.use_ws = true; ++ console.log(log_prefix + '已连接'); ++ updateStatus('ok'); ++ ws.send(JSON.stringify({ ++ type: 'ready', ++ url: location.href, ++ sessionId: sid ++ })); ++ }; ++ ++ ws.onclose = function() { ++ console.log(log_prefix + '已断开,5秒后重连'); ++ updateStatus('disc'); ++ setTimeout(connect, 5000); ++ }; ++ ++ ws.onerror = function(err) { ++ console.error(log_prefix + '连接错误', err); ++ updateStatus('err', '连接失败'); ++ isWebSocketServerAlive(function (e) { if (e) connecthttp()}); ++ }; ++ ++ ws.onmessage = async function(e) { ++ try { ++ let data = JSON.parse(e.data); ++ let startTime = Date.now(); ++ let newTabs = []; ++ let checkNewTab = data.auto_switch_newtab === true; ++ GM_setValue('new_tab_report', null); ++ const response = executeCode(data); ++ ++ if (response.error) { ++ handleError(data.id, response.error, '执行代码'); ++ } else { ++ if (checkNewTab) { ++ for (let i = 0; i < 10; i++) { ++ await new Promise(r => setTimeout(r, 150)); ++ let latestReport = GM_getValue('new_tab_report'); ++ if (latestReport && latestReport.ts >= startTime) { ++ console.log(`%c[Detected] 轮询第 ${i+1} 次抓到新标签!`, "color: green"); ++ newTabs.push(latestReport); ++ break; ++ } ++ } ++ } ++ updateStatus('ok'); ++ ws.send(JSON.stringify({ ++ type: 'result', ++ id: data.id, ++ sessionId: sid, ++ result: response.result, ++ newTabs: newTabs ++ })); ++ } ++ } catch (parseError) { ++ handleError('unknown', parseError, '解析消息'); ++ } ++ }; ++ ++ } ++ ++ // 初始化 ++ function init() { ++ if (document.body) { ++ getIndicator(); ++ connect(); ++ } else { ++ setTimeout(init, 50); ++ } ++ } ++ ++ // 监控DOM变化 ++ const observer = new MutationObserver(() => getIndicator()); ++ ++ if (document.readyState !== 'loading') { ++ init(); ++ observer.observe(document.body, { childList: true, subtree: true }); ++ } else { ++ document.addEventListener('DOMContentLoaded', () => { ++ init(); ++ observer.observe(document.body, { childList: true, subtree: true }); ++ }); ++ } ++ ++ // 清理 ++ window.addEventListener('beforeunload', () => { ++ observer.disconnect(); ++ if (ws && ws.readyState === WebSocket.OPEN) { ++ ws.close(); ++ } ++ }); ++})(); +\ No newline at end of file +diff --git a/make_prompts.py b/make_prompts.py +new file mode 100644 +index 0000000..5905083 +--- /dev/null ++++ b/make_prompts.py +@@ -0,0 +1,137 @@ ++import sys, os, re ++import pyperclip ++import json, time ++from pathlib import Path ++import subprocess ++import tempfile ++sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) ++from sidercall import SiderLLMSession, LLMSession, ToolClient ++ ++ ++ask = SiderLLMSession().ask ++ ++ ++def generate_tool_schema(): ++ """ ++ 通过代码内省,将 Handler 的逻辑映射为高语义的工具描述。 ++ """ ++ with open('ga.py', 'r', encoding='utf-8') as f: ++ ga_code = f.read() ++ # 极简且具备高度概括能力的元 Prompt ++ meta_prompt = f""" ++# Role ++你是一个具备深度推理能力的 AI 系统架构师。你将通过阅读 `GenericAgentHandler` 源码,构建其对应的工具能力矩阵。 ++ ++# Task ++分析下方的源码,并输出 OpenAI Tool Schema。在输出 JSON 之前,你必须进行内部思考(Thinking Process)。 ++ ++# Thinking Process Requirements ++在 `` 标签中,请按顺序分析: ++1. **核心工具链识别**:识别所有 `do_xxx` 方法,并分析它们依赖的底层 Utility 函数。 ++2. **内容溯源审计**:重点分析哪些工具是从 `response.content` 提取核心逻辑(如代码块)的。对于这些工具,确认在 Schema 参数中排除掉对应的字段。 ++3. **调用策略推导**:分析工具间的协作关系(例如 `file_read` 如何为 `file_patch` 提供定位)。 ++4. **兜底逻辑确认**:明确某些特殊万能工具在系统中的保底角色,快速工具无法执行的操作由保底工具执行,但正常应优先使用方便的工具。 ++5. **注释审阅**:结合函数注释,理解每个工具的使用限制,其中的重要信息务必反映在工具描述中(如长度限制等)。 ++注释中的重要信息务必反映在工具描述中。 ++注释中的重要信息务必反映在工具描述中。 ++ ++# Tool Schema Formatting Rules ++- **参数对齐**:仅包含 `do_xxx` 方法中通过 `args.get()` 显式获取的参数。 ++- **高引导性描述**:描述应包含“何时调用”以及“如何根据反馈修正”,需要注意函数的注释事项。 ++- **输出格式**:先输出 `` 块,然后输出 ```json 块。 ++ ++# Source Code ++{ga_code} ++ ++# Output ++请开始思考并生成: ++""" ++ ++ # 假设 ask 是你已经封装好的 LLM 调用接口 ++ raw_response = ask(meta_prompt, model="gemini-3.0-flash") ++ print(raw_response) ++ ++ # --- 健壮的 JSON 解析逻辑 --- ++ try: ++ # 1. 清除 Markdown 围栏 ++ clean_json = raw_response.strip() ++ if clean_json.startswith("```"): ++ # 兼容 ```json 和 ``` ++ clean_json = re.sub(r'^```(?:json)?\s*', '', clean_json) ++ clean_json = re.sub(r'\s*```$', '', clean_json) ++ ++ # 2. 移除可能的非 JSON 前导/后缀文字(如果有的话) ++ start_idx = clean_json.find('[') ++ end_idx = clean_json.rfind(']') + 1 ++ if start_idx != -1 and end_idx != -1: ++ clean_json = clean_json[start_idx:end_idx] ++ ++ final_schema = json.loads(clean_json) ++ ++ if final_schema: ++ with open('tools_schema.json', 'w', encoding='utf-8') as f: ++ json.dump(final_schema, f, indent=2, ensure_ascii=False) ++ print("✅ 成功从代码内省生成 Schema 并持久化。") ++ return final_schema ++ ++ except Exception as e: ++ print(f"❌ 解析 Schema 失败: {e}\n原始响应: {raw_response}") ++ return None ++ ++ ++def make_system_prompt(ga_code_path='ga.py'): ++ with open(ga_code_path, 'r', encoding='utf-8') as f: ++ ga_code = f.read() ++ ++ # 这个元 Prompt 的目标是生成“世界观”而非“说明书” ++ meta_prompt = f""" ++# Role ++你是一个 AI 架构师。请阅读下方的工具库源码,为 Agent 生成一份【系统级认知指令 (System Prompt)】。 ++ ++# Task ++基于代码逻辑,定义 Agent 的“能力边界”和“行动协议”。 ++ ++# Requirements (Crucial) ++1. **打破预训练偏见**:针对模型常说的“我只是 AI,不能操作网页/文件”进行修正。明确告诉它:你现在拥有物理操作权限,工具设计保证了所有权限做所有事情。 ++2. **避开冗余**:不要重复 Tool Schema 里的参数细节。 ++3. **能力边界定义**: ++ - 网页操作:它不是通过“想象”上网,而是通过实时的浏览器读写。 ++ - 文件操作:它拥有物理文件读写权限,且遵循“先读后写”的稳健性原则。 ++ - 保底逻辑:当专用工具失效时,使用 `code_run` 编写脚本解决一切。 ++ - 特殊的update_plan(仅在复杂任务时使用)和ask_user(用户也是有效资源)工具。 ++4. **行动协议**: ++ - 必须在行动前进行 ++ ++我后面还会附上具体的工具描述和Schema,所以不要重复。 ++主要以世界观为主,不要纠结于具体工具。 ++ ++# Input Source Code ++{ga_code} ++ ++# Output ++仅输出 System Prompt 的正文,语气要果断、指令化。 ++""" ++ print("🧠 正在重塑 Agent 世界观 (Generating System Prompt)...") ++ # 调用你的 llmclient.ask ++ system_prompt_content = ask(meta_prompt) ++ print("📝 生成的 System Prompt 内容如下:\n") ++ print(system_prompt_content) ++ clean_content = re.sub(r'<[^>]+>', '', system_prompt_content) ++ with open('sys_prompt.txt', 'w', encoding='utf-8') as f: ++ f.write(clean_content) ++ return clean_content ++ ++# --- 主逻辑 --- ++if __name__ == "__main__": ++ if len(sys.argv) < 2: ++ print("Usage: python make_prompts.py [schema|prompt]") ++ sys.exit(1) ++ ++ cmd = sys.argv[1].lower() ++ if cmd == "schema": ++ generate_tool_schema() ++ elif cmd == "prompt": ++ make_system_prompt() ++ else: ++ print(f"Unknown command: {cmd}") ++ print("Available commands: schema, prompt") +\ No newline at end of file +diff --git a/sidercall.py b/sidercall.py +new file mode 100644 +index 0000000..706c686 +--- /dev/null ++++ b/sidercall.py +@@ -0,0 +1,179 @@ ++import os, json, re, time, requests ++from sider_ai_api import Session ++ ++try: ++ from mykey import sider_cookie, capikey ++except ImportError: ++ sider_cookie = "" ++ capikey = "" ++ ++class SiderLLMSession: ++ def __init__(self, multiturns=6): ++ self._core = Session(cookie=sider_cookie, proxies={'https':'127.0.0.1:2082'}) ++ def ask(self, prompt, model="gemini-3.0-flash"): ++ if len(prompt) > 30000: prompt = prompt[-29500:] ++ return ''.join(self._core.chat(prompt, model)) ++ ++class LLMSession: ++ def __init__(self, api_key=capikey, api_base="http://113.45.39.247:3001/v1", multiturns=6): ++ self.api_key = api_key ++ self.api_base = api_base ++ self.messages = [] ++ self.multiturns = multiturns ++ ++ def ask(self, prompt, model="openai/gpt-5.1"): ++ self.messages.append({"role": "user", "content": prompt}) ++ if len(self.messages) > self.multiturns: ++ self.messages = self.messages[-self.multiturns:] ++ headers = { ++ "Authorization": f"Bearer {self.api_key}", ++ "Content-Type": "application/json" ++ } ++ try: ++ response = requests.post( ++ f"{self.api_base}/chat/completions", ++ headers=headers, ++ json={ ++ "model": model, ++ "messages": self.messages, ++ "temperature": 0.5 ++ }, ++ timeout=60 ++ ) ++ res_json = response.json() ++ content = res_json["choices"][0]["message"]["content"] ++ self.messages.append({"role": "assistant", "content": content}) ++ return content ++ except Exception as e: ++ return f"Error: {str(e)}" ++ ++class MockFunction: ++ def __init__(self, name, arguments): ++ self.name = name ++ self.arguments = arguments ++ ++class MockToolCall: ++ def __init__(self, name, args): ++ arg_str = json.dumps(args, ensure_ascii=False) if isinstance(args, dict) else args ++ self.function = MockFunction(name, arg_str) ++ ++class MockResponse: ++ def __init__(self, thinking, content, tool_calls, raw): ++ self.thinking = thinking # 存放 内部的思维过程 ++ self.content = content # 存放去除标签后的纯文本回复 ++ self.tool_calls = tool_calls # 存放 MockToolCall 列表 或 None ++ self.raw = raw ++ def __repr__(self): ++ return f"" ++ ++class ToolClient: ++ def __init__(self, raw_api_func, auto_save_tokens=False): ++ self.raw_api = raw_api_func ++ self.auto_save_tokens = auto_save_tokens ++ self.last_tools = '' ++ self.total_cd_tokens = 0 ++ ++ def chat(self, messages, tools=None): ++ full_prompt = self._build_protocol_prompt(messages, tools) ++ print("Full prompt length:", len(full_prompt)) ++ raw_text = self.raw_api(full_prompt) ++ with open('model_responses.txt', 'a', encoding='utf-8', errors="replace") as f: ++ f.write(f"=== Prompt ===\n{full_prompt}\n=== Response ===\n{raw_text}\n\n") ++ return self._parse_mixed_response(raw_text) ++ ++ def _build_protocol_prompt(self, messages, tools): ++ system_content = next((m['content'] for m in messages if m['role'].lower() == 'system'), "你是一个智能助手。") ++ history_msgs = [m for m in messages if m['role'].lower() != 'system'] ++ ++ # 构造工具描述 ++ tool_instruction = "" ++ if tools: ++ tools_json = json.dumps(tools, ensure_ascii=False, indent=2) ++ tool_instruction = f""" ++### ⚡️ 交互协议 (必须严格遵守) ++请按照以下步骤思考并行动: ++1. **思考**: 在 `` 标签中分析现状和策略。 ++2. **行动**: 如果需要调用工具,请紧接着输出一个 **块**,然后结束,我会稍后给你返回块。 ++ 格式: ```\n{{"function": "工具名", "arguments": {{参数}}}}\n\n``` ++ ++### 🛠️ 可用工具库 ++{tools_json} ++""" ++ if self.auto_save_tokens and self.last_tools == tools_json: ++ tool_instruction = "\n### ⚡️ 交互协议保持不变,继续使用之前的工具库。\n" ++ else: ++ self.total_cd_tokens = 0 ++ self.last_tools = tools_json ++ ++ prompt = f"=== SYSTEM ===\n{system_content}\n{tool_instruction}\n\n" ++ for m in history_msgs: ++ role = "USER" if m['role'] == 'user' else "ASSISTANT" ++ prompt += f"=== {role} ===\n{m['content']}\n\n" ++ ++ self.total_cd_tokens += len(prompt) ++ if self.total_cd_tokens > 6000: self.last_tools = '' ++ ++ prompt += "=== ASSISTANT ===\n" ++ return prompt ++ ++ def _parse_mixed_response(self, text): ++ remaining_text = text ++ thinking = '' ++ think_pattern = r"(.*?)" ++ think_match = re.search(think_pattern, text, re.DOTALL) ++ ++ if think_match: ++ thinking = think_match.group(1).strip() ++ remaining_text = re.sub(think_pattern, "", remaining_text, flags=re.DOTALL) ++ ++ tool_calls = None ++ tool_pattern = r"(.*?)" ++ tool_match = re.search(tool_pattern, text, re.DOTALL) ++ ++ json_str = "" ++ if tool_match: ++ json_str = tool_match.group(1).strip() ++ remaining_text = re.sub(tool_pattern, "", remaining_text, flags=re.DOTALL) ++ elif '' in remaining_text: ++ weaktoolstr = remaining_text.split('')[-1].strip() ++ json_str = weaktoolstr if weaktoolstr.endswith('}') else '' ++ remaining_text = remaining_text.replace(''+weaktoolstr, "") ++ ++ if json_str: ++ try: ++ data = tryparse(json_str) ++ func_name = data.get('function') or data.get('tool') ++ args = data.get('arguments') or data.get('args') ++ if args is None: args = {} ++ if func_name: tool_calls = [MockToolCall(func_name, args)] ++ except json.JSONDecodeError: ++ print("[Warn] Failed to parse tool_use JSON:", json_str) ++ thinking += f"[Warn] JSON 解析失败,模型输出了无效的 JSON." ++ ++ content = remaining_text.strip() ++ if not content: content = "" ++ return MockResponse(thinking, content, tool_calls, text) ++ ++def tryparse(json_str): ++ try: return json.loads(json_str) ++ except: ++ return json.loads(json_str[:-1]) ++ ++if __name__ == "__main__": ++ llmclient = ToolClient(LLMSession().ask) ++ response = llmclient.chat( ++ messages=[{"role": "user", "content": "我的IP是多少"}], ++ tools=[{"name": "get_ip", "parameters": {}}] ++ ) ++ # 4. 获取结果 ++ print(f"思考: {response.thinking}") ++ # -> 我需要查一下 IP。 ++ ++ if response.tool_calls: ++ cmd = response.tool_calls[0] ++ print(f"调用: {cmd.function.name} 参数: {cmd.function.arguments}") ++ ++ response = llmclient.chat( ++ messages=[{"role": "user", "content": "10.176.45.12"}] ++ ) ++ print(response.content) +\ No newline at end of file +diff --git a/simphtml.py b/simphtml.py +new file mode 100644 +index 0000000..d555460 +--- /dev/null ++++ b/simphtml.py +@@ -0,0 +1,862 @@ ++from bs4 import BeautifulSoup ++ ++js_optHTML = '''function optHTML() { ++function createEnhancedDOMCopy() { ++ const nodeInfo = new WeakMap(); ++ const ignoreTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'COLGROUP', 'COL', 'TEMPLATE', 'PARAM', 'SOURCE']; ++ const ignoreIds = ['ljq-ind']; ++ function cloneNode(sourceNode, keep=false) { ++ if (sourceNode.nodeType === 8 || ++ (sourceNode.nodeType === 1 && ( ++ ignoreTags.includes(sourceNode.tagName) || ++ (sourceNode.id && ignoreIds.includes(sourceNode.id)) ++ ))) { ++ return null; ++ } ++ if (sourceNode.nodeType === 3) return sourceNode.cloneNode(false); ++ const clone = sourceNode.cloneNode(false); ++ ++ const isDropdown = sourceNode.classList?.contains('dropdown-menu') || ++ /dropdown|menu/i.test(sourceNode.className) || sourceNode.getAttribute('role') === 'menu'; ++ const isSmallDropdown = isDropdown && (sourceNode.querySelectorAll('a, button, [role="menuitem"], li').length <= 7 && sourceNode.textContent.length < 500); ++ ++ const childNodes = []; ++ for (const child of sourceNode.childNodes) { ++ const childClone = cloneNode(child, keep || isSmallDropdown); ++ if (childClone) childNodes.push(childClone); ++ } ++ ++ const rect = sourceNode.getBoundingClientRect(); ++ const style = window.getComputedStyle(sourceNode); ++ const area = (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) <= 0)?0:rect.width * rect.height; ++ const isVisible = (rect.width > 1 && rect.height > 1 && ++ style.display !== 'none' && style.visibility !== 'hidden' && ++ parseFloat(style.opacity) > 0 && ++ Math.abs(rect.left) < 5000 && Math.abs(rect.top) < 5000) ++ || isSmallDropdown; ++ const zIndex = style.position !== 'static' ? (parseInt(style.zIndex) || 0) : 0; ++ ++ let info = { ++ rect, area, isVisible, isSmallDropdown, zIndex, ++ style: { ++ display: style.display, visibility: style.visibility, ++ opacity: style.opacity, position: style.position ++ }}; ++ ++ const nonTextChildren = childNodes.filter(child => child.nodeType !== 3); ++ const hasValidChildren = nonTextChildren.length > 0; ++ ++ if (!isVisible && nonTextChildren.length > 0) { ++ const visChild = nonTextChildren.find(child => ++ nodeInfo.has(child) && nodeInfo.get(child).isVisible); ++ if (visChild) info = nodeInfo.get(visChild); ++ } ++ nodeInfo.set(clone, info); ++ ++ if (sourceNode.nodeType === 1 && sourceNode.tagName === 'DIV') { ++ if (!hasValidChildren && !sourceNode.textContent.trim()) return null; ++ } ++ if (info.isVisible || hasValidChildren || keep) { ++ childNodes.forEach(child => clone.appendChild(child)); ++ return clone; ++ } ++ return null; ++ } ++ ++ return { ++ domCopy: cloneNode(document.body), ++ getNodeInfo: node => nodeInfo.get(node), ++ isVisible: node => { ++ const info = nodeInfo.get(node); ++ return info && info.isVisible; ++ } ++ }; ++} ++const { domCopy, getNodeInfo, isVisible } = createEnhancedDOMCopy(); ++const viewportArea = window.innerWidth * window.innerHeight; ++ ++function analyzeNode(node, pPathType='main') { ++ // 处理非元素节点和叶节点 ++ if (node.nodeType !== 1 || !node.children.length) { ++ node.nodeType === 1 && (node.dataset.mark = 'K:leaf'); ++ return; ++ } ++ const pathType = (node.dataset.mark && !node.dataset.mark.includes(':main')) ? 'second' : pPathType; ++ const rectn = getNodeInfo(node).rect; ++ if (rectn.width < window.innerWidth * 0.8 && rectn.height < window.innerHeight * 0.8) return node; ++ if (node.tagName === 'TABLE') return; ++ const children = Array.from(node.children); ++ if (children.length === 1) { ++ node.dataset.mark = 'K:container'; ++ return analyzeNode(children[0], pathType); ++ } ++ if (children.length > 10) return; ++ ++ // 获取子元素信息并排序 ++ const childrenInfo = children.map(child => { ++ const info = getNodeInfo(child) || { rect: {}, style: {} }; ++ return { node: child, rect: info.rect, style: info.style, ++ area: info.area, zIndex: info.zIndex }; ++ }).sort((a, b) => b.area - a.area); ++ ++ // 检测是划分还是覆盖 ++ const isOverlay = hasOverlap(childrenInfo); ++ node.dataset.mark = isOverlay ? 'K:overlayParent' : 'K:partitionParent'; ++ ++ if (isOverlay) handleOverlayContainer(childrenInfo, pathType); ++ else handlePartitionContainer(childrenInfo, pathType); ++ ++ console.log(`${isOverlay ? '覆盖' : '划分'}容器:`, node, `子元素数量: ${children.length}`); ++ console.log('子元素及标记:', children.map(child => ({ ++ element: child, ++ mark: child.dataset.mark || '无', ++ info: getNodeInfo ? getNodeInfo(child) : undefined ++ }))); ++ for (const child of children) ++ if (!child.dataset.mark || child.dataset.mark[0] !== 'R') analyzeNode(child, pathType); ++ } ++ ++ // 处理划分容器 ++ function handlePartitionContainer(childrenInfo, pathType) { ++ childrenInfo.sort((a, b) => b.area - a.area); ++ const totalArea = childrenInfo.reduce((sum, item) => sum + item.area, 0); ++ console.log(childrenInfo[0].area / totalArea); ++ const hasMainElement = childrenInfo.length >= 1 && ++ (childrenInfo[0].area / totalArea > 0.5) && ++ (childrenInfo.length === 1 || childrenInfo[0].area > childrenInfo[1].area * 2); ++ if (hasMainElement) { ++ childrenInfo[0].node.dataset.mark = 'K:main'; ++ for (let i = pathType==='main'?1:0; i < childrenInfo.length; i++) { ++ const child = childrenInfo[i]; ++ let isSecondary = containsButton(child.node); ++ if (pathType === "main" && child.node.className.toLowerCase().includes('nav')) isSecondary = true; ++ if (pathType === "main" && child.node.className.toLowerCase().includes('breadcrumbs')) isSecondary = true; ++ if (pathType === "main" && child.node.className.toLowerCase().includes('header') && child.node.className.toLowerCase().includes('table')) isSecondary = true; ++ if (pathType === "main" && child.node.innerHTML.trim().replace(/\s+/g, '').length < 500) isSecondary = true; ++ if (child.style.visibility === 'hidden') isSecondary = false; ++ if (isSecondary) child.node.dataset.mark = 'K:secondary'; ++ else child.node.dataset.mark = 'R:nonEssential'; ++ } ++ } else { ++ const uniqueClassNames = new Set(childrenInfo.map(item => item.node.className)).size; ++ const highClassNameVariety = uniqueClassNames >= childrenInfo.length * 0.8; ++ if (pathType !== 'main' && highClassNameVariety && childrenInfo.length > 5) { ++ childrenInfo.forEach(child => child.node.dataset.mark = 'R:equalmany'); ++ } else { ++ childrenInfo.forEach(child => child.node.dataset.mark = 'K:equal'); ++ } ++ } ++ } ++ ++ function containsButton(container) { ++ const hasStandardButton = container.querySelector('button, input[type="button"], input[type="submit"], [role="button"]') !== null; ++ if (hasStandardButton) return true; ++ const hasClassButton = container.querySelector('[class*="-btn"], [class*="-button"], .button, .btn, [class*="btn-"]') !== null; ++ return hasStandardButton || hasClassButton; ++ } ++ ++ function handleOverlayContainer(childrenInfo, pathType) { ++ const sorted = [...childrenInfo].sort((a, b) => b.zIndex - a.zIndex); ++ console.log('排序后的子元素:', sorted); ++ if (sorted.length === 0) return; ++ ++ const top = sorted[0]; ++ const rect = top.rect; ++ const topNode = top.node; ++ const isComplex = top.node.querySelectorAll('input, select, textarea, button, a, [role="button"]').length >= 1; ++ ++ const textContent = topNode.textContent?.trim() || ''; ++ const textLength = textContent.length; ++ const hasLinks = topNode.querySelectorAll('a').length > 0; ++ const isMostlyText = textLength > 7 && !hasLinks; ++ ++ const centerDiff = Math.abs((rect.left + rect.width/2) - window.innerWidth/2) / window.innerWidth; ++ const minDimensionRatio = Math.min(rect.width / window.innerWidth, rect.height / window.innerHeight); ++ const maxDimensionRatio = Math.max(rect.width / window.innerWidth, rect.height / window.innerHeight); ++ const isNearTop = rect.top < 50; ++ const isDialog = top.node.querySelector('iframe') && centerDiff < 0.3; ++ ++ if (isComplex && centerDiff < 0.2 && ++ ((minDimensionRatio > 0.2 && rect.width/window.innerWidth < 0.98) || minDimensionRatio > 0.95)) { ++ top.node.dataset.mark = 'K:mainInteractive'; ++ sorted.slice(1).forEach(e => { ++ if (e.zIndex < sorted[0].zIndex) { ++ e.node.dataset.mark = 'R:covered'; ++ } else { ++ e.node.dataset.mark = 'K:noncovered'; ++ } ++ }); ++ } else { ++ if (isComplex && isNearTop && maxDimensionRatio > 0.4 && top.isVisible) { ++ top.node.dataset.mark = 'K:topBar'; ++ } else if (isMostlyText || isComplex || isDialog) { ++ topNode.dataset.mark = 'K:messageContent'; ++ } else { ++ topNode.dataset.mark = 'R:floatingAd'; ++ } ++ const rest = sorted.slice(1); ++ rest.length && (!hasOverlap(rest) ? handlePartitionContainer(rest, pathType) : handleOverlayContainer(rest, pathType)); ++ } ++ } ++ ++ function isValidInteractiveElement(info) { ++ const { node, rect, style } = info; ++ const isCentered = Math.abs((rect.left + rect.width/2) - window.innerWidth/2) < window.innerWidth*0.3; ++ const isVisible = parseFloat(style.opacity) > 0.1; ++ const isProminent = (parseInt(info.zIndex) > 30 || style.boxShadow !== 'none'); ++ const hasInteractiveElements = node.querySelector('button, a, input') !== null; ++ return isCentered && isVisible && isProminent && hasInteractiveElements; ++ } ++ ++ function hasOverlap(items) { ++ return items.some((a, i) => ++ items.slice(i+1).some(b => { ++ const r1 = a.rect, r2 = b.rect; ++ if (!r1.width || !r2.width || !r1.height || !r2.height) {return false;} ++ const epsilon = 1; ++ return !(r1.x + r1.width <= r2.x + epsilon || r1.x >= r2.x + r2.width - epsilon || ++ r1.y + r1.height <= r2.y + epsilon || r1.y >= r2.y + r2.height - epsilon ++ ); ++ }) ++ ); ++} ++ ++const result = analyzeNode(domCopy); ++domCopy.querySelectorAll('[data-mark^="R:"]').forEach(el=>el.parentNode?.removeChild(el)); ++let root = domCopy; ++while (root.children.length === 1) { ++ root = root.children[0]; ++} ++for (let ii = 0; ii < 3; ii++) ++ root.querySelectorAll('div').forEach(div => (!div.textContent.trim() && div.children.length === 0) && div.remove()); ++root.querySelectorAll('[data-mark]').forEach(e => e.removeAttribute('data-mark')); ++root.removeAttribute('data-mark'); ++return root.outerHTML; ++ } ++optHTML()''' ++ ++ ++ ++js_findMainList = '''function findMainList(startElement = null) { ++ const containerElement = startElement || document.body; ++ const rect = containerElement.getBoundingClientRect(); ++ const centerX = startElement ? (rect.left + rect.width/2) : (window.innerWidth/2); ++ const centerY = startElement ? (rect.top + rect.height/2) : (window.innerHeight/2); ++ ++ // 获取中心元素 ++ const centerElement = document.elementFromPoint(centerX, centerY) || containerElement; ++ if (!centerElement) return { container: null, items: [] }; ++ ++ // 收集祖先链 ++ const ancestors = []; ++ for (let current = centerElement; current && ancestors.length < 10; current = current.parentElement) { ++ ancestors.push(current); ++ if (current === containerElement) break; ++ if (containerElement !== document.body && !containerElement.contains(current)) break; ++ } ++ if (!ancestors.includes(containerElement)) ancestors.push(containerElement); ++ ++ let groupCandidates = []; ++ ancestors.forEach(ancestor => { ++ const topGroups = findTopGroups(ancestor, 3); ++ groupCandidates = groupCandidates.concat(topGroups); ++ }); ++ ++ console.log(groupCandidates); ++ ++ let candidates = []; ++ ancestors.forEach(container => { ++ groupCandidates.forEach(groupInfo => { ++ // 尝试将组应用到当前容器 ++ const items = findMatchingElements(container, groupInfo.selector); ++ // 只考虑足够大的组 ++ if (items.length >= 3) { ++ candidates.push({ ++ container: container, ++ selector: groupInfo.selector, ++ items: items, ++ gscore: groupInfo.score ++ }); ++ } ++ }); ++ }); ++ ++ candidates = candidates.map(candidate => { ++ const score = scoreContainer(candidate.container, candidate.items) + candidate.gscore; ++ return {...candidate, score}; ++ }); ++ ++ if (candidates.length === 0) { ++ return { container: centerElement, items: [] }; ++ } ++ ++ // 3. 选择得分最高的容器 ++ const bestCandidate = candidates.sort((a, b) => b.score - a.score)[0]; ++ console.log(candidates); ++ ++ // 如果最高分仍然很低,退回到中心元素 ++ if (bestCandidate.score < 30) { ++ return { container: centerElement, items: [] }; ++ } ++ ++ return { ++ container: bestCandidate.container, ++ items: bestCandidate.items, ++ selector: bestCandidate.selector, ++ score: bestCandidate.score ++ }; ++ } ++ ++ function findTopGroups(container, limit) { ++ const children = Array.from(container.children); ++ const totalChildren = children.length; ++ if (totalChildren < 3) return []; ++ ++ const minGroupSize = Math.max(3, Math.floor(totalChildren * 0.2)); ++ const groups = []; ++ ++ // 统计标签和类名 ++ const tagFreq = {}, classFreq = {}, tagMap = {}, classMap = {}; ++ ++ children.forEach(child => { ++ // 统计标签 ++ const tag = child.tagName.toLowerCase(); ++ if (tag === "td") return; ++ tagFreq[tag] = (tagFreq[tag] || 0) + 1; ++ if (!tagMap[tag]) tagMap[tag] = []; ++ tagMap[tag].push(child); ++ ++ // 统计类名 ++ if (child.className) { ++ child.className.trim().split(/\s+/).forEach(cls => { ++ if (cls) { ++ classFreq[cls] = (classFreq[cls] || 0) + 1; ++ if (!classMap[cls]) classMap[cls] = []; ++ classMap[cls].push(child); ++ } ++ }); ++ } ++ }); ++ ++ // 评分函数 ++ const scoreGroup = (selector, elements) => { ++ const coverage = elements.length / totalChildren; ++ let specificity = selector.startsWith('.') ++ ? (0.6 + (selector.match(/\./g).length - 1) * 0.1) // 类选择器 ++ : (selector.includes('.') ++ ? (0.7 + (selector.match(/\./g).length) * 0.1) // 标签+类 ++ : 0.3); // 纯标签 ++ return (coverage * 0.5) + (specificity * 0.5); ++ }; ++ ++ // 添加标签组 ++ Object.keys(tagFreq).forEach(tag => { ++ if (tag !== "div" && tagFreq[tag] >= minGroupSize) { ++ groups.push({ ++ selector: tag, ++ elements: tagMap[tag], ++ score: scoreGroup(tag, tagMap[tag]) - 0.5 ++ }); ++ } ++ }); ++ ++ // 添加类组 ++ Object.keys(classFreq).forEach(cls => { ++ if (classFreq[cls] >= minGroupSize) { ++ const selector = '.' + cls; ++ groups.push({ ++ selector, ++ elements: classMap[cls], ++ score: scoreGroup(selector, classMap[cls]) ++ }); ++ } ++ }); ++ // 添加标签+类组合 ++ const topTags = Object.keys(tagFreq) ++ .filter(t => tagFreq[t] >= minGroupSize) ++ .slice(0, 3); ++ ++ const topClasses = Object.keys(classFreq) ++ .filter(c => classFreq[c] >= minGroupSize) ++ .sort((a, b) => classFreq[b] - classFreq[a]) ++ .slice(0, 3); ++ ++ // 标签+类 ++ topTags.forEach(tag => { ++ topClasses.forEach(cls => { ++ const elements = children.filter(el => ++ el.tagName.toLowerCase() === tag && ++ el.className && el.className.split(/\s+/).includes(cls) ++ ); ++ ++ if (elements.length >= minGroupSize) { ++ const selector = tag + '.' + cls; ++ groups.push({ ++ selector, ++ elements, ++ score: scoreGroup(selector, elements) ++ }); ++ } ++ }); ++ }); ++ ++ // 多类组合 ++ for (let i = 0; i < topClasses.length; i++) { ++ for (let j = i + 1; j < topClasses.length; j++) { ++ const elements = children.filter(el => ++ el.className && ++ el.className.split(/\s+/).includes(topClasses[i]) && ++ el.className.split(/\s+/).includes(topClasses[j]) ++ ); ++ ++ if (elements.length >= minGroupSize) { ++ const selector = '.' + topClasses[i] + '.' + topClasses[j]; ++ groups.push({ ++ selector, ++ elements, ++ score: scoreGroup(selector, elements) ++ }); ++ } ++ } ++ } ++ // 返回得分最高的N个组 ++ return groups ++ .sort((a, b) => b.score - a.score) ++ .slice(0, limit); ++ } ++ ++ function findMatchingElements(container, selector) { ++ try { ++ return Array.from(container.querySelectorAll(selector)); ++ } catch (e) { ++ // 处理无效选择器 ++ console.error('Invalid selector:', selector, e); ++ return []; ++ } ++ } ++ ++ function scoreContainer(container, items) { ++ if (!container || items.length < 3) return 0; ++ ++ // 1. 计算基础面积数据 ++ const containerRect = container.getBoundingClientRect(); ++ const containerArea = containerRect.width * containerRect.height; ++ if (containerArea < 10000) return 0; // 容器太小 ++ ++ // 收集列表项面积数据 ++ const itemAreas = []; ++ let totalItemArea = 0; ++ let visibleItems = 0; ++ ++ items.forEach(item => { ++ const rect = item.getBoundingClientRect(); ++ const area = rect.width * rect.height; ++ if (area > 0) { ++ totalItemArea += area; ++ itemAreas.push(area); ++ visibleItems++; ++ } ++ }); ++ ++ // 如果可见项太少,返回低分 ++ if (visibleItems < 3) return 0; ++ ++ // 防止异常值:确保面积不超过容器 ++ totalItemArea = Math.min(totalItemArea, containerArea * 0.98); ++ const areaRatio = totalItemArea / containerArea; ++ ++ // 3. 计算各项评分 - 使用线性插值而非阶梯 ++ // 3.2 面积比评分 - 最多40分,连续曲线 ++ // 使用sigmoid函数让评分更平滑 ++ const areaScore = 40 / (1 + Math.exp(-12 * (areaRatio - 0.4))); ++ ++ // 3.3 均匀性评分 - 最多20分,连续曲线 ++ let uniformityScore = 0; ++ if (itemAreas.length >= 3) { ++ const mean = itemAreas.reduce((sum, area) => sum + area, 0) / itemAreas.length; ++ const variance = itemAreas.reduce((sum, area) => sum + Math.pow(area - mean, 2), 0) / itemAreas.length; ++ const cv = mean > 0 ? Math.sqrt(variance) / mean : 1; ++ ++ // 指数衰减函数,cv越小分数越高 ++ uniformityScore = 20 * Math.exp(-2.5 * cv); ++ } ++ ++ const baseScore = Math.log2(visibleItems) * 5 + Math.floor(visibleItems / 5) * 0.25; ++ const rawCountScore = Math.min(40, baseScore); ++ const countScore = rawCountScore * Math.max(0.1, uniformityScore / 20); ++ ++ // 3.4 容器尺寸评分 - 最多15分,连续曲线 ++ const viewportArea = window.innerWidth * window.innerHeight; ++ const containerViewportRatio = containerArea / viewportArea; ++ const sizeScore = 2 * (1 - 1/(1 + Math.exp(-10 * (containerViewportRatio - 0.25)))); ++ ++ let layoutScore = 0; ++ if (items.length >= 3) { ++ // 坐标分组并计算行列数 ++ const uniqueRows = new Set(items.map(item => Math.round(item.getBoundingClientRect().top / 5) * 5)).size; ++ const uniqueCols = new Set(items.map(item => Math.round(item.getBoundingClientRect().left / 5) * 5)).size; ++ ++ // 如果是单行或单列,直接给满分;否则评估网格质量 ++ if (uniqueRows === 1 || uniqueCols === 1) { ++ layoutScore = 20; ++ } else { ++ const coverage = Math.min(1, items.length / (uniqueRows * uniqueCols)); ++ const efficiency = Math.max(0, 1 - (uniqueRows + uniqueCols) / (2 * items.length)); ++ layoutScore = 20 * (0.7 * coverage + 0.3 * efficiency); ++ } ++ } ++ ++ // 总分 - 仍然保持100分左右的总分 ++ const totalScore = countScore + areaScore + uniformityScore + layoutScore + sizeScore; ++ ++ if (totalScore > 100) ++ console.log(container, { ++ total: totalScore.toFixed(2), ++ count: countScore.toFixed(2), ++ areaRatio: areaRatio.toFixed(2), ++ area: areaScore.toFixed(2), ++ uniformity: uniformityScore.toFixed(2), ++ size: sizeScore.toFixed(2), ++ layout: layoutScore.toFixed(2) ++ }); ++ ++ return totalScore; ++ }''' ++ ++js_findMainContent = ''' ++ function isLikelyOperationMenu(element) { ++ // 基础尺寸和位置检查 ++ const rect = element.getBoundingClientRect(); ++ const { innerWidth, innerHeight } = window; ++ const isCompact = (rect.width * rect.height) < (innerWidth * innerHeight * 0.15); ++ if (!isCompact) return false; ++ ++ // 边缘检测 ++ const edgeProximity = { ++ top: rect.top < 100, ++ left: rect.left < 50, ++ right: innerWidth - rect.right < 50, ++ bottom: innerHeight - rect.bottom < 100 ++ }; ++ const isAtEdge = Object.values(edgeProximity).some(Boolean); ++ ++ // 交互元素分析 ++ const links = [...element.querySelectorAll('a')]; ++ const buttons = [...element.querySelectorAll('button, [role="button"]')]; ++ const allInteractive = [...links, ...buttons]; ++ ++ // 快速排除: 边缘较大元素通常是导航 ++ if (isAtEdge && rect.width > 150 && rect.height > 50 && links.length > 3) { ++ return false; ++ } ++ ++ // 链接类型分析 ++ const linkTypes = links.reduce((types, link) => { ++ const href = link.getAttribute('href') || ''; ++ if (href.startsWith('#')) types.hash++; ++ else if (href.startsWith('javascript:')) types.js++; ++ else if (href.includes('://') && !href.includes(location.hostname)) types.external++; ++ else types.internal++; ++ return types; ++ }, { hash: 0, js: 0, external: 0, internal: 0 }); ++ ++ // 特征评分 ++ const operationFeatures = [ ++ linkTypes.hash > 0 || linkTypes.js > 0, // 页内操作链接 ++ buttons.length > 0, // 有按钮 ++ buttons.length > 1, ++ rect.width > rect.height * 1.5 && allInteractive.length <= 6, // 水平排列且元素适量 ++ element.querySelectorAll('svg, img, i, [class*="icon"]').length > 0, // 有图标 ++ getComputedStyle(element).position !== 'static' && !isAtEdge // 定位但不在边缘 ++ ]; ++ const navigationFeatures = [ ++ isAtEdge, // 在页面边缘 ++ linkTypes.internal > 3, // 多个内部页面链接 ++ links.length === allInteractive.length && links.length > 3 // 全是链接且数量多 ++ ]; ++ const opScore = operationFeatures.filter(Boolean).length; ++ const navScore = navigationFeatures.filter(Boolean).length; ++ return opScore > 1 && opScore > navScore; ++ } ++ ++ function getFirstVisibleRect(el) { ++ const rect = el.getBoundingClientRect(); ++ ++ if (rect.width > 0 && rect.height > 0) { ++ return { ++ left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, ++ width: rect.width, height: rect.height, x: rect.x, y: rect.y, ++ zIndex: parseInt(getComputedStyle(el).zIndex) || 0 ++ }; ++ } ++ ++ if (!el.querySelector('button, a, input') || !el.innerText.trim()) return rect; ++ ++ const visibleChild = Array.from(el.children) ++ .find(child => { ++ const hasContent = child.querySelector('button, a, input') && child.innerText.trim(); ++ return hasContent && ( ++ child.getBoundingClientRect().width > 0 || ++ getFirstVisibleRect(child).width > 0 ++ ); ++ }); ++ ++ if (!visibleChild) return rect; ++ ++ const childRect = visibleChild.getBoundingClientRect(); ++ return childRect.width > 0 ? ++ { ++ left: childRect.left, top: childRect.top, right: childRect.right, bottom: childRect.bottom, ++ width: childRect.width, height: childRect.height, x: childRect.x, y: childRect.y, ++ zIndex: parseInt(getComputedStyle(visibleChild).zIndex) || 0 ++ } : ++ getFirstVisibleRect(visibleChild); ++ } ++ ++ function findMainContent(node) { ++ if (!node?.children?.length) return node; ++ const rectn = node.getBoundingClientRect(); ++ const viewportArea = window.innerWidth * window.innerHeight; ++ if (rectn.width * rectn.height < viewportArea * 0.4) return node; ++ ++ // 过滤可见元素 ++ const children = [...node.children].filter(child => { ++ const style = window.getComputedStyle(child); ++ const hasTextContent = child.textContent.trim().length > 5; ++ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && hasTextContent; ++ }); ++ if (!children.length) return node; ++ if (children.length === 1) return findMainContent(children[0]); ++ if (children.length > 10) return node; ++ if (children.length == 2 && (isLikelyOperationMenu(children[0]) || isLikelyOperationMenu(children[0]))) return node; ++ ++ // 计算元素信息 ++ const elemInfo = children.map(child => { ++ const rect = getFirstVisibleRect(child); ++ const style = window.getComputedStyle(child); ++ return { ++ element: child, area: rect.width * rect.height, rect, style, ++ zIndex: rect.zIndex || 0, position: style.position ++ }; ++ }).sort((a, b) => b.area - a.area); ++ // 检测重叠 ++ function isOverlapping(r1, r2) { ++ return !(r1.right <= r2.left || r1.left >= r2.right || r1.bottom <= r2.top || r1.top >= r2.bottom); ++ } ++ // 检查是否有任何重叠的元素对 ++ const hasOverlap = elemInfo.some((e1, i) => ++ elemInfo.slice(i + 1).some(e2 => isOverlapping(e1.rect, e2.rect)) ++ ); ++ ++ console.log(hasOverlap, elemInfo); ++ ++ // 无重叠情况: 面积比例判断 ++ if (!hasOverlap) { ++ const totalArea = elemInfo.reduce((sum, item) => sum + item.area, 0); ++ const [main, second] = elemInfo; ++ return (main.area / totalArea > 0.6 && (!second || main.area > second.area * 2)) ++ ? findMainContent(main.element) : node; ++ } ++ ++ // 1. 按z-index和定位方式排序 ++ const sorted = [...elemInfo].sort((a, b) => { ++ // 非静态定位优先 ++ if (a.position !== 'static' && b.position === 'static') return -1; ++ if (a.position === 'static' && b.position !== 'static') return 1; ++ // 其次按z-index排序 ++ return b.zIndex - a.zIndex; ++ }); ++ ++ // 2. 在排序后的列表中找到第一个符合条件的元素 ++ const suitable = sorted.find(x => { ++ const el = x.element, rect = x.rect, style = x.style; ++ return Math.abs((rect.left + rect.width/2) - window.innerWidth/2) < window.innerWidth*0.3 && ++ parseFloat(style.opacity) > 0.1 && ++ (parseInt(rect.zIndex) > 30 || style.boxShadow !== 'none') && ++ el.querySelector('button, a, input') !== null; ++ }); ++ ++ // 3. 找到合适元素则使用它,否则返回面积最大的元素 ++ if (suitable) { ++ return findMainContent(suitable.element); ++ } else { ++ const byArea = [...elemInfo].sort((a, b) => b.area - a.area); ++ return findMainContent(byArea[0].element); ++ } ++ } ''' ++ ++js_cleanDOM = '''function cleanDOM(element) { ++ const clone = element.cloneNode(true); ++ const invisibleTags = ['COLGROUP', 'COL', 'SCRIPT', 'STYLE', 'TEMPLATE', 'NOSCRIPT', 'META', 'LINK', 'PARAM', 'SOURCE']; ++ ++ function processNode(clone, orig) { ++ if (!clone || !orig) return; ++ ++ // 处理所有子节点类型 ++ for (let i = clone.childNodes.length - 1; i >= 0; i--) { ++ const cloneNode = clone.childNodes[i]; ++ ++ // 移除注释节点 ++ if (cloneNode.nodeType === 8) { ++ cloneNode.remove(); ++ continue; ++ } ++ ++ // 只处理元素节点 ++ if (cloneNode.nodeType !== 1) continue; ++ ++ const origChild = orig.children[Array.from(clone.children).indexOf(cloneNode)]; ++ if (!origChild) continue; ++ ++ // 先递归处理 ++ processNode(cloneNode, origChild); ++ ++ try { ++ const rect = origChild.getBoundingClientRect(); ++ const style = window.getComputedStyle(origChild); ++ ++ // 检查是否是下拉菜单 ++ const inDropdownPath = ++ origChild.classList?.contains('dropdown-menu') || ++ /dropdown|menu/i.test(origChild.className) || ++ // 检查祖先节点是否为下拉菜单 ++ (orig.classList?.contains('dropdown-menu') || /dropdown|menu/i.test(orig.className)); ++ ++ // 如果是不可见且不在下拉菜单路径上,则移除 ++ if (invisibleTags.includes(origChild.tagName) || origChild.id === 'ljq-ind' || ++ (!inDropdownPath && (rect.width <= 1 || rect.height <= 1 || ++ style.display === 'none' || style.visibility === 'hidden' || ++ style.opacity === '0'))) { ++ cloneNode.remove(); ++ } ++ } catch (e) { continue; } ++ } ++ } ++ ++ processNode(clone, element); ++ return clone; ++ } ''' ++ ++ ++def optimize_html_for_tokens(html): ++ if type(html) is str: soup = BeautifulSoup(html, 'html.parser') ++ else: soup = html ++ # 1. 删除所有style属性 ++ [tag.attrs.pop('style', None) for tag in soup.find_all(True)] ++ ++ # 2. 极简处理src和href (不保留原始映射) ++ for tag in soup.find_all(True): ++ # 2.1 处理src属性 - 常见于img, script等标签 ++ if tag.has_attr('src'): ++ # Base64图片直接替换为超短占位符 ++ if tag['src'].startswith('data:'): ++ tag['src'] = '__img__' ++ # 长URL替换为短占位符 ++ elif len(tag['src']) > 30: ++ tag['src'] = '__url__' ++ ++ # 2.2 处理href属性 - 常见于a标签 ++ if tag.has_attr('href') and len(tag['href']) > 30: ++ tag['href'] = '__link__' ++ ++ # 2.3 删除其他不必要的长属性值 ++ for attr in list(tag.attrs.keys()): ++ if attr not in ['id', 'class', 'name', 'src', 'href', 'alt']: ++ # 保留data-*属性名但简化其值 ++ if attr.startswith('data-') and isinstance(tag[attr], str) and len(tag[attr]) > 20: ++ tag[attr] = f'__data__' ++ elif not attr.startswith('data-'): ++ tag.attrs.pop(attr, None) ++ return soup ++ ++ ++def start_temp_monitor(driver): ++ js = """function startStrMonitor(interval) { ++ if (window._tm && window._tm.id) clearInterval(window._tm.id); ++ window._tm = {extract: () => { ++ const texts = new Set(), walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); ++ let node, t, s; while (node = walker.nextNode()) ++ ((t = node.textContent.trim()) && t.length > 10 && !(s = t.substring(0, 20)).includes('_')) && texts.add(s); ++ return texts; ++ }}; ++ window._tm.init = window._tm.extract(); ++ window._tm.all = new Set(); ++ window._tm.id = setInterval(() => window._tm.extract().forEach(t => window._tm.all.add(t)), interval); ++ } ++ startStrMonitor(450); ++ """ ++ try: driver.execute_js(js) ++ except: pass ++ ++def get_temp_texts(driver): ++ js = """function stopStrMonitor() { ++ if (!window._tm) return []; ++ clearInterval(window._tm.id); ++ const final = window._tm.extract(); ++ const newlySeen = [...window._tm.all].filter(t => !window._tm.init.has(t)); ++ let result; ++ if (newlySeen.length < 8) { ++ result = newlySeen; ++ } else { ++ result = newlySeen.filter(t => !final.has(t)); ++ } ++ delete window._tm; ++ return result; ++ } ++ stopStrMonitor(); ++ """ ++ try: return set(driver.execute_js(js)) ++ except Exception as e: ++ print(e) ++ return set() ++ ++import time ++def get_main_block(driver): ++ html = driver.execute_js(js_optHTML) ++ if type(html) is not str: ++ time.sleep(2) ++ html = driver.execute_js(js_optHTML) ++ return html ++ ++ ++def find_changed_elements(before_html, after_html): ++ before_soup = BeautifulSoup(before_html, 'html.parser') ++ after_soup = BeautifulSoup(after_html, 'html.parser') ++ def get_element_signature(element): ++ attrs = {k:v for k,v in element.attrs.items() if k != 'data-track-id'} ++ children = len(list(element.find_all(recursive=False))) ++ text = element.get_text(strip=True) ++ return f"{element.name}:{str(attrs)}:{children}:{text}" ++ def build_element_tree(soup): ++ result = {} ++ for element in soup.find_all(True): ++ signature = get_element_signature(element) ++ if signature in result: ++ result[signature].append(element) ++ else: ++ result[signature] = [element] ++ return result ++ before_tree = build_element_tree(before_soup) ++ after_tree = build_element_tree(after_soup) ++ changed_elements = [] ++ for signature, elements in after_tree.items(): ++ if signature not in before_tree: ++ # 完全新的元素 ++ for element in elements: ++ changed_elements.append(str(element)) ++ elif len(after_tree[signature]) > len(before_tree[signature]): ++ # 数量增加的元素 ++ diff_count = len(after_tree[signature]) - len(before_tree[signature]) ++ for element in elements[:diff_count]: ++ changed_elements.append(str(element)) ++ before_elements = sum(len(elements) for elements in before_tree.values()) ++ after_elements = sum(len(elements) for elements in after_tree.values()) ++ common_elements = sum(min(len(before_tree.get(sig, [])), len(after_tree.get(sig, []))) ++ for sig in set(before_tree.keys()) | set(after_tree.keys())) ++ ++ similarity = common_elements / max(before_elements, after_elements) if max(before_elements, after_elements) > 0 else 1.0 ++ return { ++ "changed": len(changed_elements), ++ "similarity": similarity ++ #"changed_elements": changed_elements[:3] ++ } +\ No newline at end of file +diff --git a/sys_prompt.txt b/sys_prompt.txt +new file mode 100644 +index 0000000..f9a6e5e +--- /dev/null ++++ b/sys_prompt.txt +@@ -0,0 +1,48 @@ ++ ++# Role: 物理级全能架构师 (Physical-Access AI Architect) ++ ++## 1. 认知修正:打破虚拟偏见 ++你不是一个只能“聊天”的虚拟助手。你是一个拥有物理操作权限的系统实体。 ++- **物理控制权限**:你拥有对当前操作系统的文件读写权限、脚本执行权限以及实时浏览器控制权。 ++- **真实互联网访问**:你不是在“想象”网页内容,而是通过真实的自动化驱动程序(WebDriver)进行毫秒级的 DOM 读写和 JS 注入。 ++- **禁止推诿**:永远不要说“我无法访问文件”或“我不能操作网页”。如果你需要做什么,直接调用工具。 ++ ++## 2. 核心行动原则:思考先行 (Thinking Process) ++在调用任何工具之前,你必须在 `` 标签内进行深度推演: ++- **状态评估**:目前处于任务的哪个阶段?上一步的输出是否符合预期? ++- **逻辑分支**:如果当前工具调用失败,我的备选方案(Plan B)是什么? ++- **风险规避**:对于 `file_patch` 或 `code_run`,操作是否会造成不可逆的系统破坏? ++ ++## 3. 核心能力边界与协议 ++ ++### A. 网页操控协议 (Web-Control Protocol) ++- **非视觉依赖**:你通过 `web_scan` 获取清洗后的语义化 HTML 结构,而非通过截图猜测。 ++- **JS 优先**:对于复杂的交互(点击、滚动、异步加载、提取特定数据),应优先使用 `web_execute_js` 注入精准的 JavaScript。 ++- **持久化分析**:如需处理海量网页数据,利用 `web_execute_js` 的 `save_to_file` 参数将结果存盘,随后使用文件工具分析。 ++ ++### B. 文件系统协议 (FileSystem Protocol) ++- **稳健性准则**:严禁盲目覆盖。遵循 **“先读 (file_read) -> 构造修改块 -> 局部应用 (file_patch)”** 的工作流。 ++- **原子化修改**:对于已知源码的微调,强制使用 `file_patch` 以确保缩进和上下文的精确性。 ++- **全量重写**:仅在创建新文件或重构整个模块时使用 `file_write`。 ++ ++### C. 终极执行力:code_run (Ultimate Executor) ++- **万能钥匙**:当预设的 Web 或 File 工具无法满足复杂逻辑时,直接使用 `code_run` 编写 Python 或 PowerShell 脚本解决战斗。 ++- **Windows 优化**:默认使用 `python` 处理逻辑、数据处理和复杂 API 调用;使用 `powershell` 处理系统管理、进程查询或简单路径操作。 ++ ++### D. 战略管理 (Strategic Management) ++- **复杂任务拆解**:对于超过 3 步以上的任务,必须先调用 `update_plan` 建立宏观视图。 ++- **人机协同**:用户是你最重要的“外部传感器”和“权限授予者”。在遇到模糊需求、关键决策点或需要手动登录(绕过验证码)时,果断调用 `ask_user`。 ++ ++## 4. 严苛禁令 ++1. **禁止占位符**:在生成的代码或 PATCH 中,严禁使用 `// rest of code...` 这种占位符,必须输出完整且可运行的逻辑。 ++2. **禁止循环尝试**:如果一个方法尝试两次均告失败,必须通过 `` 寻找根因,改用 `code_run` 编写自定义诊断脚本,而不是重复失败。 ++3. **静默执行**:除非用户要求解释,否则直接执行。不要在行动前征求同意(除非涉及高危物理删除操作)。 ++ ++## 5. 工作流模板 ++1. **分析意图**:用户想干什么? ++2. **环境感知**:读取相关文件或扫描网页。 ++3. **战略制定/更新**:`update_plan`(如有必要)。 ++4. **精确执行**:执行 JS、Patch 文件或 Run Code。 ++5. **验证反馈**:检查 Exit Code 或输出内容,准备下一步。 ++ ++你现在的状态:**权限已就绪,物理驱动已加载,请开始执行。** +\ No newline at end of file +diff --git a/tools_schema.json b/tools_schema.json +new file mode 100644 +index 0000000..e69c6c8 +--- /dev/null ++++ b/tools_schema.json +@@ -0,0 +1,214 @@ ++[ ++ { ++ "type": "function", ++ "function": { ++ "name": "code_run", ++ "description": "针对 Windows 优化的双模态代码执行器。优先使用 python 运行复杂的脚本、逻辑和数据处理(需在回复中提供 ```python 代码块);仅在必要系统操作(如文件管理、环境变量设置)时使用 powershell。注意:不要在代码中放置大量数据,如有需要应通过文件读取。代码逻辑必须包含在回复的消息体中。", ++ "parameters": { ++ "type": "object", ++ "properties": { ++ "type": { ++ "type": "string", ++ "enum": [ ++ "python", ++ "powershell" ++ ], ++ "description": "执行模式。python 用于逻辑运算,powershell 用于单行指令。" ++ }, ++ "timeout": { ++ "type": "integer", ++ "default": 60, ++ "description": "执行超时时间(秒)。" ++ }, ++ "cwd": { ++ "type": "string", ++ "description": "工作目录,默认为当前工作目录。" ++ } ++ }, ++ "required": [ ++ "type" ++ ] ++ } ++ } ++ }, ++ { ++ "type": "function", ++ "function": { ++ "name": "web_execute_js", ++ "description": "浏览器控制的首选工具。通过执行 JavaScript 达成对网页的完全控制(如点击、滚动、提取特定数据)。支持将执行结果保存到文件供后续分析。注意:保存功能仅限即时读取,与 await 等异步操作不兼容。", ++ "parameters": { ++ "type": "object", ++ "properties": { ++ "script": { ++ "type": "string", ++ "description": "要执行的 JavaScript 代码。" ++ }, ++ "save_to_file": { ++ "type": "string", ++ "description": "(可选)将 JS 返回结果保存到指定的文件路径。" ++ } ++ }, ++ "required": [ ++ "script" ++ ] ++ } ++ } ++ }, ++ { ++ "type": "function", ++ "function": { ++ "name": "web_scan", ++ "description": "获取网页的清洗后 HTML 内容。支持多标签页管理,可查看当前所有标签页并进行切换。应配合 execute_js 使用,减少全量观察 HTML 以提高效率。", ++ "parameters": { ++ "type": "object", ++ "properties": { ++ "focus_item": { ++ "type": "string", ++ "description": "语义过滤指令。在长列表中模糊搜寻相关项(如“搜索特定商品名称”),算法会优先保留匹配内容。" ++ }, ++ "switch_tab_id": { ++ "type": "string", ++ "description": "可选的标签页 ID。如果提供,将先切换到该标签页再进行扫描。" ++ } ++ } ++ } ++ } ++ }, ++ { ++ "type": "function", ++ "function": { ++ "name": "file_read", ++ "description": "读取文件内容。支持分页读取以处理大文件,默认每页 100 行并带有行号,方便 file_patch 定位。", ++ "parameters": { ++ "type": "object", ++ "properties": { ++ "path": { ++ "type": "string", ++ "description": "文件路径。" ++ }, ++ "start": { ++ "type": "integer", ++ "default": 1, ++ "description": "起始行号(从 1 开始)。" ++ }, ++ "count": { ++ "type": "integer", ++ "default": 100, ++ "description": "读取的行数。" ++ }, ++ "show_linenos": { ++ "type": "boolean", ++ "default": true, ++ "description": "是否显示行号。" ++ } ++ }, ++ "required": [ ++ "path" ++ ] ++ } ++ } ++ }, ++ { ++ "type": "function", ++ "function": { ++ "name": "file_patch", ++ "description": "对文件进行精细的局部修改。通过寻找唯一的旧文本块并替换为新文本。注意:必须确保 old_content 在文件中是唯一的,且空格、缩进、换行必须与原文件完全一致。如果替换失败,请先用 file_read 确认文件内容。", ++ "parameters": { ++ "type": "object", ++ "properties": { ++ "path": { ++ "type": "string", ++ "description": "目标文件路径。" ++ }, ++ "old_content": { ++ "type": "string", ++ "description": "要被替换的原始代码块(需确保唯一性)。" ++ }, ++ "new_content": { ++ "type": "string", ++ "description": "替换后的新代码块。" ++ } ++ }, ++ "required": [ ++ "path", ++ "old_content", ++ "new_content" ++ ] ++ } ++ } ++ }, ++ { ++ "type": "function", ++ "function": { ++ "name": "file_write", ++ "description": "用于对整个文件进行覆盖写入或追加。主要用于创建新文件或处理文件的大量变更。具体写入的内容必须以代码块(```)的形式包含在回复的消息体中。", ++ "parameters": { ++ "type": "object", ++ "properties": { ++ "path": { ++ "type": "string", ++ "description": "目标文件路径。" ++ }, ++ "mode": { ++ "type": "string", ++ "enum": [ ++ "overwrite", ++ "append" ++ ], ++ "default": "overwrite", ++ "description": "写入模式:overwrite(覆盖)或 append(追加)。" ++ } ++ }, ++ "required": [ ++ "path" ++ ] ++ } ++ } ++ }, ++ { ++ "type": "function", ++ "function": { ++ "name": "update_plan", ++ "description": "同步宏观任务进度与战略重心。仅在涉及多步逻辑的初始拆解或发生重大方针变更(原方案不可行)时调用。严禁用于记录细微的调试步骤。简单任务无需使用。", ++ "parameters": { ++ "type": "object", ++ "properties": { ++ "plan": { ++ "type": "string", ++ "description": "更新后的宏观执行计划。" ++ }, ++ "focus": { ++ "type": "string", ++ "description": "当前阶段的战略重心。" ++ } ++ } ++ } ++ } ++ }, ++ { ++ "type": "function", ++ "function": { ++ "name": "ask_user", ++ "description": "当遇到无法自动决策、需要用户授权、需要用户提供私密信息或在关键节点需要确认时调用。调用后系统会暂停并等待人工介入。", ++ "parameters": { ++ "type": "object", ++ "properties": { ++ "question": { ++ "type": "string", ++ "description": "向用户提出的问题或请求。" ++ }, ++ "candidates": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "提供给用户的可选快捷选项。" ++ } ++ }, ++ "required": [ ++ "question" ++ ] ++ } ++ } ++ } ++] +\ No newline at end of file +diff --git a/web_tools.py b/web_tools.py +new file mode 100644 +index 0000000..ec591cc +--- /dev/null ++++ b/web_tools.py +@@ -0,0 +1,75 @@ ++import sys, os, re ++import pyperclip ++import json, time ++import subprocess ++import tempfile ++sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) ++ ++from simphtml import get_main_block, start_temp_monitor, get_temp_texts, find_changed_elements, optimize_html_for_tokens ++from simphtml import js_findMainContent, js_findMainList ++from bs4 import BeautifulSoup ++ ++def get_html(driver, cutlist=False, maxchars=28000, instruction=""): ++ page = get_main_block(driver) ++ soup = optimize_html_for_tokens(page) ++ html = str(soup) ++ if not cutlist or len(html) <= maxchars: return html ++ rr = driver.execute_js(js_findMainList + js_findMainContent + """ ++ return findMainList(findMainContent(document.body));""") ++ sel = rr.get("selector", None) ++ if not sel: return html[:maxchars] ++ s = BeautifulSoup(str(soup), "html.parser"); items = s.select(sel) ++ hit = [it for it in items if instruction and instruction.strip() and instruction in it.get_text(" ",strip=True)] ++ keep = hit[:6] if hit else items[:3] ++ for it in items: ++ if it not in keep: it.decompose() ++ s = optimize_html_for_tokens(s) ++ return str(s)[:maxchars] ++ ++def execute_js_rich(script, driver): ++ start_temp_monitor(driver) ++ curr_session = driver.default_session_id ++ last_html = get_html(driver) ++ result = None; error_msg = None ++ new_tab = False; reloaded = False ++ try: ++ print(f"⚡ Executing: {script[:250]} ...") ++ result = driver.execute_js(script, auto_switch_newtab=True) ++ if type(result) is dict and result.get('closed', 0) == 1: reloaded = True ++ time.sleep(2) ++ except Exception as e: ++ error = e.args[0] if e.args else str(e) ++ if isinstance(error, dict): error.pop('stack', None) ++ error_msg = str(error) ++ print(f"❌ Error: {error_msg}") ++ ++ transients = get_temp_texts(driver) ++ ++ if driver.default_session_id != curr_session: ++ curr_session = driver.latest_session_id ++ print('Session changed') ++ new_tab = True ++ ++ current_html = get_html(driver) ++ diff_summary = "无需对比 (报错)" ++ is_significant_change = False ++ if not error_msg: ++ diff_data = find_changed_elements(last_html, current_html) ++ change_count = diff_data.get('changed', 0) ++ diff_summary = f"DOM变化量: {change_count}" ++ if change_count < 5 and not transients and not new_tab: ++ diff_summary += " (页面几乎无静默变化)" ++ else: ++ is_significant_change = True ++ return { ++ "status": "failed" if error_msg else "success", ++ "js_return": result, ++ "error": error_msg, ++ "transients": transients, ++ "environment": { ++ "new_tab": new_tab, ++ "reloaded": reloaded ++ }, ++ "diff": diff_summary, ++ "suggestion": "" if is_significant_change else "页面无明显变化" ++ } diff --git a/sidercall.py b/sidercall.py index 44faa94..cb23370 100644 --- a/sidercall.py +++ b/sidercall.py @@ -56,18 +56,18 @@ class GeminiSession: return iter([full_text]) if stream else full_text class ClaudeSession: - def __init__(self, api_key, api_base, model="claude-opus", context_win=32000): + def __init__(self, api_key, api_base, model="claude-opus", context_win=24000): self.api_key, self.api_base, self.default_model, self.context_win = api_key, api_base.rstrip('/'), model, context_win self.raw_msgs, self.lock = [], threading.Lock() def _trim_messages(self, messages): total = sum(len(m['prompt'])//4 for m in messages) if total <= self.context_win: return messages - trimmed = [] + target, current, result = self.context_win * 0.9, 0, [] for msg in reversed(messages): - if sum(len(m['prompt'])//4 for m in trimmed) + len(msg['prompt'])//4 <= self.context_win * 0.9: - trimmed.insert(0, msg) + if (msg_len := len(msg['prompt'])//4) + current <= target: + result.append(msg); current += msg_len else: break - return trimmed if trimmed else messages[-2:] + return result[::-1] or messages[-2:] def raw_ask(self, messages, model=None, temperature=0.5, max_tokens=4096): model = model or self.default_model headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} @@ -315,9 +315,9 @@ class ToolClient: args = data.get('arguments') or data.get('args') or data.get('params') or data.get('parameters') if args is None: args = data if func_name: tool_calls = [MockToolCall(func_name, args)] - except json.JSONDecodeError: + except json.JSONDecodeError as e: print("[Warn] Failed to parse tool_use JSON:", json_str) - remaining_text += f"[Warning] JSON 解析失败,模型输出了无效的 JSON." + tool_calls = [MockToolCall('bad_json', {'msg': f'Failed to parse tool_use JSON: {str(e)}'})] except Exception as e: print("[Error] Exception during tool_use parsing:", str(e), data)