410 lines
18 KiB
JavaScript
410 lines
18 KiB
JavaScript
import {
|
||
runnerTraceSummary as compactRunnerTraceSummary,
|
||
skillsSummary as compactSkillsSummary,
|
||
toolCallsSummary as compactToolCallsSummary
|
||
} from "./code-agent-facts.mjs";
|
||
|
||
const GREEN_TONES = new Set(["ok", "available", "ready", "dev-live", "live", "pass"]);
|
||
const SECRET_MARKER = /\b(?:OPENAI_API_KEY|DATABASE_URL|secret|secretref|token|password|passwd|credential|private key|kubeconfig|密钥|密码|凭证)\b/iu;
|
||
const NON_LIVE_VALUES = new Set(["", "n/a", "none", "null", "undefined", "unknown", "not_observed", "not-observed"]);
|
||
const MAX_FIELD_LENGTH = 96;
|
||
|
||
export function classifyCodeAgentStatusSummary({ availability = null, latestMessage = null, live = null } = {}) {
|
||
const observedAvailability = availability ?? codeAgentAvailabilityFromLive(live);
|
||
const payload = safeMerge(observedAvailability, latestMessage);
|
||
const blockers = readinessBlockers(payload);
|
||
const classification = classifyPayload(payload, blockers);
|
||
const safeTone = unsafeGreenForNonReady(classification, blockers) ? "warn" : classification.tone;
|
||
const provider = safeField(firstString(payload.provider, observedAvailability?.provider));
|
||
const mode = safeField(firstString(payload.mode, payload.providerTrace?.mode, observedAvailability?.mode));
|
||
const backend = safeField(firstString(payload.backend, observedAvailability?.backend));
|
||
const codeAgentStatus = safeField(firstString(payload.status, payload.readinessStatus, observedAvailability?.status));
|
||
const capabilityLevel = safeField(firstString(payload.capabilityLevel, payload.runner?.capabilityLevel, observedAvailability?.capabilityLevel));
|
||
const sessionMode = safeField(firstString(payload.sessionMode, payload.session?.sessionMode, payload.runnerTrace?.sessionMode, payload.runner?.sessionMode));
|
||
const sessionId = safeField(firstString(payload.session?.sessionId, payload.sessionId, payload.runner?.sessionId, payload.sessionReuse?.sessionId));
|
||
const sessionStatus = safeField(firstString(payload.session?.status, payload.sessionStatus, payload.runnerTrace?.sessionStatus, payload.sessionReuse?.status));
|
||
const workspace = safeField(firstString(payload.workspace, payload.session?.workspace, payload.runner?.workspace, payload.sessionReuse?.workspace));
|
||
const sandbox = safeField(firstString(payload.sandbox, payload.session?.sandbox, payload.runner?.sandbox));
|
||
const runnerKind = safeField(firstString(payload.runner?.kind, payload.session?.runnerKind, payload.runnerTrace?.runnerKind));
|
||
const toolCalls = safeField(compactToolCallsSummary(payload.toolCalls));
|
||
const skills = safeField(compactSkillsSummary(payload.skills));
|
||
const runnerTrace = safeField(compactRunnerTraceSummary(payload.runnerTrace));
|
||
const lastTraceId = safeTraceId(firstString(
|
||
latestMessage?.traceId,
|
||
payload.traceId,
|
||
payload.session?.lastTraceId,
|
||
payload.runnerTrace?.lastTraceId,
|
||
payload.runnerTrace?.traceId,
|
||
payload.providerTrace?.traceId,
|
||
observedAvailability?.traceId
|
||
));
|
||
const deploymentRevision = currentDeploymentRevision(live);
|
||
|
||
return {
|
||
...classification,
|
||
tone: safeTone,
|
||
provider,
|
||
mode,
|
||
backend,
|
||
providerModeBackend: compactJoin([provider, mode, backend], " / "),
|
||
codeAgentStatus,
|
||
capabilityLevel,
|
||
sessionMode,
|
||
sessionId,
|
||
sessionStatus,
|
||
sessionSummary: compactJoin([sessionMode, sessionStatus], " / "),
|
||
sessionIdStatus: compactJoin([sessionId, sessionStatus], " / "),
|
||
workspace,
|
||
sandbox,
|
||
runnerKind,
|
||
toolCalls,
|
||
skills,
|
||
runnerTrace,
|
||
readinessBlockers: blockers,
|
||
blockersLabel: blockers.length > 0 ? blockers.join(", ") : "无",
|
||
lastTraceId: lastTraceId || "未观测",
|
||
deploymentRevision,
|
||
fields: {
|
||
provider,
|
||
mode,
|
||
backend,
|
||
codeAgentStatus,
|
||
capabilityLevel,
|
||
sessionMode,
|
||
sessionId,
|
||
sessionStatus,
|
||
workspace,
|
||
sandbox,
|
||
runnerKind,
|
||
toolCalls,
|
||
skills,
|
||
runnerTrace,
|
||
readinessBlockers: blockers,
|
||
lastTraceId: lastTraceId || null,
|
||
deploymentRevision
|
||
}
|
||
};
|
||
}
|
||
|
||
export function codeAgentAvailabilityFromLive(live = {}) {
|
||
return normalizedCodeAgentAvailability(
|
||
live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? live?.health?.data?.codeAgent ?? null,
|
||
live?.restIndex?.data?.readiness?.codeAgent ?? live?.healthLive?.data?.readiness?.codeAgent ?? live?.health?.data?.readiness?.codeAgent ?? null
|
||
);
|
||
}
|
||
|
||
export function currentDeploymentRevision(live = {}) {
|
||
const payloads = [
|
||
live?.healthLive?.data,
|
||
live?.restIndex?.data,
|
||
live?.health?.data,
|
||
live?.adapter?.data
|
||
].filter(Boolean);
|
||
for (const payload of payloads) {
|
||
const revision = firstString(
|
||
payload?.commit?.id,
|
||
payload?.commitId,
|
||
payload?.revision,
|
||
payload?.runtime?.commitId,
|
||
payload?.image?.tag,
|
||
payload?.imageTag,
|
||
payload?.runtime?.imageTag
|
||
);
|
||
if (revision && !NON_LIVE_VALUES.has(revision.toLowerCase())) {
|
||
return shortRevision(revision);
|
||
}
|
||
}
|
||
return "未观测";
|
||
}
|
||
|
||
function classifyPayload(payload, blockers) {
|
||
if (!payload || Object.keys(payload).length === 0) {
|
||
return status("unverified", "探测中", "pending", "○", "等待同源接口返回 Code Agent 状态。");
|
||
}
|
||
|
||
if (isSkillCliApiControl(payload)) {
|
||
const ready = isReadySkillCli(payload);
|
||
return ready
|
||
? status("skill-cli-api-control", "HWLAB Skill CLI 控制", "ok", "◆", "通过 HWLAB API 受控路径执行指定能力。")
|
||
: status("skill-cli-api-blocked", "Skill CLI 受阻", "blocked", "×", "HWLAB Skill CLI 控制路径当前受阻。");
|
||
}
|
||
|
||
if (isLongLivedReady(payload)) {
|
||
return status("long-lived-session", "长会话可用", "ok", "●", "repo-owned 长会话通道可复用。");
|
||
}
|
||
|
||
if (isReadOnlySessionTools(payload)) {
|
||
return status("read-only-session-tools", "只读会话工具", "warn", "◇", "只读 runner 工具可用,不能写工作区或冒充完整长会话。");
|
||
}
|
||
|
||
if (isStatelessOneShot(payload)) {
|
||
return status("stateless-one-shot", "一次性执行", "warn", "△", "当前回复来自一次性执行,不是可复用长会话。");
|
||
}
|
||
|
||
if (isBlocked(payload)) {
|
||
return status("blocked", "服务受阻", "blocked", "×", "Code Agent 通道暂不可用或 readiness 未通过。");
|
||
}
|
||
|
||
if (isFallbackTextChatOnly(payload)) {
|
||
return status("fallback-text-chat-only", "文本备用通道", "warn", "◌", "只有文本对话能力,没有 workspace tools 或 session 控制。");
|
||
}
|
||
|
||
if (isDegraded(payload, blockers)) {
|
||
return status("degraded", "降级可用", "warn", "!", "Code Agent 有部分能力,但 readiness 仍有阻塞项。");
|
||
}
|
||
|
||
if (payload.status === "available" || payload.ready === true || payload.status === "completed") {
|
||
return status("available", "通道可用", "ok", "●", "受控对话通道已返回可用状态。");
|
||
}
|
||
|
||
return status("unverified", "探测中", "pending", "○", "等待同源接口返回 Code Agent 状态。");
|
||
}
|
||
|
||
function normalizedCodeAgentAvailability(availability, structuredReadiness) {
|
||
const base = availability && typeof availability === "object" ? { ...availability } : {};
|
||
const structured = structuredReadiness && typeof structuredReadiness === "object" ? structuredReadiness : null;
|
||
if (!structured) return Object.keys(base).length > 0 ? base : null;
|
||
|
||
const provider = structured.provider ?? {};
|
||
const sessionRunner = structured.sessionRunner ?? {};
|
||
const codexStdio = structured.codexStdio ?? {};
|
||
const merged = {
|
||
...base,
|
||
structuredReadiness: structured,
|
||
status: firstString(base.status, structured.status),
|
||
ready: base.ready === true || structured.ready === true,
|
||
readinessStatus: firstString(base.readinessStatus, structured.status),
|
||
providerReady: structured.providerReady === true,
|
||
durableDbReady: structured.durableDbReady === true,
|
||
sessionRunnerReady: structured.sessionRunnerReady === true,
|
||
codexStdioFeasible: structured.codexStdioFeasible === true,
|
||
provider: firstString(base.provider, provider.provider),
|
||
mode: firstString(base.mode, provider.mode, sessionRunner.mode),
|
||
backend: firstString(base.backend, provider.backend),
|
||
capabilityLevel: firstString(base.capabilityLevel, sessionRunner.capabilityLevel),
|
||
sessionMode: firstString(base.sessionMode, sessionRunner.mode),
|
||
currentBlockers: Array.isArray(structured.currentBlockers) ? [...structured.currentBlockers] : [],
|
||
blockerCodes: [
|
||
...(Array.isArray(structured.currentBlockers) ? structured.currentBlockers : []),
|
||
...(Array.isArray(codexStdio.blockerCodes) ? codexStdio.blockerCodes : [])
|
||
],
|
||
blockers: [
|
||
...(Array.isArray(structured.currentBlockers)
|
||
? structured.currentBlockers.map((code) => ({ code }))
|
||
: [])
|
||
],
|
||
runner: {
|
||
...(base.runner ?? {}),
|
||
kind: firstString(base.runner?.kind, sessionRunner.kind),
|
||
provider: firstString(base.runner?.provider, provider.provider),
|
||
backend: firstString(base.runner?.backend, provider.backend),
|
||
sessionMode: firstString(base.runner?.sessionMode, sessionRunner.mode),
|
||
capabilityLevel: firstString(base.runner?.capabilityLevel, sessionRunner.capabilityLevel),
|
||
longLivedSession: base.runner?.longLivedSession === true || sessionRunner.longLivedSession === true,
|
||
durableSession: base.runner?.durableSession === true || sessionRunner.durableSession === true,
|
||
codexStdio: base.runner?.codexStdio === true || sessionRunner.codexStdio === true,
|
||
writeCapable: base.runner?.writeCapable === true || sessionRunner.writeCapable === true
|
||
},
|
||
codexStdioFeasibility: {
|
||
...(base.codexStdioFeasibility ?? base.codexStdio ?? {}),
|
||
ready: codexStdio.ready === true,
|
||
canStartLongLivedCodexStdio: codexStdio.feasible === true || codexStdio.ready === true,
|
||
status: codexStdio.status,
|
||
blockerCodes: Array.isArray(codexStdio.blockerCodes) ? [...codexStdio.blockerCodes] : [],
|
||
blockers: codexStdio.blocker ? [{ code: codexStdio.blocker }] : []
|
||
}
|
||
};
|
||
if (structured.currentBlocker) merged.reason = firstString(base.reason, structured.currentBlocker);
|
||
return merged;
|
||
}
|
||
|
||
function status(kind, label, tone, icon, summary) {
|
||
return { kind, label, tone, icon, summary };
|
||
}
|
||
|
||
function safeMerge(availability, latestMessage) {
|
||
const base = availability && typeof availability === "object" ? availability : {};
|
||
const message = latestMessage && typeof latestMessage === "object" ? latestMessage : {};
|
||
const merged = {
|
||
...base,
|
||
...message,
|
||
runner: { ...(base.runner ?? {}), ...(message.runner ?? {}) },
|
||
session: mergeSession(base.session, message.session),
|
||
runnerTrace: { ...(base.runnerTrace ?? {}), ...(message.runnerTrace ?? {}) },
|
||
providerTrace: { ...(base.providerTrace ?? {}), ...(message.providerTrace ?? {}) },
|
||
longLivedSessionGate: { ...(base.longLivedSessionGate ?? {}), ...(message.longLivedSessionGate ?? {}) },
|
||
codexStdioFeasibility: { ...(base.codexStdioFeasibility ?? base.codexStdio ?? {}), ...(message.codexStdioFeasibility ?? {}) }
|
||
};
|
||
if (base.blockers || message.blockers) merged.blockers = [...(base.blockers ?? []), ...(message.blockers ?? [])];
|
||
if (base.blockerCodes || message.blockerCodes) merged.blockerCodes = [...(base.blockerCodes ?? []), ...(message.blockerCodes ?? [])];
|
||
if (base.runnerLimitations || message.runnerLimitations) merged.runnerLimitations = [...(base.runnerLimitations ?? []), ...(message.runnerLimitations ?? [])];
|
||
if (message.toolCalls !== undefined) merged.toolCalls = message.toolCalls;
|
||
if (message.skills !== undefined) merged.skills = message.skills;
|
||
return merged;
|
||
}
|
||
|
||
function mergeSession(baseSession, messageSession) {
|
||
const base = typeof baseSession === "string" ? { status: baseSession } : (baseSession ?? {});
|
||
const message = typeof messageSession === "string" ? { status: messageSession } : (messageSession ?? {});
|
||
return { ...base, ...message };
|
||
}
|
||
|
||
function isSkillCliApiControl(payload) {
|
||
return (
|
||
payload.provider === "hwlab-skill-cli" ||
|
||
payload.runner?.kind === "hwlab-m3-io-skill-cli" ||
|
||
payload.sessionMode === "controlled-m3-io-skill-cli" ||
|
||
String(payload.capabilityLevel ?? "").startsWith("hwlab-api-control") ||
|
||
array(payload.toolCalls).some((tool) => tool?.type === "skill-cli" || tool?.route === "/v1/m3/io")
|
||
);
|
||
}
|
||
|
||
function isReadySkillCli(payload) {
|
||
return (
|
||
payload.capabilityLevel === "hwlab-api-control-ready" ||
|
||
payload.capabilityLevel === "hwlab-api-control-with-approval" ||
|
||
payload.providerTrace?.controlReady === true ||
|
||
array(payload.toolCalls).some((tool) => tool?.controlReady === true && tool?.status === "completed")
|
||
) && !hasBlockingSkillCliSignal(payload);
|
||
}
|
||
|
||
function hasBlockingSkillCliSignal(payload) {
|
||
return payload.capabilityLevel === "hwlab-api-control-blocked" ||
|
||
payload.providerTrace?.controlReady === false ||
|
||
array(payload.toolCalls).some((tool) => tool?.status === "blocked" || tool?.controlReady === false);
|
||
}
|
||
|
||
function isLongLivedReady(payload) {
|
||
return (
|
||
payload.longLivedSessionGate?.status === "pass" ||
|
||
payload.longLivedSessionGate?.pass === true ||
|
||
payload.codexStdioFeasibility?.canStartLongLivedCodexStdio === true ||
|
||
payload.capabilityLevel === "long-lived-codex-stdio-session" ||
|
||
payload.sessionMode === "codex-mcp-stdio-long-lived"
|
||
) && payload.status !== "failed" && payload.status !== "blocked" && payload.ready !== false;
|
||
}
|
||
|
||
function isReadOnlySessionTools(payload) {
|
||
return (
|
||
payload.capabilityLevel === "read-only-session-tools" ||
|
||
payload.capabilityLevel === "read-only-tools" ||
|
||
payload.agentKind === "controlled-readonly-session-registry" ||
|
||
payload.sessionMode === "controlled-readonly-session-registry" ||
|
||
payload.implementationType === "controlled-readonly-session-registry" ||
|
||
payload.runner?.kind === "hwlab-readonly-runner" ||
|
||
(payload.runner?.writeCapable === false &&
|
||
(payload.runner?.ready === true || payload.partialReady === true || payload.sessionRunnerReady === true) &&
|
||
payload.capabilityLevel !== "text-chat-only" &&
|
||
payload.runner?.kind !== "openai-responses-fallback") ||
|
||
payload.partialReady === true
|
||
);
|
||
}
|
||
|
||
function isStatelessOneShot(payload) {
|
||
return (
|
||
payload.sessionMode === "ephemeral-one-shot" ||
|
||
payload.runner?.kind === "codex-cli-one-shot-ephemeral" ||
|
||
payload.implementationType === "codex-cli-one-shot-ephemeral" ||
|
||
array(payload.runnerLimitations).includes("one-shot")
|
||
);
|
||
}
|
||
|
||
function isFallbackTextChatOnly(payload) {
|
||
return (
|
||
payload.capabilityLevel === "text-chat-only" ||
|
||
payload.runner?.kind === "openai-responses-fallback" ||
|
||
payload.implementationType === "openai-responses-fallback" ||
|
||
payload.agentKind === "openai-fallback" ||
|
||
payload.provider === "openai-responses"
|
||
);
|
||
}
|
||
|
||
function isBlocked(payload) {
|
||
return payload.status === "blocked" ||
|
||
payload.status === "failed" ||
|
||
payload.ready === false ||
|
||
payload.capabilityLevel === "blocked" ||
|
||
Boolean(payload.error);
|
||
}
|
||
|
||
function isDegraded(payload, blockers) {
|
||
return payload.status === "partial" ||
|
||
payload.status === "degraded" ||
|
||
payload.readinessStatus === "partial" ||
|
||
payload.capabilityStatus === "controlled-readonly-session-registry" ||
|
||
blockers.length > 0;
|
||
}
|
||
|
||
function readinessBlockers(payload) {
|
||
const values = [
|
||
...array(payload?.blockerCodes),
|
||
...array(payload?.blockers).map((item) => item?.code ?? item),
|
||
...array(payload?.longLivedSessionGate?.blockers).map((item) => item?.code ?? item),
|
||
...array(payload?.codexStdioFeasibility?.blockers).map((item) => item?.code ?? item),
|
||
payload?.runtimeContract?.binary?.status === "missing" ? "codex_cli_binary_missing" : null,
|
||
payload?.runtimeContract?.lifecycleSupervisor?.status === "blocked" ? "runner_lifecycle_missing" : null,
|
||
payload?.runtimeContract?.stdioProtocol?.status === "blocked" ? "stdio_protocol_not_wired" : null,
|
||
payload?.reason,
|
||
payload?.error?.code,
|
||
payload?.providerStatus ? `provider_http_${payload.providerStatus}` : null,
|
||
payload?.error?.providerStatus ? `provider_http_${payload.error.providerStatus}` : null,
|
||
...array(payload?.missingEnv).map(() => "provider_config_blocked")
|
||
];
|
||
return [...new Set(values.map(safeBlocker).filter(Boolean))].slice(0, 4);
|
||
}
|
||
|
||
function safeBlocker(value) {
|
||
if (value && typeof value === "object" && !Array.isArray(value) && !value.code && !value.reason && !value.message) return null;
|
||
const text = safeField(typeof value === "object" ? value?.code ?? value?.reason ?? value?.message : value);
|
||
if (!text || text === "未观测" || /^_+$/u.test(text)) return null;
|
||
if (SECRET_MARKER.test(text)) return "provider_config_blocked";
|
||
return text.replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 72);
|
||
}
|
||
|
||
function unsafeGreenForNonReady(classification, blockers) {
|
||
return GREEN_TONES.has(classification.tone) &&
|
||
(classification.kind.includes("blocked") || classification.kind === "degraded" || blockers.some((blocker) => /blocked|unavailable|failed|degraded/iu.test(blocker)));
|
||
}
|
||
|
||
function currentString(value) {
|
||
const text = String(value ?? "").trim();
|
||
if (!text || NON_LIVE_VALUES.has(text.toLowerCase())) return null;
|
||
return text;
|
||
}
|
||
|
||
function firstString(...values) {
|
||
for (const value of values) {
|
||
const text = currentString(value);
|
||
if (text) return text;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function safeField(value) {
|
||
const text = currentString(value);
|
||
if (!text) return "未观测";
|
||
if (SECRET_MARKER.test(text)) return "已隐藏";
|
||
return text.replace(/\s+/gu, " ").slice(0, MAX_FIELD_LENGTH);
|
||
}
|
||
|
||
function safeTraceId(value) {
|
||
const text = currentString(value);
|
||
if (!text || SECRET_MARKER.test(text)) return null;
|
||
return text.replace(/\s+/gu, "").slice(0, MAX_FIELD_LENGTH);
|
||
}
|
||
|
||
function shortRevision(value) {
|
||
const text = safeTraceId(value);
|
||
if (!text) return "未观测";
|
||
return /^[a-f0-9]{12,40}$/iu.test(text) ? text.slice(0, 12) : text.slice(0, 24);
|
||
}
|
||
|
||
function compactJoin(values, separator) {
|
||
const parts = values.map((value) => value === "未观测" ? null : value).filter(Boolean);
|
||
return parts.length > 0 ? parts.join(separator) : "未观测";
|
||
}
|
||
|
||
function array(value) {
|
||
return Array.isArray(value) ? value : [];
|
||
}
|