monkeycode-sdk 1.7.0__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.
monkeycode/stream.py ADDED
@@ -0,0 +1,381 @@
1
+ """MonkeyCode 任务 WebSocket 会话流。
2
+
3
+ 基于 websockets 库,连接 /api/v1/users/tasks/stream?id=&mode=,
4
+ 发送 user-input 让 agent 执行,并读取下行事件。
5
+
6
+ 协议参考:前端 src/api/task-stream-client.ts 与桌面端实现。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import base64
12
+ import json
13
+ from typing import Any
14
+
15
+ import websockets
16
+
17
+ from monkeycode.models import TaskStreamChunk, UserInputPayload
18
+
19
+
20
+ def b64e(s: str | bytes) -> str:
21
+ """编码为 base64 字符串。"""
22
+ if isinstance(s, str):
23
+ s = s.encode("utf-8")
24
+ return base64.b64encode(s).decode("ascii")
25
+
26
+
27
+ def b64d(s: str) -> str:
28
+ """解码 base64 为字符串。"""
29
+ return base64.b64decode(s).decode("utf-8")
30
+
31
+
32
+ def _ws_url(base_url: str, path: str) -> str:
33
+ """将 HTTP(S) base_url 转为 ws(s) 前缀并拼接 API 路径。
34
+
35
+ 自托管部署时 base_url 传私有地址,协议自动转换。
36
+ """
37
+ if base_url.startswith("https://"):
38
+ ws_base = "wss://" + base_url[len("https://"):]
39
+ elif base_url.startswith("http://"):
40
+ ws_base = "ws://" + base_url[len("http://"):]
41
+ else:
42
+ ws_base = base_url
43
+ return f"{ws_base}{path}"
44
+
45
+
46
+ def _session_headers(client: Any) -> dict[str, str]:
47
+ """提取登录会话 Cookie 作为 WebSocket 请求头。
48
+
49
+ 同时兼容用户登录与团队登录两种会话 Cookie。
50
+ """
51
+ cookies = []
52
+ for name in ("monkeycode_ai_session", "monkeycode_ai_team_session"):
53
+ cookie = client._client.cookies.get(name)
54
+ if cookie:
55
+ cookies.append(f"{name}={cookie}")
56
+ if cookies:
57
+ return {"Cookie": "; ".join(cookies)}
58
+ return {}
59
+
60
+
61
+ def encode_user_input(
62
+ content: str, attachments: list[dict] | None = None
63
+ ) -> str:
64
+ """构造上行 user-input 消息(外层 JSON 字符串)。
65
+
66
+ 格式:{"type": "user-input", "data": "<base64(内层JSON)>"}
67
+ """
68
+ payload = UserInputPayload(content=content, attachments=attachments or [])
69
+ outer = {"type": "user-input", "data": b64e(payload.encode())}
70
+ return json.dumps(outer, ensure_ascii=False)
71
+
72
+
73
+ def decode_chunk(raw: str) -> TaskStreamChunk:
74
+ """解析服务器下行事件。data 可能是 base64 的 JSON 字符串。"""
75
+ try:
76
+ obj = json.loads(raw)
77
+ except json.JSONDecodeError:
78
+ return TaskStreamChunk(type="raw", data=raw, raw={"raw": raw})
79
+
80
+ ctype = str(obj.get("type", ""))
81
+ data = obj.get("data")
82
+ if isinstance(data, str):
83
+ try:
84
+ decoded = b64d(data)
85
+ try:
86
+ data = json.loads(decoded)
87
+ except json.JSONDecodeError:
88
+ data = decoded
89
+ except Exception:
90
+ pass
91
+
92
+ return TaskStreamChunk(
93
+ type=ctype,
94
+ data=data,
95
+ timestamp=int(obj.get("timestamp", 0) or 0),
96
+ turn_seq=int(obj.get("turn_seq", 0) or 0),
97
+ raw=obj,
98
+ )
99
+
100
+
101
+ class TaskStream:
102
+ """任务实时流(WebSocket 会话)。
103
+
104
+ 用法:
105
+ stream = TaskStream(client, task_id, mode="new")
106
+ await stream.connect()
107
+ await stream.send_user_input("继续")
108
+ chunk = await stream.recv(timeout=60)
109
+ await stream.close()
110
+ """
111
+
112
+ def __init__(self, client: Any, task_id: str, mode: str = "new") -> None:
113
+ self.client = client
114
+ self.task_id = task_id
115
+ self.mode = mode
116
+ self._ws: Any = None
117
+
118
+ def _url(self) -> str:
119
+ return _ws_url(
120
+ self.client.base_url,
121
+ f"/api/v1/users/tasks/stream?id={self.task_id}&mode={self.mode}",
122
+ )
123
+
124
+ async def connect(self) -> None:
125
+ """建立 WebSocket 连接(携带登录 Cookie)。"""
126
+ self._ws = await websockets.connect(
127
+ self._url(), additional_headers=_session_headers(self.client)
128
+ )
129
+
130
+ async def send_user_input(
131
+ self, content: str, attachments: list[dict] | None = None
132
+ ) -> None:
133
+ """发送用户文字/附件到任务会话。"""
134
+ msg = encode_user_input(content, attachments)
135
+ await self._ws.send(msg)
136
+
137
+ async def send_cancel(self) -> None:
138
+ """发送取消指令。"""
139
+ await self._ws.send(json.dumps({"type": "user-cancel", "data": ""}))
140
+
141
+ async def send_control(self, ctype: str, data: str = "") -> None:
142
+ """发送任意控制类型消息(如 reply-question / auto-approve)。"""
143
+ await self._ws.send(json.dumps({"type": ctype, "data": data}))
144
+
145
+ async def send_reply_question(
146
+ self, request_id: str, answers: list[str]
147
+ ) -> None:
148
+ """回复 AI 的提问(reply-question)。"""
149
+ inner = json.dumps(
150
+ {"request_id": request_id, "answers": answers}, ensure_ascii=False
151
+ )
152
+ await self.send_control("reply-question", b64e(inner))
153
+
154
+ async def send_auto_approve(self, enable: bool = True) -> None:
155
+ """开启/关闭自动批准(auto-approve / disable-auto-approve)。"""
156
+ ctype = "auto-approve" if enable else "disable-auto-approve"
157
+ await self.send_control(ctype, "")
158
+
159
+ async def recv(self, timeout: float | None = None) -> TaskStreamChunk:
160
+ """读取并解码一条事件。"""
161
+ raw = await asyncio.wait_for(self._ws.recv(), timeout=timeout)
162
+ return decode_chunk(raw)
163
+
164
+ async def close(self) -> None:
165
+ if self._ws is not None:
166
+ await self._ws.close()
167
+ self._ws = None
168
+
169
+ async def __aenter__(self) -> "TaskStream":
170
+ await self.connect()
171
+ return self
172
+
173
+ async def __aexit__(self, *exc: Any) -> None:
174
+ await self.close()
175
+
176
+
177
+ class ControlStream:
178
+ """任务控制通道(WebSocket)。
179
+
180
+ 用于同步调用类操作:switch_model / restart / 文件浏览 / 端口转发等。
181
+ 端点:/api/v1/users/tasks/control?id=<task_id>
182
+
183
+ 上行消息:
184
+ {"type": "call", "kind": "<kind>", "data": "<base64(JSON)>"}
185
+ 下行消息:
186
+ {"type": "call-response", "kind": "<kind>",
187
+ "data": "<base64(响应JSON)>", "timestamp": 0}
188
+
189
+ 注意:data 必须是 base64(JSON字符串),服务端 Go 的 []byte 字段
190
+ 会自动 base64 解码后再反序列化。
191
+ """
192
+
193
+ def __init__(self, client: Any, task_id: str) -> None:
194
+ self.client = client
195
+ self.task_id = task_id
196
+ self._ws: Any = None
197
+
198
+ def _url(self) -> str:
199
+ return _ws_url(
200
+ self.client.base_url,
201
+ f"/api/v1/users/tasks/control?id={self.task_id}",
202
+ )
203
+
204
+ async def connect(self) -> None:
205
+ """建立 WebSocket 连接(携带登录 Cookie)。"""
206
+ self._ws = await websockets.connect(
207
+ self._url(), additional_headers=_session_headers(self.client)
208
+ )
209
+
210
+ async def send_call(self, kind: str, data: dict[str, Any]) -> None:
211
+ """发送控制调用。data 会被 base64(JSON) 编码后放入外层 data。"""
212
+ inner = json.dumps(data, ensure_ascii=False)
213
+ msg = {"type": "call", "kind": kind, "data": b64e(inner)}
214
+ await self._ws.send(json.dumps(msg, ensure_ascii=False))
215
+
216
+ async def send_raw(self, ctype: str, data: str = "") -> None:
217
+ """发送任意类型控制消息(非 call 调用,如 sync-my-ip)。
218
+
219
+ data 为原始字符串,会原样放入外层 data 字段。
220
+ """
221
+ msg = {"type": ctype, "data": data}
222
+ await self._ws.send(json.dumps(msg, ensure_ascii=False))
223
+
224
+ async def recv(self, timeout: float | None = None) -> TaskStreamChunk:
225
+ """读取并解码一条控制通道事件。"""
226
+ raw = await asyncio.wait_for(self._ws.recv(), timeout=timeout)
227
+ return decode_chunk(raw)
228
+
229
+ async def close(self) -> None:
230
+ if self._ws is not None:
231
+ await self._ws.close()
232
+ self._ws = None
233
+
234
+ async def __aenter__(self) -> "ControlStream":
235
+ await self.connect()
236
+ return self
237
+
238
+ async def __aexit__(self, *exc: Any) -> None:
239
+ await self.close()
240
+
241
+
242
+ class TerminalStream:
243
+ """虚拟机终端流(WebSocket)。
244
+
245
+ 端点:/api/v1/users/hosts/vms/{vm_id}/terminals/connect
246
+
247
+ 上行消息:
248
+ {"type": "data", "data": "<base64(输入字节)>"}
249
+ {"type": "resize", "data": "<base64({\"col\":..,\"row\":..})>"}
250
+ 下行消息:
251
+ connected / data / resize / error / ping / pong
252
+ """
253
+
254
+ def __init__(
255
+ self,
256
+ client: Any,
257
+ vm_id: str,
258
+ terminal_id: str | None = None,
259
+ col: int = 80,
260
+ row: int = 24,
261
+ ) -> None:
262
+ self.client = client
263
+ self.vm_id = vm_id
264
+ self.terminal_id = terminal_id
265
+ self.col = col
266
+ self.row = row
267
+ self._ws: Any = None
268
+
269
+ def _url(self) -> str:
270
+ path = f"/api/v1/users/hosts/vms/{self.vm_id}/terminals/connect"
271
+ params = [f"col={self.col}", f"row={self.row}"]
272
+ if self.terminal_id:
273
+ params.append(f"terminal_id={self.terminal_id}")
274
+ return _ws_url(self.client.base_url, f"{path}?{'&'.join(params)}")
275
+
276
+ async def connect(self) -> None:
277
+ """建立 WebSocket 连接(携带登录 Cookie)。"""
278
+ self._ws = await websockets.connect(
279
+ self._url(), additional_headers=_session_headers(self.client)
280
+ )
281
+
282
+ async def send_data(self, data: bytes | str) -> None:
283
+ """发送终端输入(字符串或字节)。"""
284
+ if isinstance(data, str):
285
+ data = data.encode("utf-8")
286
+ msg = {"type": "data", "data": b64e(data)}
287
+ await self._ws.send(json.dumps(msg, ensure_ascii=False))
288
+
289
+ async def send_resize(self, col: int, row: int) -> None:
290
+ """调整终端尺寸。"""
291
+ inner = json.dumps({"col": col, "row": row})
292
+ msg = {"type": "resize", "data": b64e(inner)}
293
+ await self._ws.send(json.dumps(msg, ensure_ascii=False))
294
+
295
+ async def recv(self, timeout: float | None = None) -> TaskStreamChunk:
296
+ """读取并解码一条终端事件(data 自动 base64 解码)。"""
297
+ raw = await asyncio.wait_for(self._ws.recv(), timeout=timeout)
298
+ return decode_chunk(raw)
299
+
300
+ async def close(self) -> None:
301
+ if self._ws is not None:
302
+ await self._ws.close()
303
+ self._ws = None
304
+
305
+ async def __aenter__(self) -> "TerminalStream":
306
+ await self.connect()
307
+ return self
308
+
309
+ async def __aexit__(self, *exc: Any) -> None:
310
+ await self.close()
311
+
312
+
313
+ class JoinTerminalStream:
314
+ """加入共享终端流(WebSocket)。
315
+
316
+ 端点:/api/v1/users/hosts/vms/terminals/join
317
+ 需要分享密码(share_terminal 返回的 password)。
318
+
319
+ 上行/下行消息格式与 TerminalStream 一致(data / resize / 心跳)。
320
+ """
321
+
322
+ def __init__(
323
+ self,
324
+ client: Any,
325
+ terminal_id: str,
326
+ password: str,
327
+ col: int = 80,
328
+ row: int = 24,
329
+ ) -> None:
330
+ self.client = client
331
+ self.terminal_id = terminal_id
332
+ self.password = password
333
+ self.col = col
334
+ self.row = row
335
+ self._ws: Any = None
336
+
337
+ def _url(self) -> str:
338
+ path = "/api/v1/users/hosts/vms/terminals/join"
339
+ params = [
340
+ f"terminal_id={self.terminal_id}",
341
+ f"password={self.password}",
342
+ f"col={self.col}",
343
+ f"row={self.row}",
344
+ ]
345
+ return _ws_url(self.client.base_url, f"{path}?{'&'.join(params)}")
346
+
347
+ async def connect(self) -> None:
348
+ """建立 WebSocket 连接(携带登录 Cookie)。"""
349
+ self._ws = await websockets.connect(
350
+ self._url(), additional_headers=_session_headers(self.client)
351
+ )
352
+
353
+ async def send_data(self, data: bytes | str) -> None:
354
+ """发送终端输入(字符串或字节)。"""
355
+ if isinstance(data, str):
356
+ data = data.encode("utf-8")
357
+ msg = {"type": "data", "data": b64e(data)}
358
+ await self._ws.send(json.dumps(msg, ensure_ascii=False))
359
+
360
+ async def send_resize(self, col: int, row: int) -> None:
361
+ """调整终端尺寸。"""
362
+ inner = json.dumps({"col": col, "row": row})
363
+ msg = {"type": "resize", "data": b64e(inner)}
364
+ await self._ws.send(json.dumps(msg, ensure_ascii=False))
365
+
366
+ async def recv(self, timeout: float | None = None) -> TaskStreamChunk:
367
+ """读取并解码一条终端事件(data 自动 base64 解码)。"""
368
+ raw = await asyncio.wait_for(self._ws.recv(), timeout=timeout)
369
+ return decode_chunk(raw)
370
+
371
+ async def close(self) -> None:
372
+ if self._ws is not None:
373
+ await self._ws.close()
374
+ self._ws = None
375
+
376
+ async def __aenter__(self) -> "JoinTerminalStream":
377
+ await self.connect()
378
+ return self
379
+
380
+ async def __aexit__(self, *exc: Any) -> None:
381
+ await self.close()
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: monkeycode-sdk
3
+ Version: 1.7.0
4
+ Summary: MonkeyCode 云平台 REST API 的 Python SDK(认证、任务提交、结果轮询)
5
+ Author: cyqmq
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/cyqmq/monkeycode-sdk
8
+ Project-URL: Repository, https://github.com/cyqmq/monkeycode-sdk
9
+ Keywords: monkeycode,ai,coding,agent,sdk
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: websockets>=12
22
+ Provides-Extra: cli
23
+ Dynamic: license-file
24
+
25
+ # monkeycode-sdk
26
+
27
+ MonkeyCode 云平台 REST API 的 Python SDK。用于在任意 Python 项目/聊天框架(AstrBot、NoneBot 等)中:
28
+ 认证登录 → 创建 AI 编程任务 → 轮询结果,让机器人像人一样使用 MonkeyCode。
29
+
30
+ **特性**
31
+ - 密码登录 / 团队登录 / PoW 验证码全自动求解
32
+ - 会话持久化:Cookie 保存到文件,长期运行自动重登(401 自动恢复)
33
+ - 任务创建(附件、技能、默认档位)、列表、轮询、WebSocket 实时会话
34
+ - AgentSession 高层封装:断线自动重连 + 事件缓冲
35
+ - 任务控制通道:切换模型、重启、技能/插件、仓库文件 diff/changes 等同步调用
36
+ - 项目管理、Git 身份、Git Bots、钱包签到、文件/VM 管理、预览链接
37
+ - 虚拟机终端:会话管理(REST)+ 实时交互/共享加入(WebSocket)
38
+ - 用户与会话:whoami / logout / 团队会话列表
39
+ - 配置管理:模型/宿主机/VM/端口转发/Git 身份 CRUD 与健康检查
40
+ - 团队管理:用户/分组/宿主/镜像/模型/技能/MCP/通知/OAuth/扩展包完整控制面
41
+ - 分页迭代:`iter_tasks` / `iter_projects` / `paginate` 自动翻页
42
+ - 健壮性:只读请求自动重试(指数退避)、429 限流等待、错误提示友好化
43
+ - CLI:25+ 命令(任务/钱包/项目/终端/API Key/会话控制)
44
+ - 覆盖完整 REST API(镜像/MCP/通知/团队管理/License/社区等)
45
+ - 支持自托管部署
46
+
47
+ ## 安装
48
+
49
+ ```bash
50
+ pip install monkeycode-sdk
51
+ ```
52
+
53
+ 或从源码安装:
54
+
55
+ ```bash
56
+ git clone https://github.com/cyqmq/monkeycode-sdk.git
57
+ cd monkeycode-sdk
58
+ pip install -e .
59
+ ```
60
+
61
+ ## 快速开始
62
+
63
+ ```python
64
+ from monkeycode import MonkeyCodeClient
65
+ from monkeycode.models import RepoReq
66
+
67
+ client = MonkeyCodeClient(email="you@example.com", password="your_password")
68
+ client.login() # PoW 验证码自动求解
69
+
70
+ # 创建任务(默认档位;repo 可空)
71
+ task = client.create_task(
72
+ content="修复登录接口的异常处理",
73
+ repo=RepoReq(repo_url="https://github.com/org/repo", branch="main"),
74
+ skill_ids=["feature-design", "project-wiki"],
75
+ )
76
+
77
+ # 轮询结果
78
+ result = client.wait_task(task.id, timeout=1800, interval=10)
79
+ print(result.status, result.pr_url)
80
+ ```
81
+
82
+ ## 文档导航
83
+
84
+ | 文档 | 内容 |
85
+ |---|---|
86
+ | [docs/认证说明.md](docs/认证说明.md) | 密码/PoW/团队登录、自托管、安全提示 |
87
+ | [docs/功能参考.md](docs/功能参考.md) | 全部功能方法示例(任务/项目/Git/钱包/文件/预览/管理) |
88
+ | [docs/API.md](docs/API.md) | 全部公开方法索引表(205 个) |
89
+ | [docs/架构说明.md](docs/架构说明.md) | 模块划分、WebSocket 协议摘要、认证流程 |
90
+ | [docs/CLI.md](docs/CLI.md) | 命令行使用 |
91
+ | [docs/已知问题.md](docs/已知问题.md) | 平台侧已知问题、开发与参考 |
92
+
93
+ 完整变更见 [CHANGELOG.md](CHANGELOG.md)。
@@ -0,0 +1,14 @@
1
+ monkeycode/__init__.py,sha256=QYz3baeNWMkoJNj9WKoNJ0i3MJMN-i_A6apA2vhZHXk,1178
2
+ monkeycode/client.py,sha256=enLVQ-6EFX2jZP0Gld-_vul--F_kqebxFD7N4Fhl1jw,77664
3
+ monkeycode/consts.py,sha256=gcnkZeg5WodHhbSC61Tu6cxwGSLsS3Fow5EAOfE1U_M,582
4
+ monkeycode/errors.py,sha256=pwbWCEs_jRPuZVmQtvxQV9nYVdHWyIOBtnnr942MPTM,810
5
+ monkeycode/links.py,sha256=IbYGJLUBUMFXn6fnBNI2bMC4bMUy9A1f3L-GS5lMF2c,4139
6
+ monkeycode/models.py,sha256=YJOecnw0O7Ll_kVclFIy0UrdP6TicAg3pKZm9g7v6C4,5737
7
+ monkeycode/pow.py,sha256=60UtR3DsqEtAm0KhmLqoskYNFXtaWupFxLhZAEdxBT4,2005
8
+ monkeycode/session.py,sha256=1R_r5zEhGBD3rV4s20uRunZHxXwKCaQIJqiQUYc5Rfw,4746
9
+ monkeycode/stream.py,sha256=2NNyXYm4LcNMseXS4EO--cnIGN0ugPgpOkdYmhpeR-o,12695
10
+ monkeycode_sdk-1.7.0.dist-info/licenses/LICENSE,sha256=p_GGS7WUJg7iHTz9NKrgWgPB6c9v1y5z6MvhLMImiys,1061
11
+ monkeycode_sdk-1.7.0.dist-info/METADATA,sha256=K7VPbMqNgB3SPMtJaA8bo3v2w0IJnn3V1X7dxOjaIyM,3726
12
+ monkeycode_sdk-1.7.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
13
+ monkeycode_sdk-1.7.0.dist-info/top_level.txt,sha256=q6KuiZpjIBTI0prd_9gqwumf1aV9DQ9ANJEDQqy3FHs,11
14
+ monkeycode_sdk-1.7.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cyqmq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ monkeycode