Files
codex-agent-manager/web/src/api/normalizers.js
2026-05-25 20:52:26 +08:00

329 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const SOURCE_KIND_LABELS = {
api_missing: '等待接口',
config_toml: '配置文件',
local_sample: '示例数据',
plan_file: '计划文件',
runtime_missing: '运行数据缺失',
sqlite_missing: 'SQLite 缺失',
sqlite_missing_table: 'SQLite 表缺失',
sqlite_partial: 'SQLite 部分可用',
sqlite_readonly: 'SQLite 只读',
sqlite_schema_drift: 'SQLite 结构变化',
sqlite_table: 'SQLite 表',
test: '测试数据',
}
const CONFIDENCE_LABELS = {
high: '高',
medium: '中',
low: '低',
: '高',
: '中',
: '低',
}
const STATUS_LABELS = {
blocked: '受阻',
clean: '无草稿',
complete: '已完成',
done: '已完成',
failed: '失败',
idle: '空闲',
invalid: '无效',
pending: '待处理',
recent: '最近活跃',
running: '运行中',
unknown: '未知',
valid: '有效',
}
const PARSE_STATUS_LABELS = {
invalid: '解析失败',
valid: '解析通过',
}
const TRUST_LABELS = {
trusted: '受信任',
untrusted: '未信任',
unknown: '未知',
}
export function formatSourceKind(kind) {
if (!kind) {
return '来源未知'
}
return SOURCE_KIND_LABELS[kind] ?? '来源未知'
}
export function formatConfidence(confidence) {
if (!confidence) {
return '低'
}
return CONFIDENCE_LABELS[confidence] ?? '低'
}
export function formatStatus(status) {
if (!status) {
return '未知'
}
return STATUS_LABELS[status] ?? '未知'
}
export function formatParseStatus(status) {
if (!status) {
return '未知'
}
return PARSE_STATUS_LABELS[status] ?? formatStatus(status)
}
export function normalizeSource(source, fallback = {}) {
const kind = source?.kind ?? fallback.kind ?? 'api_missing'
const confidence = source?.confidence ?? fallback.confidence ?? 'low'
return {
kind,
label: formatSourceKind(kind),
confidence,
confidenceLabel: formatConfidence(confidence),
path: source?.path ?? fallback.path ?? '',
message: source?.message ?? fallback.message ?? '',
}
}
export function normalizeProject(project) {
const source = normalizeSource(project?.source, { kind: 'config_toml', confidence: 'high' })
const path = project?.path ?? ''
return {
id: path || project?.displayName || 'project',
name: project?.displayName || basename(path) || '未命名项目',
path,
status: project?.directoryExists ? 'complete' : 'unknown',
statusZh: project?.directoryExists ? '目录存在' : '目录不可用',
trust: TRUST_LABELS[project?.trustLevel] ?? '未知',
directoryExists: Boolean(project?.directoryExists),
source: source.label,
confidence: source.confidenceLabel,
sourceDetail: source,
}
}
export function normalizeProjects(payload = {}) {
const projects = Array.isArray(payload.items) ? payload.items.map(normalizeProject) : []
return {
projects,
isEmpty: projects.length === 0,
emptyTitle: '没有项目配置',
emptyText: '后端没有返回项目条目。请确认 .codex/config.toml 中是否存在项目配置;这里不会用示例数据伪装真实结果。',
}
}
export function normalizeAgent(agent = {}) {
const fileName = agent.fileName || basename(agent.filePath) || agent.id || '未命名智能体'
const parseStatus = agent.parseStatus || 'unknown'
const isInvalid = parseStatus === 'invalid'
const name = agent.name || fileName
const description = isInvalid
? `TOML 解析失败:${agent.parseError || '后端未返回具体错误'}`
: agent.description || '没有描述'
const source = normalizeSource(null, {
kind: isInvalid ? 'api_missing' : 'config_toml',
confidence: isInvalid ? 'low' : 'high',
})
return {
id: agent.id || fileName,
file: agent.filePath || fileName,
fileName,
name,
description,
role: agent.developerInstructions || '没有角色设定字段',
status: isInvalid ? 'unknown' : 'complete',
statusLabel: isInvalid ? 'TOML 无效' : 'TOML 有效',
parseStatus,
parseStatusLabel: formatParseStatus(parseStatus),
parseError: agent.parseError || '',
draftStatus: agent.draftStatus || 'unknown',
draftStatusLabel: formatStatus(agent.draftStatus),
extraFields: agent.extraFields || {},
modifiedAt: formatDateTime(agent.modifiedAt),
source: source.label,
confidence: source.confidenceLabel,
toml: synthesizeAgentPreview(agent, { isInvalid, description }),
}
}
export function normalizeAgents(payload = {}) {
const agents = Array.isArray(payload.items) ? payload.items.map(normalizeAgent) : []
return {
agents,
isEmpty: agents.length === 0,
emptyTitle: '没有智能体定义',
emptyText: '后端没有返回 .codex/agents/*.toml 条目。当前显示为空状态,不会把示例智能体当作真实数据。',
}
}
export function normalizeRuntime(payload = {}) {
const threads = Array.isArray(payload.items) ? payload.items : []
const goals = Array.isArray(payload.goals) ? payload.goals : []
const goalsByThread = groupBy(goals, (goal) => goal.threadId)
const agents = threads.map((thread) => {
const source = normalizeSource(thread.source, payload.source)
const threadGoals = goalsByThread.get(thread.id) ?? []
return {
id: thread.id,
name: thread.role || thread.id || '未命名线程',
role: thread.id,
status: thread.status || 'unknown',
statusZh: formatStatus(thread.status),
goal: threadGoals.map((goal) => goal.goal).filter(Boolean).join('') || '没有目标记录',
process: 'SQLite 只读快照',
source: source.label,
confidence: source.confidenceLabel,
lastActivity: thread.updatedAt || thread.createdAt || '没有时间记录',
}
})
const source = normalizeSource(payload.source)
return {
agents,
threads,
goals,
edges: Array.isArray(payload.edges) ? payload.edges : [],
source,
isEmpty: threads.length === 0,
emptyTitle: '没有运行线程',
emptyText: source.message || '后端返回了空的运行线程列表;这里保持空状态并展示来源证据。',
}
}
export function normalizeWorkflow(payload = {}) {
const source = normalizeSource(payload.source)
const phases = Array.isArray(payload.phases)
? payload.phases.filter(isDisplayPhase).map((phase) => {
const phaseSource = normalizeSource(phase.source, payload.source)
const phaseName = normalizePhaseName(phase.name)
return {
name: phaseName,
status: phase.status || 'unknown',
label: formatStatus(phase.status),
gate: phaseName,
evidence: phaseSource.label,
confidence: phaseSource.confidenceLabel,
source: phaseSource,
}
})
: []
const handoffs = Array.isArray(payload.handoffEdges)
? payload.handoffEdges.map((edge) => {
const edgeSource = normalizeSource(edge.source, payload.source)
return {
from: edge.fromThreadId || '未知线程',
to: edge.toThreadId || '未知线程',
summary: edge.label || '没有交接说明',
time: '后端事件',
source: edgeSource.label,
confidence: edgeSource.confidenceLabel,
}
})
: []
const edges = handoffs.map((handoff) => ({
parent: handoff.from,
child: handoff.to,
status: handoff.summary,
source: handoff.source,
confidence: handoff.confidence,
}))
const events = Array.isArray(payload.items) ? payload.items : []
return {
events,
phases,
handoffs,
edges,
source,
isEmpty: events.length === 0 && phases.length === 0 && handoffs.length === 0,
emptyTitle: '没有工作流事件',
emptyText: source.message || '后端返回了空的工作流事件流;这里不会回退到伪装真实的示例关系。',
emptyHandoffsText: '后端没有返回交接边;当前保持空状态。',
}
}
function synthesizeAgentPreview(agent, { isInvalid, description }) {
if (isInvalid) {
return `# TOML 无效,仅只读展示解析错误\n# ${description}`
}
const lines = [
'# 接口未返回原始 TOML以下为只读展示的已解析字段',
`name = ${quote(agent.name || '')}`,
`description = ${quote(agent.description || '')}`,
]
if (agent.developerInstructions) {
lines.push(`角色设定 = ${quote(agent.developerInstructions)}`)
}
for (const [key, value] of Object.entries(agent.extraFields || {})) {
lines.push(`${key} = ${quote(value)}`)
}
return lines.join('\n')
}
function basename(path) {
if (!path) {
return ''
}
return String(path).split('/').filter(Boolean).at(-1) ?? String(path)
}
function formatDateTime(value) {
if (!value) {
return '没有时间记录'
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return value
}
return new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date)
}
function groupBy(items, keyOf) {
const groups = new Map()
for (const item of items) {
const key = keyOf(item)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push(item)
}
return groups
}
function quote(value) {
return JSON.stringify(String(value ?? ''))
}
function isDisplayPhase(phase) {
const name = String(phase?.name ?? '').trim().toLowerCase()
const status = String(phase?.status ?? '').trim().toLowerCase()
if (!name) {
return false
}
if (name === 'phase' || status === 'status') {
return false
}
if (name === 'time' && status === 'phase') {
return false
}
return true
}
function normalizePhaseName(name) {
if (!name) {
return '未命名阶段'
}
return /^\d+$/.test(String(name)) ? `阶段 ${name}` : String(name)
}