This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-MVP/packages/contracts/src/envelope.ts
T
JCAI Main AgentandClaude Opus 5 14d4fa046a M1: 底座 —— @contracts 首版 + event / ledger / org / llm
@contracts 首版(§8.1 九个文件):
- id 品牌 ID / terminal 终态五名与归侧四标签 / envelope 信封六字段与四类事件
- card 工单两模式 + 对话卡族(形状对齐 2026-07-26 原型定稿)
- contract7 契约七件 / ledger 三级寻址与写三分 / process-package 流程包 DSL 与触发器三类
- manifest 活动包清单六区(含 CMMN·DMN·步序声明的资产形状)/ org / api 路由契约

模块(L1):
- event   信封存储、归属解析三级短路、投递五诺、监听注册表、三级消费裁定
- ledger  schema 自生长(最大化合并/最宽类型/只增不破坏)、任务视图叠加、写三分、冲突检查
- org     岗位授权(客体三类×动作三种)、停用前置检查、会话、种子播种
- llm     四元绑定、ModelGateway 三模式(live/record/replay)—— 两缝之一

app 装配:Hono 服务 + 鉴权中间件 + 治理台/工作台路由;drizzle 迁移;AES-256-GCM 凭证密封

ADR-007 工具链与门禁 · ADR-008 投递队列 · ADR-009 凭证加密
(ADR 编号自 007 续:编码规范 §14 已用到 006,README 写「从 005 续」是笔误)

门禁五道全绿;96 个测试通过,含对真实 PG 的 ledger 集成测试

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-27 00:59:14 +08:00

223 lines
9.9 KiB
TypeScript

