fix: proxy code agent steer through cloud web
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
| `GET /help` | 返回可用 route 摘要。 |
|
||||
| `GET /v1`、`GET /v1/...` | 同源代理到 `hwlab-cloud-api`;公开的 Code Agent result/trace 轮询按 route policy 处理。 |
|
||||
| `GET/PATCH /v1/workbench/workspace...` | 同源代理到 cloud-api 的账号 workspace authority,用于 Web/CLI 共享工作区和 revision 冲突保护。 |
|
||||
| `POST /v1/agent/chat`、`POST /v1/agent/chat/cancel` | 同源代理到 cloud-api 的 Code Agent 入口。 |
|
||||
| `POST /v1/agent/chat`、`POST /v1/agent/chat/steer`、`POST /v1/agent/chat/cancel` | 同源代理到 cloud-api 的 Code Agent 入口;steer 必须走同一个 `19666` Web path,由 cloud-api/AgentRun 判断目标 turn 是否可接收。 |
|
||||
| `POST /v1/device-pods/...` | 受控同源代理到 cloud-api 的 Device Pod job/操作入口;只要 Cloud API 已提供对应能力,Cloud Web 不能只代理 list/status 而让 job POST 在 `19666` 返回 404。 |
|
||||
| `POST /v1/m3/io`、`POST /json-rpc` | 同源代理到受控 API;不能绕过 cloud-api 直连硬件服务。 |
|
||||
|
||||
@@ -53,6 +53,8 @@ Cloud Web check 通过后仍需执行 bundle build 和 dist freshness 校验,
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:从同源 `19666` 提交 Code Agent 短连接请求并轮询 result,确认请求经 cloud-web proxy 到 `hwlab-cloud-api`,且 trace 可回放。
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:对运行中 Code Agent trace 调用 `hwlab-cli client agent steer <traceId> --message ...`,确认请求走 Cloud Web 同源 `POST /v1/agent/chat/steer`;Web 层不能返回 `serviceId=hwlab-cloud-web` 的 404,目标不存在、非运行中或 runner 拒绝时必须透传 cloud-api/AgentRun 的结构化业务状态。
|
||||
|
||||
## T2.1
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:对浏览器暴露的 Code Agent trace 运行 `hwlab-cli client agent trace <traceId> --render web --limit 80`,确认 CLI 与 Web 使用同一 trace row 转换;若 final response 缺失、assistant row 顺序错乱或噪声事件过多,先用 CLI 固定复现再修实现。
|
||||
|
||||
@@ -6,6 +6,7 @@ const POST_PROXY_ROUTES = new Set([
|
||||
"/json-rpc",
|
||||
"/v1/agent/chat",
|
||||
"/v1/agent/chat/cancel",
|
||||
"/v1/agent/chat/steer",
|
||||
"/v1/m3/io",
|
||||
"/v1/skills/uploads"
|
||||
]);
|
||||
@@ -14,7 +15,8 @@ const PUBLIC_PROXY_ROUTES = new Set([
|
||||
"POST /auth/login",
|
||||
"POST /auth/logout",
|
||||
"POST /v1/agent/chat",
|
||||
"POST /v1/agent/chat/cancel"
|
||||
"POST /v1/agent/chat/cancel",
|
||||
"POST /v1/agent/chat/steer"
|
||||
]);
|
||||
|
||||
export function cloudWebProxyRoutePolicy(method, pathname) {
|
||||
|
||||
@@ -89,3 +89,12 @@ test("cloud web proxies authenticated account workbench write routes", () => {
|
||||
routeKey: "POST /v1/workbench/workspace/ws_default/events"
|
||||
});
|
||||
});
|
||||
|
||||
test("cloud web proxies Code Agent steer through the Web-equivalent public API path", () => {
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/agent/chat/steer"), {
|
||||
proxy: true,
|
||||
authRequired: false,
|
||||
publicRoute: true,
|
||||
routeKey: "POST /v1/agent/chat/steer"
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,31 +109,16 @@ export function proxyHttpRequest({ request, response, upstream, timeoutMs = DEFA
|
||||
const target = new URL(request.url || "/", upstream);
|
||||
const traceId = request.headers["x-trace-id"] ?? null;
|
||||
let timedOut = false;
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
proxy.destroy(new Error(`upstream timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
const proxy = httpRequest(
|
||||
target,
|
||||
{
|
||||
method: request.method,
|
||||
headers: {
|
||||
...request.headers,
|
||||
host: target.host
|
||||
}
|
||||
},
|
||||
(upstreamResponse) => {
|
||||
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
||||
upstreamResponse.on("end", () => clearTimeout(timeout));
|
||||
upstreamResponse.on("close", () => clearTimeout(timeout));
|
||||
upstreamResponse.pipe(response);
|
||||
}
|
||||
);
|
||||
let settled = false;
|
||||
|
||||
proxy.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
if (settled || response.headersSent) {
|
||||
let timeout = null;
|
||||
const clearProxyTimeout = () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = null;
|
||||
};
|
||||
const sendProxyFailure = (error) => {
|
||||
clearProxyTimeout();
|
||||
if (settled) return;
|
||||
if (response.headersSent) {
|
||||
response.destroy(error);
|
||||
return;
|
||||
}
|
||||
@@ -170,7 +155,37 @@ export function proxyHttpRequest({ request, response, upstream, timeoutMs = DEFA
|
||||
traceId,
|
||||
message: userMessage
|
||||
});
|
||||
});
|
||||
};
|
||||
const proxy = httpRequest(
|
||||
target,
|
||||
{
|
||||
method: request.method,
|
||||
headers: {
|
||||
...request.headers,
|
||||
host: target.host
|
||||
}
|
||||
},
|
||||
(upstreamResponse) => {
|
||||
if (settled) {
|
||||
upstreamResponse.resume();
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
||||
upstreamResponse.on("end", clearProxyTimeout);
|
||||
upstreamResponse.on("close", clearProxyTimeout);
|
||||
upstreamResponse.pipe(response);
|
||||
}
|
||||
);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
const error = new Error(`upstream timed out after ${timeoutMs}ms`);
|
||||
sendProxyFailure(error);
|
||||
proxy.destroy(error);
|
||||
}, timeoutMs);
|
||||
|
||||
proxy.on("error", sendProxyFailure);
|
||||
|
||||
request.pipe(proxy);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ test("cloud web route policy proxies public Code Agent chat without gating other
|
||||
publicRoute: true,
|
||||
routeKey: "POST /v1/agent/chat/cancel"
|
||||
});
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/agent/chat/steer"), {
|
||||
proxy: true,
|
||||
authRequired: false,
|
||||
publicRoute: true,
|
||||
routeKey: "POST /v1/agent/chat/steer"
|
||||
});
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/m3/io"), {
|
||||
proxy: true,
|
||||
authRequired: true,
|
||||
|
||||
Reference in New Issue
Block a user