1
0

feat: 结构化 observation 替代 statusDetail,API 层动态构造 detail

- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生)
- 存储: status_detail 列 -> observation TEXT (JSON)
- CheckerDefinition: 新增 buildDetail(observation) 方法
- 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail
- HTTP: bodyPreview 在 status/header 失败时也提前采集
- UDP: observation 包含 durationMs,未响应归为 error failure
- CMD: 超时/输出超限时保留已收集 observation
- TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待
- 新增 buildDetail 单测和 mapCheckResult 覆盖测试
- 同步 openspec 主规范,归档 checker-observation 变更
This commit is contained in:
2026-05-19 22:49:00 +08:00
parent 22c06820fa
commit 375dd3492b
64 changed files with 915 additions and 965 deletions

View File

@@ -4,11 +4,15 @@ import type {
LlmCheckObservation,
LlmHttpMetadata,
LlmMode,
LlmPersistedHttpMetadata,
LlmPersistedObservation,
LlmProvider,
LlmStreamObservation,
LlmUsageObservation,
} from "./types";
import { LLM_HEADERS_MAX, LLM_OUTPUT_PREVIEW_MAX } from "./types";
export function buildObservationFromApiCallError(
error: APICallError,
provider: LlmProvider,
@@ -129,3 +133,42 @@ export async function buildObservationFromStreamText(
warnings,
};
}
export function toPersistedObservation(obs: LlmCheckObservation): LlmPersistedObservation {
const outputText = obs.outputText;
const outputPreview =
outputText !== null
? outputText.length <= LLM_OUTPUT_PREVIEW_MAX
? outputText
: outputText.slice(0, LLM_OUTPUT_PREVIEW_MAX)
: null;
const outputLength = outputText !== null ? outputText.length : null;
const http: LlmPersistedHttpMetadata | null = obs.http
? {
headers: truncateHeaders(obs.http.headers, LLM_HEADERS_MAX),
status: obs.http.status,
statusText: obs.http.statusText,
}
: null;
return {
finishReason: obs.finishReason,
http,
mode: obs.mode,
model: obs.model,
outputLength,
outputPreview,
provider: obs.provider,
rawFinishReason: obs.rawFinishReason,
stream: obs.stream,
usage: obs.usage,
warnings: obs.warnings,
};
}
function truncateHeaders(headers: Record<string, string>, maxCount: number): Record<string, string> {
const entries = Object.entries(headers);
if (entries.length <= maxCount) return headers;
return Object.fromEntries(entries.slice(0, maxCount));
}