# claw-agent-py **Repository Path**: North_Fan/claw-agent-py ## Basic Information - **Project Name**: claw-agent-py - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-01 - **Last Updated**: 2026-10-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # claw-agentscope-python 以 **AgentScope 2.0.8 官方 Agent Service(`create_app`)为内核**的智能体平台。 本项目**只写胶水,不写骨架**:路由、lifespan、会话存储、事件总线、沙箱管理、 多智能体团队、AG-UI 协议转换全部复用官方实现。 - 设计文档:[`2026-09-19-claw-agentscope-python-design.md`](./2026-09-19-claw-agentscope-python-design.md) - 实施方案:[`2026-09-20-claw-agentscope-python-implementation-plan.md`](./2026-09-20-claw-agentscope-python-implementation-plan.md) ## 已确认决策 | 决策点 | 结论 | |---|---| | 复用深度 | 官方 `create_app` 为内核,自研仅胶水层 | | 状态存储 | `AsyncSQLAlchemyStorage` → **MySQL** | | 消息总线 | `RedisMessageBus`(valkey,**仅总线,不存状态**) | | 沙箱隔离 | `IsolationPolicy.PER_SESSION`(每会话独立沙箱) | | 沙箱回收 | **休眠范式**:短 TTL(180s)→ pause 承载空闲工作集 + PAUSED 配额硬闸 | | 对话协议 | 官方双通道:`POST /chat/` 触发 + `GET /sessions/{sid}/stream` 订阅 | | AG-UI 转换 | 官方 `AGUIProtocolMiddleware`(不自研映射表) | | 身份 | 覆盖官方 `get_current_user_id` 依赖,对接 claw-ruoyi JWT | ## 自研范围(就这 5 块) 1. **JWT 鉴权**:`app/auth/` —— 替代官方临时 `X-User-ID` 方案 2. **sys_model 映射**:`app/model/` —— 表行 → 官方 `CredentialRecord` + `ChatModelConfig` 3. **沙箱装配与回收**:`app/sandbox/` —— `OpenSandboxWorkspaceManager(PER_SESSION)` 参数落地、 休眠范式回收(pause 承载 + PAUSED 配额闸 + 删除会话即时回收) 4. **运维端点**:`app/core/health.py` —— `/readyz` 依赖探针 5. **前端**:`frontend/` —— 双通道桥接 + AG-UI 事件归约 + 文件面板 ## 快速开始(本地,零外部依赖) 本地默认走 SQLite + 进程内总线 + Local 沙箱,不需要 MySQL / valkey / OpenSandbox。 ```bash python3.11 -m venv .venv .venv/bin/pip install -e ".[dev]" # 生产加 ".[mysql]" cp .env.example .env # 默认已是开发配置 .venv/bin/python -m app.main # http://127.0.0.1:8030 ``` 前端: ```bash cd frontend npm install npm run dev # http://127.0.0.1:5173,已代理到后端 8030 npm run build # 产物由后端以 /app 托管 ``` 打开 ,在左侧「环境准备」填 Provider / API Key / Base URL / 模型,点「创建凭据 / Agent / 会话」即可开始对话。 ## 生产部署 ```bash cp deploy/.env.example deploy/.env # 填 MySQL / valkey / JWT 密钥 bash deploy/deploy.sh # 构建 → 迁移 → 拉起 → 健康检查 REPLICAS=8 bash deploy/deploy.sh up # 多副本:API×8 + scheduler×1 ``` 裸 compose 用法(不推荐,需自行编排迁移): `docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.local.yml up -d`。 单副本叠加 local 文件获得宿主端口映射;多副本【不要】加载 local(端口映射与 `--scale` 冲突),流量由前端 nginx 经服务名 `claw-agentscope-python:8080` 轮询。 启动顺序(多副本,`deploy.sh REPLICAS>1` 自动编排): 1. 部署前置 job:跑一次迁移(`MIGRATE_ON_BOOT=true` 或独立步骤) 2. 拉起 API 副本;`DOWNLOAD_SECRET` 所有副本一致 3. 调度/通道 worker 由独立服务 `claw-agentscope-python-scheduler` 固定单副本 承担(API 副本默认关闭;单容器极简模式见 `deploy.sh` 的 `SCHEDULER_REPLICAS=0`) 4. 健康探针:`GET /readyz`(`deploy.sh` 经容器内探测,不依赖宿主端口) ### 启动期硬校验(配置不对直接拒绝启动) 生产形态(`STORAGE_DATABASE_URL` 非 sqlite)下,以下任一情况启动即失败, 不留「装上就能跑但其实是裸奔」的中间态: | 配置 | 为什么不能留默认值 | |---|---| | `AUTH_MODE` | `dev` 固定返回同一用户 = 无鉴权多租户服务 | | `DOWNLOAD_SECRET` | 默认值可预测 → 可伪造任意用户的工作区文件下载 token | | `ASYNC_TASK_CALLBACK_SECRET` | 空 = 任何人可伪造回调,以任意用户身份触发会话续跑 | | `SANDBOX_SHARED_SERVER` × `SANDBOX_ORPHAN_SWEEP_ON_START` | 多副本共享 opensandbox-server 时启动清场会 kill 别的副本的沙箱 | `AUTH_MODE=header` 的身份(`X-User-ID` / `?user_id=`)是客户端自称,默认仅 `127.0.0.1` 直连可用;其他来源一律 401,除非显式设 `ALLOW_INSECURE_AUTH=true`。 ### P0 阻塞项(上线前必须先做掉) | # | 阻塞项 | 说明 | 产物 | |---|---|---|---| | 1 | **内网沙箱镜像** | 官方 bootstrap 走 `apt-get` + `astral.sh` + PyPI,与内网离线冲突 | [`deploy/sandbox-image/`](./deploy/sandbox-image/README.md) | | 2 | **MySQL schema 比对** | SDK 的 11 张表**无前缀**,与 claw-base 同库极可能撞 `sessions`/`messages`/`agents`;改名等于改 SDK(违反硬约束)→ 冲突就申请**独立 database** | 《schema 比对结论》 | | 3 | **MySQL 异步驱动入库** | `storage-sql` extra 不含 MySQL 驱动,需自选 `asyncmy`(推荐)或 `aiomysql` 入内网私仓 | 私仓包 | ## 沙箱回收(休眠范式) 生产环境「每个用户保留沙箱」会撑爆 server——但撑爆的原因不是 per-session 隔离,而是**容器从不被 kill**。本项目的回收分四层: | 层 | 机制 | 效果 | |---|---|---| | 热层 | 官方 TTL sweeper,`SANDBOX_TTL=180s` | 空闲即 `pause()`,常量级只占活跃会话 | | 休眠层 | PAUSED 承载空闲工作集 | `resume` 秒级恢复(官方 reattach 支持 paused) | | **硬闸** | `SANDBOX_MAX_PAUSED` 配额裁决 | **唯一**能保证 server 不被撑爆的机制,超限按「最久未访问优先」kill | | 最终层 | `SANDBOX_COLD_TTL`(默认关) | days 级天花板,tar → 对象存储 → kill | 配套的三处约束: - **续期上限** `SANDBOX_LEASE_MAX_SECONDS`:心跳不许把过期时间推到上限之外。 没有它,只要进程活着,「有活跃会话」的沙箱就被无限续命,最终只有进程 死亡才回收。 - **删除即回收**:`delete_session` / `delete_agent` 后立刻 kill 容器 (官方只删 workspace 内的会话目录,不碰容器)。否则无主容器会白占 PAUSED 配额。 - **共享 server 只碰自己的**:`SANDBOX_SHARED_SERVER=true` 时配额裁决与 自愈对账都只处理带本实例标签的沙箱。 > 为什么不用「冷档 tar 当主力」:冷档要 resume → tar → 上传 → kill 四次 > 远端往返,而 pause/resume 是同一份状态的原地冻结。只有跨节点/跨重启 > 恢复才需要 tar,那是 days 级场景。 ## 异步任务(回调驱动 + 兜底对账) 长耗时任务(如语音转写)提交到**远端任务服务器**执行,完成后回调续跑会话。 链路上有三处「丢了就丢用户数据/通知」的地方,都已收口: | 环节 | 做法 | 为什么 | |---|---|---| | 续跑唤醒 | 官方 `enqueue_run_trigger(kind="message")` + `WakeupDispatcher` | 持久化在 valkey、跨副本认领、会话忙时重排队**永不丢弃**。自写 `registry.spawn` + 3 次退避最多 6 秒必放弃(会话锁 TTL 是 600s),且进程重启即丢 | | 回调幂等 | 单条 `UPDATE ... WHERE status NOT IN ('done','error')` 乐观锁,rowcount 定 fresh | 终态判定与写入之间**不得有 await**。否则并发/乱序回调会让后到者覆盖先到者的 `result`——用户看到「失败」而产物其实已生成 | | 卡死兜底 | `ASYNC_TASK_RECONCILE_INTERVAL_SECONDS` 扫超时任务,有机会先向上游 `GET /tasks/{id}` 补真实结果 | 回调是单向推送、无重试保证。网络抖动/上游重启会让任务永久停 `pending`/`running`,用户永远等不到结果 | 配套细节: - 产物上传按**官方契约**传同步文件对象(`write_stream(key, IO[bytes])`), 失败只告警不阻断提交,`blob_uri` 为空时明确记录(不再静默)。 - 转交上游失败时**不登记任务**并回收已上传产物,不留孤儿对象。 - `meta` 来自 LLM,超过 16KB 丢弃内容只留标记,避免撑爆 DB 行。 - 列表支持 `limit` + `before` 游标翻页(原先硬编码最近 50 条,历史不可见)。 - 转交带有限重试(连接错误/5xx/429),4xx 立即上报(重试无意义)。 > 上游契约缺口:任务服务器目前只保证 `POST /tasks`,**没有** `GET /tasks/{id}`。 > 有则对账可补齐真实结果,无则退回「超时收尾」。 ## 接口一览(自研仅补差异,其余用官方) | 能力 | 端点 | 归属 | |---|---|---| | 对话触发 | `POST /chat/` | 官方 | | 事件流(SSE) | `GET /sessions/{sid}/stream?agent_id=` | 官方 | | 会话 | `GET/POST /sessions/`、`GET/PATCH/DELETE /sessions/{id}`、`/messages`、`/status`、`/interrupt` | 官方 | | Agent | `GET/POST /agent/`、`PATCH/DELETE /agent/{id}`、`/agent/schema/v2` | 官方 | | 凭据 / 模型 | `GET/POST /credential/`、`GET /model/?provider=` | 官方 | | 工作区文件 | `GET /workspace/files`、`POST /workspace/files/download-token`、`GET /workspace/directories` | 官方 | | 健康 | `GET /health`(无 I/O) | 官方 | | 依赖探针 | `GET /readyz` | **自研** | | 异步任务 | `GET /async-tasks?session_id=&limit=&before=`、`GET /async-tasks/{id}`、`POST /async-tasks/callback/{task_id}` | **自研** | | 产物导出 | `GET /workspace/archive` | **自研** | | 前端 | `GET /app/*` | **自研** | > 注意:官方**没有** `GET /agent/{id}`(只有 list/create/patch/delete),前端需按此口径实现。 ## 前端要点 官方是「先订阅、再触发」的双通道,顺序反了会丢掉 run 的前几个事件: ```js const stream = subscribeSession({ agentId, sessionId, onEvent }) // 1. 先建 SSE await api.chat(agentId, sessionId, userMsg('你好')) // 2. 再触发 ``` `frontend/src/agui.js` 是 AG-UI 事件归约器,已按 2.0.8 的**真实映射**实现: - `RUN_STARTED` / `RUN_FINISHED` / `RUN_ERROR` - `STEP_STARTED` / `STEP_FINISHED` - `TEXT_MESSAGE_START` / `CONTENT` / `END` - `REASONING_MESSAGE_*`(对应 `ThinkingBlock*`) - `TOOL_CALL_START` / `ARGS` / `END` / `RESULT` - HITL、DataBlock 等统一走 `CUSTOM` HITL 恢复复用 `POST /chat/`,`input` 传 `UserConfirmResultEvent`,**没有**独立的 `/agui/resume`。 > 与 Java 版 `agentscope-extensions-agui` 的差异:Java 的 `STATE_SNAPSHOT` / > `STATE_DELTA` 与 CUSTOM `interrupt` 在 Python 官方侧**不存在**,前端不要照搬。 ## 测试 ```bash .venv/bin/python -m pytest tests/ -q ``` - `tests/contract/` —— 锁定官方 2.0.8 真实签名与枚举(官方升级先在这里红) - `tests/test_auth.py` —— JWT 有效/过期/伪造/缺头四态 + 用户隔离 + 豁免路径 + header 模式收紧 - `tests/test_async_tasks.py` / `test_async_task_api.py` —— 流式上传、产物上限、回调鉴权与幂等 - `tests/test_sandbox_lifecycle.py` —— 共享 server 安全阀、冷档自毁、租约失效自愈 - `tests/test_settings_guard.py` —— 生产前置校验 - `tests/test_model_mapper.py` —— sys_model 映射及边界 - `tests/test_e2e_chat.py` —— 对话落库 + **真实 uvicorn 下的 SSE 双通道 + AG-UI 事件** > SSE 用例必须走真实 uvicorn:`httpx.ASGITransport` 会把长连接缓冲到结束才返回, > 会误报「零事件」。 ## 目录结构 ``` app/ main.py # create_app 装配 + uvicorn 入口(唯一入口) settings.py # pydantic-settings:环境变量 → create_app 参数 auth/ # 胶水①:JWT → user_id,覆盖官方依赖 model/ # 胶水②:sys_model → 官方 Credential + ChatModelConfig sandbox/ # 胶水③:OpenSandboxWorkspaceManager(PER_SESSION) 装配 core/ # 胶水④:日志 + /readyz 依赖探针 static/ # (预留) frontend/ # 胶水⑤:双通道桥接 + AG-UI 归约 + 文件面板 deploy/ # Dockerfile / compose / 内网沙箱镜像 tests/ # 契约 / 单元 / E2E ```