bug修复
This commit is contained in:
parent
52a67967b9
commit
df19773751
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@ -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})。
|
||||
|
||||
成功返回 True;Agent 不存在 / 端点缺失 / 网络失败 / 非 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}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
125
gateway-task/SKILL.md
Normal 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.html(18026 字节)..."
|
||||
}
|
||||
```
|
||||
|
||||
- `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,再提交任务,从事件流收结果)。
|
||||
208
gateway-task/references/api_reference.md
Normal file
208
gateway-task/references/api_reference.md
Normal 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.html(17236 字节)。实现: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 | 请求体字段缺失或类型错误 |
|
||||
Loading…
Reference in New Issue
Block a user