/** * 系统消息的种类与投影 —— 纯逻辑,零 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> = { 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; 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; }