feat(contracts): 日志列表 / 日志流与资源监控契约,审计新增登录失败
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
ad3bf8926b
commit
01c68f9771
9 files changed
+245
No files matched your search
@@ -5,6 +5,7 @@ import { episodeMappingApi } from "./api/episode-mapping.ts";
|
||||
import { executionApi } from "./api/execution.ts";
|
||||
import { instancesApi } from "./api/instances.ts";
|
||||
import { libraryApi } from "./api/library.ts";
|
||||
import { logsApi } from "./api/logs.ts";
|
||||
import { opsApi } from "./api/ops.ts";
|
||||
import { organizeApi } from "./api/organize.ts";
|
||||
import { platformApi } from "./api/platform.ts";
|
||||
@@ -41,6 +42,7 @@ export const Api = {
|
||||
...organizeApi,
|
||||
...platformApi,
|
||||
...systemApi,
|
||||
...logsApi,
|
||||
...libraryApi,
|
||||
...browserApi,
|
||||
...schedulesApi,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as logs from "../logs.ts";
|
||||
import { ep } from "./shared.ts";
|
||||
|
||||
// 日志模块(界面第二轮 V 线,docs/runbook/logs.md):业务任务、定时 / 自动任务、登录、审计合成一份列表。仅 admin。
|
||||
export const logsApi = {
|
||||
logs: {
|
||||
list: ep({
|
||||
method: "get",
|
||||
path: "/logs",
|
||||
scope: "admin",
|
||||
summary: "日志列表:任务 / 定时与自动 / 登录 / 审计合并,按时间倒序,游标分页",
|
||||
query: logs.LogQuery,
|
||||
response: logs.LogPage,
|
||||
}),
|
||||
stream: ep({
|
||||
method: "get",
|
||||
path: "/logs/stream",
|
||||
scope: "admin",
|
||||
summary: "日志流(text/event-stream):按同一组筛选推新日志与状态变化,事件名 log.entry",
|
||||
query: logs.LogStreamQuery,
|
||||
response: logs.LogEntry,
|
||||
}),
|
||||
},
|
||||
} as const;
|
||||
@@ -3,6 +3,7 @@ import * as auth from "../auth.ts";
|
||||
import { cursorPage, HealthStatus, IdParam } from "../common.ts";
|
||||
import * as events from "../events.ts";
|
||||
import * as ops from "../ops.ts";
|
||||
import * as resources from "../system-resources.ts";
|
||||
import { EngineRunIdParam, ep, OkResult } from "./shared.ts";
|
||||
|
||||
export const systemApi = {
|
||||
@@ -170,6 +171,14 @@ export const systemApi = {
|
||||
query: ops.SystemHealthHistoryQuery,
|
||||
response: ops.SystemHealthHistory,
|
||||
}),
|
||||
resources: ep({
|
||||
method: "get",
|
||||
path: "/system/resources",
|
||||
scope: "admin",
|
||||
summary: "资源监控:CPU / 内存 / 网络 / 磁盘读写的当前值与最近一段时间的趋势(api 进程内存采样)",
|
||||
query: resources.SystemResourcesQuery,
|
||||
response: resources.SystemResources,
|
||||
}),
|
||||
storage: ep({
|
||||
method: "get",
|
||||
path: "/system/storage",
|
||||
|
||||
@@ -194,6 +194,7 @@ export type PasskeyRenameRequest = z.infer<typeof PasskeyRenameRequest>;
|
||||
export const AuditAction = z.enum([
|
||||
"admin.setup",
|
||||
"admin.login_success",
|
||||
"admin.login_failed",
|
||||
"admin.logout",
|
||||
"admin.password_change",
|
||||
"admin.totp_enable",
|
||||
|
||||
@@ -18,6 +18,7 @@ export * from "./identity.ts";
|
||||
export * from "./instances.ts";
|
||||
export * from "./library-versions.ts";
|
||||
export * from "./local-metadata.ts";
|
||||
export * from "./logs.ts";
|
||||
export * from "./ops.ts";
|
||||
export * from "./organization.ts";
|
||||
export * from "./organize-sources.ts";
|
||||
@@ -29,3 +30,4 @@ export * from "./scrape.ts";
|
||||
export * from "./settings.ts";
|
||||
export * from "./sources.ts";
|
||||
export * from "./subtitles.ts";
|
||||
export * from "./system-resources.ts";
|
||||
@@ -0,0 +1,117 @@
|
||||
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";
|
||||
@@ -0,0 +1,72 @@
|
||||
import { z } from "zod";
|
||||
import { Timestamp } from "./common.ts";
|
||||
|
||||
// 资源监控(界面第二轮 V 线,参考 MoviePilot 仪表盘的 CPU / 内存 / 网络组件):
|
||||
// api 进程每 5 秒读一次 /proc 与 cgroup v2,采样只放内存(保留 6 小时,进程重启后从头攒)。
|
||||
// 容器里优先用 cgroup(本容器的 CPU 占用、内存、磁盘读写),取不到时退回 /proc 整机口径;都取不到的字段为 null。
|
||||
|
||||
/** 采集口径:cgroup = 本容器(cgroup v2);host = /proc 整机;none = 取不到 */
|
||||
export const ResourceScope = z.enum(["cgroup", "host", "none"]);
|
||||
export type ResourceScope = z.infer<typeof ResourceScope>;
|
||||
|
||||
const Num = z.number().nonnegative().nullable();
|
||||
|
||||
export const ResourceSample = z.object({
|
||||
at: Timestamp,
|
||||
/** CPU 占用 %(相对可用核数,0–100) */
|
||||
cpuPercent: Num,
|
||||
memoryUsedBytes: Num,
|
||||
memoryPercent: Num,
|
||||
netRxBps: Num,
|
||||
netTxBps: Num,
|
||||
diskReadBps: Num,
|
||||
diskWriteBps: Num,
|
||||
});
|
||||
export type ResourceSample = z.infer<typeof ResourceSample>;
|
||||
|
||||
export const SystemResources = z.object({
|
||||
at: Timestamp,
|
||||
/** 采样间隔(秒) */
|
||||
intervalSec: z.number().positive(),
|
||||
/** 返回的趋势每点代表的秒数(长时间窗口按桶取平均) */
|
||||
bucketSec: z.number().positive(),
|
||||
cpu: z.object({
|
||||
scope: ResourceScope,
|
||||
percent: Num,
|
||||
/** 可用核数:cgroup cpu.max 的限额,否则为在线核数 */
|
||||
cores: Num,
|
||||
/** 整机 CPU 占用 %(/proc/stat),与 scope 无关 */
|
||||
hostPercent: Num,
|
||||
/** 整机 1 / 5 / 15 分钟平均负载(3 个数) */
|
||||
load: z.array(z.number()).nullable(),
|
||||
}),
|
||||
memory: z.object({
|
||||
scope: ResourceScope,
|
||||
/** 已用:cgroup 为 memory.current − inactive_file(同 docker stats);整机为 MemTotal − MemAvailable */
|
||||
usedBytes: Num,
|
||||
/** 上限:cgroup memory.max(不限时退回整机总量) */
|
||||
limitBytes: Num,
|
||||
percent: Num,
|
||||
hostTotalBytes: Num,
|
||||
hostAvailableBytes: Num,
|
||||
swapUsedBytes: Num,
|
||||
}),
|
||||
network: z.object({
|
||||
scope: ResourceScope,
|
||||
rxBps: Num,
|
||||
txBps: Num,
|
||||
rxTotalBytes: Num,
|
||||
txTotalBytes: Num,
|
||||
}),
|
||||
diskIo: z.object({ scope: ResourceScope, readBps: Num, writeBps: Num }),
|
||||
/** 整机已运行秒数(/proc/uptime) */
|
||||
hostUptimeSec: Num,
|
||||
samples: z.array(ResourceSample),
|
||||
});
|
||||
export type SystemResources = z.infer<typeof SystemResources>;
|
||||
|
||||
export const SystemResourcesQuery = z.object({
|
||||
/** 趋势窗口(分钟),5–360,缺省 60 */
|
||||
minutes: z.coerce.number().int().min(5).max(360).default(60),
|
||||
});
|
||||
export type SystemResourcesQuery = z.infer<typeof SystemResourcesQuery>;
|
||||
@@ -101,6 +101,9 @@ const OVERRIDES: Record<string, (v: Record<string, unknown>) => Record<string, u
|
||||
OrganizeRequest: (v) => ({ ...v, sourcePaths: undefined }),
|
||||
FilesListQuery: (v) => ({ ...v, filter: "video,unidentified" }),
|
||||
EngineRunQuery: (v) => ({ ...v, status: "running,pending", triggeredBy: "user,api_key" }),
|
||||
LogFilter: (v) => ({ ...v, type: "task,auth", level: "error,warn" }),
|
||||
LogQuery: (v) => ({ ...v, type: "task,auth", level: "error,warn" }),
|
||||
LogStreamQuery: (v) => ({ ...v, type: "auto", level: "running" }),
|
||||
CatalogLookupInput: (v) => v,
|
||||
IdentityResolveInput: (v) => v,
|
||||
AssetLocationCreate: (v) => v,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { execDdl } from "../src/migration-helpers.ts";
|
||||
|
||||
// 日志模块(界面第二轮 V 线):GET /logs 把定时任务运行记录按开始时间倒序与其它来源合并,
|
||||
// 原有索引只有 (job_id, started_at),全局倒序要整表排序;补一个按时间的索引。
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, `create index if not exists scheduled_job_run_started_idx on scheduled_job_run (started_at desc);`);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, `drop index if exists scheduled_job_run_started_idx;`);
|
||||
}
|
||||
Reference in new issue
Block a user