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:
co-authored by
Claude Opus 5
parent
14d4fa046a
commit
f0444f1417
@@ -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}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user