交互式终端
这个示例模拟一个需要持续交互的命令:先创建终端会话并连接到 PTY,运行命令、输入内容;连接断开后,再重新接回原会话。

要求
GET /v2/sandbox 的 capabilities.exec.pty 应为可用状态。
GET /v1/capabilities 的返回结果中包含 exec.pty 能力。
创建会话并连接
套接字可以连接已有会话。不指定会话时,daemon 会创建一个空闲后自动回收的 shell。
如果要保留会话,请先创建会话。使用 durable 或 restore 时也必须提供显式 id,否则返回 400:
BASE_URL = "http://127.0.0.1:18091"
WS_BASE_URL = "ws://127.0.0.1:18091"
sb = Aio(BASE_URL)
session_id = sb.post("/v2/pty/sessions", id="demo-1")["session_id"]
WS_URL = f"{WS_BASE_URL}/v2/pty/sessions/{session_id}/ws"
print(session_id)
# demo-1
id 由调用方指定,因此可以直接使用任务名称作为终端的 id。
BASE_URL = "http://127.0.0.1:18091"
WS_BASE_URL = "ws://127.0.0.1:18091"
sb = Aio(BASE_URL)
session_id = sb.post("/v1/shell/sessions/create")["session_id"]
WS_URL = f"{WS_BASE_URL}/v1/shell/ws?session_id={session_id}"
print(session_id)
# 3c2a99fd-c16c-408b-9298-5126e1d00def
id 由 daemon 生成,没有办法指定。
执行命令不需要建立套接字连接。
POST /v2/pty/sessions/{id}/exec {command} 在同一个终端中执行命令。
POST /v1/shell/exec {command, id} 在同一个终端中执行命令。
响应包含以下字段:
session_id, command, status, output, console, exit_code
REST 和 WebSocket 共享同一个 shell。因此,通过 REST 创建的文件可以立即在终端中用 ls 看到。
连接客户端
两个路由使用相同的消息格式,因此客户端无需区分它们。
下面的客户端发送一条命令,并持续打印收到的输出,直到连接关闭:
import asyncio, json, websockets
async def main():
# Append &api_key=<key> to WS_URL when AIO_API_KEY is set.
async with websockets.connect(WS_URL) as ws:
await ws.send(json.dumps({"type": "input", "data": "uname -s\n"}))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] in ("output", "restore_output"):
print(msg["data"], end="", flush=True)
asyncio.run(main())
# ~ $ uname -s
# Darwin
# ~ $
消息
{"type":"ready","session_id":"…","transport":"json","backend":"tmux","resumed":false}
{"type":"input","data":"ls\n"}
{"type":"resize","cols":120,"rows":40}
{"type":"ping","timestamp":<t>}
{"type":"output","data":"…"}
{"type":"restore_output","data":"…"}
{"type":"pong","timestamp":<t>}
{"type":"error","data":"…"}
| 方向 | type | 含义 |
|---|
| ← 接收 | ready | 升级后的第一帧 |
| → 发送 | input | 按键或命令文本 |
| → 发送 | resize | 调整 PTY 尺寸 |
| → 发送 | ping | 可选的保活 |
| ← 接收 | output | 终端输出,含 ANSI |
| ← 接收 | restore_output | 重新连接时重放的缓冲历史 |
| ← 接收 | pong | 对 ping 的回应 |
| ← 接收 | error | 连接或会话失败 |
input 里包含 \n 才会执行该行。非 JSON 文本按原始输入处理。cols 和 rows 也可以放在 data 下。
protocol=binary 只改数据帧:二进制帧在两个方向上都是原始 PTY 字节,文本帧仍然是 JSON 控制消息 —— ready、resize、ping、pong。
不指定会话的连接
不带会话 id 打开的套接字同样会创建会话,两个路由的区别在于返回会话 id 的方式:
GET /v2/pty/ws 不带会话 id 时,shell 的生命周期与套接字完全相同。关闭套接字后无法再次访问这个 shell。
服务不会额外发送会话 id 帧;ready 帧本身包含该 id,随后会发送空屏幕画面:
{"backend":"native","resumed":false,"session_id":"afc14051-206a-4640-82c7-c46119cbaa3b","transport":"json","type":"ready"}
{"data":"\u001b[H\u001b[2J~ $ ","type":"output"}
GET /v1/shell/ws 不带 session_id 时,创建的会话不会随着套接字关闭,空闲后才会回收。因此,该会话 id 可以继续使用。
服务会在 ready 和空屏幕画面之前,单独发送一帧 session_id:
{"data":"a0bcaea7-16fe-4439-8c9f-45516c81a32e","type":"session_id"}
{"backend":"native","resumed":false,"session_id":"a0bcaea7-16fe-4439-8c9f-45516c81a32e","transport":"json","type":"ready"}
{"data":"\u001b[H\u001b[2J~ $ ","type":"output"}
后端与会话限制
AIO_SHELL_BACKEND=auto(默认)会在检测到 tmux 时使用 tmux,否则使用原生 PTY。只有 tmux 后端的会话能在 daemon 重启后保留。
native 会固定使用原生 PTY。
cd 和环境变量的改动会在会话中保持。默认最多同时运行 20 个会话,空闲 3600 秒后自动关闭;可通过 AIO_SHELL_MAX_SESSIONS 和 AIO_SHELL_SESSION_TIMEOUT_SECS 调整。
按 id 创建的会话会一直存在,直到被删除:
DELETE /v2/pty/sessions/{id}:删除会话。
DELETE /v1/shell/sessions/{session_id}:删除会话。
断连后恢复
durable=true 会在没有客户端连接时保留终端,让下一个套接字接管它。
下面先运行一个耗时命令,断开套接字,再重新连接:

