import { CAPABILITY_CATALOG, type EngineBusinessKind, type KnownCapabilityId, type LogEntry, type LogLevel, type LogType, } from "@umt/contracts"; import type { StatusTone } from "~/components/console/status-badge.tsx"; import { BUSINESS_KIND_LABEL, workflowLabel } from "~/components/tasks/labels.ts"; import { actorTypeLabel } from "~/lib/labels.ts"; import { formatDateTime } from "~/lib/utils.ts"; // 日志页的标签与纯函数(界面第二轮 V 线):类型、级别、审计动作、标题 / 摘要、筛选参数、给智能体的上下文。 export const LOG_PATH = "/console/logs"; export const LOG_TYPE_LABEL: Record = { task: "任务", auto: "定时与自动", auth: "登录", audit: "审计", }; export const LOG_TYPES = Object.keys(LOG_TYPE_LABEL) as LogType[]; export const LOG_LEVEL: Record = { running: { label: "进行中", tone: "neutral" }, success: { label: "成功", tone: "success" }, info: { label: "信息", tone: "neutral" }, warn: { label: "警告", tone: "warning" }, error: { label: "失败", tone: "destructive" }, }; export const LOG_LEVELS = Object.keys(LOG_LEVEL) as LogLevel[]; /** 时间范围预设 → 毫秒 */ export const LOG_SINCE: { value: string; label: string; ms: number }[] = [ { value: "1h", label: "1 小时内", ms: 3600_000 }, { value: "24h", label: "24 小时内", ms: 86400_000 }, { value: "7d", label: "7 天内", ms: 7 * 86400_000 }, { value: "30d", label: "30 天内", ms: 30 * 86400_000 }, ]; // ── 审计动作(原审计页) ── const ACTION_LABEL: Record = { "admin.setup": "初始化管理员", "admin.login_success": "登录", "admin.login_failed": "登录失败", "admin.logout": "退出登录", "admin.password_change": "修改密码", "admin.totp_enable": "启用二次验证", "admin.totp_disable": "停用二次验证", "admin.recovery_codes_regenerate": "重新生成恢复码", "admin.recovery_code_login": "用恢复码登录", "admin.passkey_register": "登记 Passkey", "admin.passkey_rename": "重命名 Passkey", "admin.passkey_remove": "删除 Passkey", "admin.passkey_login": "用 Passkey 登录", "api_key.create": "创建 API Key", "api_key.update": "修改 API Key", "api_key.revoke": "撤销 API Key", "settings.update": "修改设置", "secret.create": "新建凭证", "secret.rotate": "轮换凭证", "secret.delete": "删除凭证", "secret.master_key_rotate": "轮换主密钥", "site_account.credentials_set": "录入站点凭证", "engine.run_cancel": "取消任务", "engine.run_retry": "重跑任务", "pi_upstream.oauth_start": "开始订阅登录", "pi_upstream.oauth_login": "完成订阅登录", "pi_upstream.oauth_logout": "退出订阅登录", "schedule.run_now": "立即执行定时任务", "schedule.skip": "跳过定时任务", "schedule.update": "修改定时任务", "storage.cleanup": "立即清理存储", "acquisition_plan.manual_submit": "手动提交下载", "acquisition_plan.cancel": "取消下载", "capability.invoke": "智能体操作", "capability.denied": "智能体操作被拒绝", "site.created": "登记站点", "site.updated": "修改站点", "feed_source.created": "新建 RSS", "identity.release_assigned": "指定资源身份", "files.mkdir": "新建目录", "files.rename": "重命名", "files.move": "移动文件", "files.copy": "复制文件", "files.organize": "整理文件", "files.delete": "删除文件", "files.trash_restore": "从回收站还原", }; const VERB: Record = { create: "新建", update: "修改", delete: "删除", test: "测试", verify: "校验" }; const NOUN: Record = { library_root: "媒体库目录", downloader_instance: "下载器", media_server_instance: "媒体服务器", path_mapping: "路径映射", storage_instance: "存储", pi_upstream: "模型服务", schedule: "定时任务", automation_grant: "持续授权", }; export const TARGET_LABEL: Record = { ...NOUN, admin_user: "管理员", admin_session: "登录会话", api_key: "API Key", settings: "设置", secret: "凭证", site: "站点", site_account: "站点账号", feed_source: "RSS", engine_run: "任务", scheduled_job: "定时任务", agent_run: "智能体会话", acquisition_plan: "下载", download_task: "下载任务", media_entity: "作品", source_release: "资源", organization_record: "整理记录", subscription: "订阅", decision: "事项", file: "文件", capability: "能力", }; export function actionLabel(a: string): string { if (ACTION_LABEL[a]) return ACTION_LABEL[a]; const [noun, verb] = a.split("."); if (noun && verb && NOUN[noun] && VERB[verb]) return `${VERB[verb]}${NOUN[noun]}`; return a; } /** 对象文字:有展示名用展示名;能力显示中文名;其余显示对象类型 */ export function objectText(o: LogEntry["object"]): string | null { if (!o) return null; if (o.label) return o.label; if (o.type === "capability" && o.id) return CAPABILITY_CATALOG[o.id as KnownCapabilityId]?.title ?? o.id; return TARGET_LABEL[o.type] ?? o.type; } // ── 标题、摘要、状态 ── const SCHEDULE_STATUS: Record = { ok: { label: "成功", tone: "success" }, failed: { label: "失败", tone: "destructive" }, skipped: { label: "已跳过", tone: "neutral" }, running: { label: "进行中", tone: "neutral" }, }; const AGENT_STATUS: Record = { queued: { label: "排队", tone: "neutral" }, waiting: { label: "等待", tone: "neutral" }, cancelled: { label: "已停止", tone: "warning" }, budget_exceeded: { label: "超出预算", tone: "warning" }, }; const ENGINE_STATUS: Record = { pending: { label: "排队", tone: "neutral" }, canceled: { label: "已取消", tone: "warning" }, }; /** 行上的状态徽章:来源有更具体的说法时用来源的(已跳过、已取消、超出预算),否则用级别 */ export function logBadge(e: Pick): { label: string; tone: StatusTone; } { const s = e.status ?? ""; if (e.source === "schedule_run") { const agent = typeof e.extra.agentStatus === "string" ? AGENT_STATUS[e.extra.agentStatus] : undefined; return agent ?? SCHEDULE_STATUS[s] ?? LOG_LEVEL[e.level]; } if (e.source === "agent_run") return AGENT_STATUS[s] ?? LOG_LEVEL[e.level]; if (e.source === "engine_run") { const outcome = typeof e.extra.outcome === "string" ? AGENT_STATUS[e.extra.outcome] : undefined; return (e.level === "warn" ? outcome : undefined) ?? ENGINE_STATUS[s] ?? LOG_LEVEL[e.level]; } return LOG_LEVEL[e.level]; } /** 一行标题:任务 = 业务对象名 > 业务类型 > 工作流做什么;审计 = 动作;其余用服务端标题 */ export function logTitle(e: LogEntry): string { if (e.source === "engine_run") { const x = e.extra as { businessTitle?: string | null; businessKind?: EngineBusinessKind | null; workflowName?: string; }; if (x.businessTitle) return x.businessTitle; if (x.businessKind && BUSINESS_KIND_LABEL[x.businessKind]) return BUSINESS_KIND_LABEL[x.businessKind]; return workflowLabel(x.workflowName ?? e.title); } if (e.source === "audit_event") return actionLabel(e.status ?? e.title); if (e.source === "agent_run") return firstLine(e.title); return e.title; } /** 第二行:任务的工作流(有业务标题时)、错误或摘要 */ export function logSubtitle(e: LogEntry): string | null { const parts: string[] = []; if (e.source === "engine_run" && (e.extra as { businessTitle?: string }).businessTitle) parts.push(workflowLabel(String(e.extra.workflowName ?? ""))); if (e.source === "schedule_run" && e.extra.jobKind === "agent_check") parts.push("智能体巡检"); if (e.source === "agent_run") parts.push("智能体后台调查"); if (e.message) parts.push(firstLine(e.message)); return parts.length ? parts.join(" · ") : null; } /** 操作者 / 触发来源 */ export function actorLabel(e: LogEntry): string | null { if (!e.actor) return null; if (e.source === "schedule_run") return e.actor.type === "manual" ? "手动" : e.actor.type === "skip" ? "跳过" : "定时"; if (e.source === "agent_run") return "事件"; return actorTypeLabel(e.actor.type); } function firstLine(s: string, max = 140): string { const line = s.split("\n").find((l) => l.trim()) ?? s; return line.length > max ? `${line.slice(0, max)}…` : line; } // ── 筛选参数(URL query) ── export const LOG_FILTER_KEYS = ["type", "level", "since", "q", "objectType", "objectId", "system"] as const; export type LogFilterKey = (typeof LOG_FILTER_KEYS)[number]; export type LogFilters = Record; /** 时间预设下界按分钟取整,查询 key 稳定 */ export function sinceToFrom(since: string, now = Date.now()): string | undefined { const ms = LOG_SINCE.find((s) => s.value === since)?.ms; return ms ? new Date(Math.floor((now - ms) / 60_000) * 60_000).toISOString() : undefined; } /** 日志页地址(其它页面链到日志用) */ export function logsPath(p: Partial & { tab?: string; job?: string; log?: string } = {}): string { const q = new URLSearchParams(); for (const [k, v] of Object.entries(p)) if (v) q.set(k, v); const s = q.toString(); return s ? `${LOG_PATH}?${s}` : LOG_PATH; } /** 某个对象的全部日志 */ export const objectLogsPath = (type: string, id: string) => logsPath({ objectType: type, objectId: id }); // ── 给智能体的上下文 ── const GOAL_MAX = 6000; /** 一条日志的文字版(给智能体看,带 id 方便它查) */ export function logLine(e: LogEntry): string { const obj = e.object ? ` 对象 ${e.object.type}${e.object.id ? `=${e.object.id}` : ""}` : ""; const msg = e.message ? ` · ${firstLine(e.message, 300)}` : ""; return `- ${formatDateTime(e.at)} [${LOG_TYPE_LABEL[e.type]} / ${logBadge(e).label}] ${logTitle(e)}${msg}(${e.id}${obj})`; } /** 选中的日志 → 问智能体的目标(按时间正序,超长截断) */ export function agentGoalFor(entries: LogEntry[]): string { const head = entries.length === 1 ? "看看这条日志,说明发生了什么;有问题的话找出原因并给出处理建议。" : `看看这 ${entries.length} 条日志,说明发生了什么;有问题的话找出原因并给出处理建议。`; const lines: string[] = []; let size = head.length; const sorted = [...entries].sort((a, b) => Date.parse(a.at) - Date.parse(b.at)); for (const e of sorted) { const l = logLine(e); if (size + l.length + 1 > GOAL_MAX) { lines.push(`- …另有 ${sorted.length - lines.length} 条未列出`); break; } lines.push(l); size += l.length + 1; } return `${head}\n${lines.join("\n")}`; } // ── 旧链接 ── /** 旧任务页的 status / tab → 级别 */ export const RUN_STATUS_LEVEL: Record = { pending: "running", running: "running", completed: "success", failed: "error", canceled: "warn", }; /** 业务对象 kind → 日志对象类型(与服务端 BUSINESS_OBJECT_TYPE 一致) */ export const BUSINESS_OBJECT_TYPE: Record = { download: "download_task", organization: "organization_record", agent: "agent_run", subscription: "subscription", feed: "feed_source", site: "site", decision: "decision", media: "media_entity", schedule: "scheduled_job", };