forked from ChaiHao/BPOS-MVP
- memory 任务记忆四性质(只追加·信封盖戳·治理先行·归侧)、活动记忆四层与条目三件套、
任务日志两级投影(结构化零 LLM / 叙述性小结不入事实库)、度量八指标只读投影
- toolbox 注册两审(静态审 + 试运行审)、效应等级决定运行时待遇、契约演进只增不破坏、
白名单与纯工具私有作用域、ToolInvoker(两缝之二,live/record/replay)
- channel 通道实例配置、入站归一(解析 → 系统写 → 数据事件)、出站发送与关联记录登记
- message 通知 / 跨任务征答(就地作答 · 驳回重答只增不改)/ 死信 / 无主兜底
app:message 接上 event 的 MessageFallback;派发器按目标 ID 取 advisory lock 串行消费 +
消费幂等表;/api/ingest 模拟邮件通道(mock 只在外界)
已验证:无主 RFQ 邮件入站 → 归属解析 no-match → 事件落库 → 兜底落 OP 岗位的消息要素
门禁五道全绿;150 个单元测试 + 1 个 PG 集成测试
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
66 lines
2.6 KiB
TypeScript
66 lines
2.6 KiB
TypeScript
/**
|
|
* 系统消息的种类与投影 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 2.1 / 06)。
|
|
*
|
|
* 「消息是**信息**,不产生责任」—— 与任务、审批(责任要素)分明。
|
|
* 三类内容:通知(承载触发器的邮件和消息、公告、提醒与告警)+ 跨任务征答(他人任务发来的卡)
|
|
* + 死信与回执的人侧呈现。
|
|
*/
|
|
|
|
export const MESSAGE_KINDS = ['notice', 'elicitation', 'dead-letter', 'unowned', 'receipt'] as const;
|
|
export type MessageKind = (typeof MESSAGE_KINDS)[number];
|
|
|
|
export const MESSAGE_KIND_LABEL: Readonly<Record<MessageKind, string>> = {
|
|
notice: '通知',
|
|
elicitation: '跨任务征答',
|
|
'dead-letter': '死信',
|
|
unowned: '无主入站',
|
|
receipt: '回执',
|
|
};
|
|
|
|
export type SystemMessage = {
|
|
readonly id: string;
|
|
readonly kind: MessageKind;
|
|
/** 收件人:员工;跨任务卡按岗位解析出人后落到具体员工 */
|
|
readonly staffId: string;
|
|
/** 岗位收件(无主兜底按默认认领岗位投递) */
|
|
readonly positionId?: string;
|
|
readonly title: string;
|
|
readonly body: string;
|
|
readonly at: string;
|
|
readonly readAt?: string;
|
|
/** 跨任务征答:卡本体与来源任务 —— 就地作答,不开放任务对话流 */
|
|
readonly card?: {
|
|
readonly cardId: string;
|
|
readonly sourceTaskId: string;
|
|
readonly payload: Record<string, unknown>;
|
|
readonly answeredAt?: string;
|
|
/** 驳回重答:新卡实例,附驳回理由与原答引用;原卡闭合 —— 只增不改 */
|
|
readonly supersededBy?: string;
|
|
readonly rejectedReason?: string;
|
|
};
|
|
readonly eventRef?: string;
|
|
};
|
|
|
|
/**
|
|
* 跨人卡是**一次性的解答请求**,不是责任转移 ——
|
|
* 作答权限只及卡面与决策包,不开放任务对话流(OS 2.1)。
|
|
*/
|
|
export function canAnswer(message: SystemMessage, staffId: string): boolean {
|
|
if (message.kind !== 'elicitation') return false;
|
|
if (message.staffId !== staffId) return false;
|
|
if (message.card === undefined) return false;
|
|
// 已作答或已被驳回重答闭合的卡不得再答 —— 每次请求与回答都是独立消息
|
|
return message.card.answeredAt === undefined && message.card.supersededBy === undefined;
|
|
}
|
|
|
|
/** 未读计数:消息不产生责任,只做信息提示 */
|
|
export function unreadCount(messages: readonly SystemMessage[]): number {
|
|
return messages.filter((m) => m.readAt === undefined).length;
|
|
}
|
|
|
|
/** 待答计数:跨任务征答里还没作答的 */
|
|
export function pendingAnswerCount(messages: readonly SystemMessage[]): number {
|
|
return messages.filter((m) => m.kind === 'elicitation' && m.card?.answeredAt === undefined && m.card?.supersededBy === undefined)
|
|
.length;
|
|
}
|