import asyncio, json, websockets
url = f"{WS_BASE_URL}/v2/pty/sessions/{session_id}/ws?durable=true"
async def main():
first = await websockets.connect(url)
await first.send(json.dumps({
"type": "input",
"data": "for i in 1 2 3 4 5; do echo tick $i; sleep 1; done\n"}))
# Two ticks in, the client drops. The loop keeps running.
await asyncio.sleep(2.5)
await first.close()
await asyncio.sleep(3.5)
async with websockets.connect(url) as second:
for _ in range(3):
print(await second.recv())
asyncio.run(main())
sb.delete(f"/v2/pty/sessions/{session_id}")
import asyncio, json, websockets
url = f"{WS_BASE_URL}/v1/shell/ws?session_id={session_id}&durable=true"
async def main():
first = await websockets.connect(url)
await first.send(json.dumps({
"type": "input",
"data": "for i in 1 2 3 4 5; do echo tick $i; sleep 1; done\n"}))
# Two ticks in, the client drops. The loop keeps running.
await asyncio.sleep(2.5)
await first.close()
await asyncio.sleep(3.5)
async with websockets.connect(url) as second:
for _ in range(4):
print(await second.recv())
asyncio.run(main())
sb.delete(f"/v1/shell/sessions/{session_id}")
重新连接时,输出可能比命令实际产生的内容多一帧。
当连接到调用方已经持有的会话 id 时,daemon 会让前台任务重绘。
这样,全屏程序会重新绘制完整画面,而不是只让渲染端收到一段残缺内容。对于普通提示符,这会表现为提示符再次打印。
第二个套接字会收到 resumed: true 和一帧 relay_resumed,之后只接收无人连接期间产生的输出。
设置 restore=true 时,服务会改为回放整个缓冲区的有限快照。快照大小由 replay_bytes 决定,默认值为 10 MiB,允许范围为 256 KiB 至 10 MiB。
保留的输出存储在 daemon 内存中。只有 tmux 后端会让 shell 进程在 daemon 重启后继续存在。
相关页面