Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
118 lines
4.9 KiB
TypeScript
118 lines
4.9 KiB
TypeScript
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<typeof LogType>;
|
|
|
|
/** 级别 / 结果:进行中、成功、信息(普通操作)、警告(取消、跳过以外的非成功,如登录失败、被拒绝、超出预算)、失败 */
|
|
export const LogLevel = z.enum(["running", "success", "info", "warn", "error"]);
|
|
export type LogLevel = z.infer<typeof LogLevel>;
|
|
|
|
/** 来源表 */
|
|
export const LogSource = z.enum(["engine_run", "schedule_run", "agent_run", "audit_event"]);
|
|
export type LogSource = z.infer<typeof LogSource>;
|
|
|
|
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<typeof LogObject>;
|
|
|
|
export const LogEntry = z.object({
|
|
/** <source>:<原始 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<typeof LogEntry>;
|
|
|
|
/** 单值或逗号分隔多值(type=task,auto) */
|
|
function csv<T extends string>(e: z.ZodType<T>, 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<typeof LogFilter>;
|
|
|
|
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<typeof LogQuery>;
|
|
|
|
export const LogPage = cursorPage(LogEntry);
|
|
export type LogPage = z.infer<typeof LogPage>;
|
|
|
|
export const LogStreamQuery = LogFilter.extend({
|
|
/** 只推这个时间之后的新日志(客户端列表里最新一条的时间);缺省为连接时刻 */
|
|
since: Timestamp.optional(),
|
|
});
|
|
export type LogStreamQuery = z.infer<typeof LogStreamQuery>;
|
|
|
|
/** SSE 事件名:entry = 新日志或已推过的日志状态有变化(按 id 覆盖) */
|
|
export const LOG_STREAM_EVENT = "log.entry";
|