/**
* 统一事件标准(编码规范 §8.1;语义出处 OS 06)。
*
* 系统向活动只以统一信封抛四类事件;一条事件流,三级消费(任务监听 / 触发器 / 消息)。
* 投递五诺(先持久后投递 / 同任务按序 / 至少一次 + 幂等 / 处理有回执 / 扇出与幂等)
* 是 `event` 模块的实现承诺,不在本文件表达 —— 本文件只定信封的形状。
*/
import { z } from 'zod';
import {
channelIdSchema,
deliveryIdSchema,
eventIdSchema,
listenerIdSchema,
positionIdSchema,
taskIdSchema,
triggerIdSchema,
workflowIdSchema,
} from './id.ts';
import { controlSemanticSchema } from './terminal.ts';
/* ── 四类事件 ─────────────────────────────────────────────────────────── */
export const EVENT_TYPES = ['data', 'time', 'human', 'control'] as const;
export const eventTypeSchema = z.enum(EVENT_TYPES);
export type EventType = z.infer<typeof eventTypeSchema>;
export const EVENT_TYPE_LABEL: Readonly<Record<EventType, string>> = {
data: '数据',
time: '时间',
human: '人',
control: '控制',
};
/* ── 关联:消费目标 + 起因 ────────────────────────────────────────────── */
/** 消费目标四类(OS 6.1):任务 / 触发器 / 工作流 / 消息收件岗位 */
export const eventTargetSchema = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('task'), id: taskIdSchema }),
z.object({ kind: z.literal('trigger'), id: triggerIdSchema }),
z.object({ kind: z.literal('workflow'), id: workflowIdSchema }),
z.object({ kind: z.literal('message-position'), id: positionIdSchema }),
]);
export type EventTarget = z.infer<typeof eventTargetSchema>;
/** 起因:数据对象 / 卡 ID / 定时器 ID —— 「这条事件是被什么激起的」 */
export const eventCauseSchema = z.object({
kind: z.enum(['ledger-write', 'card-reply', 'timer', 'inbound', 'governance', 'engine']),
ref: z.string().min(1),
});
export type EventCause = z.infer<typeof eventCauseSchema>;
/* ── 来源 ─────────────────────────────────────────────────────────────── */
export const EVENT_SOURCES = ['channel', 'engine', 'desk', 'governance'] as const;
export const eventSourceKindSchema = z.enum(EVENT_SOURCES);
export type EventSourceKind = z.infer<typeof eventSourceKindSchema>;
export const eventSourceSchema = z.object({
kind: eventSourceKindSchema,
/** 通道来源必带通道实例 ID */
channelId: channelIdSchema.optional(),
detail: z.string().optional(),
});
export type EventSource = z.infer<typeof eventSourceSchema>;
/* ── 载荷 ─────────────────────────────────────────────────────────────── */
/** 载荷:schema 化内容 + 原件指针(如邮件原文的存档地址) */
export const eventPayloadSchema = z.object({
data: z.record(z.string(), z.unknown()),
/** 原件指针 —— 存档地址,不是内容本身 */
originRef: z.string().optional(),
});
export type EventPayload = z.infer<typeof eventPayloadSchema>;
/* ── 信封六字段 ───────────────────────────────────────────────────────── */
export const envelopeSchema = z.object({
/** ① 事件 ID —— 全局唯一,幂等键 */
id: eventIdSchema,
/** ② 类型 —— 四类之一 */
type: eventTypeSchema,
/**
* ③ 关联 —— 消费目标 + 起因。
*
* `target` 可空:存储态的事件未必知道自己该被谁消费(归属解析与三级消费判定在后)。
* **投递态**的单目标由 `Delivery.target` 承载 —— 「信封关联 = 单目标」的承诺(OS 6.3 扇出)
* 在投递记录上成立,存储的原始事件只有一份。
*/
correlation: z.object({
target: eventTargetSchema.optional(),
cause: eventCauseSchema,
/** 关联键:归属解析与监听过滤的匹配面(单号、线程 ID、域.对象 主键…) */
keys: z.record(z.string(), z.string()).default({}),
}),
/** ④ 载荷 */
payload: eventPayloadSchema,
/** ⑤ 时间与来源 */
occurredAt: z.iso.datetime(),
source: eventSourceSchema,
/** ⑥ 留痕指针 —— 事件存储位置;投递入任务后另记任务记忆指针 */
trace: z.object({
storeRef: z.string().min(1),
taskMemoryRef: z.string().optional(),
}),
});
export type Envelope = z.infer<typeof envelopeSchema>;
/**
* 尚未落存储的信封草稿 —— `publish` 的入参。
*
* `target` 在此**可空**:原始事件未必知道自己该被谁消费。归属解析(6.2)与三级消费判定
* (任务监听 / 触发器 / 消息兜底,6.3)由 `event` 模块完成,并为每个命中目标生成一份
* 独立投递 —— 投递出去的信封「关联 = 单目标」,这正是扇出与幂等承诺的形态。
* `storeRef` 由 event adapter 落库时补。
*/
export const envelopeDraftSchema = envelopeSchema
.omit({ id: true, trace: true })
.extend({ id: eventIdSchema.optional() });
export type EnvelopeDraft = z.infer<typeof envelopeDraftSchema>;
/* ── 四类事件的载荷细化 ──────────────────────────────────────────────── */
/** 控制事件载荷:三语义 + 发起人 */
export const controlPayloadSchema = z.object({
semantic: controlSemanticSchema,
reason: z.string().optional(),
issuedBy: z.string().min(1),
});
export type ControlPayload = z.infer<typeof controlPayloadSchema>;
/** 时间事件载荷:哪个监听 / 定时器到点 */
export const timePayloadSchema = z.object({
listenerId: listenerIdSchema.optional(),
timerRef: z.string().min(1),
/** 时限阶梯:提醒 → 升级(OS 03 催办阶梯) */
escalation: z.enum(['nudge', 'reroute', 'escalate']).optional(),
});
export type TimePayload = z.infer<typeof timePayloadSchema>;
/** 数据事件载荷:域.对象.主键 的正式版写入 —— 草稿写不抛事件(OS 09) */
export const dataPayloadSchema = z.object({
domain: z.string().min(1),
object: z.string().min(1),
primaryKey: z.record(z.string(), z.union([z.string(), z.number()])),
changed: z.array(z.string()).default([]),
operation: z.enum(['insert', 'update', 'delete']),
});
export type DataPayload = z.infer<typeof dataPayloadSchema>;
/* ── 监听声明(OS 6.3 / AM 4.4) ─────────────────────────────────────── */
/**
* 处理语义:loop 事件即观察 / flow 事件即步的完成条件。
* 这是**引擎层**的区分,交互层不可见(编码规范 §9 双层边界)。
*/
export const LISTENER_SEMANTICS = ['observe', 'complete-step'] as const;
export const listenerSemanticSchema = z.enum(LISTENER_SEMANTICS);
export type ListenerSemantic = z.infer<typeof listenerSemanticSchema>;
export const listenerSpecSchema = z.object({
/** 静态(随业务模型)/ 动态(运行中由 Agent 机制临场登记) */
origin: z.enum(['static', 'dynamic']),
eventType: eventTypeSchema,
/** 关联键过滤 —— 「凭什么认出就是它」 */
keyFilter: z.record(z.string(), z.string()).default({}),
/** 内容条件:S-FEEL 表达式,对信封与载荷字段求值 */
condition: z.string().optional(),
semantic: listenerSemanticSchema,
/**
* 时限必配 —— 无来源又无时限的监听是「幽灵等待」,安装静态审必须拒绝(AM 4.4)。
* ISO-8601 duration,如 P3D。
*/
deadline: z.string().min(2),
/** 到期动作阶梯:提醒 → 换道 → 升级 */
escalation: z.array(z.enum(['nudge', 'reroute', 'escalate'])).default(['nudge', 'escalate']),
});
export type ListenerSpec = z.infer<typeof listenerSpecSchema>;
export const registeredListenerSchema = listenerSpecSchema.extend({
id: listenerIdSchema,
taskId: taskIdSchema,
registeredAt: z.iso.datetime(),
expiresAt: z.iso.datetime(),
state: z.enum(['armed', 'fired', 'expired', 'cancelled']),
});
export type RegisteredListener = z.infer<typeof registeredListenerSchema>;
/* ── 投递记录 ─────────────────────────────────────────────────────────── */
export const deliverySchema = z.object({
id: deliveryIdSchema,
eventId: eventIdSchema,
target: eventTargetSchema,
attempt: z.number().int().min(1),
state: z.enum(['pending', 'delivered', 'failed', 'dead-letter']),
lastError: z.string().optional(),
});
export type Delivery = z.infer<typeof deliverySchema>;
/* ── 归属解析(OS 6.2) ──────────────────────────────────────────────── */
/** 匹配次序确定、逐级短路:① 线程精确 ② 关联键命中监听 ③ 地址 + 时间窗 */
export const OWNERSHIP_RULES = ['thread', 'listener-key', 'address-window'] as const;
export const ownershipRuleSchema = z.enum(OWNERSHIP_RULES);
export type OwnershipRule = z.infer<typeof ownershipRuleSchema>;
export const ownershipResolutionSchema = z.discriminatedUnion('outcome', [
z.object({
outcome: z.literal('owned'),
rule: ownershipRuleSchema,
taskId: taskIdSchema,
}),
z.object({
outcome: z.literal('unowned'),
/** 宁交人,不错投 —— 歧义与提取失败一律降级为无主 */
reason: z.enum(['no-match', 'ambiguous', 'extract-failed']),
/** 无主入站的相关人 = 通道实例配置的默认认领岗位 */
fallbackPosition: positionIdSchema.optional(),
}),
]);
export type OwnershipResolution = z.infer<typeof ownershipResolutionSchema>;