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
This commit is contained in:
JCAI Main Agent
2026-07-27 01:16:52 +08:00
co-authored by Claude Opus 5
parent 14d4fa046a
commit f0444f1417
73 changed files with 7488 additions and 31 deletions
@@ -0,0 +1,96 @@
/**
* channel 存储与传输 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。
*/
import { eq } from 'drizzle-orm';
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import type { Db } from '@shared/db.ts';
import { positionIdSchema } from '@contracts';
import type { AttachmentArchive, ChannelRepository, MailTransport } from '../../port.ts';
import type { ChannelInstanceConfig, ExtractRule, LedgerMapping } from '../domain/payload-mapping.ts';
import { channelTable, outboundMailTable } from './channel-schema.ts';
type Row = typeof channelTable.$inferSelect;
function toConfig(row: Row): ChannelInstanceConfig {
return {
id: row.id,
name: row.name,
kind: row.kind as ChannelInstanceConfig['kind'],
address: row.address,
enabled: row.enabled,
extract: row.extract as ExtractRule[],
mapping: row.mapping as LedgerMapping[],
fallbackPosition: row.fallbackPosition === null ? undefined : positionIdSchema.parse(row.fallbackPosition),
addressWindowMs: row.addressWindowMs,
};
}
export function createChannelRepository(db: Db): ChannelRepository {
return {
async list() {
const rows = await db.select().from(channelTable).orderBy(channelTable.name);
return rows.map(toConfig);
},
async get(channelId) {
const rows = await db.select().from(channelTable).where(eq(channelTable.id, channelId)).limit(1);
return rows[0] === undefined ? undefined : toConfig(rows[0]);
},
async upsert(config) {
const values = {
id: config.id,
name: config.name,
kind: config.kind,
address: config.address,
enabled: config.enabled,
extract: config.extract,
mapping: config.mapping,
fallbackPosition: config.fallbackPosition ?? null,
addressWindowMs: config.addressWindowMs,
};
const [row] = await db
.insert(channelTable)
.values(values)
.onConflictDoUpdate({ target: channelTable.id, set: values })
.returning();
if (row === undefined) throw new Error('通道实例写入未返回行');
return toConfig(row);
},
};
}
/**
* 模拟邮件通道 —— **mock 只允许出现在外界**(README MVP 完成判据):
* 出站信落库不真发,入站经 /api/ingest 注入。系统内部机制一律真跑。
*/
export function createSimulatedMailTransport(db: Db, ids: { next(): string }): MailTransport {
return {
async send(input) {
const messageId = ids.next();
const threadId = input.threadId ?? messageId;
await db.insert(outboundMailTable).values({
id: messageId,
threadId,
fromAddr: input.from,
toAddrs: [...input.to],
ccAddrs: [...input.cc],
subject: input.subject,
body: input.body,
attachments: [...input.attachments],
});
return { messageId, threadId };
},
};
}
/** 原件存档 —— 落本地目录,返回存档地址作为信封的原件指针 */
export function createFileAttachmentArchive(root: string): AttachmentArchive {
return {
async put(name, content) {
const path = join(root, name);
await mkdir(dirname(path), { recursive: true });
await writeFile(path, content, 'utf8');
return `file://${path}`;
},
};
}