import { codeAgentRuntimePathFromMessage, runnerTraceSummary as compactRunnerTraceSummary, skillsSummary as compactSkillsSummary, toolCallsSummary as compactToolCallsSummary } from "./code-agent-facts.ts"; 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; const CODEX_APP_SERVER_RUNNER_KIND = "codex-app-server-stdio-runner"; const CODEX_APP_SERVER_SESSION_MODE = "codex-app-server-stdio-long-lived"; const CODEX_APP_SERVER_IMPLEMENTATION_TYPE = "repo-owned-codex-app-server-stdio-session"; 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 threadId = safeField(firstString(payload.threadId, payload.session?.threadId, payload.sessionReuse?.threadId, payload.providerTrace?.threadId, payload.runnerTrace?.threadId)); const sessionStatus = safeField(firstString(payload.session?.status, payload.sessionStatus, payload.runnerTrace?.sessionStatus, payload.sessionReuse?.status)); const lifecycle = sessionLifecycleSummary(payload); const sessionLifecycleStatus = safeField(lifecycle.status); const sessionLifecycleHint = safeField(lifecycle.hint); const sessionLifecycleLabel = safeField(lifecycle.label); 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 runtimePath = codeAgentRuntimePathFromMessage({ role: "agent", ...payload }); const protocol = safeField(runtimePath?.protocol); const implementationType = safeField(runtimePath?.implementationType); const providerTraceCommand = safeField(runtimePath?.command); const providerTraceTerminalStatus = safeField(runtimePath?.terminalStatus); const providerTraceFailureKind = safeField(runtimePath?.failureKind); const runtimePathLabel = runtimePath?.label ?? "运行路径证据不足"; const runtimePathSummary = runtimePath?.summary ?? "providerTrace 未观测;不会推断完整 Code Agent。"; const runtimePathTone = runtimePath?.tone ?? "warn"; 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, threadId, sessionStatus, sessionLifecycleStatus, sessionLifecycleHint, sessionLifecycleLabel, sessionSummary: compactJoin([sessionMode, sessionStatus, sessionLifecycleStatus], " / "), sessionIdStatus: compactJoin([sessionId, threadId, sessionStatus, sessionLifecycleStatus], " / "), workspace, sandbox, runnerKind, protocol, implementationType, providerTraceCommand, providerTraceTerminalStatus, providerTraceFailureKind, runtimePathLabel, runtimePathSummary, runtimePathTone, toolCalls, skills, runnerTrace, readinessBlockers: blockers, blockersLabel: blockers.length > 0 ? blockers.join(", ") : "无", lastTraceId: lastTraceId || "未观测", deploymentRevision, fields: { provider, mode, backend, codeAgentStatus, capabilityLevel, sessionMode, sessionId, threadId, sessionStatus, sessionLifecycleStatus, sessionLifecycleHint, sessionLifecycleLabel, workspace, sandbox, runnerKind, protocol, implementationType, providerTraceCommand, providerTraceTerminalStatus, providerTraceFailureKind, runtimePathLabel, runtimePathSummary, runtimePathTone, 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 状态。"); } const lifecycle = sessionLifecycleSummary(payload); if (payload.status === "running" || lifecycle.status === "busy" || payload.session?.status === "busy" || payload.runnerTrace?.sessionStatus === "busy") { return status("busy", "会话忙/请等待", "pending", "●", lifecycle.hint || "当前 Codex stdio 请求仍在 in-flight 状态,trace 会继续更新。"); } if (lifecycle.status === "expired") { return status("session-expired", "会话已过期", "blocked", "×", lifecycle.hint || "会话已过期,可重新发送建立新的 Code Agent session。"); } if (lifecycle.status === "failed" && (payload.session?.status === "failed" || payload.sessionLifecycle?.status === "failed" || payload.sessionSummary?.status === "failed") && !isFallbackTextChatOnly(payload)) { return status("session-failed", "会话失败", "blocked", "×", lifecycle.hint || "当前 Code Agent session 已失败,需重新发送建立新会话。"); } if (lifecycle.status === "interrupted" && !["canceled", "timeout", "error"].includes(payload.status)) { return status("session-interrupted", lifecycle.label || "会话已中断", "blocked", "×", lifecycle.hint || "当前 Code Agent session 已中断,可重试或重新发送。"); } if (payload.status === "canceled" || payload.session?.status === "canceled" || payload.error?.code === "codex_stdio_canceled") { return status("canceled", "请求已取消", "blocked", "×", "当前请求已取消,输入、sessionId 和 traceId 已保留,可重试。"); } if (payload.status === "timeout" || payload.session?.status === "timeout") { return status("timeout", "等待超时", "blocked", "×", "当前请求超时,输入、sessionId 和 traceId 已保留,可重试。"); } if (payload.status === "error" || payload.session?.status === "error") { return status("error", "请求错误", "blocked", "×", "当前请求返回错误,输入、sessionId 和 traceId 已保留,可重试。"); } if (isLongLivedReady(payload)) { return status("long-lived-session", "长会话可用", "ok", "●", "repo-owned 长会话通道可复用。"); } if (payload.provider === "codex-stdio") { const runtimePath = codeAgentRuntimePathFromMessage({ role: "agent", ...payload }); if (runtimePath?.kind !== "codex-app-server-stdio" || runtimePath?.criticalMissing === true || runtimePath?.terminalStatusOk === false) { return isBlocked(payload) ? status("blocked", "服务受阻", "blocked", "×", runtimePath?.summary ?? "Codex stdio 缺少 app-server providerTrace 终态证据。") : status("degraded", "运行路径降级", "warn", "!", runtimePath?.summary ?? "Codex stdio 缺少 app-server providerTrace 终态证据。"); } } if (isReadOnlySessionTools(payload)) { return status("read-only-session-tools", "只读长会话工具", "warn", "◇", "只读 runner 会话可复用,不能写工作区或冒充完整 Codex stdio。"); } 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", "◌", lifecycle.hint || "只有文本对话能力,没有 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 isLongLivedReady(payload) { return ( ( payload.longLivedSessionGate?.status === "pass" || payload.longLivedSessionGate?.pass === true || payload.codexStdioFeasibility?.canStartLongLivedCodexStdio === true ) && ( payload.provider === "codex-stdio" || payload.runner?.kind === CODEX_APP_SERVER_RUNNER_KIND || payload.runner?.codexStdio === true || payload.session?.codexStdio === true ) && payload.capabilityLevel === "long-lived-codex-stdio-session" && payload.sessionMode === CODEX_APP_SERVER_SESSION_MODE && ( !payload.implementationType || payload.implementationType === CODEX_APP_SERVER_IMPLEMENTATION_TYPE ) ) && 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 sessionLifecycleSummary(payload = {}) { let status = normalizeSessionLifecycleStatus(firstString( payload?.sessionLifecycleStatus, payload?.sessionLifecycle?.status, payload?.sessionSummary?.status, payload?.session?.lifecycleStatus, payload?.session?.lifecycle?.status, payload?.runnerTrace?.sessionLifecycleStatus, payload?.session?.status, payload?.sessionStatus, payload?.runnerTrace?.sessionStatus, payload?.sessionReuse?.status, payload?.status ), { fallback: fallbackLifecycleStatus(payload) }); const fallback = isFallbackTextChatOnly(payload) || payload?.fallbackUsed === true || payload?.sessionLifecycle?.fallback === true || payload?.sessionSummary?.fallback === true; const degraded = fallback || payload?.degraded === true || payload?.sessionLifecycle?.degraded === true || payload?.sessionSummary?.degraded === true || payload?.longLivedSessionGate?.status === "blocked" || payload?.blocker?.code === "code_agent_session_evidence_missing" || payload?.error?.code === "code_agent_session_evidence_missing"; const evidenceSessionId = firstString( payload?.session?.sessionId, payload?.sessionReuse?.sessionId, payload?.runnerTrace?.sessionId ); const topLevelSessionId = firstString(payload?.sessionId); const hasSession = Boolean(evidenceSessionId || (topLevelSessionId && topLevelSessionId !== firstString(payload?.conversationId))); if ((fallback || degraded) && !hasSession) status = "failed"; const concreteLifecycleBlock = ["busy", "expired", "failed", "interrupted"].includes(status) && hasSession; const evidenceMissing = payload?.blocker?.code === "code_agent_session_evidence_missing" || payload?.error?.code === "code_agent_session_evidence_missing"; const reused = payload?.sessionSummary?.reused ?? payload?.sessionLifecycle?.reused ?? payload?.sessionReuse?.reused ?? payload?.session?.reused ?? payload?.runnerTrace?.sessionReused ?? null; if (fallback) { return { status: status ?? "failed", label: "会话降级", hint: "会话降级:当前响应来自 fallback/text-only/read-only 路径,不按完整 Code Agent 长会话成功展示。" }; } if (degraded && (evidenceMissing || !concreteLifecycleBlock)) { return { status: status ?? "failed", label: "会话降级", hint: "会话降级:会话证据不完整或运行路径降级,输入已保留,可重新发送建立新会话。" }; } if (status === "busy") { return { status, label: "会话忙", hint: "会话忙/请等待:上一轮仍在处理,稍后重试或取消当前请求。" }; } if (status === "expired") { return { status, label: "会话已过期", hint: "会话已过期,可重新发送建立新的 Code Agent session。" }; } if (status === "failed") { return { status, label: "会话失败", hint: "当前 Code Agent session 已失败,需重新发送建立新会话。" }; } if (status === "interrupted") { return { status, label: "会话已中断", hint: "当前 Code Agent session 已中断;输入和 trace 已保留,可重试或重新发送。" }; } if (status === "creating") { return { status, label: "正在建立会话", hint: "正在建立 Code Agent session,请等待后端返回 session/thread。" }; } if (status === "ready" || status === "idle") { return reused === true ? { status, label: "继续当前会话", hint: "可继续当前会话:后端已确认复用现有 conversation/session/thread。" } : { status, label: "新会话已建立", hint: "新会话已建立:后端返回了可复用的 session/thread。" }; } return { status, label: "会话未确认", hint: "尚未观测到可复用的 Code Agent session 状态。" }; } function normalizeSessionLifecycleStatus(value, { fallback = null } = {}) { const text = firstString(value); const status = String(text ?? "").toLowerCase(); if (["creating", "ready", "busy", "idle", "interrupted", "expired", "failed"].includes(status)) return status; if (status === "canceled" || status === "cancelled") return "interrupted"; if (status === "timeout" || status === "error" || status === "blocked" || status === "degraded") return "failed"; if (status === "running") return "busy"; if (status === "completed" || status === "available" || status === "feasible" || status === "pass") return "idle"; return fallback; } function fallbackLifecycleStatus(payload) { if (isFallbackTextChatOnly(payload) || payload?.degraded === true) return "failed"; if (payload?.status === "running") return "busy"; if (["failed", "timeout", "error"].includes(payload?.status)) return "failed"; if (payload?.status === "canceled") return "interrupted"; return null; } 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?.binary?.present === true && payload?.runtimeContract?.binary?.executable === false ? "codex_cli_not_executable" : null, payload?.runtimeContract?.binary?.present === true && payload?.runtimeContract?.binary?.nativeDependencyPresent === false ? "codex_cli_native_dependency_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 : []; }