/** * message 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。 */ import type { StaffId } from '@contracts'; import type { MessageService, SendElicitation, SendNotice } from '../../contract.ts'; import { MessageNotAnswerableError } from '../../contract.ts'; import type { Clock, IdGenerator, MessageRepository, PositionResolver } from '../../port.ts'; import type { MessageKind, SystemMessage } from '../domain/message-kind.ts'; import { canAnswer } from '../domain/message-kind.ts'; export type MessageServiceDeps = { readonly repo: MessageRepository; readonly org: PositionResolver; readonly clock: Clock; readonly ids: IdGenerator; }; export function createMessageService(deps: MessageServiceDeps): MessageService { function build( kind: MessageKind, staffId: StaffId, title: string, body: string, extra: Partial = {}, ): SystemMessage { return { id: deps.ids.next(), kind, staffId, title, body, at: deps.clock.now().toISOString(), ...extra, }; } async function fanOut(kind: MessageKind, staff: ReadonlyArray<{ id: StaffId }>, title: string, body: string, eventRef?: string) { for (const person of staff) { await deps.repo.insert(build(kind, person.id, title, body, eventRef === undefined ? {} : { eventRef })); } } return { async inbox(staffId, options) { return deps.repo.list(staffId, options?.unreadOnly ?? false, options?.limit ?? 100); }, async markRead(messageId) { await deps.repo.markRead(messageId, deps.clock.now()); }, async notify(input: SendNotice) { return deps.repo.insert( build('notice', input.staffId, input.title, input.body, input.eventRef === undefined ? {} : { eventRef: input.eventRef }), ); }, async sendElicitation(input: SendElicitation) { return deps.repo.insert( build('elicitation', input.staffId, input.title, input.body, { card: { cardId: input.cardId, sourceTaskId: input.sourceTaskId, payload: input.payload }, }), ); }, async answer(messageId, staffId, answer) { const message = await deps.repo.get(messageId); if (message === undefined || !canAnswer(message, staffId)) { throw new MessageNotAnswerableError(messageId); } await deps.repo.markAnswered(messageId, deps.clock.now(), answer); const updated = await deps.repo.get(messageId); if (updated === undefined) throw new MessageNotAnswerableError(messageId); return updated; }, /** 驳回重答:新卡实例(附驳回理由与原答引用),原卡闭合 —— 每次请求与回答都是独立消息 */ async rejectAndReask(messageId, reason) { const original = await deps.repo.get(messageId); if (original === undefined || original.card === undefined) { throw new MessageNotAnswerableError(messageId); } const reask = build('elicitation', original.staffId as StaffId, original.title, original.body, { card: { cardId: deps.ids.next(), sourceTaskId: original.card.sourceTaskId, payload: original.card.payload, rejectedReason: reason, }, }); const created = await deps.repo.insert(reask); await deps.repo.supersede(messageId, created.id, reason); return created; }, /** 死信:送相关人的消息要素**与**治理台 · 任务管理(OS 6.3) */ async deadLetter(envelope, error) { const governance = await deps.org.governanceStaff(); await fanOut( 'dead-letter', governance, `事件投递失败入死信:${envelope.type}`, `事件 ${envelope.id} 重试超限。原因:${error}`, envelope.trace.storeRef, ); }, /** 无主兜底:相关人 = 通道实例配置的默认认领岗位;未配置则落治理台 */ async unowned(envelope, fallbackPosition) { const recipients = fallbackPosition === undefined ? await deps.org.governanceStaff() : await deps.org.staffOfPosition(fallbackPosition); const targets = recipients.length > 0 ? recipients : await deps.org.governanceStaff(); const subject = typeof envelope.payload.data['subject'] === 'string' ? envelope.payload.data['subject'] : '(无主题)'; await fanOut( 'unowned', targets, `无主入站待认领:${subject}`, `事件 ${envelope.id} 未命中任何任务监听与触发器。原件:${envelope.payload.originRef ?? '无'}`, envelope.trace.storeRef, ); }, }; }