634 lines
26 KiB
TypeScript
634 lines
26 KiB
TypeScript
function traceEventMeta(event) {
|
||
return [
|
||
event.toolName ? `tool=${event.toolName}` : null,
|
||
event.waitingFor ? `waiting=${event.waitingFor}` : null,
|
||
typeof event.timeoutMs === "number" ? `timeoutMs=${event.timeoutMs}` : null,
|
||
typeof event.hardTimeoutMs === "number" ? `hardTimeoutMs=${event.hardTimeoutMs}` : null,
|
||
typeof event.idleMs === "number" ? `idleMs=${event.idleMs}` : null,
|
||
event.lastActivityAt ? `lastActivityAt=${event.lastActivityAt}` : null,
|
||
typeof event.elapsedMs === "number" ? `${event.elapsedMs}ms` : null,
|
||
event.errorCode ? `error=${event.errorCode}` : null,
|
||
event.outputSummary,
|
||
event.message
|
||
].filter(Boolean).join(" / ");
|
||
}
|
||
|
||
function toolCallsSummary(toolCalls) {
|
||
const summary = compactToolCallsSummary(toolCalls);
|
||
return summary === "none" ? null : summary;
|
||
}
|
||
|
||
function skillsSummary(skills) {
|
||
const summary = compactSkillsSummary(skills);
|
||
return summary === "none" ? null : summary;
|
||
}
|
||
|
||
function runnerTraceSummary(runnerTrace) {
|
||
const summary = compactRunnerTraceSummary(runnerTrace);
|
||
return summary === "none" ? null : summary;
|
||
}
|
||
|
||
function conversationFactsSummary(conversationFacts) {
|
||
if (!conversationFacts || typeof conversationFacts !== "object") return "none";
|
||
return [
|
||
recordField("turns", conversationFacts.turnCount),
|
||
recordField("sessionId", conversationFacts.sessionId),
|
||
recordField("status", conversationFacts.sessionStatus),
|
||
recordField("workspace", conversationFacts.workspace),
|
||
recordField("mode", conversationFacts.sessionMode),
|
||
conversationFacts.valuesRedacted === true ? "valuesRedacted=true" : null
|
||
].filter(Boolean).join(" / ") || "none";
|
||
}
|
||
|
||
function conversationFactTracesSummary(conversationFacts) {
|
||
if (!conversationFacts || typeof conversationFacts !== "object") return "none";
|
||
const traces = Array.isArray(conversationFacts.traceIds) ? conversationFacts.traceIds.filter(Boolean) : [];
|
||
return traces.length ? traces.slice(-4).join(",") : conversationFacts.latestTraceId ?? "none";
|
||
}
|
||
|
||
function conversationFactSkillsSummary(conversationFacts) {
|
||
const skills = conversationFacts?.latestSkills;
|
||
if (!skills || typeof skills !== "object") return "none";
|
||
const names = Array.isArray(skills.names) ? skills.names.filter(Boolean).slice(0, 6).join(",") : "";
|
||
return [
|
||
skills.status ?? "unknown",
|
||
recordField("count", skills.totalCount ?? skills.count),
|
||
names ? `items=${names}` : null
|
||
].filter(Boolean).join(":") || "none";
|
||
}
|
||
|
||
function conversationFactToolsSummary(conversationFacts) {
|
||
const toolCalls = Array.isArray(conversationFacts?.recentToolCalls) ? conversationFacts.recentToolCalls : [];
|
||
if (toolCalls.length === 0) return "none";
|
||
return toolCalls
|
||
.map((toolCall) => [toolCall.name ?? "tool", toolCall.status ? `(${toolCall.status})` : null].filter(Boolean).join(""))
|
||
.slice(-5)
|
||
.join(",");
|
||
}
|
||
|
||
function sessionSummary(session) {
|
||
if (!session || typeof session !== "object") return null;
|
||
const parts = [
|
||
session.sessionId,
|
||
session.status,
|
||
typeof session.turn === "number" ? `turn${session.turn}` : null,
|
||
session.threadId ? `thread=${session.threadId}` : null,
|
||
session.lastTraceId ? `lastTrace=${session.lastTraceId}` : null
|
||
].filter(Boolean);
|
||
return parts.join(":");
|
||
}
|
||
|
||
function statusCard(item) {
|
||
const article = document.createElement("article");
|
||
article.className = "status-card";
|
||
article.append(textSpan(item.title, "metric-label"));
|
||
article.append(textSpan(String(item.value), `status-value tone-${toneClass(item.tone)}`));
|
||
article.append(textSpan(item.meta, "status-meta"));
|
||
return article;
|
||
}
|
||
|
||
function infoCard(item) {
|
||
const article = document.createElement("article");
|
||
article.className = "info-card";
|
||
article.append(badge(statusLabel(item.tone), item.tone));
|
||
const body = document.createElement("div");
|
||
body.append(textSpan(item.title, "info-title"));
|
||
body.append(textSpan(item.detail, "info-detail"));
|
||
article.append(body);
|
||
return article;
|
||
}
|
||
|
||
function badge(text, tone = text) {
|
||
const span = document.createElement("span");
|
||
span.className = `badge tone-${toneClass(tone)}`;
|
||
span.textContent = text ?? "无";
|
||
return span;
|
||
}
|
||
|
||
function copyButton(value, label = "value") {
|
||
const button = document.createElement("button");
|
||
button.className = "copy-chip";
|
||
button.type = "button";
|
||
button.textContent = "复制";
|
||
button.title = `复制 ${label}`;
|
||
button.addEventListener("click", async () => {
|
||
await copyTextToClipboard(value);
|
||
button.textContent = "已复制";
|
||
window.setTimeout(() => {
|
||
button.textContent = "复制";
|
||
}, 1200);
|
||
});
|
||
return button;
|
||
}
|
||
|
||
async function copyTextToClipboard(value) {
|
||
const text = String(value ?? "");
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
}
|
||
const input = document.createElement("textarea");
|
||
input.value = text;
|
||
input.setAttribute("readonly", "");
|
||
input.style.position = "fixed";
|
||
input.style.left = "-9999px";
|
||
document.body.append(input);
|
||
input.select();
|
||
document.execCommand("copy");
|
||
input.remove();
|
||
}
|
||
|
||
function safeLink(href, text, className) {
|
||
const anchor = document.createElement("a");
|
||
anchor.href = href;
|
||
anchor.textContent = text ?? href;
|
||
anchor.rel = "noreferrer";
|
||
if (className) anchor.className = className;
|
||
return anchor;
|
||
}
|
||
|
||
function textSpan(text, className) {
|
||
const span = document.createElement("span");
|
||
span.textContent = text ?? "无";
|
||
if (className) span.className = className;
|
||
return span;
|
||
}
|
||
|
||
function cell(text, className) {
|
||
const td = document.createElement("td");
|
||
td.textContent = text ?? "无";
|
||
if (className) td.className = className;
|
||
return td;
|
||
}
|
||
|
||
function formatTimestamp(value) {
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return "未知";
|
||
return shortTime(date.toISOString());
|
||
}
|
||
|
||
function tableCell(text, className, colspan = 1) {
|
||
const td = cell(text, className);
|
||
if (colspan > 1) td.colSpan = colspan;
|
||
return td;
|
||
}
|
||
|
||
function runtimeSummaryFrom(live) {
|
||
return live.health.data?.runtime ?? live.healthLive.data?.runtime ?? live.adapter.data?.runtime ?? live.restIndex.data?.runtime ?? null;
|
||
}
|
||
|
||
function messageFromPayload(payload) {
|
||
if (!payload || typeof payload !== "object") return "非 JSON 或空响应";
|
||
if (payload.error && typeof payload.error === "object") {
|
||
return payload.error.message || payload.error.code || payload.reason || payload.message || JSON.stringify(payload.error).slice(0, 160);
|
||
}
|
||
if (typeof payload.error === "string") {
|
||
return payload.message ? `${payload.error}:${payload.message}` : payload.error;
|
||
}
|
||
return payload.code || payload.message || payload.reason || JSON.stringify(payload).slice(0, 160);
|
||
}
|
||
|
||
function normalizeBlockedAgentResult(payload, httpStatus, traceId, fallbackMessage) {
|
||
const { reply, ...safePayload } = payload;
|
||
const payloadError = payload.error && typeof payload.error === "object" ? payload.error : {};
|
||
const payloadErrorCode = payloadError.code ?? (typeof payload.error === "string" ? payload.error : null);
|
||
const providerStatus = payloadError.providerStatus ?? httpStatus;
|
||
const error = {
|
||
...payloadError,
|
||
code: payloadErrorCode ?? "provider_unavailable",
|
||
message: payloadError.message ?? fallbackMessage ?? `Code Agent provider HTTP ${httpStatus}`,
|
||
providerStatus
|
||
};
|
||
return {
|
||
...safePayload,
|
||
status: "failed",
|
||
traceId: payload.traceId || traceId,
|
||
error,
|
||
availability: payload.availability ?? codeAgentAvailabilityFromResult({ error })
|
||
};
|
||
}
|
||
|
||
function isBlockedAgentResponse(payload, httpStatus) {
|
||
return Boolean(
|
||
payload &&
|
||
typeof payload === "object" &&
|
||
(
|
||
isHttpNon2xxStatus(httpStatus) ||
|
||
isHttpNon2xxStatus(payload.error?.providerStatus) ||
|
||
payload.status === "blocked" ||
|
||
payload.error?.code === "provider_unavailable" ||
|
||
payload.error?.code === "security_blocked" ||
|
||
payload.error?.code === "tool_unavailable" ||
|
||
payload.error?.code === "skills_unavailable" ||
|
||
payload.availability?.status === "blocked"
|
||
)
|
||
);
|
||
}
|
||
|
||
function isHttpNon2xxStatus(value) {
|
||
return typeof value === "number" && Number.isInteger(value) && (value < 200 || value > 299);
|
||
}
|
||
|
||
function codeAgentAvailabilityFrom(live) {
|
||
return sanitizeCodeAgentAvailability(codeAgentAvailabilityFromLive(live));
|
||
}
|
||
|
||
function codeAgentAvailabilityFromResult(result) {
|
||
const missingEnv = Array.isArray(result?.error?.missingEnv) ? result.error.missingEnv : [];
|
||
const providerStatus = result?.error?.providerStatus;
|
||
const credentialBlocked = missingEnv.includes("OPENAI_API_KEY");
|
||
return {
|
||
status: "blocked",
|
||
blocker: credentialBlocked ? "凭证缺口" : "provider_unavailable",
|
||
reason: result?.error?.code ?? "provider_unavailable",
|
||
summary: providerStatus
|
||
? `真实后端已接入,但当前 provider 上游返回 HTTP ${providerStatus};本次保持 BLOCKED/provider_unavailable,不会冒充真实 Code Agent 回复。`
|
||
: "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。",
|
||
missingEnv,
|
||
secretRefs: credentialBlocked ? [
|
||
{
|
||
env: "OPENAI_API_KEY",
|
||
secretName: "hwlab-code-agent-provider",
|
||
secretKey: "openai-api-key",
|
||
redacted: true
|
||
}
|
||
] : []
|
||
};
|
||
}
|
||
|
||
function sanitizeCodeAgentAvailability(availability) {
|
||
if (!availability || typeof availability !== "object") return availability;
|
||
if (availability.status !== "blocked") return availability;
|
||
return {
|
||
...availability,
|
||
blocker: "服务受阻",
|
||
summary: codeAgentBlockedSummary(availability)
|
||
};
|
||
}
|
||
|
||
function codeAgentStatusMessage(availability) {
|
||
if (availability?.longLivedSessionGate?.status === "pass" && availability?.codexStdioFeasibility?.canStartLongLivedCodexStdio === true) {
|
||
return {
|
||
role: "system",
|
||
title: "Code Agent 状态:Codex stdio 长会话可用",
|
||
text: "当前通道可创建和复用 repo-owned Codex stdio session,并暴露 cancel、reap、idle timeout 与 trace capture;硬件控制仍只走 cloud-api/HWLAB API/skill CLI。",
|
||
status: "source"
|
||
};
|
||
}
|
||
if (availability?.runner?.ready === true) {
|
||
return {
|
||
role: "system",
|
||
title: "Code Agent 状态:Codex stdio gate 未通过",
|
||
text: `同源服务可响应,但完整 Codex stdio 长会话仍未通过 gate;不会用本地技能发现、shell/file 快捷命令或文本 fallback 冒充回复。${codeAgentBlockerDetail(availability)}`,
|
||
status: "blocked"
|
||
};
|
||
}
|
||
if (availability?.status === "blocked") {
|
||
return {
|
||
role: "system",
|
||
title: "Code Agent 状态:服务受阻",
|
||
text: codeAgentBlockedSummary(availability),
|
||
status: "blocked"
|
||
};
|
||
}
|
||
if (String(availability?.sourceKind ?? availability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
|
||
return {
|
||
role: "system",
|
||
title: "Code Agent 状态:本地验证记录",
|
||
text: "当前只用于本地浏览器验证:可以检查发送、处理中、回复渲染和证据展示,但不会标记为实况完成。",
|
||
status: "source"
|
||
};
|
||
}
|
||
if (availability?.status === "available") {
|
||
return {
|
||
role: "system",
|
||
title: "Code Agent 状态:真实通道已接入",
|
||
text: "受控对话通道已接入;发送后只有真实完成的回复才会标记为已完成。",
|
||
status: "source"
|
||
};
|
||
}
|
||
return {
|
||
role: "system",
|
||
title: "Code Agent 状态:等待 Codex stdio 探测",
|
||
text: "正在确认服务状态;首屏不会宣称 Code Agent 可用,也不会把失败或静态内容当成真实回复。",
|
||
status: "source"
|
||
};
|
||
}
|
||
|
||
function codeAgentPromptText(availability) {
|
||
if (availability?.status === "blocked") {
|
||
return "可以继续输入并保留草稿;发送会走受控对话通道。服务恢复前会显示结构化失败,不会冒充真实 Codex 回复。";
|
||
}
|
||
return "请在底部输入中文消息并点击发送;完成态只来自真实完成的后端回复。";
|
||
}
|
||
|
||
function codeAgentControlSummary(availability) {
|
||
if (latestCompletedAgentMessage()) {
|
||
return state.conversationId
|
||
? `当前会话 ${state.conversationId};最近一次真实完成回复来自受控 Code Agent 接口,并带有可核验记录。`
|
||
: "最近一次真实完成回复来自受控 Code Agent 接口,并带有可核验记录。";
|
||
}
|
||
if (isSourceFixtureCompletedChatMessage(latestChatResult())) {
|
||
return "最近一次为本地验证回复;它只证明浏览器对话体验,不作为实况完成证据。";
|
||
}
|
||
if (availability?.status === "blocked") {
|
||
return codeAgentBlockedSummary(availability);
|
||
}
|
||
if (availability?.partialReady === true || availability?.runner?.ready === true) {
|
||
return `输入区会调用受控接口;完整 Codex stdio 长会话尚未可用时只返回结构化失败,不能用本地 shortcut 或 text fallback 冒充回复。${codeAgentBlockerDetail(availability)}`;
|
||
}
|
||
return "输入区会调用受控 Code Agent 接口;只有真实完成回复才显示为完成,不能因为只有会话编号就当成实况完成。";
|
||
}
|
||
|
||
function codeAgentBlockedSummary(availability) {
|
||
const structured = availability?.error?.userMessage ?? availability?.blocker?.userMessage;
|
||
if (structured) return structured;
|
||
const providerStatus = availability?.error?.providerStatus ?? availability?.providerStatus;
|
||
if (providerStatus) {
|
||
return `真实后端已接入,但当前上游响应 HTTP ${providerStatus};工作台保持只读和可重试,不会冒充真实 Code Agent 回复。`;
|
||
}
|
||
const blocker = codeAgentBlockerDetail(availability);
|
||
return `真实后端已接入,但 Code Agent 服务暂不可用;完整 Codex stdio Code Agent 仍受阻,工作台保持只读和可重试,不会冒充真实回复。${blocker}`;
|
||
}
|
||
|
||
function codeAgentBlockerDetail(availability) {
|
||
const blockers = codeAgentBlockerCodes(availability);
|
||
if (blockers.length === 0 && availability?.reason) blockers.push(availability.reason);
|
||
if (blockers.length === 0) return "";
|
||
const labels = blockers.slice(0, 3).map(codeAgentBlockerChineseLabel);
|
||
return ` 阻塞:${labels.join(";")}。证据 code=${blockers.slice(0, 3).join(",")}。`;
|
||
}
|
||
|
||
function codeAgentBlockerCodes(availability) {
|
||
const values = [
|
||
...(Array.isArray(availability?.blockers) ? availability.blockers.map((blocker) => blocker?.code) : []),
|
||
...(Array.isArray(availability?.blockerCodes) ? availability.blockerCodes : []),
|
||
...(Array.isArray(availability?.longLivedSessionGate?.blockers) ? availability.longLivedSessionGate.blockers.map((blocker) => blocker?.code) : []),
|
||
...(Array.isArray(availability?.codexStdioFeasibility?.blockers) ? availability.codexStdioFeasibility.blockers.map((blocker) => blocker?.code) : []),
|
||
...(Array.isArray(availability?.runtimeContract?.stdioProtocol?.missingTools) && availability.runtimeContract.stdioProtocol.missingTools.length > 0 ? ["stdio_protocol_not_wired"] : []),
|
||
availability?.runtimeContract?.binary?.status === "missing" ? "codex_cli_binary_missing" : null,
|
||
availability?.runtimeContract?.lifecycleSupervisor?.status === "blocked" ? "runner_lifecycle_missing" : null
|
||
].filter(Boolean).map((code) => String(code));
|
||
return [...new Set(values)];
|
||
}
|
||
|
||
function codeAgentBlockerChineseLabel(code) {
|
||
return {
|
||
codex_cli_binary_missing: "未找到受控 Codex CLI binary",
|
||
codex_cli_not_executable: "Codex CLI 无法执行 --version",
|
||
codex_cli_native_dependency_missing: "Codex CLI native 依赖缺失",
|
||
runner_lifecycle_missing: "缺少 repo-owned lifecycle supervisor",
|
||
stdio_protocol_not_wired: "Codex stdio 协议尚未接入",
|
||
codex_stdio_supervisor_disabled: "Codex stdio supervisor 未启用",
|
||
codex_stdio_blocked_readonly_session_available: "旧 partial runner 不能作为 Codex stdio",
|
||
controlled_readonly_not_long_lived_stdio: "非 Codex stdio 长会话",
|
||
openai_responses_fallback_not_session: "文本 fallback 不是长会话",
|
||
one_shot_runner_not_long_lived: "一次性执行不是可复用 session",
|
||
provider_token_boundary: "token 边界未配置",
|
||
codex_home_missing: "CODEX_HOME 不存在或不可读",
|
||
codex_home_write_blocked: "CODEX_HOME 不可写",
|
||
workspace_mount_missing: "工作区挂载不可读",
|
||
workspace_write_boundary_blocked: "workspace-write 沙箱不可写",
|
||
codex_stdio_egress_boundary: "DEV egress 边界不合规"
|
||
}[code] ?? code;
|
||
}
|
||
|
||
function untrustedCompletionMessage(result) {
|
||
const provider = result?.provider ?? "unknown";
|
||
return `Code Agent 返回 completed,但缺少真实 provider/model/trace/conversation 证据,或 provider=${provider} 属于 echo/mock/stub;本次不会标记为真实完成。SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE。`;
|
||
}
|
||
|
||
function codeAgentAvailabilitySummary() {
|
||
if (state.codeAgentAvailability?.longLivedSessionGate?.status === "pass" && state.codeAgentAvailability?.codexStdioFeasibility?.canStartLongLivedCodexStdio === true) {
|
||
return "同源 API 可响应;Codex stdio 长会话 gate 已通过,支持真实 session 复用和 trace capture。";
|
||
}
|
||
if (state.codeAgentAvailability?.runner?.ready === true) {
|
||
return `同源 API 可响应;完整 Codex stdio 长会话仍受阻,本地 shortcut 与文本 fallback 不会冒充真实 Code Agent 回复。${codeAgentBlockerDetail(state.codeAgentAvailability)}`;
|
||
}
|
||
if (state.codeAgentAvailability?.status === "blocked") {
|
||
return "同源 API 可响应;Code Agent 服务暂不可用,工作台保持只读和可重试。";
|
||
}
|
||
if (String(state.codeAgentAvailability?.sourceKind ?? state.codeAgentAvailability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
|
||
return "同源 API 可响应;当前 Code Agent 回复来源是本地验证记录,仅用于浏览器验证,不是实况完成。";
|
||
}
|
||
if (state.codeAgentAvailability?.status === "available") {
|
||
return "同源 API 可响应;Code Agent 真实通道已接入,只有真实完成回复才显示为完成。";
|
||
}
|
||
return "同源接口可响应。技术复核详情已放在二级页面。";
|
||
}
|
||
|
||
function latestCompletedAgentMessage() {
|
||
return [...state.chatMessages].reverse().find((message) =>
|
||
message.role === "agent" &&
|
||
isRealCompletedChatMessage(message)
|
||
) ?? null;
|
||
}
|
||
|
||
function isRealCompletedChatResult(result) {
|
||
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
|
||
return result?.status === "completed" && assistantReply.length > 0 && !isSourceFixtureCompletion(result) && hasRealCodeAgentEvidence(result);
|
||
}
|
||
|
||
function isSourceFixtureChatResult(result) {
|
||
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
|
||
return result?.status === "completed" && assistantReply.length > 0 && isSourceFixtureCompletion(result);
|
||
}
|
||
|
||
function isTextFallbackChatResult(result) {
|
||
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
|
||
return result?.status === "completed" &&
|
||
assistantReply.length > 0 &&
|
||
result?.provider === "openai-responses" &&
|
||
(result?.capabilityLevel === "text-chat-only" || result?.runner?.kind === "openai-responses-fallback") &&
|
||
result?.longLivedSessionGate?.status !== "pass";
|
||
}
|
||
|
||
function isRealCompletedChatMessage(message) {
|
||
return message?.status === "completed" && !message.error && hasRealCodeAgentEvidence(message);
|
||
}
|
||
|
||
function isSourceFixtureCompletedChatMessage(message) {
|
||
return message?.status === "source" && !message.error && message.sourceKind === "SOURCE";
|
||
}
|
||
|
||
function hasRealCodeAgentEvidence(value) {
|
||
if (isCodexRunnerCapableEvidence(value)) {
|
||
return true;
|
||
}
|
||
if (CODEX_RUNNER_CAPABLE_PROVIDERS.includes(String(value?.provider ?? "").trim().toLowerCase())) {
|
||
return false;
|
||
}
|
||
if (isTextFallbackChatResult(value)) {
|
||
return false;
|
||
}
|
||
return (
|
||
isTrustedCodeAgentProvider(value?.provider) &&
|
||
Boolean(value?.model) &&
|
||
Boolean(value?.backend) &&
|
||
Boolean(value?.traceId) &&
|
||
Boolean(value?.conversationId || value?.sessionId) &&
|
||
hasProviderTrace(value) &&
|
||
Boolean(sessionIdFrom(value)) &&
|
||
(!requiresCodexThreadEvidence(value) || Boolean(threadIdFrom(value))) &&
|
||
!UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN.test(`${value?.provider ?? ""} ${value?.backend ?? ""}`)
|
||
);
|
||
}
|
||
|
||
function isCodexRunnerCapableEvidence(value) {
|
||
return (
|
||
CODEX_RUNNER_CAPABLE_PROVIDERS.includes(String(value?.provider ?? "").trim().toLowerCase()) &&
|
||
value?.runner?.kind === CODEX_APP_SERVER_RUNNER_KIND &&
|
||
value?.capabilityLevel === "long-lived-codex-stdio-session" &&
|
||
value?.session?.status === "idle" &&
|
||
typeof value?.session?.idleTimeoutMs === "number" &&
|
||
Boolean(value?.session?.lastTraceId) &&
|
||
value?.sessionMode === CODEX_APP_SERVER_SESSION_MODE &&
|
||
value?.implementationType === CODEX_APP_SERVER_IMPLEMENTATION_TYPE &&
|
||
Boolean(value?.sessionReuse) &&
|
||
hasProviderTrace(value) &&
|
||
Boolean(sessionIdFrom(value)) &&
|
||
Boolean(threadIdFrom(value)) &&
|
||
value?.session?.longLivedSession === true &&
|
||
value?.session?.codexStdio === true &&
|
||
value?.runner?.codexStdio === true &&
|
||
value?.runner?.writeCapable === true &&
|
||
value?.runner?.durableSession === true &&
|
||
Boolean(value?.workspace || value?.runner?.workspace) &&
|
||
["workspace-write", "danger-full-access"].includes(value?.sandbox || value?.runner?.sandbox) &&
|
||
Array.isArray(value?.toolCalls) &&
|
||
value.toolCalls.some((tool) => tool?.status === "completed") &&
|
||
value?.longLivedSessionGate?.status === "pass" &&
|
||
value?.providerTrace?.protocol === CODEX_APP_SERVER_PROTOCOL &&
|
||
/\bcodex\s+app-server\s+--listen\s+stdio:\/\//u.test(String(value?.providerTrace?.command ?? "")) &&
|
||
["completed", "succeeded"].includes(String(value?.providerTrace?.terminalStatus ?? "").toLowerCase()) &&
|
||
Boolean(value?.runnerTrace) &&
|
||
Boolean(value?.skills)
|
||
);
|
||
}
|
||
|
||
function isTrustedCodeAgentProvider(provider) {
|
||
const normalized = String(provider ?? "").trim().toLowerCase();
|
||
return TRUSTED_CODE_AGENT_PROVIDERS.includes(normalized);
|
||
}
|
||
|
||
function isSourceFixtureCompletion(value) {
|
||
const sourceKind = String(value?.sourceKind ?? value?.evidenceLevel ?? value?.providerTrace?.sourceKind ?? "").trim().toUpperCase();
|
||
const traceSource = String(value?.providerTrace?.source ?? value?.providerTrace?.mode ?? "").trim().toUpperCase();
|
||
return sourceKind === "SOURCE" || traceSource.startsWith("SOURCE");
|
||
}
|
||
|
||
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 === "source") return "source";
|
||
if (latest?.status === "canceled") return "canceled";
|
||
if (latest?.status === "timeout") return "timeout";
|
||
if (latest?.status === "error") return "error";
|
||
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 (isSourceFixtureCompletedChatMessage(latestChatResult())) return "source";
|
||
if (state.codeAgentAvailability?.status === "blocked" || ["failed", "timeout", "canceled", "error"].includes(latestChatResult()?.status)) return "blocked";
|
||
return "source";
|
||
}
|
||
|
||
function statusLabel(value) {
|
||
const key = String(value ?? "unknown").trim();
|
||
const normalized = key.toLowerCase();
|
||
return STATUS_LABELS[normalized] ?? key;
|
||
}
|
||
|
||
function gateStatusLabel(value, statusKey) {
|
||
const explicit = String(value ?? "").trim();
|
||
const labels = {
|
||
pass: "通过",
|
||
blocked: "阻塞",
|
||
failed: "失败",
|
||
pending: "待验证",
|
||
info: "信息"
|
||
};
|
||
return labels[statusKey] ?? (["通过", "阻塞", "失败", "待验证", "信息"].includes(explicit) ? explicit : "阻塞");
|
||
}
|
||
|
||
function normalizeGateStatusKey(value) {
|
||
const text = String(value ?? "").trim().toLowerCase();
|
||
if (["通过", "pass", "passed", "ok", "ready"].includes(text)) return "pass";
|
||
if (["失败", "failed", "failure", "error"].includes(text)) return "failed";
|
||
if (["待验证", "pending", "loading", "unknown"].includes(text)) return "pending";
|
||
if (["信息", "info", "information"].includes(text)) return "info";
|
||
return "blocked";
|
||
}
|
||
|
||
function normalizeEvidenceList(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, 240);
|
||
}
|
||
|
||
function roleLabel(role) {
|
||
return (
|
||
{
|
||
agent: "Agent",
|
||
blocker: "阻塞",
|
||
system: "系统",
|
||
user: "用户"
|
||
}[String(role ?? "").toLowerCase()] ?? statusLabel(role)
|
||
);
|
||
}
|
||
|
||
function recordKindLabel(kind) {
|
||
return (
|
||
{
|
||
audit: "审计",
|
||
evidence: "证据",
|
||
operation: "操作",
|
||
record: "记录",
|
||
report: "报告",
|
||
trace: "轨迹"
|
||
}[String(kind ?? "").toLowerCase()] ?? statusLabel(kind)
|
||
);
|
||
}
|
||
|
||
function internalGatePathnames() {
|
||
return new Set(["/gate", "/diagnostics/gate"]);
|
||
}
|
||
|
||
function helpPathnames() {
|
||
return new Set(["/help"]);
|
||
}
|
||
|
||
function skillsPathnames() {
|
||
return new Set(["/skills"]);
|
||
}
|
||
|
||
function valueOrUnavailable(liveValue, sourceValue) {
|
||
if (liveValue !== undefined && liveValue !== null) return liveValue;
|
||
if (sourceValue !== undefined && sourceValue !== null) return `${sourceValue} 来源 SOURCE`;
|
||
return "不可用";
|
||
}
|
||
|
||
function shortTime(value) {
|
||
return new Date(value).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
||
}
|
||
|
||
function toneClass(tone) {
|
||
return String(tone ?? "unknown").replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
|
||
}
|
||
|
||
function replaceChildren(parent, ...children) {
|
||
parent.replaceChildren(...children.filter((child) => child && typeof child !== "string"));
|
||
}
|