fix: proxy code agent steer through cloud web

This commit is contained in:
Codex
2026-06-02 10:48:50 +08:00
parent 506397b08e
commit 8e9bea487d
5 changed files with 61 additions and 27 deletions
+3 -1
View File
@@ -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 固定复现再修实现。
+3 -1
View File
@@ -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"
});
});
+40 -25
View File
@@ -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);
}
+6
View File
@@ -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,