bug修复

This commit is contained in:
HandsomeAq 2026-08-05 23:24:57 +08:00
parent 52a67967b9
commit df19773751
9 changed files with 446 additions and 22 deletions

View File

@ -107,6 +107,22 @@ async def agent_available(
return {"ok": True}
@router.delete("/agents/{agent_id}", summary="删除 Agent无心跳的僵尸 Agent")
async def agent_delete(
agent_id: str,
svc: AgentService = Depends(get_agent_service),
_admin: None = Depends(require_admin_dep),
):
"""删除 Agent 池中的 Agent。
用于清理无心跳的僵尸 Agent Agent 进程仍存活删除后其心跳/注册会
触发重新注册自动重新连接回池中
"""
if not await svc.unregister(agent_id):
raise HTTPException(status_code=404, detail="agent not found")
return {"ok": True}
@router.post("/agents/{agent_id}/priority", response_model=AgentInfo, summary="设置 Agent 优先级")
async def agent_priority(
agent_id: str,

View File

@ -23,12 +23,21 @@ class AgentRepo:
# ---------- 写入 ----------
async def upsert(self, agent: AgentInfo, *, heartbeat: bool = False) -> bool:
"""写入 Agent 信息,返回是否新建。非心跳时为全量注册。"""
"""写入 Agent 信息,返回是否新建。非心跳时为全量注册。
非心跳全量注册时会重建标签索引删旧增新避免 Agent 换标签后
索引陈旧导致 by_tags 匹配不到
"""
key = self._info_key(agent.agent_id)
existed = await self.redis.exists(key)
if not existed:
await self.redis.sadd(C.AGENT_ALL_SET, agent.agent_id)
# 建立标签索引
if not heartbeat:
# 全量注册:先清理旧标签索引,再建立当前标签索引
old = await self.get(agent.agent_id)
if old and old.agent_tags:
for tag in old.agent_tags:
await self.redis.srem(self._tag_key(tag), agent.agent_id)
for tag in agent.agent_tags:
await self.redis.sadd(self._tag_key(tag), agent.agent_id)
mapping: dict[str, Any] = {
@ -93,12 +102,16 @@ class AgentRepo:
await self.redis.hset(key, "status", AgentStatus.READY.value)
return True
async def beat(self, agent_id: str, current_load: int) -> bool:
"""更新心跳时间与负载,按状态机规则流转状态。
async def beat(self, agent_id: str, current_load: int | None = None) -> bool:
"""更新心跳时间,按状态机规则流转状态。
- 不可用unavailable保持不可用不因心跳复活
- 离线offline心跳失联恢复 ready
- ready/processing/stopping保持原状态
注意负载current_load一律由网关调度器通过 adjust_load 记账
心跳不再覆写避免与调度器计数互相覆盖导致负载错乱
current_load 参数仅作兼容保留不再写入
"""
key = self._info_key(agent_id)
if not await self.redis.exists(key):
@ -117,7 +130,6 @@ class AgentRepo:
key,
mapping={
"last_heartbeat": str(now),
"current_load": str(current_load),
"status": status.value,
},
)

View File

@ -22,19 +22,19 @@ class RelayService:
self.task_repo = task_repo or TaskRepo(redis)
self.agent_repo = AgentRepo(redis)
async def dispatch_command(self, agent_id: str, request_id: str, payload: dict) -> bool:
async def dispatch_command(self, agent_id: str, request_id: str, payload: dict) -> tuple[bool, str]:
"""正向:向 Agent 真实 HTTP 推送任务指令POST {agent.endpoint}/tasks/{request_id})。
成功返回 TrueAgent 不存在 / 端点缺失 / 网络失败 / 202 均返回 False
由调度器回退任务状态并释放负载
返回 (ok, reason)成功时 (True, "")Agent 不存在 / 端点缺失 / 网络失败 /
202 均返回 (False, 原因描述)由调度器回退任务状态并释放负载
"""
agent = await self.agent_repo.get(agent_id)
if not agent:
logger.warning("relay dispatch failed: agent not found agent=%s request=%s", agent_id, request_id)
return False
return False, "Agent 不存在或已注销"
if not agent.endpoint:
logger.warning("relay dispatch failed: endpoint empty agent=%s request=%s", agent_id, request_id)
return False
return False, "Agent 未配置 endpoint"
task = await self.task_repo.get(request_id)
url = f"{agent.endpoint.rstrip('/')}/tasks/{request_id}"
body = {
@ -52,15 +52,15 @@ class RelayService:
except httpx.HTTPError as e:
logger.error("relay dispatch network error agent=%s request=%s err=%s", agent_id, request_id, e)
await self._log(agent_id, request_id, "dispatch", f"push failed: {e}")
return False
return False, f"推送失败: {e}"
if resp.status_code != 202:
logger.warning("relay dispatch http %s agent=%s request=%s body=%s",
resp.status_code, agent_id, request_id, resp.text[:200])
await self._log(agent_id, request_id, "dispatch", f"push failed http {resp.status_code}")
return False
return False, f"推送失败: Agent 返回 HTTP {resp.status_code}(期望 202"
await self._log(agent_id, request_id, "dispatch", f"command pushed to {agent.endpoint}")
logger.info("relay dispatch ok agent=%s request=%s url=%s", agent_id, request_id, url)
return True
return True, ""
_TERMINAL = {TaskStatus.SUCCESS, TaskStatus.FAILED}

View File

@ -37,13 +37,23 @@ class Scheduler:
return candidates[0]
async def dispatch(self, request_id: str) -> bool:
"""将单个 pending 任务下发给匹配 Agent绑定 → 推送 payload → 失败回退)。"""
"""将单个 pending 任务下发给匹配 Agent绑定 → 推送 payload → 失败回退)。
成功 / 无可用 Agent / 推送失败时都会在任务上记录 error_info成功时清空
便于前端与日志定位为什么没分配
"""
task = await self.task_repo.get(request_id)
if not task or task.status != TaskStatus.PENDING:
return False
agent = await self.pick_candidate(task.task_tags)
if not agent:
return False # 暂无可用 Agent保持 pending等待下次调度
# 暂无可用 Agent标签不匹配 / 离线 / 满载),保持 pending等待下次调度
await self.task_repo.update(
request_id,
error_info="无可用 Agent标签不匹配或全部离线/满载",
)
logger.info("task no candidate request_id=%s tags=%s", request_id, task.task_tags)
return False
# 绑定 Agent标记 running
await self.task_repo.update(
request_id,
@ -52,20 +62,33 @@ class Scheduler:
progress=0,
)
await self.task_repo.mark_running(request_id)
# 记录调度前状态,供推送失败回退时恢复
prev_status = agent.status
# 标记 Agent 为处理中
await self.agent_repo.update_status(agent.agent_id, AgentStatus.PROCESSING)
# 更新 Agent 负载
await self.agent_repo.adjust_load(agent.agent_id, 1)
# 真实推送 payload 到 Agent 端点
pushed = await self.relay.dispatch_command(agent.agent_id, request_id, task.payload or {})
pushed, reason = await self.relay.dispatch_command(agent.agent_id, request_id, task.payload or {})
if not pushed:
# 推送失败:回退 pending 并恢复 agent 就绪、释放负载,等待下次调度(避免重复扣负载)
await self.task_repo.update(request_id, status=TaskStatus.PENDING, agent_id="", progress=0)
# 推送失败:回退 pending 并释放负载,按原状态恢复 Agent等待下次调度
await self.task_repo.update(
request_id,
status=TaskStatus.PENDING,
agent_id="",
progress=0,
error_info=f"推送失败: {reason}",
)
await self.task_repo.mark_pending(request_id)
await self.agent_repo.adjust_load(agent.agent_id, -1)
await self.agent_repo.update_status(agent.agent_id, AgentStatus.READY)
logger.warning("task dispatch push failed, reverted request_id=%s agent=%s", request_id, agent.agent_id)
# 若该 Agent 本为 PROCESSING还有并发容量保持 PROCESSING否则置回 READY
restore = prev_status if prev_status == AgentStatus.PROCESSING else AgentStatus.READY
await self.agent_repo.update_status(agent.agent_id, restore)
logger.warning("task dispatch push failed, reverted request_id=%s agent=%s reason=%s",
request_id, agent.agent_id, reason)
return False
# 推送成功:清空 error_info
await self.task_repo.update(request_id, error_info="")
logger.info("task dispatched request_id=%s -> agent=%s", request_id, agent.agent_id)
return True

View File

@ -92,6 +92,7 @@ export const api = {
listAgents: () => request<AgentInfo[]>('/agents'),
unavailableAgent: (id: string) => request<{ ok: boolean }>(`/agents/${id}/unavailable`, { method: 'POST' }),
availableAgent: (id: string) => request<{ ok: boolean }>(`/agents/${id}/available`, { method: 'POST' }),
deleteAgent: (id: string) => request<{ ok: boolean }>(`/agents/${id}`, { method: 'DELETE' }),
setAgentPriority: (id: string, priority: number) =>
request<AgentInfo>(`/agents/${id}/priority`, {
method: 'POST',

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { api, type AgentInfo } from '../api'
const agents = ref<AgentInfo[]>([])
@ -66,6 +67,31 @@ async function setAvailable(a: AgentInfo) {
}
}
async function removeAgent(a: AgentInfo) {
try {
await ElMessageBox.confirm(
`确定删除 Agent「${a.agent_id}」吗?\n若 Agent 进程仍存活,其心跳会触发重新注册,自动重新连接。`,
'删除 Agent',
{
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger',
}
)
} catch {
return
}
try {
await api.deleteAgent(a.agent_id)
ElMessage.success('已删除')
await load()
} catch (e) {
console.error(e)
ElMessage.error('删除失败')
}
}
onMounted(() => {
load()
timer = window.setInterval(load, 5000)
@ -160,6 +186,14 @@ onUnmounted(() => {
>
置为可用
</el-button>
<el-button
v-if="a.status === 'offline' || a.status === 'unavailable'"
size="small"
type="danger"
@click="removeAgent(a)"
>
删除
</el-button>
</div>
</div>
</div>

View File

@ -115,9 +115,14 @@ onUnmounted(() => {
<el-table-column label="任务描述" prop="description" min-width="220" show-overflow-tooltip>
<template #default="{ row }">{{ row.description || '-' }}</template>
</el-table-column>
<el-table-column label="状态" width="100">
<el-table-column label="状态" min-width="150">
<template #default="{ row }">
<el-tag :type="statusMeta[row.status]?.type" effect="light">{{ statusMeta[row.status]?.label }}</el-tag>
<div class="flex items-center gap-1">
<el-tag :type="statusMeta[row.status]?.type" effect="light">{{ statusMeta[row.status]?.label }}</el-tag>
<el-tooltip v-if="row.status === 'pending' && row.error_info" :content="row.error_info" placement="top">
<el-tag type="danger" effect="dark" size="small">待分配原因</el-tag>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column label="Agent" width="150">

125
gateway-task/SKILL.md Normal file
View File

@ -0,0 +1,125 @@
---
name: gateway-task
description: 向 A2A 智能网关gateway提交任务、订阅完成事件、查询状态、取消任务的 API 使用指南。当用户CLI/模型对话场景)要求"提交任务"、"发任务"、"上报任务"、"查询任务状态/结果"、"取消任务"、"给网关下发任务",或需要 Agent 注册/心跳/回传结果时使用本技能。
---
# Gateway Task
## Overview
A2A 智能网关通过 REST API 接收 CLI 提交的任务、让 Agent 注册并领取任务,并持久化任务状态供随时查询。本技能提供任务从提交到拿到结果的**完整流程接口用法**,覆盖 **SSE 事件订阅(推荐,免轮询)****轮询查询(兜底)** 两种取结果方式,保证请求体格式、认证字段与 `cli_session_id` 用法、中文编码处理正确。
### 关键实践经验(务必遵守)
- **中文 payload 必须用 UTF-8 文件 + `--data-binary @file` 提交**。在 Windows PowerShell 下若直接用 `curl -d '{"prompt":"中文"}'``Invoke-WebRequest` 传中文,会被转成 `?` 乱码agent 收到损坏的指令。正确做法见 `references/api_reference.md` 的"中文编码"小节。
- **先订阅 SSE 再提交任务**,且 `cli_session_id` 必须一致,即可免轮询拿到结果。
- **任务卡在 `pending` 且 `error_info` 提示"无可用 Agent"时**,先查 Agent 状态(`GET /api/admin/agents`,用 `X-Admin-Auth: <ADMIN_AUTH>`),确认标签是否匹配、状态是否为 `ready`/`processing`、是否满载(`current_load < max_concurrent`)。Agent 处于 `stopping`/`unavailable`/`offline` 或满载时都不会被分配
- **成功任务的 `result` 结构**`{"reply": "Agent接受任务后的首条回复", "output": "Agent执行的最终总结"}`。前端据此展示"Agent 接受任务回复"和"Agent 总结"。
## 前置条件
- 网关 Base URL`http://localhost:8000`(生产环境以实际地址为准)。
- 认证 `GATEWAY_AUTH`:读取网关 `.env` 中的 `GATEWAY_AUTH` 值(未配置时默认 `dev-gateway-auth`)。
- **认证传递方式不同**
- **POST 写入型接口**提交任务、Agent 注册/心跳/结果/注销):`auth` 放在**请求体 JSON** 中。
- **GET 查询、SSE 订阅、取消**`auth` 作为 **query 参数**`?auth=<GATEWAY_AUTH>`)。
- 缺失或错误返回 `401`;字段缺失返回 `422`
- **任务要能被分配,`task_tags` 必须与某个已注册 Agent 的 `agent_tags` 至少有一个交集**,且该 Agent 状态为 `ready`/`processing` 且未满载,否则任务会一直卡在 `pending``error_info` 提示"无可用 Agent")。
## 提交任务携带 `cli_session_id`(关键)
- 若希望通过 **SSE 免轮询**拿到任务完成结果,**提交任务时必须传 `cli_session_id`**(自定义会话标识,如 `cli-123`)。
- 网关仅在任务到达终态时,向该 `cli_session_id` 对应的 SSE 通道推送 `task_done` 事件;**未传 `cli_session_id` 时网关不会推送**,只能靠轮询查询。
- 因此推荐流程:**先订阅 SSE → 再提交任务(带 `cli_session_id`)→ 从事件流中收结果**。
## 任务提交流程Workflow
### 推荐流程SSE 免轮询)
1. **订阅完成事件**`GET /api/cli/events?cli_session_id=<sid>&auth=<AUTH>` 建立 SSE 长连接。
- 连接建立后先**回放**该会话已完成的 `task_done` 事件(连接晚于任务完成也不丢),再实时接收新事件。
- 事件帧格式:`data: {"event": "task_done", "request_id": "...", "status": "...", "result": ..., "error_info": ...}`。
2. **提交任务**`POST /api/cli/tasks`,请求体**必须带与上面相同的 `cli_session_id`**。
3. **收结果**:从 SSE 事件流中收到该 `request_id``task_done` 事件即完成,无需轮询。
### 兜底流程(轮询)
提交任务后 `GET /api/cli/tasks/{request_id}?auth=<AUTH>` 每 2~5 秒轮询,直到 `success` / `failed`
### 1. 提交任务
`POST /api/cli/tasks`
请求体字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `auth` | string | 是 | 网关认证密码body 内) |
| `task_type` | string | 是 | 任务类型,如 `compile` / `build` / `test` / `generate` |
| `task_tags` | list[str] | 否 | 能力标签,**必须与某 Agent 的 `agent_tags` 有交集**才可被调度 |
| `payload` | dict | 否 | 任务载荷(`prompt` / `cmd` 等) |
| `request_id` | string | 否 | 幂等键,不传自动生成 |
| `cli_session_id` | string | 否 | **若要 SSE 免轮询必须传**,且与订阅用的 session 一致 |
| `timeout` | int | 否 | 超时秒数0 使用默认值 |
响应返回 `TaskInfo`(含 `request_id`、`status`)。**务必从响应中记录 `request_id`,后续查询/取消都依赖它。**
curl 示例见 `references/api_reference.md`
### 2. 订阅任务完成事件SSE推荐
`GET /api/cli/events?cli_session_id=<sid>&auth=<AUTH>`
- 长连接,任务到达终态时网关主动推送 `task_done`**无需轮询**。
- 支持回放:连接晚于任务完成也能先收到历史事件。
- 事件字段:`event`、`request_id`、`status`、`result`、`error_info`。
### 3. 查询任务状态(兜底)
`GET /api/cli/tasks/{request_id}?auth=<AUTH>`
任务不存在返回 `404`。`status` 为 `pending` / `running` / `success` / `failed``success` 后结果在 `result` 字段,`failed` 时原因在 `error_info`
**`result` 结构(成功任务):**
```json
{
"reply": "我先查看工作目录环境,然后创建文件。",
"output": "生成的文件路径:.../cool_clock.html18026 字节)..."
}
```
- `reply`Agent **接受任务后的首条回复**(前端"Agent 接受任务回复")。
- `output`Agent **执行完的最终总结**(前端"Agent 总结")。
- 失败/取消/超时任务的 `result``null`,此时看 `error_info` 判断原因。
### 排查"任务卡在 pending / 无可用 Agent"
任务一直 `pending``error_info` 含"无可用 Agent"时,按序排查:
1. 查 Agent 列表:`GET /api/admin/agents`Header `X-Admin-Auth: <ADMIN_AUTH>`,认证书见网关 `.env``ADMIN_AUTH`)。
2. 关注三件事:
- **标签匹配**:任务的 `task_tags` 与 Agent 的 `agent_tags` 是否有交集;
- **状态可用**Agent 须为 `ready``processing``stopping`/`unavailable`/`offline` 不参与调度;
- **未满载**`current_load < max_concurrent``max_concurrent=1` 时任何遗留负载都会导致不再分配)。
3. 若 Agent 状态为 `stopping`/异常,可尝试 `POST /api/admin/agents/{id}/available` 置为可用(部分场景心跳会自动恢复)。
4. 若 Agent 负载计数异常(完成的任务未释放负载),检查是否有 `running` 但实际已超时的任务残留,必要时取消以释放负载。
### 4. 取消任务
`POST /api/cli/tasks/{request_id}/cancel?auth=<AUTH>`
`pending` / `running` 可取消,已结束任务返回 `400`。成功返回 `{"ok": true, "request_id": "..."}`
## Agent 协议
Agent 是任务的执行方,接入网关同样需要认证(`auth` 在请求体 JSON 中):
- `POST /api/agent/register`:启动注册,请求体含 `auth`、`agent_id`、`endpoint`、`agent_tags`、`max_concurrent`。
- `POST /api/agent/heartbeat`:约每 10 秒心跳。
- `POST /api/agent/result`:任务完成回传 `request_id` + `agent_id` + `status` + `result`
- `POST /api/agent/unregister`:优雅注销。
## 完整示例与典型对话引导
`references/api_reference.md` 中查看带真实参数的 curl 示例、`TaskInfo` 完整响应结构、SSE 订阅示例,以及典型对话引导(先取 `GATEWAY_AUTH`,再订阅 SSE再提交任务从事件流收结果

View File

@ -0,0 +1,208 @@
# 网关任务 API 参考
网关 Base URL`http://localhost:8000`。
## 认证
```bash
# 本机开发环境:
# - 后端 D:\workspace\ADK-gateway .env 中 GATEWAY_AUTH=<实际值>
# - 若未配置,默认值为 dev-gateway-auth
AUTH="<GATEWAY_AUTH>"
```
**认证传递方式:**
- POST 写入型接口提交任务、Agent 注册/心跳/结果/注销):`auth` 在请求体 JSON 中。
- GET 查询、SSE 订阅、取消:`auth` 作为 query 参数 `?auth=<AUTH>`
## 任务接口
### 1. 提交任务(推荐:带 `cli_session_id` 以便 SSE 免轮询)
```bash
curl -X POST http://localhost:8000/api/cli/tasks \
-H "Content-Type: application/json" \
-d '{
"auth": "'"$AUTH"'",
"task_type": "generate",
"task_tags": ["dev"],
"cli_session_id": "cli-123",
"payload": {"prompt": "生成一个炫酷的时钟 HTML 文件"},
"timeout": 300
}'
```
> **关键**:若要免轮询通过 SSE 拿结果,`cli_session_id` **必须传**,且与订阅 `GET /api/cli/events` 时用的 session 完全一致。
### 中文编码重要Windows 下必读)
Windows PowerShell 下直接用 `curl -d '{...中文...}'``Invoke-WebRequest` 传中文,会把中文转成 `?` 乱码。**必须用 UTF-8 编码的 JSON 文件 + `--data-binary @file`**
1. 用 UTF-8无 BOM写入请求文件 `request.json`
```json
{
"auth": "gw_xxx",
"task_type": "generate",
"task_tags": ["dev"],
"cli_session_id": "cli-123",
"timeout": 300,
"payload": {"prompt": "请在您的工作目录下创建一个炫酷的时钟 HTML 文件"}
}
```
2. 提交:
```bash
curl -s -X POST "http://localhost:8000/api/cli/tasks?auth=<AUTH>" \
-H "Content-Type: application/json" \
--data-binary "@request.json"
```
> 注意 `auth` 放在 query 或 body 均可,但 body 内的 `auth` 也需保留。用 `--data-binary @file` 可避免 Shell 对引号/中文的再转义。
响应(`TaskInfo`
```json
{
"request_id": "9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c",
"task_type": "generate",
"task_tags": ["dev"],
"payload": {"prompt": "生成一个炫酷的时钟 HTML 文件"},
"status": "pending",
"agent_id": null,
"cli_session_id": "cli-123",
"create_time": 1710000000.0,
"timeout": 300,
"progress": 0,
"result": null,
"error_info": null
}
```
### 2. 订阅任务完成事件SSE推荐免轮询
```bash
curl -N http://localhost:8000/api/cli/events?cli_session_id=cli-123\&auth="$AUTH"
```
- SSE 长连接(`text/event-stream`)。**先订阅,再提交带相同 `cli_session_id` 的任务**。
- 任务到达终态(`success` / `failed`,含取消/超时)时,网关推送:
```
data: {"event": "task_done", "request_id": "9f8e...", "status": "success", "result": {...}, "error_info": null}
```
- 支持回放:即使连接晚于任务完成,也会先收到历史完成事件,再收实时事件。
- 收到目标 `request_id``task_done` 即完成,无需轮询。
### 3. 查询任务状态(兜底)
```bash
curl "http://localhost:8000/api/cli/tasks/<request_id>?auth=$AUTH"
```
- 未找到返回 `404`
- `status` 取值:`pending`(排队)、`running`(执行中)、`success`(成功)、`failed`(失败/取消/超时)。
- `success``result` 为结果字典;`failed` 时 `error_info` 说明原因(如 `cancelled by user`、`timeout`、`无可用 Agent`)。
- `progress` 为 0~100 的整数进度。
**成功任务的 `result` 结构:**
```json
{
"reply": "我先查看工作目录环境,然后创建烟花特效文件。",
"output": "已生成文件D:\\workspace\\firework.html17236 字节。实现Canvas 粒子系统、火箭拖尾升空、爆炸粒子、点击放花与自动循环。"
}
```
- `reply`Agent 接受任务后的**首条完整回复**(展示为"Agent 接受任务回复")。
- `output`Agent 执行完成后的**最终总结**(展示为"Agent 总结")。
- 失败/取消/超时任务 `result``null`,此时以 `error_info` 为准。
### 4. 取消任务
```bash
curl -X POST "http://localhost:8000/api/cli/tasks/<request_id>/cancel?auth=$AUTH"
```
- 仅 `pending` / `running` 可取消;已终态返回 `400`
- 成功:`{"ok": true, "request_id": "<request_id>"}`。
## Agent 接口
所有 Agent 接口 `auth` 在请求体 JSON 中。
### 注册
```bash
curl -X POST http://localhost:8000/api/agent/register \
-H "Content-Type: application/json" \
-d '{
"auth": "'"$AUTH"'",
"agent_id": "agent-001",
"endpoint": "http://agent-host:9000",
"agent_tags": ["dev"],
"max_concurrent": 2,
"current_load": 0
}'
```
### 心跳
```bash
curl -X POST http://localhost:8000/api/agent/heartbeat \
-H "Content-Type: application/json" \
-d '{"agent_id": "agent-001"}'
```
未注册返回 `404`
### 结果回传
```bash
curl -X POST http://localhost:8000/api/agent/result \
-H "Content-Type: application/json" \
-d '{
"request_id": "<request_id>",
"agent_id": "agent-001",
"status": "success",
"progress": 100,
"result": {"output": "build ok"},
"error_info": null
}'
```
### 注销
```bash
curl -X POST http://localhost:8000/api/agent/unregister \
-H "Content-Type: application/json" \
-d '{"agent_id": "agent-001"}'
```
## 典型对话引导CLI 中提交任务)
当用户CLI 中)请求提交任务时,按以下顺序执行:
1. 读取网关 `.env` 获取 `GATEWAY_AUTH`(默认 `dev-gateway-auth`)与 `ADMIN_AUTH`
2. 与用户确认 `task_type` 与关键参数(`task_tags`、`payload`),不明确时做合理假设并说明。
3. **生成一个 `cli_session_id`**(如 `cli-<时间戳>`)。
4. **先订阅 SSE**`GET /api/cli/events?cli_session_id=<sid>&auth=<AUTH>`(后台发起)。
5. **再提交任务**`POST /api/cli/tasks`,请求体带相同的 `cli_session_id`。**若 payload 含中文,务必用 UTF-8 文件 + `--data-binary @file`**(见上文"中文编码")。
6. **从 SSE 事件流**中等待该 `request_id``task_done` 事件,拿到 `status` / `result` / `error_info`;若 SSE 不可用则回退为 `GET /api/cli/tasks/{request_id}` 轮询。
7. 向用户汇报最终状态与 `result``reply` = Agent 接受任务回复、`output` = Agent 总结);失败时汇报 `error_info`
### 任务卡住时的排查步骤
若任务长时间 `pending``failed`
1. 查 `error_info` 定位原因(`无可用 Agent` / `timeout` / `cancelled by user` / 具体异常)。
2. **`无可用 Agent`**:调 `GET /api/admin/agents`Header `X-Admin-Auth: <ADMIN_AUTH>`)核对 Agent 的标签交集、状态(须 `ready`/`processing`)、是否满载。
3. **`timeout`/`cancelled`**:多为 Agent 侧执行慢、被取消,或 Agent 进程异常;先确认 Agent 进程存活(访问其 endpoint 健康路径),必要时重启 Agent。
4. 展示时注意:**PowerShell 会把嵌套 JSON 包装成 CLIXML**,直接用 `type file.json` 看会误以为 `result``{"type":"execute_command_result",...}`。应改用 `python -c "import json,sys; print(json.load(open(f))['result'])"``python -m json.tool` 等干净解析,再判断 reply/output 是否真实存在。
## 错误码
| HTTP | 含义 |
| --- | --- |
| 400 | 请求不合法(如取消已结束任务、缺少 agent_id |
| 401 | `auth``GATEWAY_AUTH` 不一致POST 在 body、GET/取消/SSE 在 query |
| 404 | 任务/Agent 不存在 |
| 422 | 请求体字段缺失或类型错误 |