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/apps/api/src/modules/message/internal/usecase/message-service.ts
T
JCAI Main AgentandClaude Opus 5 f0444f1417 M2: 基座能力 —— memory / toolbox / channel / message
- 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
2026-07-27 01:16:52 +08:00

126 lines
4.5 KiB
TypeScript

/**
* 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> = {},
): 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,
);
},
};
}