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.
Files changed (60) hide show
  1. claxcode/__init__.py +3 -0
  2. claxcode/api/__init__.py +1 -0
  3. claxcode/api/chat.py +51 -0
  4. claxcode/api/session.py +66 -0
  5. claxcode/api/subagents.py +82 -0
  6. claxcode/api/tools.py +38 -0
  7. claxcode/cli.py +1214 -0
  8. claxcode/cli_completion.py +174 -0
  9. claxcode/config.py +54 -0
  10. claxcode/core/__init__.py +1 -0
  11. claxcode/core/agent.py +327 -0
  12. claxcode/core/checkpoint.py +100 -0
  13. claxcode/core/context.py +114 -0
  14. claxcode/core/models.py +446 -0
  15. claxcode/extensions/__init__.py +1 -0
  16. claxcode/extensions/hooks.py +131 -0
  17. claxcode/extensions/mcp.py +13 -0
  18. claxcode/extensions/plugins.py +10 -0
  19. claxcode/extensions/skills.py +205 -0
  20. claxcode/main.py +55 -0
  21. claxcode/memory/__init__.py +23 -0
  22. claxcode/memory/auto_memory.py +68 -0
  23. claxcode/memory/long_term.py +196 -0
  24. claxcode/memory/project_context.py +20 -0
  25. claxcode/memory/project_memory.py +194 -0
  26. claxcode/memory/session_store.py +206 -0
  27. claxcode/models/__init__.py +1 -0
  28. claxcode/models/session.py +25 -0
  29. claxcode/models/subagent.py +25 -0
  30. claxcode/models/tool.py +24 -0
  31. claxcode/security/__init__.py +1 -0
  32. claxcode/security/audit.py +54 -0
  33. claxcode/security/permissions.py +29 -0
  34. claxcode/skills/bug-fix/SKILL.md +33 -0
  35. claxcode/skills/code-review/SKILL.md +35 -0
  36. claxcode/skills/python-refactor/SKILL.md +22 -0
  37. claxcode/skills/task-workflow/SKILL.md +38 -0
  38. claxcode/subagents/__init__.py +1 -0
  39. claxcode/subagents/base.py +108 -0
  40. claxcode/subagents/executor.py +16 -0
  41. claxcode/subagents/explore.py +25 -0
  42. claxcode/subagents/planner.py +25 -0
  43. claxcode/tools/__init__.py +14 -0
  44. claxcode/tools/admin.py +170 -0
  45. claxcode/tools/base.py +19 -0
  46. claxcode/tools/browser.py +499 -0
  47. claxcode/tools/code_intelligence.py +68 -0
  48. claxcode/tools/custom_example.py +76 -0
  49. claxcode/tools/execution.py +114 -0
  50. claxcode/tools/file_operations.py +287 -0
  51. claxcode/tools/memory_tool.py +29 -0
  52. claxcode/tools/registry.py +35 -0
  53. claxcode/tools/search.py +157 -0
  54. claxcode/tools/ssh.py +534 -0
  55. claxcode/tools/web.py +83 -0
  56. claxcode-0.1.1.dist-info/METADATA +359 -0
  57. claxcode-0.1.1.dist-info/RECORD +60 -0
  58. claxcode-0.1.1.dist-info/WHEEL +4 -0
  59. claxcode-0.1.1.dist-info/entry_points.txt +3 -0
  60. 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}"