/** * 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}`; }, }; }