fix: integrate Code Agent readiness state

fix: integrate Code Agent readiness state
This commit is contained in:
Lyon
2026-05-22 23:30:51 +08:00
committed by GitHub
8 changed files with 739 additions and 102 deletions
+70 -4
View File
@@ -10,6 +10,7 @@ const DEFAULT_CODE_AGENT_TIMEOUT_MS = 120000;
const DEFAULT_CODEX_COMMAND = "codex";
const DEFAULT_MODEL = "gpt-5.2";
const DEFAULT_PROJECT_ID = "prj_hwlab-cloud-workbench";
const CODE_AGENT_PROVIDER_SECRET_REF = "hwlab-code-agent-provider/openai-api-key";
const CODE_AGENT_SYSTEM_PROMPT = [
"你是 HWLAB 云工作台的 Code Agent。",
"请用中文直接回答用户的工作台问题。",
@@ -52,6 +53,14 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
now: options.now,
callProvider: options.callProvider
});
const content = typeof providerResult.content === "string" ? providerResult.content.trim() : "";
if (!content) {
throw providerUnavailable("Code Agent provider returned no assistant text", {
provider: providerResult.provider ?? base.provider,
model: providerResult.model ?? base.model,
backend: providerResult.backend ?? base.backend
});
}
const completedAt = nowIso(options.now);
return {
...base,
@@ -63,7 +72,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
reply: {
messageId,
role: "assistant",
content: providerResult.content,
content,
createdAt: completedAt
},
usage: providerResult.usage ?? null,
@@ -71,12 +80,16 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
};
} catch (error) {
const failedAt = nowIso(options.now);
return {
const payload = {
...base,
status: "failed",
updatedAt: failedAt,
error: normalizeChatError(error)
};
if (error.code === "provider_unavailable") {
payload.availability = describeCodeAgentAvailability(options.env ?? process.env, options);
}
return payload;
}
}
@@ -107,11 +120,64 @@ export function validateCodeAgentChatSchema(payload) {
if (payload.status === "failed" && typeof payload.error?.message !== "string") {
throw new Error("failed code agent chat response must include error.message");
}
if (payload.status === "completed" && typeof payload.reply?.content !== "string") {
throw new Error("completed code agent chat response must include reply.content");
if (payload.status === "completed" && (typeof payload.reply?.content !== "string" || !payload.reply.content.trim())) {
throw new Error("completed code agent chat response must include non-empty reply.content");
}
}
export function describeCodeAgentAvailability(env = process.env, options = {}) {
const providerPlan = resolveProviderPlan(env, options);
const missingEnv = [];
if (!env.OPENAI_API_KEY) {
missingEnv.push("OPENAI_API_KEY");
}
const blocked = missingEnv.length > 0;
return {
endpoint: "POST /v1/agent/chat",
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
mode: providerPlan.mode,
schema: [
"conversationId",
"sessionId",
"messageId",
"status",
"createdAt",
"updatedAt",
"traceId",
"provider",
"model",
"backend",
"error.message",
"error.code",
"error.missingEnv",
"availability.status"
],
status: blocked ? "blocked" : "available",
blocker: blocked ? "凭证缺口" : null,
reason: blocked ? "provider_unavailable" : null,
summary: blocked
? `真实后端已接入,但当前 DEV provider Secret ${CODE_AGENT_PROVIDER_SECRET_REF} 未注入,因此不能返回真实回复。`
: "真实后端已接入,发送会走真实 /v1/agent/chat;只有实际 completed 回复才标记 DEV-LIVE。",
missingEnv,
secretRefs: blocked
? [
{
env: "OPENAI_API_KEY",
secretName: "hwlab-code-agent-provider",
secretKey: "openai-api-key",
present: false,
redacted: true
}
]
: [],
ready: !blocked
};
}
function resolveProviderPlan(env, options = {}) {
const provider = String(env.HWLAB_CODE_AGENT_PROVIDER || "auto").trim().toLowerCase();
const model = firstNonEmpty(
+4 -18
View File
@@ -12,7 +12,7 @@ import {
createErrorEnvelope,
handleJsonRpcRequest
} from "./json-rpc.mjs";
import { handleCodeAgentChat } from "./code-agent-chat.mjs";
import { describeCodeAgentAvailability, handleCodeAgentChat } from "./code-agent-chat.mjs";
import { buildDbRuntimeReadiness } from "./db-contract.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
@@ -41,6 +41,7 @@ export async function buildHealthPayload(options = {}) {
const imageReference = process.env.HWLAB_IMAGE || "unknown";
const imageTag = process.env.HWLAB_IMAGE_TAG || commitId.slice(0, 7) || "unknown";
const db = await buildDbRuntimeReadiness(process.env, options.dbProbe);
const codeAgent = describeCodeAgentAvailability(options.env ?? process.env, options);
return {
serviceId,
@@ -64,6 +65,7 @@ export async function buildHealthPayload(options = {}) {
endpoint: process.env.HWLAB_PUBLIC_ENDPOINT || DEV_ENDPOINT,
observedAt: new Date().toISOString(),
db,
codeAgent,
runtime: options.runtimeStore?.summary?.() ?? createCloudRuntimeStore().summary()
};
}
@@ -147,23 +149,7 @@ async function handleRestAdapter(request, response, url, options) {
adapter: "rest",
status: "degraded",
rpcBridge: "POST /v1/rpc/{method}",
codeAgent: {
endpoint: "POST /v1/agent/chat",
status: "available",
schema: [
"conversationId",
"sessionId",
"messageId",
"status",
"createdAt",
"updatedAt",
"traceId",
"provider",
"model",
"backend",
"error.message"
]
},
codeAgent: describeCodeAgentAvailability(options.env ?? process.env, options),
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db: await buildDbRuntimeReadiness(process.env, options.dbProbe),
+107 -1
View File
@@ -10,7 +10,13 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
delete process.env.HWLAB_CLOUD_DB_URL;
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
const server = createCloudApiServer();
const server = createCloudApiServer({
env: {
PATH: "",
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
HWLAB_CODE_AGENT_MODEL: "gpt-test"
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
@@ -36,6 +42,14 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
assert.equal(healthPayload.db.safety.liveDbEvidence, false);
assert.equal(healthPayload.runtime.durable, false);
assert.equal(healthPayload.runtime.status, "degraded");
assert.equal(healthPayload.codeAgent.status, "blocked");
assert.equal(healthPayload.codeAgent.blocker, "凭证缺口");
assert.equal(healthPayload.codeAgent.reason, "provider_unavailable");
assert.deepEqual(healthPayload.codeAgent.missingEnv, ["OPENAI_API_KEY"]);
assert.equal(healthPayload.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider");
assert.equal(healthPayload.codeAgent.secretRefs[0].secretKey, "openai-api-key");
assert.equal(healthPayload.codeAgent.secretRefs[0].redacted, true);
assert.equal(JSON.stringify(healthPayload.codeAgent).includes("sk-"), false);
assert.deepEqual(healthPayload.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
assert.equal(healthPayload.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db");
assert.equal(healthPayload.db.secretRefs[0].redacted, true);
@@ -47,6 +61,8 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
assert.equal(healthLivePayload.status, "degraded");
assert.equal(healthLivePayload.commit.id.length > 0, true);
assert.equal(healthLivePayload.db.ready, false);
assert.equal(healthLivePayload.codeAgent.status, "blocked");
assert.equal(healthLivePayload.codeAgent.ready, false);
const readiness = await fetch(`http://127.0.0.1:${port}/health/live`);
assert.equal(readiness.status, 200);
@@ -179,6 +195,45 @@ test("cloud api health does not treat DB env presence-only as live readiness", a
}
});
test("cloud api /v1 describes Code Agent provider blocker without leaking secret values", async () => {
const server = createCloudApiServer({
env: {
PATH: "",
HWLAB_CODE_AGENT_PROVIDER: "openai",
HWLAB_CODE_AGENT_MODEL: "gpt-test"
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1`);
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.codeAgent.endpoint, "POST /v1/agent/chat");
assert.equal(payload.codeAgent.provider, "openai-responses");
assert.equal(payload.codeAgent.model, "gpt-test");
assert.equal(payload.codeAgent.backend, "hwlab-cloud-api/openai-responses");
assert.equal(payload.codeAgent.mode, "openai");
assert.equal(payload.codeAgent.status, "blocked");
assert.equal(payload.codeAgent.blocker, "凭证缺口");
assert.equal(payload.codeAgent.reason, "provider_unavailable");
assert.deepEqual(payload.codeAgent.missingEnv, ["OPENAI_API_KEY"]);
assert.equal(payload.codeAgent.secretRefs[0].env, "OPENAI_API_KEY");
assert.equal(payload.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider");
assert.equal(payload.codeAgent.secretRefs[0].secretKey, "openai-api-key");
assert.equal(payload.codeAgent.secretRefs[0].redacted, true);
assert.equal(payload.codeAgent.ready, false);
assert.match(payload.codeAgent.summary, /真实后端已接入/u);
assert.match(payload.codeAgent.summary, /hwlab-code-agent-provider\/openai-api-key/u);
assert.equal(JSON.stringify(payload).includes("sk-"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat returns structured completed Code Agent payload", async () => {
const server = createCloudApiServer({
callCodeAgentProvider: async ({ providerPlan, message, traceId }) => ({
@@ -220,6 +275,7 @@ test("cloud api /v1/agent/chat returns structured completed Code Agent payload",
assert.equal(Number.isNaN(Date.parse(payload.createdAt)), false);
assert.equal(Number.isNaN(Date.parse(payload.updatedAt)), false);
assert.match(payload.reply.content, /HWLAB 工作台/);
assert.equal(Object.hasOwn(payload, "error"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
@@ -262,6 +318,56 @@ test("cloud api /v1/agent/chat reports provider gaps without faking a reply", as
assert.match(payload.error.message, /Codex CLI command is not available/);
assert.deepEqual(payload.error.missingCommands, ["codex"]);
assert.ok(payload.error.missingEnv.includes("OPENAI_API_KEY"));
assert.equal(payload.availability.status, "blocked");
assert.equal(payload.availability.blocker, "凭证缺口");
assert.equal(payload.availability.reason, "provider_unavailable");
assert.equal(payload.availability.secretRefs[0].secretName, "hwlab-code-agent-provider");
assert.equal(payload.availability.secretRefs[0].secretKey, "openai-api-key");
assert.equal(payload.availability.secretRefs[0].redacted, true);
assert.match(payload.availability.summary, /真实后端已接入/u);
assert.match(payload.availability.summary, /hwlab-code-agent-provider\/openai-api-key/u);
assert.equal(JSON.stringify(payload).includes("sk-"), false);
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat does not complete on empty provider text", async () => {
const server = createCloudApiServer({
callCodeAgentProvider: async ({ providerPlan }) => ({
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
content: " ",
usage: null,
providerTrace: {
source: "empty-test-provider"
}
})
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
conversationId: "cnv_empty-provider-text",
message: "你好"
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.conversationId, "cnv_empty-provider-text");
assert.equal(payload.status, "failed");
assert.equal(payload.error.code, "provider_unavailable");
assert.match(payload.error.message, /no assistant text/);
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
+314 -60
View File
@@ -1,80 +1,334 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import http from "node:http";
import https from "node:https";
import {
handleCodeAgentChat,
validateCodeAgentChatSchema
} from "../internal/cloud/code-agent-chat.mjs";
const defaultLiveUrl = "http://74.48.78.17:16666/";
const defaultLiveMessage = "请用一句话回复 HWLAB Code Agent readiness。";
const args = parseArgs(process.argv.slice(2));
if (args.mode === "help") {
process.stdout.write(`${JSON.stringify(usage(), null, 2)}\n`);
} else if (args.mode === "live") {
const report = await runLiveReadinessSmoke(args);
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
process.exitCode = report.status === "pass" ? 0 : 2;
} else {
await runLocalContractSmoke();
}
function logOk(name) {
process.stdout.write(`[code-agent-chat-smoke] ok ${name}\n`);
}
const completed = await handleCodeAgentChat(
{
conversationId: "cnv_code-agent-chat-smoke",
traceId: "trc_code-agent-chat-smoke",
projectId: "prj_code-agent-chat-smoke",
message: "请用一句话说明当前 HWLAB 工作台可以做什么。"
},
{
now: () => "2026-05-22T00:00:00.000Z",
callProvider: async ({ providerPlan }) => ({
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
content: "HWLAB 工作台可以用 Code Agent 对话来整理任务、查看资源并保持硬件变更受控。",
usage: null,
providerTrace: {
source: "schema-smoke-provider"
async function runLocalContractSmoke() {
const completed = await handleCodeAgentChat(
{
conversationId: "cnv_code-agent-chat-smoke",
traceId: "trc_code-agent-chat-smoke",
projectId: "prj_code-agent-chat-smoke",
message: "请用一句话说明当前 HWLAB 工作台可以做什么。"
},
{
now: () => "2026-05-22T00:00:00.000Z",
callProvider: async ({ providerPlan }) => ({
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
content: "HWLAB 工作台可以用 Code Agent 对话来整理任务、查看资源并保持硬件变更受控。",
usage: null,
providerTrace: {
source: "schema-smoke-provider"
}
})
}
);
validateCodeAgentChatSchema(completed);
assert.equal(completed.conversationId, "cnv_code-agent-chat-smoke");
assert.equal(completed.sessionId, "cnv_code-agent-chat-smoke");
assert.match(completed.messageId, /^msg_/);
assert.equal(completed.status, "completed");
assert.equal(completed.createdAt, "2026-05-22T00:00:00.000Z");
assert.equal(completed.updatedAt, "2026-05-22T00:00:00.000Z");
assert.equal(completed.traceId, "trc_code-agent-chat-smoke");
assert.equal(completed.error, undefined);
assert.match(completed.reply.content, /Code Agent/);
logOk("completed schema");
const sourceReadiness = classifyCodeAgentChatReadiness(completed, { realDevLive: false });
assert.equal(sourceReadiness.status, "blocked");
assert.equal(sourceReadiness.level, "SOURCE");
assert.equal(sourceReadiness.devLiveReplyPass, false);
logOk("local stub completion is not #143 DEV-LIVE pass");
const failed = await handleCodeAgentChat(
{
traceId: "trc_code-agent-chat-smoke-failed",
message: "你好"
},
{
now: () => "2026-05-22T00:01:00.000Z",
env: {
PATH: "",
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
HWLAB_CODE_AGENT_MODEL: "gpt-test"
}
})
}
);
validateCodeAgentChatSchema(failed);
assert.match(failed.conversationId, /^cnv_/);
assert.match(failed.sessionId, /^cnv_/);
assert.match(failed.messageId, /^msg_/);
assert.equal(failed.status, "failed");
assert.equal(failed.createdAt, "2026-05-22T00:01:00.000Z");
assert.equal(failed.updatedAt, "2026-05-22T00:01:00.000Z");
assert.equal(failed.traceId, "trc_code-agent-chat-smoke-failed");
assert.equal(failed.provider, "codex-cli");
assert.equal(failed.model, "gpt-test");
assert.equal(failed.backend, "hwlab-cloud-api/codex-cli");
assert.equal(failed.error.code, "provider_unavailable");
assert.match(failed.error.message, /Codex CLI command is not available/);
assert.deepEqual(failed.error.missingCommands, ["codex"]);
assert.ok(failed.error.missingEnv.includes("OPENAI_API_KEY"));
assert.equal(failed.availability.status, "blocked");
assert.equal(failed.availability.blocker, "凭证缺口");
assert.equal(failed.availability.reason, "provider_unavailable");
assert.match(failed.availability.summary, /真实后端已接入/u);
assert.match(failed.availability.summary, /hwlab-code-agent-provider\/openai-api-key/u);
assert.equal(JSON.stringify(failed).includes("sk-"), false);
assert.equal(Object.hasOwn(failed, "reply"), false);
logOk("failed provider-gap schema");
const credentialReadiness = classifyCodeAgentChatReadiness(failed, { realDevLive: true });
assert.equal(credentialReadiness.status, "blocked");
assert.equal(credentialReadiness.level, "BLOCKED/credential");
assert.equal(credentialReadiness.blocker, "credential");
assert.equal(credentialReadiness.devLiveReplyPass, false);
logOk("provider credential blocker readiness");
process.stdout.write("[code-agent-chat-smoke] passed\n");
}
async function runLiveReadinessSmoke(args) {
const endpoint = agentChatEndpoint(args.url);
const traceId = protocolId("trc_code-agent-chat-live");
const conversationId = protocolId("cnv_code-agent-chat-live");
let httpStatus = null;
let payload = null;
let transportError = null;
try {
const response = await postJson(endpoint, {
headers: {
"x-trace-id": traceId
},
body: {
conversationId,
traceId,
projectId: "prj_hwlab-cloud-workbench",
message: args.message
}
});
httpStatus = response.status;
payload = response.json;
} catch (error) {
transportError = error instanceof Error ? error.message : String(error);
}
);
validateCodeAgentChatSchema(completed);
assert.equal(completed.conversationId, "cnv_code-agent-chat-smoke");
assert.equal(completed.sessionId, "cnv_code-agent-chat-smoke");
assert.match(completed.messageId, /^msg_/);
assert.equal(completed.status, "completed");
assert.equal(completed.createdAt, "2026-05-22T00:00:00.000Z");
assert.equal(completed.updatedAt, "2026-05-22T00:00:00.000Z");
assert.equal(completed.traceId, "trc_code-agent-chat-smoke");
assert.equal(completed.error, undefined);
assert.match(completed.reply.content, /Code Agent/);
logOk("completed schema");
const readiness = payload
? classifyCodeAgentChatReadiness(payload, { realDevLive: true, httpStatus })
: {
status: "blocked",
level: "BLOCKED",
blocker: "transport",
devLiveReplyPass: false,
reason: "real DEV /v1/agent/chat did not return a JSON Code Agent payload"
};
const failed = await handleCodeAgentChat(
{
traceId: "trc_code-agent-chat-smoke-failed",
message: "你好"
},
{
now: () => "2026-05-22T00:01:00.000Z",
env: {
PATH: "",
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
HWLAB_CODE_AGENT_MODEL: "gpt-test"
if (payload) {
try {
validateCodeAgentChatSchema(payload);
} catch (error) {
readiness.status = "blocked";
readiness.level = "BLOCKED";
readiness.blocker = "schema";
readiness.devLiveReplyPass = false;
readiness.reason = error instanceof Error ? error.message : String(error);
}
}
);
validateCodeAgentChatSchema(failed);
assert.match(failed.conversationId, /^cnv_/);
assert.match(failed.sessionId, /^cnv_/);
assert.match(failed.messageId, /^msg_/);
assert.equal(failed.status, "failed");
assert.equal(failed.createdAt, "2026-05-22T00:01:00.000Z");
assert.equal(failed.updatedAt, "2026-05-22T00:01:00.000Z");
assert.equal(failed.traceId, "trc_code-agent-chat-smoke-failed");
assert.equal(failed.provider, "codex-cli");
assert.equal(failed.model, "gpt-test");
assert.equal(failed.backend, "hwlab-cloud-api/codex-cli");
assert.equal(failed.error.code, "provider_unavailable");
assert.match(failed.error.message, /Codex CLI command is not available/);
assert.deepEqual(failed.error.missingCommands, ["codex"]);
assert.ok(failed.error.missingEnv.includes("OPENAI_API_KEY"));
assert.equal(Object.hasOwn(failed, "reply"), false);
logOk("failed provider-gap schema");
return {
status: readiness.status,
mode: "live-readiness",
url: endpoint.href,
httpStatus,
readiness,
response: summarizePayload(payload),
...(transportError ? { transportError } : {}),
safety: {
prodTouched: false,
servicesRestarted: false,
readsSecretValues: false,
printsAssistantReply: false,
statement: "This smoke posts only to the configured DEV Code Agent chat endpoint and prints reply presence, not reply content or secret values."
}
};
}
process.stdout.write("[code-agent-chat-smoke] passed\n");
function classifyCodeAgentChatReadiness(payload, { realDevLive = false, httpStatus = null } = {}) {
const assistantReply = typeof payload?.reply?.content === "string" ? payload.reply.content.trim() : "";
if (payload?.status === "completed" && assistantReply) {
if (realDevLive) {
return {
status: "pass",
level: "#143 DEV-LIVE reply pass",
blocker: null,
devLiveReplyPass: true,
reason: "real DEV /v1/agent/chat completed with a non-empty assistant reply"
};
}
return {
status: "blocked",
level: "SOURCE",
blocker: "not-dev-live",
devLiveReplyPass: false,
reason: "mock, fixture, local stub, or source-only completion cannot be counted as #143 DEV-LIVE reply pass"
};
}
const missingEnv = Array.isArray(payload?.error?.missingEnv) ? payload.error.missingEnv : [];
if (payload?.status === "failed" && payload.error?.code === "provider_unavailable" && missingEnv.includes("OPENAI_API_KEY")) {
return {
status: "blocked",
level: "BLOCKED/credential",
blocker: "credential",
devLiveReplyPass: false,
reason: "provider_unavailable with missing OPENAI_API_KEY means the provider credential is absent"
};
}
return {
status: "blocked",
level: "BLOCKED",
blocker: "runtime",
devLiveReplyPass: false,
reason: `Code Agent chat did not produce a completed non-empty assistant reply${httpStatus ? ` (HTTP ${httpStatus})` : ""}`
};
}
function summarizePayload(payload) {
const assistantReply = typeof payload?.reply?.content === "string" ? payload.reply.content.trim() : "";
const error = payload?.error
? {
code: payload.error.code,
missingEnv: payload.error.missingEnv,
missingCommands: payload.error.missingCommands,
providerStatus: payload.error.providerStatus
}
: null;
return {
status: payload?.status ?? null,
provider: payload?.provider ?? null,
model: payload?.model ?? null,
backend: payload?.backend ?? null,
assistantReplyNonEmpty: assistantReply.length > 0,
assistantReplyLength: assistantReply.length,
error
};
}
function parseArgs(argv) {
const parsed = { mode: "local", url: defaultLiveUrl, message: defaultLiveMessage };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--live") {
parsed.mode = "live";
} else if (arg === "--url") {
index += 1;
if (!argv[index]) throw new Error("--url requires a value");
parsed.url = argv[index];
} else if (arg === "--message") {
index += 1;
if (!argv[index]) throw new Error("--message requires a value");
parsed.message = argv[index];
} else if (arg === "--help") {
return { mode: "help" };
} else {
throw new Error(`unknown argument ${arg}`);
}
}
return parsed;
}
function usage() {
return {
status: "usage",
command: "node scripts/code-agent-chat-smoke.mjs [--live --url http://74.48.78.17:16666/]",
notes: [
"Default mode is local schema/readiness contract only and cannot pass #143 DEV-LIVE.",
"--live posts a minimal chat prompt to real DEV and passes only on completed plus non-empty assistant reply.",
"provider_unavailable with missing OPENAI_API_KEY is reported as BLOCKED/credential."
]
};
}
function agentChatEndpoint(value) {
const url = new URL(value);
if (url.pathname.endsWith("/v1/agent/chat")) return url;
return new URL("/v1/agent/chat", url);
}
function postJson(url, { headers = {}, body, timeoutMs = 10000 }) {
const client = url.protocol === "https:" ? https : http;
const payload = JSON.stringify(body);
return new Promise((resolve, reject) => {
const request = client.request(
url,
{
method: "POST",
headers: {
"content-type": "application/json",
"content-length": Buffer.byteLength(payload),
...headers
},
insecureHTTPParser: true,
timeout: timeoutMs
},
(response) => {
const chunks = [];
response.on("data", (chunk) => chunks.push(chunk));
response.on("end", () => {
const text = Buffer.concat(chunks).toString("utf8");
resolve({
status: response.statusCode ?? null,
body: text,
json: parseJsonOrNull(text)
});
});
}
);
request.on("timeout", () => request.destroy(new Error(`request timed out after ${timeoutMs}ms`)));
request.on("error", reject);
request.end(payload);
});
}
function parseJsonOrNull(value) {
try {
return JSON.parse(value);
} catch {
return null;
}
}
function protocolId(prefix) {
return `${prefix}_${Date.now().toString(36)}`;
}
+61 -1
View File
@@ -7,6 +7,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
import { validateCodeAgentChatSchema } from "../internal/cloud/code-agent-chat.mjs";
import { runCli } from "../tools/hwlab-cli/lib/cli.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -141,7 +142,11 @@ async function smokeCloudApi() {
HWLAB_CLOUD_API_HOST: "127.0.0.1",
HWLAB_CLOUD_API_PORT: String(port),
HWLAB_CLOUD_DB_URL: "",
HWLAB_CLOUD_DB_SSL_MODE: ""
HWLAB_CLOUD_DB_SSL_MODE: "",
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
HWLAB_CODE_AGENT_MODEL: "gpt-m1-local",
OPENAI_API_KEY: "",
PATH: ""
});
const baseUrl = `http://127.0.0.1:${port}`;
@@ -162,6 +167,14 @@ async function smokeCloudApi() {
assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
assert.equal(health.body.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db");
assert.equal(health.body.db.secretRefs[0].redacted, true);
assert.equal(health.body.codeAgent.status, "blocked");
assert.equal(health.body.codeAgent.blocker, "凭证缺口");
assert.equal(health.body.codeAgent.reason, "provider_unavailable");
assert.ok(health.body.codeAgent.missingEnv.includes("OPENAI_API_KEY"));
assert.equal(health.body.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider");
assert.equal(health.body.codeAgent.secretRefs[0].secretKey, "openai-api-key");
assert.equal(health.body.codeAgent.secretRefs[0].redacted, true);
assert.equal(JSON.stringify(health.body.codeAgent).includes("sk-"), false);
const live = await requestJson(`${baseUrl}/live`);
assert.equal(live.response.status, 200);
@@ -176,6 +189,53 @@ async function smokeCloudApi() {
assert.ok(adapter.body.methods.includes("hardware.operation.request"));
assert.ok(adapter.body.methods.includes("audit.event.query"));
assert.ok(adapter.body.methods.includes("evidence.record.query"));
assert.equal(adapter.body.codeAgent.endpoint, "POST /v1/agent/chat");
assert.equal(adapter.body.codeAgent.status, "blocked");
assert.equal(adapter.body.codeAgent.blocker, "凭证缺口");
assert.equal(adapter.body.codeAgent.provider, "codex-cli");
assert.equal(adapter.body.codeAgent.model, "gpt-m1-local");
assert.equal(adapter.body.codeAgent.backend, "hwlab-cloud-api/codex-cli");
assert.equal(adapter.body.codeAgent.ready, false);
assert.ok(adapter.body.codeAgent.missingEnv.includes("OPENAI_API_KEY"));
assert.equal(adapter.body.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider");
assert.equal(adapter.body.codeAgent.secretRefs[0].secretKey, "openai-api-key");
assert.equal(adapter.body.codeAgent.secretRefs[0].redacted, true);
assert.match(adapter.body.codeAgent.summary, /hwlab-code-agent-provider\/openai-api-key/u);
assert.equal(JSON.stringify(adapter.body.codeAgent).includes("sk-"), false);
const agentChat = await requestJson(`${baseUrl}/v1/agent/chat`, {
method: "POST",
body: JSON.stringify({
conversationId: "cnv_m1_local_chat",
traceId: "trc_m1_local_chat",
projectId: "prj_m1_local_contract",
message: "请用一句话说明本地 SOURCE/LOCAL 合同。"
})
});
assert.equal(agentChat.response.status, 200);
validateCodeAgentChatSchema(agentChat.body);
assert.equal(agentChat.body.conversationId, "cnv_m1_local_chat");
assert.equal(agentChat.body.sessionId, "cnv_m1_local_chat");
assert.match(agentChat.body.messageId, /^msg_/);
assert.equal(agentChat.body.status, "failed");
assert.equal(agentChat.body.traceId, "trc_m1_local_chat");
assert.equal(agentChat.body.projectId, "prj_m1_local_contract");
assert.equal(agentChat.body.provider, "codex-cli");
assert.equal(agentChat.body.model, "gpt-m1-local");
assert.equal(agentChat.body.backend, "hwlab-cloud-api/codex-cli");
assert.equal(agentChat.body.error.code, "provider_unavailable");
assert.match(agentChat.body.error.message, /Codex CLI command is not available/);
assert.deepEqual(agentChat.body.error.missingCommands, ["codex"]);
assert.ok(agentChat.body.error.missingEnv.includes("OPENAI_API_KEY"));
assert.equal(agentChat.body.availability.status, "blocked");
assert.equal(agentChat.body.availability.blocker, "凭证缺口");
assert.equal(agentChat.body.availability.reason, "provider_unavailable");
assert.equal(agentChat.body.availability.secretRefs[0].secretName, "hwlab-code-agent-provider");
assert.equal(agentChat.body.availability.secretRefs[0].secretKey, "openai-api-key");
assert.equal(agentChat.body.availability.secretRefs[0].redacted, true);
assert.match(agentChat.body.availability.summary, /hwlab-code-agent-provider\/openai-api-key/u);
assert.equal(JSON.stringify(agentChat.body).includes("sk-"), false);
assert.equal(Object.hasOwn(agentChat.body, "reply"), false);
const rpcHealth = await requestJson(`${baseUrl}/rpc`, {
method: "POST",
@@ -207,6 +207,11 @@ function runStaticSmoke() {
evidence: ["command-send", "agent-chat-status", "/v1/agent/chat", "conversationId/messageId/status/timestamps/error.message"]
});
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider credential blockers and only real completed replies can become DEV-LIVE.", {
blocker: "observability_blocker",
evidence: ["BLOCKED 凭证缺口", "provider_unavailable", "OPENAI_API_KEY", "hwlab-code-agent-provider/openai-api-key", "completed -> dev-live guard"]
});
addCheck(checks, blockers, "no-hardware-write-api", noForbiddenWriteSurface(source), "Workbench source does not expose hardware write APIs or live mutation commands.", {
blocker: "safety_blocker",
evidence: forbiddenWritePatterns.map(String)
@@ -475,6 +480,29 @@ function hasCodeAgentChatContract({ html, app }) {
);
}
function hasCodeAgentReadinessVisibility({ html, app }) {
const source = `${html}\n${app}`;
return (
/codeAgentAvailability/u.test(app) &&
/codeAgentAvailabilityFrom/u.test(app) &&
/latestCompletedAgentMessage/u.test(app) &&
/BLOCKED 凭证缺口/u.test(app) &&
/provider_unavailable/u.test(app) &&
/OPENAI_API_KEY/u.test(app) &&
/hwlab-code-agent-provider\/openai-api-key/u.test(app) &&
/只有真实 completed 回复才标 DEV-LIVE/u.test(app) &&
/不能因为只有 conversationId 标为开发实况/u.test(app) &&
/Boolean\(message\.provider\)/u.test(app) &&
/Boolean\(message\.model\)/u.test(app) &&
/Boolean\(message\.backend\)/u.test(app) &&
/status === "completed" \? "dev-live"/u.test(app) &&
!/status === "failed" \? "dev-live"/u.test(app) &&
!/tone:\s*state\.conversationId\s*\?\s*"dev-live"/u.test(app) &&
!/provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu.test(source) &&
!/sk-[A-Za-z0-9._-]{8,}/u.test(source)
);
}
function noForbiddenWriteSurface(source) {
return forbiddenWritePatterns.every((pattern) => !pattern.test(source));
}
+138 -18
View File
@@ -13,6 +13,7 @@ const rpcReadMethods = Object.freeze([
const STATUS_LABELS = Object.freeze({
active: "活动",
available: "可用",
blocked: "阻塞 BLOCKED",
completed: "完成",
connected: "已连接",
degraded: "降级",
@@ -33,8 +34,7 @@ const STATUS_LABELS = Object.freeze({
requires: "依赖",
source: "来源 SOURCE",
"dry-run": "演练 DRY-RUN",
"dev-live": "开发实况 DEV-LIVE",
blocked: "阻塞 BLOCKED"
"dev-live": "开发实况 DEV-LIVE"
});
const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view));
@@ -77,6 +77,7 @@ const el = {
const state = {
conversationId: null,
codeAgentAvailability: null,
chatMessages: [],
chatPending: false
};
@@ -247,8 +248,12 @@ function initCommandBar() {
model: result.model,
backend: result.backend,
updatedAt: result.updatedAt,
error: result.error
error: result.error,
availability: result.availability
};
if (result.availability) {
state.codeAgentAvailability = result.availability;
}
if (result.status !== "completed") {
el.commandInput.value = value;
}
@@ -441,14 +446,18 @@ function renderLiveSurface(live) {
el.liveStatus.textContent = reachable ? "只读连接可用" : "离线查看";
el.liveStatus.className = `tone-${toneClass(reachable ? "dev-live" : "source")}`;
state.codeAgentAvailability = codeAgentAvailabilityFrom(live);
el.liveDetail.textContent = reachable
? "同源只读接口可响应。技术复核详情已放在二级页面。"
? codeAgentAvailabilitySummary()
: "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。";
renderAgentChatStatus(deriveAgentChatStatus());
renderHardwareStatus(runtimeSummary);
renderRecords(live);
renderDiagnostics(live);
renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded");
renderConversation();
renderDrafts();
}
function renderResourceTree() {
@@ -540,12 +549,7 @@ function renderConversation() {
text: "这里是用户工作台:可以整理任务、查看资源、核对接线和可信记录。对话会发送到受控 Code Agent 后端;页面不会直接发送硬件变更。",
status: "source"
},
{
role: "agent",
title: "Code Agent",
text: "请在底部输入中文消息并点击发送,我会通过 cloud-api 调用真实 Code Agent / Codex 能力返回回复。",
status: "source"
}
codeAgentStatusMessage(state.codeAgentAvailability)
];
replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard));
}
@@ -648,10 +652,8 @@ function controlRows() {
},
{
title: "Code Agent 对话",
detail: state.conversationId
? `当前会话 ${state.conversationId};回复来自 cloud-api 受控 Code Agent 接口。`
: "输入区会调用 cloud-api 受控 Code Agent 接口,不会直接调用硬件写入方法。",
tone: state.conversationId ? "dev-live" : "source"
detail: codeAgentControlSummary(state.codeAgentAvailability),
tone: currentConversationTone()
},
{
title: "只读探测",
@@ -672,8 +674,8 @@ function renderDrafts() {
? [
{
title: "对话状态",
detail: "暂无消息。输入区会发送到 Code Agent 后端,失败时会保留输入内容。",
tone: "source"
detail: codeAgentPromptText(state.codeAgentAvailability),
tone: state.codeAgentAvailability?.status === "blocked" ? "blocked" : "source"
}
]
: []),
@@ -860,10 +862,11 @@ function renderAgentChatStatus(status, result = null) {
idle: "等待输入",
running: "处理中",
completed: "已回复",
failed: "发送失败"
failed: "发送失败",
blocked: "BLOCKED 凭证缺口"
};
el.agentChatStatus.textContent = labels[status] ?? statusLabel(status);
el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : status === "failed" ? "blocked" : "source")}`;
el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : status === "failed" || status === "blocked" ? "blocked" : "source")}`;
el.commandInput.disabled = status === "running";
el.commandSend.disabled = status === "running";
el.commandSend.textContent = status === "running" ? "发送中" : "发送";
@@ -879,6 +882,13 @@ function sourceTitle(result) {
}
function failureMessage(result) {
if (result?.error?.code === "provider_unavailable" || result?.availability?.status === "blocked") {
const availability = result.availability ?? codeAgentAvailabilityFromResult(result);
const missing = Array.isArray(result.error?.missingEnv) && result.error.missingEnv.length > 0
? ` 当前缺口:${result.error.missingEnv.join("、")}`
: "";
return `${availability.summary}${missing}`;
}
const missing = [
...(result.error?.missingCommands ?? []),
...(result.error?.missingEnv ?? [])
@@ -985,6 +995,116 @@ function messageFromPayload(payload) {
return payload.error?.message || payload.error?.code || payload.message || JSON.stringify(payload).slice(0, 160);
}
function codeAgentAvailabilityFrom(live) {
return live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? null;
}
function codeAgentAvailabilityFromResult(result) {
return {
status: "blocked",
blocker: "凭证缺口",
reason: result?.error?.code ?? "provider_unavailable",
summary: "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。",
missingEnv: result?.error?.missingEnv ?? [],
secretRefs: [
{
env: "OPENAI_API_KEY",
secretName: "hwlab-code-agent-provider",
secretKey: "openai-api-key",
redacted: true
}
]
};
}
function codeAgentStatusMessage(availability) {
if (availability?.status === "blocked") {
return {
role: "system",
title: "Code Agent 状态:BLOCKED 凭证缺口",
text: codeAgentBlockedSummary(availability),
status: "blocked"
};
}
if (availability?.status === "available") {
return {
role: "system",
title: "Code Agent 状态:真实通道已接入",
text: "真实 /v1/agent/chat 后端已接入;发送后只有实际 completed 回复才会标记为 DEV-LIVE。",
status: "source"
};
}
return {
role: "system",
title: "Code Agent 状态:等待只读探测",
text: "正在读取 same-origin /v1 与 /health/live;首屏不会宣称 Code Agent 可用,也不会把失败或静态内容标成 DEV-LIVE。",
status: "source"
};
}
function codeAgentPromptText(availability) {
if (availability?.status === "blocked") {
return "可以继续输入并保留草稿;发送会走真实 /v1/agent/chat,但当前 DEV provider 凭证缺口会返回结构化失败,不会冒充真实 Codex 回复。";
}
return "请在底部输入中文消息并点击发送;完成态只来自真实 /v1/agent/chat completed 回复。";
}
function codeAgentControlSummary(availability) {
if (latestCompletedAgentMessage()) {
return state.conversationId
? `当前会话 ${state.conversationId};最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口。`
: "最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口。";
}
if (availability?.status === "blocked") {
return codeAgentBlockedSummary(availability);
}
return "输入区会调用 cloud-api 受控 Code Agent 接口;只有真实 completed 回复才标 DEV-LIVE,不能因为只有 conversationId 标为开发实况。";
}
function codeAgentBlockedSummary(availability) {
return availability?.summary ?? "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。";
}
function codeAgentAvailabilitySummary() {
if (state.codeAgentAvailability?.status === "blocked") {
return "同源 API 可响应;Code Agent 为 BLOCKED/凭证缺口,真实后端已接入但 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入。";
}
if (state.codeAgentAvailability?.status === "available") {
return "同源 API 可响应;Code Agent 真实通道已接入,只有 completed 回复才标 DEV-LIVE。";
}
return "同源只读接口可响应。技术复核详情已放在二级页面。";
}
function latestCompletedAgentMessage() {
return [...state.chatMessages].reverse().find((message) =>
message.role === "agent" &&
message.status === "completed" &&
!message.error &&
Boolean(message.provider) &&
Boolean(message.model) &&
Boolean(message.backend)
) ?? null;
}
function latestChatResult() {
return [...state.chatMessages].reverse().find((message) => message.role === "agent") ?? null;
}
function deriveAgentChatStatus() {
if (state.chatPending) return "running";
const latest = latestChatResult();
if (latest?.status === "completed") return "completed";
if (latest?.status === "failed") return latest.error?.code === "provider_unavailable" ? "blocked" : "failed";
if (state.codeAgentAvailability?.status === "blocked") return "blocked";
return "idle";
}
function currentConversationTone() {
if (latestCompletedAgentMessage()) return "dev-live";
if (state.codeAgentAvailability?.status === "blocked" || latestChatResult()?.status === "failed") return "blocked";
return "source";
}
function statusLabel(value) {
const key = String(value ?? "unknown").trim();
const normalized = key.toLowerCase();
+17
View File
@@ -183,6 +183,23 @@ assert.doesNotMatch(html, /主流程[\s\S]*添加草稿/u);
assert.match(app, /sendAgentMessage/);
assert.match(app, /fetchJson\("\/v1\/agent\/chat"/);
assert.match(app, /Code Agent 调用失败/);
assert.match(app, /codeAgentAvailability/);
assert.match(app, /codeAgentAvailabilityFrom/);
assert.match(app, /latestCompletedAgentMessage/);
assert.match(app, /BLOCKED 凭证缺口/);
assert.match(app, /provider_unavailable/);
assert.match(app, /OPENAI_API_KEY/);
assert.match(app, /hwlab-code-agent-provider\/openai-api-key/);
assert.match(app, /只有真实 completed 回复才标 DEV-LIVE/);
assert.match(app, /不能因为只有 conversationId 标为开发实况/);
assert.match(app, /Boolean\(message\.provider\)/);
assert.match(app, /Boolean\(message\.model\)/);
assert.match(app, /Boolean\(message\.backend\)/);
assert.match(app, /status === "completed" \? "dev-live"/);
assert.doesNotMatch(app, /status === "failed" \? "dev-live"/);
assert.doesNotMatch(`${html}\n${app}`, /provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu);
assert.doesNotMatch(app, /tone:\s*state\.conversationId\s*\?\s*"dev-live"/);
assert.doesNotMatch(`${html}\n${app}`, /sk-[A-Za-z0-9._-]{8,}/u);
assert.match(app, /state\.conversationId/);
assert.match(app, /conversationId/);
assert.match(app, /messageId/);