import { z } from "zod"; import { cursorPage, Timestamp } from "./common.ts"; // 日志模块(界面第二轮 V 线):业务任务、定时 / 自动任务运行、登录、审计合成一份按时间倒序的列表。 // 不新建表:GET /logs 在查询时把各来源表 union 起来(engine workflow_runs / scheduled_job_run / // agent_run(trigger=event)/ audit_event),游标按 (时间, id) 分页;GET /logs/stream 用 SSE 追加新日志。 /** * 类型: * - task 业务任务(工作流引擎的一次运行) * - auto 定时与自动:定时任务运行记录(含智能体巡检)、智能体后台调查 * - auth 登录:管理员登录 / 退出 / 登录失败 / 二次验证 / Passkey / 改密、API Key 的增改撤 * - audit 其余审计:设置、凭证、实例、文件操作、智能体操作等 */ export const LogType = z.enum(["task", "auto", "auth", "audit"]); export type LogType = z.infer; /** 级别 / 结果:进行中、成功、信息(普通操作)、警告(取消、跳过以外的非成功,如登录失败、被拒绝、超出预算)、失败 */ export const LogLevel = z.enum(["running", "success", "info", "warn", "error"]); export type LogLevel = z.infer; /** 来源表 */ export const LogSource = z.enum(["engine_run", "schedule_run", "agent_run", "audit_event"]); export type LogSource = z.infer; export const LogObject = z.object({ /** 对象类型:审计的 target_type;任务按业务对象换算(download_task / organization_record / agent_run / scheduled_job …) */ type: z.string(), id: z.string().nullable(), /** 展示名(作品、下载任务、定时任务名等);取不到为 null */ label: z.string().nullable(), }); export type LogObject = z.infer; export const LogEntry = z.object({ /** :<原始 id>,全局唯一,也是游标的一部分 */ id: z.string(), type: LogType, source: LogSource, /** 原始记录 id(引擎 run id、scheduled_job_run id、agent_run id、audit_event id) */ refId: z.string(), /** 发生时间:任务为创建时间、定时任务为开始时间、审计为事件时间 */ at: Timestamp, finishedAt: Timestamp.nullable(), durationMs: z.number().nonnegative().nullable(), level: LogLevel, /** 来源表的原始状态(completed / failed / ok / skipped / admin.login_success …) */ status: z.string().nullable(), /** 一行摘要:任务业务标题或工作流名、定时任务名、智能体目标、审计动作 */ title: z.string(), /** 第二行:错误、运行摘要、登录 IP 等 */ message: z.string().nullable(), object: LogObject.nullable(), /** 操作者(审计)或触发来源(任务 / 定时任务) */ actor: z.object({ type: z.string(), id: z.string().nullable() }).nullable(), /** 各来源特有字段(workflowName、business、trigger、action、payload …),详情页用 */ extra: z.record(z.string(), z.unknown()), }); export type LogEntry = z.infer; /** 单值或逗号分隔多值(type=task,auto) */ function csv(e: z.ZodType, name: string) { return z .string() .max(200) .transform((s, ctx) => { const out: T[] = []; for (const p of s .split(",") .map((x) => x.trim()) .filter(Boolean)) { const r = e.safeParse(p); if (!r.success) { ctx.addIssue({ code: "custom", message: `未知 ${name}: ${p}` }); return z.NEVER; } out.push(r.data); } return out; }) .optional(); } /** 列表与流共用的筛选 */ export const LogFilter = z.object({ /** 逗号分隔多值;缺省全部 */ type: csv(LogType, "type"), level: csv(LogLevel, "level"), from: Timestamp.optional(), to: Timestamp.optional(), /** 关键词:标题、摘要、错误、动作、对象 id(不区分大小写) */ q: z.string().trim().max(200).optional(), /** 对象:与 LogEntry.object 的 type / id 匹配;只给 objectId 时不限类型 */ objectType: z.string().max(100).optional(), objectId: z.string().max(200).optional(), /** "1" = 也列系统任务(没有业务对象的引擎运行、定时任务触发的成功运行);缺省隐藏,按对象筛选时自动包含 */ system: z.enum(["0", "1"]).optional(), }); export type LogFilter = z.infer; export const LogQuery = LogFilter.extend({ cursor: z.string().optional(), limit: z.coerce.number().int().min(1).max(200).default(50), }); export type LogQuery = z.infer; export const LogPage = cursorPage(LogEntry); export type LogPage = z.infer; export const LogStreamQuery = LogFilter.extend({ /** 只推这个时间之后的新日志(客户端列表里最新一条的时间);缺省为连接时刻 */ since: Timestamp.optional(), }); export type LogStreamQuery = z.infer; /** SSE 事件名:entry = 新日志或已推过的日志状态有变化(按 id 覆盖) */ export const LOG_STREAM_EVENT = "log.entry";