import { z } from "zod"; import { AgentRunStatus, DecisionKind, DecisionStatus, Severity } from "./agent.ts"; import { Id, NonNegInt, Timestamp } from "./common.ts"; import { DownloadStatus, PauseReason, TaskKind, TaskStatus } from "./execution.ts"; import { EvidenceLevel } from "./identity.ts"; import { OrganizationStatus } from "./organization.ts"; import { FeedHealth } from "./sources.ts"; /** SSE 事件信封:seq 在进程内单调递增,客户端断线后用 ?after= 补读 */ export const EventEnvelope = z.object({ seq: z.number().int().nonnegative(), type: z.string().min(1), at: Timestamp, payload: z.unknown(), }); export type EventEnvelope

= { seq: number; type: string; at: string; payload: P; }; /** 已知事件类型:`域.动作`。新增事件先在这里加类型与 payload schema */ export const EventType = { heartbeat: "system.heartbeat", resync: "system.resync", taskCreated: "task.created", taskStatusChanged: "task.status_changed", downloadSubmitted: "download.submitted", downloadProgress: "download.progress", downloadStatusChanged: "download.status_changed", downloadThreshold: "download.threshold", organizationChanged: "organization.changed", agentRunStatus: "agent.run_status", agentTextDelta: "agent.text_delta", agentToolCall: "agent.tool_call", feedFetched: "feed.fetched", feedHealthChanged: "feed.health_changed", releaseResolved: "release.resolved", decisionCreated: "decision.created", decisionUpdated: "decision.updated", } as const; export type EventType = (typeof EventType)[keyof typeof EventType]; /** 心跳 payload */ export const HeartbeatPayload = z.object({ role: z.string(), }); /** 服务端环形缓冲不足以补读时发出,客户端应整页刷新 */ export const ResyncPayload = z.object({ reason: z.string() }); export const TaskCreatedPayload = z.object({ taskId: Id, kind: TaskKind, subjectType: z.string().nullable(), subjectId: Id.nullable(), }); export const TaskStatusChangedPayload = z.object({ taskId: Id, kind: TaskKind, from: TaskStatus.nullable(), to: TaskStatus, error: z.string().nullable(), }); export const DownloadSubmittedPayload = z.object({ downloadTaskId: Id, instanceId: Id, torrentHash: z.string(), planId: Id.nullable(), name: z.string(), }); export const DownloadProgressPayload = z.object({ downloadTaskId: Id, progress: z.number().min(0).max(1), downloadedBytes: NonNegInt, sizeBytes: NonNegInt.nullable(), }); export const DownloadStatusChangedPayload = z.object({ downloadTaskId: Id, from: DownloadStatus.nullable(), to: DownloadStatus, pauseReason: PauseReason.nullable(), }); /** 缓存卷越过硬阈值(tripped)或回到恢复水位(recovered) */ export const DownloadThresholdPayload = z.object({ volumeKey: z.string(), state: z.enum(["tripped", "recovered", "unknown"]), usedPercent: z.number().min(0).max(100).nullable(), pausedTaskIds: z.array(Id), }); /** 整理记录新增或状态变化;changeSeq 与公开 API 的游标一致 */ export const OrganizationChangedPayload = z.object({ recordId: Id, status: OrganizationStatus, changeSeq: z.string().regex(/^\d+$/), mediaEntityId: Id.nullable(), }); export const AgentRunStatusPayload = z.object({ runId: Id, status: AgentRunStatus, summary: z.string().nullable(), }); /** Agent 文本增量:runSeq 为该 run 内序号,配合 agent_run.last_event_seq 断点续读 */ export const AgentTextDeltaPayload = z.object({ runId: Id, runSeq: NonNegInt, delta: z.string(), }); export const AgentToolCallPayload = z.object({ runId: Id, runSeq: NonNegInt, capabilityId: z.string(), phase: z.enum(["started", "succeeded", "failed", "denied"]), /** 已脱敏的摘要,不含凭据/私有 URL */ summary: z.string(), }); export const FeedFetchedPayload = z.object({ feedId: Id, newItems: NonNegInt, updatedItems: NonNegInt, notModified: z.boolean(), }); export const FeedHealthChangedPayload = z.object({ feedId: Id, from: FeedHealth.nullable(), to: FeedHealth, reason: z.string().nullable(), }); /** 来源发布已识别到本地作品(W2-B 发出,W2-C 的 evaluateAcquisition 消费);歧义/无匹配走 decision.created */ export const ReleaseResolvedPayload = z.object({ sourceReleaseId: Id, observationId: Id.nullable(), mediaEntityId: Id, contentUnitIds: z.array(Id), evidenceLevel: EvidenceLevel, }); export const DecisionCreatedPayload = z.object({ decisionId: Id, kind: DecisionKind, severity: Severity, title: z.string(), }); export const DecisionUpdatedPayload = z.object({ decisionId: Id, status: DecisionStatus, }); /** 事件类型 → payload schema */ export const EVENT_PAYLOADS = { "system.heartbeat": HeartbeatPayload, "system.resync": ResyncPayload, "task.created": TaskCreatedPayload, "task.status_changed": TaskStatusChangedPayload, "download.submitted": DownloadSubmittedPayload, "download.progress": DownloadProgressPayload, "download.status_changed": DownloadStatusChangedPayload, "download.threshold": DownloadThresholdPayload, "organization.changed": OrganizationChangedPayload, "agent.run_status": AgentRunStatusPayload, "agent.text_delta": AgentTextDeltaPayload, "agent.tool_call": AgentToolCallPayload, "feed.fetched": FeedFetchedPayload, "feed.health_changed": FeedHealthChangedPayload, "release.resolved": ReleaseResolvedPayload, "decision.created": DecisionCreatedPayload, "decision.updated": DecisionUpdatedPayload, } as const satisfies Record; export type EventPayload = z.infer<(typeof EVENT_PAYLOADS)[T]>; /** 已知事件的判别联合:`{ type, payload }` */ export type TypedEvent = { [T in EventType]: { type: T; payload: EventPayload } }[EventType]; /** 已知事件信封的 schema(判别联合) */ export const TypedEventEnvelope = z.discriminatedUnion( "type", Object.entries(EVENT_PAYLOADS).map(([type, payload]) => z.object({ seq: z.number().int().nonnegative(), at: Timestamp, type: z.literal(type), payload }), ) as unknown as [z.ZodObject, ...z.ZodObject[]], ); export function isEventType(type: string): type is EventType { return Object.hasOwn(EVENT_PAYLOADS, type); } /** 校验一个事件 payload;不合法返回 null */ export function parseEventPayload(type: T, payload: unknown): EventPayload | null { const schema: z.ZodType = EVENT_PAYLOADS[type]; const r = schema.safeParse(payload); return r.success ? (r.data as EventPayload) : null; }