Files
pikasTech-HWLAB/internal/cloud/gate-diagnostics.ts
T

321 lines
13 KiB
TypeScript

import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs";
import { describeCodeAgentAvailability } from "./code-agent-chat.ts";
import {
applyRuntimeDbReadinessLayers,
buildDbRuntimeReadiness
} from "./db-contract.ts";
import { buildCloudApiReadiness } from "./health-contract.ts";
import {
M3_IO_CONTROL_ROUTE,
describeM3IoControlLive
} from "./m3-io-control.ts";
const STATUS = Object.freeze({
pass: "通过",
blocked: "阻塞",
failed: "失败",
pending: "待验证",
info: "信息"
});
export async function buildGateDiagnosticsRows(options = {}) {
const observedAt = new Date().toISOString();
const rows = [];
const health = await observeHealth(options, observedAt);
const m3 = await observeM3(options, observedAt);
const audit = await observeAudit(options, observedAt);
const evidence = await observeEvidence(options, observedAt);
rows.push(...health.rows, m3.row, audit.row, evidence.row);
const status = rows.some((row) => row.statusKey === "failed")
? "failed"
: rows.some((row) => row.statusKey === "blocked")
? "blocked"
: rows.some((row) => row.statusKey === "pending")
? "pending"
: "pass";
return {
serviceId: CLOUD_API_SERVICE_ID,
route: "/v1/diagnostics/gate",
contractVersion: "gate-diagnostics-table-v1",
status,
sourceKind: "LIVE-BACKEND",
liveBackend: true,
observedAt,
rowCount: rows.length,
columns: ["类别", "检查项", "状态", "归因对象", "关键细节", "证据/trace", "更新时间", "下一步"],
rows,
safety: {
frontendHardcodedRows: false,
sourceFixturePromoted: false,
localStorageUsed: false,
blockedIsGreen: false,
hardwareControlSurface: false,
codeAgentConversationSurface: false
}
};
}
async function observeHealth(options, observedAt) {
try {
const env = options.env ?? process.env;
const dbProbe = await buildDbRuntimeReadiness(env, options.dbProbe);
const runtime = await runtimeReadiness(options.runtimeStore);
const db = applyRuntimeDbReadinessLayers(dbProbe, runtime);
const codeAgent = await describeCodeAgentAvailability(env, options);
const readiness = buildCloudApiReadiness({ db, codeAgent, runtime });
const commitId = env.HWLAB_COMMIT_ID || env.HWLAB_GIT_SHA || "unknown";
const imageTag = env.HWLAB_IMAGE_TAG || shortHash(commitId);
return {
rows: [
row({
category: "部署",
check: "Cloud API 实况身份",
statusKey: commitId === "unknown" ? "info" : "pass",
owner: CLOUD_API_SERVICE_ID,
detail: `service=${CLOUD_API_SERVICE_ID}; env=dev; status=${readiness.status}; imageTag=${imageTag}`,
evidence: [`commit=${commitId}`, "GET /health/live"],
updatedAt: observedAt,
next: readiness.ready ? "继续保持与 DEV desired state 对齐。" : "先处理下方阻塞行,再重新执行 DEV live 复核。"
}),
row({
category: "健康检查",
check: "/health/live readiness",
statusKey: readiness.ready ? "pass" : "blocked",
owner: CLOUD_API_SERVICE_ID,
detail: `ready=${readiness.ready}; status=${readiness.status}; blockers=${readiness.blockerCodes.join(",") || "无"}`,
evidence: readiness.blockerCodes.length > 0 ? readiness.blockerCodes.map((code) => `blocker=${code}`) : ["GET /health/live"],
updatedAt: observedAt,
next: readiness.ready ? "无额外动作。" : "按 blocker code 分别处理 DB、runtime、Code Agent 或 M3 依赖。"
}),
row({
category: "DB",
check: "DB live readiness",
statusKey: db?.ready && db?.connected && db?.liveDbEvidence ? "pass" : "blocked",
owner: "hwlab-cloud-api / DEV Postgres",
detail: `ready=${Boolean(db?.ready)}; connected=${Boolean(db?.connected)}; liveDbEvidence=${Boolean(db?.liveDbEvidence)}; endpointSource=${db?.endpointSource ?? "unknown"}`,
evidence: db?.blocker ? [`blocker=${db.blocker}`] : ["db.liveDbEvidence=true"],
updatedAt: observedAt,
next: db?.ready && db?.connected && db?.liveDbEvidence
? "继续保持 Secret URL host 与 live DB 证据分层。"
: "修复 DEV DB env/连接/auth,且不打印 Secret 值。"
}),
row({
category: "运行态",
check: "runtime durable adapter",
statusKey: runtime?.durable === true && runtime?.ready === true && runtime?.liveRuntimeEvidence === true ? "pass" : "blocked",
owner: "hwlab-cloud-api runtime-store",
detail: `adapter=${runtime?.adapter ?? "unknown"}; durable=${Boolean(runtime?.durable)}; ready=${Boolean(runtime?.ready)}; liveRuntimeEvidence=${Boolean(runtime?.liveRuntimeEvidence)}`,
evidence: [
runtime?.blocker ? `blocker=${runtime.blocker}` : null,
runtime?.durabilityContract?.blockedLayer ? `blockedLayer=${runtime.durabilityContract.blockedLayer}` : null,
runtime?.connection?.queryResult ? `queryResult=${runtime.connection.queryResult}` : null
].filter(Boolean),
updatedAt: observedAt,
next: runtime?.durable === true && runtime?.ready === true && runtime?.liveRuntimeEvidence === true
? "可作为可信记录持久化前置。"
: "修复 runtime durable auth/schema/migration/readiness 后再升级可信闭环。"
}),
row({
category: "Code Agent",
check: "provider readiness",
statusKey: codeAgent?.ready && codeAgent?.status !== "blocked" ? "pass" : "blocked",
owner: "hwlab-cloud-api / Code Agent provider",
detail: `status=${codeAgent?.status ?? "unknown"}; provider=${codeAgent?.provider ?? "unknown"}; model=${codeAgent?.model ?? "unknown"}; backend=${codeAgent?.backend ?? "unknown"}`,
evidence: [
codeAgent?.reason ? `reason=${codeAgent.reason}` : null,
...(Array.isArray(codeAgent?.missingEnv) ? codeAgent.missingEnv.map((name) => `missingEnv=${name}`) : []),
"Secret 值未读取/未展示"
].filter(Boolean),
updatedAt: observedAt,
next: codeAgent?.ready && codeAgent?.status !== "blocked"
? "继续用受控 /v1/agent/chat 验证真实 runner 能力。"
: "补齐 provider env/Secret/egress 后复测,不用 mock 回复冒充。"
})
]
};
} catch (error) {
return {
rows: [
row({
category: "健康检查",
check: "后端聚合读取",
statusKey: "failed",
owner: CLOUD_API_SERVICE_ID,
detail: `live 聚合失败:${oneLine(error.message)}`,
evidence: "GET /v1/diagnostics/gate",
updatedAt: observedAt,
next: "先修复 cloud-api 聚合接口,再重新刷新内部复核表。"
})
]
};
}
}
async function observeM3(options, observedAt) {
try {
const payload = await describeM3IoControlLive(options);
const ready = payload?.readiness?.status === "ready" && payload?.readiness?.controlReady === true;
return {
row: row({
category: "M3",
check: "DO1 经 hwlab-patch-panel 同步到 DI1",
statusKey: ready ? "pass" : payload?.readiness?.status === "pending" ? "pending" : "blocked",
owner: "hwlab-cloud-api / hwlab-patch-panel",
detail: payload?.readiness?.summary ?? payload?.blockedReason ?? "M3 IO readiness 未返回摘要。",
evidence: [
`route=${M3_IO_CONTROL_ROUTE}`,
payload?.readiness?.sourceKind ? `sourceKind=${payload.readiness.sourceKind}` : null,
payload?.readiness?.blocker?.code ? `blocker=${payload.readiness.blocker.code}` : null,
payload?.readiness?.blocker?.layer ? `layer=${payload.readiness.blocker.layer}` : null
].filter(Boolean),
updatedAt: payload?.readiness?.observedAt ?? observedAt,
next: ready
? "只表示控制路径可执行;M3 验收仍需 operation/trace/audit/evidence 闭环。"
: payload?.readiness?.blocker?.zh ?? "修复 gateway-simu、box-simu 或 patch-panel 只读 readiness 后再复核。"
})
};
} catch (error) {
return {
row: row({
category: "M3",
check: "DO1 经 hwlab-patch-panel 同步到 DI1",
statusKey: "failed",
owner: "hwlab-cloud-api / hwlab-patch-panel",
detail: `M3 live readiness 读取失败:${oneLine(error.message)}`,
evidence: `GET ${M3_IO_CONTROL_ROUTE}`,
updatedAt: observedAt,
next: "先恢复 M3 IO readiness 接口,再验证 patch-panel 目标链路。"
})
};
}
}
async function observeAudit(options, observedAt) {
try {
const result = await options.runtimeStore?.queryAuditEvents?.({ limit: 6 });
const events = Array.isArray(result?.events) ? result.events : [];
return {
row: row({
category: "证据",
check: "audit.event.query",
statusKey: events.length > 0 ? "info" : "pending",
owner: "runtime-store / audit",
detail: `只读查询返回 ${events.length} 条 audit;该行不单独证明 M3 通过。`,
evidence: events.slice(0, 3).map((event) => [
event.auditId ? `audit=${event.auditId}` : null,
event.traceId ? `trace=${event.traceId}` : null
].filter(Boolean).join(" / ")).filter(Boolean),
updatedAt: observedAt,
next: events.length > 0 ? "将 audit 与 operation/evidence 绑定后再判断可信闭环。" : "等待 live audit 记录,或先修复 runtime durable blocker。"
})
};
} catch (error) {
return {
row: row({
category: "证据",
check: "audit.event.query",
statusKey: "blocked",
owner: "runtime-store / audit",
detail: `只读 audit 查询受阻:${oneLine(error.message)}`,
evidence: "POST /json-rpc audit.event.query",
updatedAt: observedAt,
next: "修复 runtime durable/readiness 后再查询 audit,不用 SOURCE 回退冒充。"
})
};
}
}
async function observeEvidence(options, observedAt) {
try {
const result = await options.runtimeStore?.queryEvidenceRecords?.({ limit: 6 });
const records = Array.isArray(result?.records) ? result.records : [];
return {
row: row({
category: "证据",
check: "evidence.record.query",
statusKey: records.length > 0 ? "info" : "pending",
owner: "runtime-store / evidence",
detail: `只读查询返回 ${records.length} 条 evidence;该行不单独证明 M3 通过。`,
evidence: records.slice(0, 3).map((record) => [
record.evidenceId ? `evidence=${record.evidenceId}` : null,
record.traceId ?? record.metadata?.traceId ? `trace=${record.traceId ?? record.metadata?.traceId}` : null
].filter(Boolean).join(" / ")).filter(Boolean),
updatedAt: observedAt,
next: records.length > 0 ? "将 evidence 与 operation/audit/patch-panel live report 绑定后再判断。" : "等待 live evidence 记录,或先修复 runtime durable blocker。"
})
};
} catch (error) {
return {
row: row({
category: "证据",
check: "evidence.record.query",
statusKey: "blocked",
owner: "runtime-store / evidence",
detail: `只读 evidence 查询受阻:${oneLine(error.message)}`,
evidence: "POST /json-rpc evidence.record.query",
updatedAt: observedAt,
next: "修复 runtime durable/readiness 后再查询 evidence,不用 SOURCE 回退冒充。"
})
};
}
}
async function runtimeReadiness(runtimeStore) {
if (typeof runtimeStore?.readiness === "function") {
return runtimeStore.readiness();
}
if (typeof runtimeStore?.summary === "function") {
return runtimeStore.summary();
}
return {
adapter: "unknown",
durable: false,
ready: false,
liveRuntimeEvidence: false,
blocker: "runtime_store_unavailable"
};
}
function row({ category, check, statusKey, owner, detail, evidence, updatedAt, next }) {
const normalizedStatusKey = normalizeStatusKey(statusKey);
return {
category,
check,
status: STATUS[normalizedStatusKey],
statusKey: normalizedStatusKey,
owner,
detail: oneLine(detail),
evidence: normalizeEvidence(evidence),
updatedAt,
next: oneLine(next),
sourceKind: "LIVE-BACKEND"
};
}
function normalizeStatusKey(statusKey) {
const normalized = String(statusKey ?? "").trim().toLowerCase();
if (normalized === "pass" || normalized === "passed" || normalized === "ok" || normalized === "ready") return "pass";
if (normalized === "failed" || normalized === "failure" || normalized === "error") return "failed";
if (normalized === "pending" || normalized === "unknown" || normalized === "loading") return "pending";
if (normalized === "info" || normalized === "information") return "info";
return "blocked";
}
function normalizeEvidence(value) {
const items = Array.isArray(value) ? value : [value];
const normalized = items.map(oneLine).filter(Boolean);
return normalized.length > 0 ? normalized : ["live 后端未返回可用 evidence 字段"];
}
function oneLine(value) {
return String(value ?? "").replace(/\s+/gu, " ").trim().slice(0, 360);
}
function shortHash(value) {
const text = String(value ?? "unknown");
return text === "unknown" ? "unknown" : text.slice(0, 7);
}