claxcode 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claxcode/__init__.py +3 -0
- claxcode/api/__init__.py +1 -0
- claxcode/api/chat.py +51 -0
- claxcode/api/session.py +66 -0
- claxcode/api/subagents.py +82 -0
- claxcode/api/tools.py +38 -0
- claxcode/cli.py +1214 -0
- claxcode/cli_completion.py +174 -0
- claxcode/config.py +54 -0
- claxcode/core/__init__.py +1 -0
- claxcode/core/agent.py +327 -0
- claxcode/core/checkpoint.py +100 -0
- claxcode/core/context.py +114 -0
- claxcode/core/models.py +446 -0
- claxcode/extensions/__init__.py +1 -0
- claxcode/extensions/hooks.py +131 -0
- claxcode/extensions/mcp.py +13 -0
- claxcode/extensions/plugins.py +10 -0
- claxcode/extensions/skills.py +205 -0
- claxcode/main.py +55 -0
- claxcode/memory/__init__.py +23 -0
- claxcode/memory/auto_memory.py +68 -0
- claxcode/memory/long_term.py +196 -0
- claxcode/memory/project_context.py +20 -0
- claxcode/memory/project_memory.py +194 -0
- claxcode/memory/session_store.py +206 -0
- claxcode/models/__init__.py +1 -0
- claxcode/models/session.py +25 -0
- claxcode/models/subagent.py +25 -0
- claxcode/models/tool.py +24 -0
- claxcode/security/__init__.py +1 -0
- claxcode/security/audit.py +54 -0
- claxcode/security/permissions.py +29 -0
- claxcode/skills/bug-fix/SKILL.md +33 -0
- claxcode/skills/code-review/SKILL.md +35 -0
- claxcode/skills/python-refactor/SKILL.md +22 -0
- claxcode/skills/task-workflow/SKILL.md +38 -0
- claxcode/subagents/__init__.py +1 -0
- claxcode/subagents/base.py +108 -0
- claxcode/subagents/executor.py +16 -0
- claxcode/subagents/explore.py +25 -0
- claxcode/subagents/planner.py +25 -0
- claxcode/tools/__init__.py +14 -0
- claxcode/tools/admin.py +170 -0
- claxcode/tools/base.py +19 -0
- claxcode/tools/browser.py +499 -0
- claxcode/tools/code_intelligence.py +68 -0
- claxcode/tools/custom_example.py +76 -0
- claxcode/tools/execution.py +114 -0
- claxcode/tools/file_operations.py +287 -0
- claxcode/tools/memory_tool.py +29 -0
- claxcode/tools/registry.py +35 -0
- claxcode/tools/search.py +157 -0
- claxcode/tools/ssh.py +534 -0
- claxcode/tools/web.py +83 -0
- claxcode-0.1.1.dist-info/METADATA +359 -0
- claxcode-0.1.1.dist-info/RECORD +60 -0
- claxcode-0.1.1.dist-info/WHEEL +4 -0
- claxcode-0.1.1.dist-info/entry_points.txt +3 -0
- claxcode-0.1.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""Playwright browser automation tools with a persistent session.
|
|
2
|
+
|
|
3
|
+
Requires: pip install playwright && playwright install chromium
|
|
4
|
+
|
|
5
|
+
Architecture: all Playwright objects live in a single dedicated asyncio
|
|
6
|
+
task (actor pattern). Tool calls from any task — the CLI loop, API SSE
|
|
7
|
+
handlers, subagents — only post commands to a queue and await results.
|
|
8
|
+
Playwright's async objects are bound to the task that created them, so
|
|
9
|
+
this actor is what makes the browser session continuous across tool
|
|
10
|
+
calls, across request tasks, and across conversation turns.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import base64
|
|
17
|
+
import time
|
|
18
|
+
from contextlib import suppress
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Literal
|
|
21
|
+
|
|
22
|
+
from claxcode.config import settings
|
|
23
|
+
from claxcode.tools.base import Tool
|
|
24
|
+
from claxcode.tools.registry import tool
|
|
25
|
+
|
|
26
|
+
# Lazy import — fail at runtime only if not installed
|
|
27
|
+
try:
|
|
28
|
+
from playwright.async_api import Browser, BrowserContext, Page, async_playwright
|
|
29
|
+
except ImportError:
|
|
30
|
+
async_playwright = None # type: ignore[assignment]
|
|
31
|
+
|
|
32
|
+
WaitUntil = Literal["commit", "domcontentloaded", "load", "networkidle"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BrowserSession:
|
|
36
|
+
"""A persistent browser session owned by a single actor task."""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self._queue: asyncio.Queue[
|
|
40
|
+
tuple[str, dict[str, Any], asyncio.Future[Any]]
|
|
41
|
+
] = asyncio.Queue()
|
|
42
|
+
self._task: asyncio.Task[Any] | None = None
|
|
43
|
+
self._playwright: Any = None
|
|
44
|
+
self._browser: Browser | None = None
|
|
45
|
+
self._context: BrowserContext | None = None
|
|
46
|
+
self._page: Page | None = None
|
|
47
|
+
self._headless = settings.browser_headless
|
|
48
|
+
self._proxy: str | None = None
|
|
49
|
+
|
|
50
|
+
async def run_command(self, action: str, **params: Any) -> str:
|
|
51
|
+
"""Post a command to the actor task and await its result."""
|
|
52
|
+
if self._task is None or self._task.done():
|
|
53
|
+
self._task = asyncio.create_task(self._actor_loop())
|
|
54
|
+
future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
|
|
55
|
+
await self._queue.put((action, params, future))
|
|
56
|
+
result: str = await future
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
# ── actor ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
async def _actor_loop(self) -> None:
|
|
62
|
+
try:
|
|
63
|
+
while True:
|
|
64
|
+
action, params, future = await self._queue.get()
|
|
65
|
+
if action == "shutdown":
|
|
66
|
+
await self._teardown()
|
|
67
|
+
if not future.done():
|
|
68
|
+
future.set_result("🛑 浏览器已关闭")
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
result = await self._dispatch(action, params)
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
if not future.done():
|
|
74
|
+
future.set_exception(exc)
|
|
75
|
+
else:
|
|
76
|
+
if not future.done():
|
|
77
|
+
future.set_result(result)
|
|
78
|
+
finally:
|
|
79
|
+
with suppress(Exception):
|
|
80
|
+
await self._teardown()
|
|
81
|
+
|
|
82
|
+
async def _dispatch(self, action: str, params: dict[str, Any]) -> Any:
|
|
83
|
+
handler = getattr(self, f"_do_{action}", None)
|
|
84
|
+
if handler is None:
|
|
85
|
+
raise ValueError(f"未知的浏览器操作: {action}")
|
|
86
|
+
return await handler(**params)
|
|
87
|
+
|
|
88
|
+
# ── lifecycle (actor task only) ──────────────────────────────────
|
|
89
|
+
|
|
90
|
+
async def _ensure_browser(self, headless: bool | None = None, proxy: str | None = None) -> None:
|
|
91
|
+
if headless is not None and self._browser is not None and headless != self._headless:
|
|
92
|
+
await self._teardown() # 切换有头/无头需要重启浏览器
|
|
93
|
+
if headless is not None:
|
|
94
|
+
self._headless = headless
|
|
95
|
+
if proxy is not None:
|
|
96
|
+
self._proxy = proxy
|
|
97
|
+
if self._browser is not None and self._browser.is_connected():
|
|
98
|
+
return
|
|
99
|
+
if async_playwright is None:
|
|
100
|
+
raise RuntimeError(
|
|
101
|
+
"Playwright 未安装。请运行: pip install playwright && playwright install chromium"
|
|
102
|
+
)
|
|
103
|
+
self._playwright = await async_playwright().start()
|
|
104
|
+
launch_kwargs: dict[str, Any] = {"headless": self._headless}
|
|
105
|
+
if self._proxy:
|
|
106
|
+
launch_kwargs["proxy"] = {"server": self._proxy}
|
|
107
|
+
try:
|
|
108
|
+
self._browser = await self._playwright.chromium.launch(**launch_kwargs)
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
if not self._headless or "Executable doesn't exist" not in str(exc):
|
|
111
|
+
raise
|
|
112
|
+
# headless shell 缺失时回退到完整版 chromium
|
|
113
|
+
self._browser = await self._playwright.chromium.launch(
|
|
114
|
+
channel="chromium", **launch_kwargs
|
|
115
|
+
)
|
|
116
|
+
self._context = None
|
|
117
|
+
self._page = None
|
|
118
|
+
|
|
119
|
+
async def _ensure_page(self) -> Page:
|
|
120
|
+
await self._ensure_browser()
|
|
121
|
+
if self._page is not None and not self._page.is_closed():
|
|
122
|
+
return self._page
|
|
123
|
+
if self._context is None:
|
|
124
|
+
assert self._browser is not None
|
|
125
|
+
self._context = await self._browser.new_context()
|
|
126
|
+
for page in reversed(self._context.pages):
|
|
127
|
+
if not page.is_closed():
|
|
128
|
+
self._page = page
|
|
129
|
+
return page
|
|
130
|
+
self._page = await self._context.new_page()
|
|
131
|
+
return self._page
|
|
132
|
+
|
|
133
|
+
async def _teardown(self) -> None:
|
|
134
|
+
with suppress(Exception):
|
|
135
|
+
if self._context is not None:
|
|
136
|
+
await self._context.close()
|
|
137
|
+
with suppress(Exception):
|
|
138
|
+
if self._browser is not None and self._browser.is_connected():
|
|
139
|
+
await self._browser.close()
|
|
140
|
+
with suppress(Exception):
|
|
141
|
+
if self._playwright is not None:
|
|
142
|
+
await self._playwright.stop()
|
|
143
|
+
self._playwright = None
|
|
144
|
+
self._browser = None
|
|
145
|
+
self._context = None
|
|
146
|
+
self._page = None
|
|
147
|
+
|
|
148
|
+
# ── commands (actor task only) ───────────────────────────────────
|
|
149
|
+
|
|
150
|
+
async def _do_navigate(
|
|
151
|
+
self, url: str, headless: bool | None = None, wait_until: WaitUntil = "load"
|
|
152
|
+
) -> str:
|
|
153
|
+
await self._ensure_browser(headless=headless)
|
|
154
|
+
page = await self._ensure_page()
|
|
155
|
+
await page.goto(url, wait_until=wait_until)
|
|
156
|
+
return f"✅ 已导航到 {url}\n 标题: {await page.title()}\n URL: {page.url}"
|
|
157
|
+
|
|
158
|
+
async def _do_click(self, selector: str, timeout: int = 5000) -> str:
|
|
159
|
+
page = await self._ensure_page()
|
|
160
|
+
await page.click(selector, timeout=timeout)
|
|
161
|
+
return f"🖱️ 已点击元素 '{selector}'"
|
|
162
|
+
|
|
163
|
+
async def _do_type(
|
|
164
|
+
self, selector: str, text: str, delay: int = 0, clear_first: bool = True
|
|
165
|
+
) -> str:
|
|
166
|
+
page = await self._ensure_page()
|
|
167
|
+
if clear_first:
|
|
168
|
+
await page.fill(selector, "")
|
|
169
|
+
await page.type(selector, text, delay=delay)
|
|
170
|
+
return f"⌨️ 已在 '{selector}' 中输入 '{text}'"
|
|
171
|
+
|
|
172
|
+
async def _do_screenshot(
|
|
173
|
+
self, path: str = "", full_page: bool = False, return_base64: bool = False
|
|
174
|
+
) -> str:
|
|
175
|
+
page = await self._ensure_page()
|
|
176
|
+
data = await page.screenshot(full_page=full_page, type="png")
|
|
177
|
+
if return_base64:
|
|
178
|
+
return f"📸 截图 (base64, {len(data)} 字节):\n{base64.b64encode(data).decode()}"
|
|
179
|
+
if not path:
|
|
180
|
+
path = f"screenshot_{int(time.time())}.png"
|
|
181
|
+
Path(path).write_bytes(data)
|
|
182
|
+
return f"📸 截图已保存: {path}"
|
|
183
|
+
|
|
184
|
+
async def _do_get_content(self, selector: str = "", max_length: int = 5000) -> str:
|
|
185
|
+
page = await self._ensure_page()
|
|
186
|
+
if selector:
|
|
187
|
+
el = await page.query_selector(selector)
|
|
188
|
+
if el is None:
|
|
189
|
+
return f"❌ 未找到元素 '{selector}'"
|
|
190
|
+
text = await el.inner_text()
|
|
191
|
+
else:
|
|
192
|
+
import re
|
|
193
|
+
|
|
194
|
+
text = re.sub(r"<[^>]+>", "", await page.content())
|
|
195
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
196
|
+
if len(text) > max_length:
|
|
197
|
+
text = text[:max_length] + "\n... [已截断]"
|
|
198
|
+
return text
|
|
199
|
+
|
|
200
|
+
async def _do_evaluate(self, script: str) -> str:
|
|
201
|
+
page = await self._ensure_page()
|
|
202
|
+
result = await page.evaluate(script)
|
|
203
|
+
return f"📜 JS 执行结果:\n{result}"
|
|
204
|
+
|
|
205
|
+
async def _do_wait(self, timeout: int = 2000, selector: str = "") -> str:
|
|
206
|
+
page = await self._ensure_page()
|
|
207
|
+
if selector:
|
|
208
|
+
await page.wait_for_selector(selector, timeout=timeout)
|
|
209
|
+
return f"⏳ 元素 '{selector}' 已就绪"
|
|
210
|
+
await page.wait_for_timeout(timeout)
|
|
211
|
+
return f"⏳ 已等待 {timeout}毫秒"
|
|
212
|
+
|
|
213
|
+
async def _do_list_pages(self) -> str:
|
|
214
|
+
if self._browser is None or not self._browser.is_connected():
|
|
215
|
+
return "ℹ️ 浏览器未打开"
|
|
216
|
+
pages = []
|
|
217
|
+
for ctx in self._browser.contexts:
|
|
218
|
+
for pg in ctx.pages:
|
|
219
|
+
pages.append(f" • {pg.url} (标题: {await pg.title()})")
|
|
220
|
+
if not pages:
|
|
221
|
+
return "ℹ️ 没有打开的页面"
|
|
222
|
+
return "📑 打开的页面:\n" + "\n".join(pages)
|
|
223
|
+
|
|
224
|
+
async def _do_close_page(self) -> str:
|
|
225
|
+
if self._page is not None and not self._page.is_closed():
|
|
226
|
+
await self._page.close()
|
|
227
|
+
self._page = None
|
|
228
|
+
return "📄 页面已关闭"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# 进程级共享会话:所有浏览器工具共用,跨调用与跨对话保持连续
|
|
232
|
+
_session = BrowserSession()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ── Tools(薄封装:投递命令到 actor,异常转为错误字符串) ────────────────
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@tool
|
|
239
|
+
class BrowserNavigate(Tool):
|
|
240
|
+
"""导航到指定 URL。"""
|
|
241
|
+
|
|
242
|
+
name = "browser_navigate"
|
|
243
|
+
description = "在浏览器中打开 URL。浏览器会话跨多次调用保持,可连续操作同一页面。"
|
|
244
|
+
input_schema = {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"properties": {
|
|
247
|
+
"url": {"type": "string", "description": "URL to navigate to"},
|
|
248
|
+
"headless": {
|
|
249
|
+
"type": "boolean",
|
|
250
|
+
"description": "有头/无头;不传则读取 BROWSER_HEADLESS 配置(默认 true)",
|
|
251
|
+
},
|
|
252
|
+
"wait_until": {
|
|
253
|
+
"type": "string",
|
|
254
|
+
"default": "load",
|
|
255
|
+
"enum": ["load", "domcontentloaded", "networkidle", "commit"],
|
|
256
|
+
"description": "When to consider navigation succeeded",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
"required": ["url"],
|
|
260
|
+
}
|
|
261
|
+
category = "browser"
|
|
262
|
+
required_permissions = ["web"]
|
|
263
|
+
|
|
264
|
+
async def execute(
|
|
265
|
+
self, url: str, headless: bool | None = None, wait_until: WaitUntil = "load", **kwargs: Any
|
|
266
|
+
) -> str:
|
|
267
|
+
try:
|
|
268
|
+
return await _session.run_command(
|
|
269
|
+
"navigate", url=url, headless=headless, wait_until=wait_until
|
|
270
|
+
)
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
return f"❌ 导航失败: {exc}"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@tool
|
|
276
|
+
class BrowserClick(Tool):
|
|
277
|
+
"""点击页面上的元素。"""
|
|
278
|
+
|
|
279
|
+
name = "browser_click"
|
|
280
|
+
description = "点击由 CSS 选择器标识的元素(作用于当前页面)。"
|
|
281
|
+
input_schema = {
|
|
282
|
+
"type": "object",
|
|
283
|
+
"properties": {
|
|
284
|
+
"selector": {"type": "string", "description": "CSS selector of the element to click"},
|
|
285
|
+
"timeout": {"type": "integer", "default": 5000, "description": "Wait timeout in ms"},
|
|
286
|
+
},
|
|
287
|
+
"required": ["selector"],
|
|
288
|
+
}
|
|
289
|
+
category = "browser"
|
|
290
|
+
required_permissions = ["web"]
|
|
291
|
+
|
|
292
|
+
async def execute(self, selector: str, timeout: int = 5000, **kwargs: Any) -> str:
|
|
293
|
+
try:
|
|
294
|
+
return await _session.run_command("click", selector=selector, timeout=timeout)
|
|
295
|
+
except Exception as exc:
|
|
296
|
+
return f"❌ 点击失败: {exc}"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@tool
|
|
300
|
+
class BrowserType(Tool):
|
|
301
|
+
"""在输入框中输入文本。"""
|
|
302
|
+
|
|
303
|
+
name = "browser_type"
|
|
304
|
+
description = "在输入字段中输入文本(作用于当前页面)。"
|
|
305
|
+
input_schema = {
|
|
306
|
+
"type": "object",
|
|
307
|
+
"properties": {
|
|
308
|
+
"selector": {"type": "string", "description": "CSS selector of the input element"},
|
|
309
|
+
"text": {"type": "string", "description": "Text to type"},
|
|
310
|
+
"delay": {"type": "integer", "default": 0, "description": "Keystroke delay in ms"},
|
|
311
|
+
"clear_first": {"type": "boolean", "default": True, "description": "Clear first"},
|
|
312
|
+
},
|
|
313
|
+
"required": ["selector", "text"],
|
|
314
|
+
}
|
|
315
|
+
category = "browser"
|
|
316
|
+
required_permissions = ["web"]
|
|
317
|
+
|
|
318
|
+
async def execute(
|
|
319
|
+
self, selector: str, text: str, delay: int = 0, clear_first: bool = True, **kwargs: Any
|
|
320
|
+
) -> str:
|
|
321
|
+
try:
|
|
322
|
+
return await _session.run_command(
|
|
323
|
+
"type", selector=selector, text=text, delay=delay, clear_first=clear_first
|
|
324
|
+
)
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
return f"❌ 输入失败: {exc}"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@tool
|
|
330
|
+
class BrowserScreenshot(Tool):
|
|
331
|
+
"""截取当前页面的屏幕截图。"""
|
|
332
|
+
|
|
333
|
+
name = "browser_screenshot"
|
|
334
|
+
description = "截取屏幕截图并保存到文件,或返回 base64 数据。"
|
|
335
|
+
input_schema = {
|
|
336
|
+
"type": "object",
|
|
337
|
+
"properties": {
|
|
338
|
+
"path": {
|
|
339
|
+
"type": "string",
|
|
340
|
+
"description": "File path to save the screenshot (e.g. screenshot.png)",
|
|
341
|
+
"default": "",
|
|
342
|
+
},
|
|
343
|
+
"full_page": {"type": "boolean", "default": False, "description": "Full-page capture"},
|
|
344
|
+
"return_base64": {
|
|
345
|
+
"type": "boolean",
|
|
346
|
+
"default": False,
|
|
347
|
+
"description": "Return base64-encoded PNG data instead of saving",
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
"required": [],
|
|
351
|
+
}
|
|
352
|
+
category = "browser"
|
|
353
|
+
required_permissions = ["web", "write"]
|
|
354
|
+
|
|
355
|
+
async def execute(
|
|
356
|
+
self, path: str = "", full_page: bool = False, return_base64: bool = False, **kwargs: Any
|
|
357
|
+
) -> str:
|
|
358
|
+
try:
|
|
359
|
+
return await _session.run_command(
|
|
360
|
+
"screenshot", path=path, full_page=full_page, return_base64=return_base64
|
|
361
|
+
)
|
|
362
|
+
except Exception as exc:
|
|
363
|
+
return f"❌ 截图失败: {exc}"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@tool
|
|
367
|
+
class BrowserGetContent(Tool):
|
|
368
|
+
"""获取页面或特定元素的文本内容。"""
|
|
369
|
+
|
|
370
|
+
name = "browser_get_content"
|
|
371
|
+
description = "获取页面或 CSS 选择元素的可见文本内容(作用于当前页面)。"
|
|
372
|
+
input_schema = {
|
|
373
|
+
"type": "object",
|
|
374
|
+
"properties": {
|
|
375
|
+
"selector": {
|
|
376
|
+
"type": "string",
|
|
377
|
+
"description": "CSS selector (empty = entire page body text)",
|
|
378
|
+
"default": "",
|
|
379
|
+
},
|
|
380
|
+
"max_length": {"type": "integer", "default": 5000, "description": "Max chars"},
|
|
381
|
+
},
|
|
382
|
+
"required": [],
|
|
383
|
+
}
|
|
384
|
+
category = "browser"
|
|
385
|
+
required_permissions = ["web"]
|
|
386
|
+
|
|
387
|
+
async def execute(self, selector: str = "", max_length: int = 5000, **kwargs: Any) -> str:
|
|
388
|
+
try:
|
|
389
|
+
return await _session.run_command(
|
|
390
|
+
"get_content", selector=selector, max_length=max_length
|
|
391
|
+
)
|
|
392
|
+
except Exception as exc:
|
|
393
|
+
return f"❌ 获取内容失败: {exc}"
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@tool
|
|
397
|
+
class BrowserEvaluate(Tool):
|
|
398
|
+
"""在浏览器页面中执行 JavaScript。"""
|
|
399
|
+
|
|
400
|
+
name = "browser_evaluate"
|
|
401
|
+
description = "在浏览器当前页面中运行 JavaScript 代码并返回结果。"
|
|
402
|
+
input_schema = {
|
|
403
|
+
"type": "object",
|
|
404
|
+
"properties": {
|
|
405
|
+
"script": {"type": "string", "description": "JavaScript code to execute"},
|
|
406
|
+
},
|
|
407
|
+
"required": ["script"],
|
|
408
|
+
}
|
|
409
|
+
category = "browser"
|
|
410
|
+
required_permissions = ["web"]
|
|
411
|
+
|
|
412
|
+
async def execute(self, script: str, **kwargs: Any) -> str:
|
|
413
|
+
try:
|
|
414
|
+
return await _session.run_command("evaluate", script=script)
|
|
415
|
+
except Exception as exc:
|
|
416
|
+
return f"❌ JS 执行失败: {exc}"
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@tool
|
|
420
|
+
class BrowserWait(Tool):
|
|
421
|
+
"""等待页面上的某个条件。"""
|
|
422
|
+
|
|
423
|
+
name = "browser_wait"
|
|
424
|
+
description = "等待一段时间,或等待某个 CSS 选择器出现在当前页面上。"
|
|
425
|
+
input_schema = {
|
|
426
|
+
"type": "object",
|
|
427
|
+
"properties": {
|
|
428
|
+
"timeout": {
|
|
429
|
+
"type": "integer",
|
|
430
|
+
"default": 2000,
|
|
431
|
+
"description": "Timeout in ms",
|
|
432
|
+
},
|
|
433
|
+
"selector": {
|
|
434
|
+
"type": "string",
|
|
435
|
+
"default": "",
|
|
436
|
+
"description": "CSS selector to wait for (empty = just sleep)",
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
"required": [],
|
|
440
|
+
}
|
|
441
|
+
category = "browser"
|
|
442
|
+
required_permissions = ["web"]
|
|
443
|
+
|
|
444
|
+
async def execute(self, timeout: int = 2000, selector: str = "", **kwargs: Any) -> str:
|
|
445
|
+
try:
|
|
446
|
+
return await _session.run_command("wait", timeout=timeout, selector=selector)
|
|
447
|
+
except Exception as exc:
|
|
448
|
+
return f"❌ 等待失败: {exc}"
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@tool
|
|
452
|
+
class BrowserClose(Tool):
|
|
453
|
+
"""关闭当前页面或整个浏览器。"""
|
|
454
|
+
|
|
455
|
+
name = "browser_close"
|
|
456
|
+
description = "关闭当前页面 ('page') 或整个浏览器会话 ('all')。"
|
|
457
|
+
input_schema = {
|
|
458
|
+
"type": "object",
|
|
459
|
+
"properties": {
|
|
460
|
+
"target": {
|
|
461
|
+
"type": "string",
|
|
462
|
+
"enum": ["page", "all"],
|
|
463
|
+
"default": "page",
|
|
464
|
+
"description": "'page' = close current tab, 'all' = close the whole browser",
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
"required": [],
|
|
468
|
+
}
|
|
469
|
+
category = "browser"
|
|
470
|
+
required_permissions = ["web"]
|
|
471
|
+
|
|
472
|
+
async def execute(self, target: str = "page", **kwargs: Any) -> str:
|
|
473
|
+
try:
|
|
474
|
+
if target == "all":
|
|
475
|
+
return await _session.run_command("shutdown")
|
|
476
|
+
return await _session.run_command("close_page")
|
|
477
|
+
except Exception as exc:
|
|
478
|
+
return f"❌ 关闭失败: {exc}"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@tool
|
|
482
|
+
class BrowserListPages(Tool):
|
|
483
|
+
"""列出所有打开的页面/标签。"""
|
|
484
|
+
|
|
485
|
+
name = "browser_list_pages"
|
|
486
|
+
description = "列出浏览器中所有打开的页面/标签。"
|
|
487
|
+
input_schema = {
|
|
488
|
+
"type": "object",
|
|
489
|
+
"properties": {},
|
|
490
|
+
"required": [],
|
|
491
|
+
}
|
|
492
|
+
category = "browser"
|
|
493
|
+
required_permissions = ["web"]
|
|
494
|
+
|
|
495
|
+
async def execute(self, **kwargs: Any) -> str:
|
|
496
|
+
try:
|
|
497
|
+
return await _session.run_command("list_pages")
|
|
498
|
+
except Exception as exc:
|
|
499
|
+
return f"❌ 获取页面列表失败: {exc}"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Code intelligence tools (stubs for Phase 2)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from claxcode.tools.base import Tool
|
|
7
|
+
from claxcode.tools.registry import tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@tool
|
|
11
|
+
class TypeCheck(Tool):
|
|
12
|
+
name = "type_check"
|
|
13
|
+
description = "Run a type checker (mypy) on the project or a file."
|
|
14
|
+
input_schema = {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"target": {"type": "string", "default": "."},
|
|
18
|
+
},
|
|
19
|
+
"required": [],
|
|
20
|
+
}
|
|
21
|
+
category = "code_intelligence"
|
|
22
|
+
required_permissions = ["execute"]
|
|
23
|
+
|
|
24
|
+
async def execute(self, target: str = ".", **kwargs: Any) -> str:
|
|
25
|
+
from claxcode.tools.execution import RunShell
|
|
26
|
+
return await RunShell().execute(command=f"mypy {target}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@tool
|
|
30
|
+
class FindDefinition(Tool):
|
|
31
|
+
name = "find_definition"
|
|
32
|
+
description = "Find the definition of a symbol in the codebase (basic text search)."
|
|
33
|
+
input_schema = {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"symbol": {"type": "string"},
|
|
37
|
+
"directory": {"type": "string", "default": "."},
|
|
38
|
+
},
|
|
39
|
+
"required": ["symbol"],
|
|
40
|
+
}
|
|
41
|
+
category = "code_intelligence"
|
|
42
|
+
required_permissions = ["read"]
|
|
43
|
+
|
|
44
|
+
async def execute(self, symbol: str, directory: str = ".", **kwargs: Any) -> str:
|
|
45
|
+
from claxcode.tools.search import GrepSearch
|
|
46
|
+
pattern = rf"(class|def|async def)\s+{re.escape(symbol)}\b"
|
|
47
|
+
return await GrepSearch().execute(pattern=pattern, directory=directory, output_mode="content")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@tool
|
|
51
|
+
class FindReferences(Tool):
|
|
52
|
+
name = "find_references"
|
|
53
|
+
description = "Find references to a symbol in the codebase."
|
|
54
|
+
input_schema = {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"symbol": {"type": "string"},
|
|
58
|
+
"directory": {"type": "string", "default": "."},
|
|
59
|
+
},
|
|
60
|
+
"required": ["symbol"],
|
|
61
|
+
}
|
|
62
|
+
category = "code_intelligence"
|
|
63
|
+
required_permissions = ["read"]
|
|
64
|
+
|
|
65
|
+
async def execute(self, symbol: str, directory: str = ".", **kwargs: Any) -> str:
|
|
66
|
+
from claxcode.tools.search import GrepSearch
|
|
67
|
+
pattern = rf"\b{re.escape(symbol)}\b"
|
|
68
|
+
return await GrepSearch().execute(pattern=pattern, directory=directory, output_mode="content")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Example: How to add a custom tool.
|
|
2
|
+
|
|
3
|
+
To make this tool available, import this module in claxcode/tools/__init__.py:
|
|
4
|
+
from claxcode.tools import custom_example
|
|
5
|
+
|
|
6
|
+
Or register it at runtime:
|
|
7
|
+
from claxcode.tools.registry import registry
|
|
8
|
+
registry.register(MyTool())
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import math
|
|
13
|
+
import operator
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from claxcode.tools.base import Tool
|
|
17
|
+
from claxcode.tools.registry import tool
|
|
18
|
+
|
|
19
|
+
_BIN_OPS = {
|
|
20
|
+
ast.Add: operator.add,
|
|
21
|
+
ast.Sub: operator.sub,
|
|
22
|
+
ast.Mult: operator.mul,
|
|
23
|
+
ast.Div: operator.truediv,
|
|
24
|
+
ast.FloorDiv: operator.floordiv,
|
|
25
|
+
ast.Mod: operator.mod,
|
|
26
|
+
ast.Pow: operator.pow,
|
|
27
|
+
}
|
|
28
|
+
_FUNC_NAMES = ("sqrt", "sin", "cos", "tan", "log", "log2", "log10", "exp", "floor", "ceil", "fabs")
|
|
29
|
+
_FUNCS = {name: getattr(math, name) for name in _FUNC_NAMES}
|
|
30
|
+
_CONSTANTS = {"pi": math.pi, "e": math.e, "tau": math.tau}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _safe_eval(expression: str) -> float:
|
|
34
|
+
"""Evaluate a math expression by walking the AST (no eval() involved)."""
|
|
35
|
+
return _eval_node(ast.parse(expression, mode="eval").body)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _eval_node(node: ast.AST) -> float:
|
|
39
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
|
40
|
+
return node.value
|
|
41
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _BIN_OPS:
|
|
42
|
+
return _BIN_OPS[type(node.op)](_eval_node(node.left), _eval_node(node.right))
|
|
43
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
|
|
44
|
+
value = _eval_node(node.operand)
|
|
45
|
+
return value if isinstance(node.op, ast.UAdd) else -value
|
|
46
|
+
if isinstance(node, ast.Name) and node.id in _CONSTANTS:
|
|
47
|
+
return _CONSTANTS[node.id]
|
|
48
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in _FUNCS:
|
|
49
|
+
return _FUNCS[node.func.id](*[_eval_node(arg) for arg in node.args])
|
|
50
|
+
raise ValueError("仅支持数字、四则运算和常用数学函数")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@tool
|
|
54
|
+
class Calculate(Tool):
|
|
55
|
+
"""一个简单的计算器工具,作为自定义扩展示例。"""
|
|
56
|
+
|
|
57
|
+
name = "calculate"
|
|
58
|
+
description = "安全地计算数学表达式(数字、四则运算、常用数学函数)。"
|
|
59
|
+
input_schema = {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"expression": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "要计算的数学表达式,例如 '2 + 2 * 3'",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
"required": ["expression"],
|
|
68
|
+
}
|
|
69
|
+
category = "math"
|
|
70
|
+
required_permissions = []
|
|
71
|
+
|
|
72
|
+
async def execute(self, expression: str, **kwargs: Any) -> str:
|
|
73
|
+
try:
|
|
74
|
+
return str(_safe_eval(expression))
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
return f"计算表达式时出错: {exc}"
|