crabcode 0.1.6__py3-none-win_amd64.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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
crabcode_cli/banner.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""CrabCode terminal banner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
# Pixel crab with open claws, eye stalks, and three pairs of splayed legs.
|
|
10
|
+
_CRAB = (
|
|
11
|
+
" ▄ ▄ ▄ ▄\n"
|
|
12
|
+
" ▀█▄█▀ ▄ ▄ ▀█▄█▀\n"
|
|
13
|
+
" ▀█▄▄ █ █ ▄▄█▀\n"
|
|
14
|
+
" ▄▄█████████████▄▄\n"
|
|
15
|
+
" ▄█▀ █████████████ ▀█▄\n"
|
|
16
|
+
" ▀ ▄█▀▀█████████▀▀█▄ ▀\n"
|
|
17
|
+
" ▀▀ ▄█▀ ▀█▄ ▀▀"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# "CrabCode" in figlet slant font
|
|
21
|
+
_CRABCODE = r""" ______ __ ______ __
|
|
22
|
+
/ ____/________ _/ /_ / ____/___ ____/ /__
|
|
23
|
+
/ / / ___/ __ `/ __ \/ / / __ \/ __ / _ \
|
|
24
|
+
/ /___/ / / /_/ / /_/ / /___/ /_/ / /_/ / __/
|
|
25
|
+
\____/_/ \__,_/_.___/\____/\____/\__,_/\___/ """
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_banner(console: Console) -> None:
|
|
29
|
+
"""Print the CrabCode banner with pixel crab art on the left."""
|
|
30
|
+
grid = Table.grid(padding=(0, 3))
|
|
31
|
+
grid.add_column(no_wrap=True)
|
|
32
|
+
grid.add_column(no_wrap=True, vertical="middle")
|
|
33
|
+
grid.add_row(
|
|
34
|
+
Text(_CRAB, style="bold red"),
|
|
35
|
+
Text(_CRABCODE, style="bold cyan"),
|
|
36
|
+
)
|
|
37
|
+
console.print()
|
|
38
|
+
console.print(grid)
|
|
39
|
+
console.print()
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""CLI adapter for the shared native executor (no Desktop, Gateway or Node)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from crabcode_core.computer_use_mode import effective_computer_use_mode
|
|
16
|
+
from crabcode_core.logging_utils import get_logger
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
_MAX_RESPONSE = 64 * 1024 * 1024
|
|
20
|
+
_HEARTBEAT_SECONDS = 30
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ComputerUseError(RuntimeError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def state_directory() -> Path:
|
|
28
|
+
if sys.platform == "darwin":
|
|
29
|
+
base = Path.home() / "Library/Application Support"
|
|
30
|
+
elif sys.platform == "win32":
|
|
31
|
+
base = Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData/Local")
|
|
32
|
+
else:
|
|
33
|
+
base = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local/share")
|
|
34
|
+
return base / "CrabCode/ComputerUse"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def bundled_executable(directory: Path) -> Path:
|
|
38
|
+
if sys.platform == "darwin":
|
|
39
|
+
app = directory if directory.suffix == ".app" else directory / "Crab Computer Use.app"
|
|
40
|
+
return app / "Contents/MacOS/crabcode-computer-use-host"
|
|
41
|
+
return directory / ("crabcode-computer-use-host.exe" if sys.platform == "win32" else "crabcode-computer-use-host")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NativeClient:
|
|
45
|
+
def __init__(self, state_dir: Path | None = None) -> None:
|
|
46
|
+
self.state_dir = state_dir or state_directory()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def installed(self) -> bool:
|
|
50
|
+
return bundled_executable(self.state_dir).is_file()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def bundle(self) -> Path | None:
|
|
54
|
+
directory = Path(__file__).with_name("_native")
|
|
55
|
+
return directory if bundled_executable(directory).is_file() else None
|
|
56
|
+
|
|
57
|
+
async def start(self) -> None:
|
|
58
|
+
# Platform wheels carry the signed app/executable. The host installs it
|
|
59
|
+
# atomically in the per-user shared location and reuses the singleton.
|
|
60
|
+
executable = bundled_executable(self.bundle or self.state_dir)
|
|
61
|
+
if not executable.is_file():
|
|
62
|
+
raise ComputerUseError(
|
|
63
|
+
"当前安装包未包含 Computer Use 原生组件;请使用包含原生组件的 CrabCode 平台安装包。"
|
|
64
|
+
)
|
|
65
|
+
process = None
|
|
66
|
+
try:
|
|
67
|
+
process = await asyncio.create_subprocess_exec(
|
|
68
|
+
str(executable), "--install-and-start", "--state-dir", str(self.state_dir),
|
|
69
|
+
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE,
|
|
70
|
+
)
|
|
71
|
+
_, stderr = await asyncio.wait_for(process.communicate(), 25)
|
|
72
|
+
if process.returncode:
|
|
73
|
+
raise ComputerUseError(stderr.decode("utf-8", errors="replace").strip() or "Computer Use 组件启动失败")
|
|
74
|
+
except (OSError, asyncio.TimeoutError) as exc:
|
|
75
|
+
raise ComputerUseError("Computer Use 组件启动失败或超时;请检查 crabcode computer-use status。") from exc
|
|
76
|
+
finally:
|
|
77
|
+
if process is not None and process.returncode is None:
|
|
78
|
+
process.kill()
|
|
79
|
+
await process.wait()
|
|
80
|
+
|
|
81
|
+
def endpoint(self) -> dict[str, Any]:
|
|
82
|
+
try:
|
|
83
|
+
endpoint = json.loads((self.state_dir / "endpoint.json").read_text(encoding="utf-8"))
|
|
84
|
+
except (OSError, ValueError) as exc:
|
|
85
|
+
raise ComputerUseError("Computer Use 执行器未连接;请运行 crabcode computer-use status。") from exc
|
|
86
|
+
if not (
|
|
87
|
+
isinstance(endpoint, dict) and endpoint.get("protocol_version") == 1
|
|
88
|
+
and type(endpoint.get("port")) is int and 0 < endpoint["port"] <= 65535
|
|
89
|
+
and isinstance(endpoint.get("token"), str) and len(endpoint["token"]) == 64
|
|
90
|
+
and isinstance(endpoint.get("instance_id"), str) and endpoint["instance_id"]
|
|
91
|
+
):
|
|
92
|
+
raise ComputerUseError("Computer Use 组件版本不兼容,请更新组件。")
|
|
93
|
+
return endpoint
|
|
94
|
+
|
|
95
|
+
async def request(
|
|
96
|
+
self, method: str, params: dict[str, Any] | None = None,
|
|
97
|
+
*, instance_id: str | None = None, timeout: float = 95,
|
|
98
|
+
) -> Any:
|
|
99
|
+
endpoint = self.endpoint()
|
|
100
|
+
if instance_id is not None and endpoint["instance_id"] != instance_id:
|
|
101
|
+
raise ComputerUseError("Computer Use 执行器已重启,请重新检测并观察窗口。")
|
|
102
|
+
message = json.dumps({
|
|
103
|
+
"token": endpoint["token"], "instance_id": endpoint["instance_id"],
|
|
104
|
+
"method": method, "params": params or {},
|
|
105
|
+
"deadline_ms": int((time.time() + min(85, timeout)) * 1000),
|
|
106
|
+
}, ensure_ascii=False).encode("utf-8") + b"\n"
|
|
107
|
+
if len(message) > 1024 * 1024:
|
|
108
|
+
raise ComputerUseError("Computer Use 请求超过大小限制")
|
|
109
|
+
|
|
110
|
+
async def exchange() -> Any:
|
|
111
|
+
reader, writer = await asyncio.open_connection("127.0.0.1", endpoint["port"], limit=_MAX_RESPONSE)
|
|
112
|
+
try:
|
|
113
|
+
writer.write(message)
|
|
114
|
+
await writer.drain()
|
|
115
|
+
# Requests are never replayed: a missing reply cannot establish
|
|
116
|
+
# whether native input was already delivered.
|
|
117
|
+
try:
|
|
118
|
+
line = await reader.readuntil(b"\n")
|
|
119
|
+
except asyncio.IncompleteReadError as exc:
|
|
120
|
+
raise ComputerUseError("Computer Use 响应不完整;请先观察结果,不要直接重复输入。") from exc
|
|
121
|
+
except asyncio.LimitOverrunError as exc:
|
|
122
|
+
raise ComputerUseError("Computer Use 响应超过大小限制") from exc
|
|
123
|
+
try:
|
|
124
|
+
response = json.loads(line)
|
|
125
|
+
except (ValueError, UnicodeError) as exc:
|
|
126
|
+
raise ComputerUseError("Computer Use 响应无效;请先观察结果。") from exc
|
|
127
|
+
if not isinstance(response, dict) or response.get("ok") is not True:
|
|
128
|
+
error = response.get("error") if isinstance(response, dict) else None
|
|
129
|
+
raise ComputerUseError(str(error or "Computer Use 执行失败"))
|
|
130
|
+
return response.get("result")
|
|
131
|
+
finally:
|
|
132
|
+
writer.close()
|
|
133
|
+
with contextlib.suppress(OSError):
|
|
134
|
+
await writer.wait_closed()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
return await asyncio.wait_for(exchange(), timeout)
|
|
138
|
+
except asyncio.TimeoutError as exc:
|
|
139
|
+
raise ComputerUseError("Computer Use 执行器响应超时;请先观察结果,不要直接重复输入。") from exc
|
|
140
|
+
except OSError as exc:
|
|
141
|
+
raise ComputerUseError("Computer Use 执行器连接中断;请重新检测并观察结果。") from exc
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class LocalComputerUse:
|
|
145
|
+
"""The CoreSession backend contract, using the same IPC as the GUI clients."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, client: NativeClient | None = None) -> None:
|
|
148
|
+
self.client = client or NativeClient()
|
|
149
|
+
self.host_id = f"cli-{uuid4()}"
|
|
150
|
+
self.connection_id = str(uuid4())
|
|
151
|
+
self._caps: dict[str, Any] = {}
|
|
152
|
+
self._heartbeat: asyncio.Task[None] | None = None
|
|
153
|
+
self._closed = False
|
|
154
|
+
|
|
155
|
+
async def connect(self) -> None:
|
|
156
|
+
await self.refresh()
|
|
157
|
+
self._heartbeat = asyncio.create_task(self._keep_alive())
|
|
158
|
+
|
|
159
|
+
async def refresh(self) -> None:
|
|
160
|
+
await self.client.start()
|
|
161
|
+
caps = await self.client.request("capabilities", timeout=25)
|
|
162
|
+
if not isinstance(caps, dict) or not caps.get("instance_id") or caps.get("host_protocol_version") != 1:
|
|
163
|
+
raise ComputerUseError("Computer Use 组件版本不兼容,请更新组件。")
|
|
164
|
+
if self._caps.get("instance_id") != caps["instance_id"]:
|
|
165
|
+
self.connection_id = str(uuid4())
|
|
166
|
+
self._caps = caps
|
|
167
|
+
|
|
168
|
+
def capabilities(self, host_id: str | None) -> dict[str, Any]:
|
|
169
|
+
return dict(self._caps) if not self._closed and host_id == self.host_id else {}
|
|
170
|
+
|
|
171
|
+
def is_available(self, host_id: str | None, mode: str | None = None) -> bool:
|
|
172
|
+
caps = self.capabilities(host_id)
|
|
173
|
+
return bool(caps.get("gui_available") and (mode is None or mode in caps.get("supported_modes", [])))
|
|
174
|
+
|
|
175
|
+
async def execute(
|
|
176
|
+
self, host_id: str, *, session_id: str, agent_id: str | None,
|
|
177
|
+
action: dict[str, Any], mode: str | None = None, target_scope: str | None = None,
|
|
178
|
+
delivery_policy: str = "strict_background",
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
scope = target_scope or ("desktop" if mode == "foreground_desktop" else "app_window")
|
|
181
|
+
expected_mode = "foreground_desktop" if scope == "desktop" else "background_app"
|
|
182
|
+
if scope not in ("desktop", "app_window") or (mode is not None and mode != expected_mode):
|
|
183
|
+
raise ComputerUseError("Invalid Computer Use mode or target scope")
|
|
184
|
+
if delivery_policy not in ("strict_background", "allow_foreground"):
|
|
185
|
+
raise ComputerUseError("Invalid Computer Use delivery policy")
|
|
186
|
+
if scope == "desktop" and delivery_policy == "strict_background":
|
|
187
|
+
raise ComputerUseError("Desktop scope requires allow_foreground")
|
|
188
|
+
if not session_id or not self.is_available(host_id, expected_mode):
|
|
189
|
+
raise ComputerUseError("Computer Use is unavailable for this session or mode")
|
|
190
|
+
if action.get("action") not in ("observe", "list_windows", "list_displays", "wait") and self._caps.get("delivery_policy_version") != 1:
|
|
191
|
+
raise ComputerUseError("Computer Use host must be upgraded to enforce the delivery policy")
|
|
192
|
+
owner = {"host_id": self.host_id, "connection_id": self.connection_id,
|
|
193
|
+
"session_id": session_id, "agent_id": agent_id}
|
|
194
|
+
request = {"mode": expected_mode, "target_scope": scope,
|
|
195
|
+
"delivery_policy": delivery_policy, "action": action}
|
|
196
|
+
if self._caps.get("ax_protocol_version") == 1:
|
|
197
|
+
request["owner"] = owner
|
|
198
|
+
instance = self._caps["instance_id"]
|
|
199
|
+
result = await self.client.request("execute", {
|
|
200
|
+
"instanceId": instance, "lease": owner, "request": request,
|
|
201
|
+
}, instance_id=instance)
|
|
202
|
+
if not isinstance(result, dict):
|
|
203
|
+
raise ComputerUseError("Computer Use 执行器返回了无效结果")
|
|
204
|
+
# CLI has no monitor; monitor-only frames must not enter model/history.
|
|
205
|
+
result.pop("preview_screenshot", None)
|
|
206
|
+
result.pop("preview_screenshot_error", None)
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
async def release(
|
|
210
|
+
self, host_id: str, *, session_id: str | None = None,
|
|
211
|
+
agent_id: str | None = None, all_agents: bool = False,
|
|
212
|
+
) -> None:
|
|
213
|
+
if host_id != self.host_id or not self._caps:
|
|
214
|
+
return
|
|
215
|
+
await self.client.request("release", {
|
|
216
|
+
"hostId": self.host_id, "connectionId": self.connection_id,
|
|
217
|
+
"sessionId": session_id, "agentId": agent_id, "allAgents": all_agents,
|
|
218
|
+
}, instance_id=self._caps["instance_id"], timeout=5)
|
|
219
|
+
|
|
220
|
+
async def _keep_alive(self) -> None:
|
|
221
|
+
while True:
|
|
222
|
+
await asyncio.sleep(_HEARTBEAT_SECONDS)
|
|
223
|
+
try:
|
|
224
|
+
await self.client.request("heartbeat", {
|
|
225
|
+
"hostId": self.host_id, "connectionId": self.connection_id,
|
|
226
|
+
}, instance_id=self._caps.get("instance_id"), timeout=5)
|
|
227
|
+
except ComputerUseError:
|
|
228
|
+
self._caps["gui_available"] = False
|
|
229
|
+
try:
|
|
230
|
+
# Also recover a stopped executor and pick up permission changes.
|
|
231
|
+
# Only capabilities are refreshed; input is never retried.
|
|
232
|
+
await self.refresh()
|
|
233
|
+
except ComputerUseError:
|
|
234
|
+
self._caps["gui_available"] = False
|
|
235
|
+
|
|
236
|
+
async def close(self) -> None:
|
|
237
|
+
self._closed = True
|
|
238
|
+
if self._heartbeat is not None:
|
|
239
|
+
self._heartbeat.cancel()
|
|
240
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
241
|
+
await self._heartbeat
|
|
242
|
+
self._heartbeat = None
|
|
243
|
+
with contextlib.suppress(ComputerUseError):
|
|
244
|
+
await self.release(self.host_id, all_agents=True)
|
|
245
|
+
self._caps = {}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def connect_session(session: Any) -> LocalComputerUse | None:
|
|
249
|
+
"""Install the bundled component or reuse the shared one, without a launch flag."""
|
|
250
|
+
backend = LocalComputerUse()
|
|
251
|
+
if not backend.client.installed and backend.client.bundle is None:
|
|
252
|
+
return None
|
|
253
|
+
try:
|
|
254
|
+
await backend.connect()
|
|
255
|
+
except ComputerUseError as exc:
|
|
256
|
+
logger.warning("Computer Use unavailable: %s", exc)
|
|
257
|
+
await backend.close()
|
|
258
|
+
return None
|
|
259
|
+
session.computer_use_backend = backend
|
|
260
|
+
session.computer_use_host_id = backend.host_id
|
|
261
|
+
session.computer_use_enabled = True
|
|
262
|
+
return backend
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def session_status(session: Any) -> str:
|
|
266
|
+
"""Compact runtime state for /status; startup itself stays quiet."""
|
|
267
|
+
backend = getattr(session, "computer_use_backend", None)
|
|
268
|
+
host_id = getattr(session, "computer_use_host_id", None)
|
|
269
|
+
if backend is None:
|
|
270
|
+
client = NativeClient()
|
|
271
|
+
return "unavailable (run crabcode computer-use status)" if client.installed or client.bundle else "not installed"
|
|
272
|
+
if not getattr(session, "computer_use_enabled", False):
|
|
273
|
+
return "disabled"
|
|
274
|
+
caps = backend.capabilities(host_id)
|
|
275
|
+
mode = effective_computer_use_mode(session)
|
|
276
|
+
if not caps.get("gui_available"):
|
|
277
|
+
state = "unavailable"
|
|
278
|
+
elif not backend.is_available(host_id, mode):
|
|
279
|
+
state = f"unavailable in {mode}"
|
|
280
|
+
else:
|
|
281
|
+
state = "ready" if caps.get("input_available") else "limited"
|
|
282
|
+
details = " · ".join(
|
|
283
|
+
f"{label}={'yes' if caps.get(key) else 'no'}"
|
|
284
|
+
for key, label in (("capture_available", "capture"), ("input_available", "input"), ("ax_available", "AX"))
|
|
285
|
+
)
|
|
286
|
+
return f"{state} · {details}"
|