app 装配 mail:channel 作首个订阅方、出站经 mail.send、mail.send 工具真发信(dryRun 只校验)、轮询工作者;上传件存取抽出共用

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
xcs
2026-08-26 02:05:50 +08:00
co-authored by Claude Fable 5
parent 58358210e2
commit 0c6e8e2c1b
10 changed files with 449 additions and 48 deletions
+46 -10
View File
@@ -74,12 +74,19 @@ import type { TraceService } from '@modules/trace';
import { wireEngines } from './wiring/engines.ts';
import { wireDeskElements } from './wiring/desk-elements.ts';
import type { ChannelService } from '@modules/channel';
import { createChannelRepository, createChannelService, createFileAttachmentArchive } from '@modules/channel';
import type { MailService } from '@modules/mail';
import {
createChannelRepository,
createChannelService,
createFileAttachmentArchive,
createSimulatedMailTransport,
} from '@modules/channel';
createImapSmtpProvider,
createInboundStore,
createMailAccountStore,
createMailService,
createOutboundStore,
createPostalMimeParser,
createWecomProvider,
} from '@modules/mail';
import { channelSubscriber, channelTransport, mailSendTool } from './mail-wiring.ts';
import { readUpload, storeUpload } from './upload-store.ts';
import type { AppConfig } from './config.ts';
import { createIdGenerator, systemClock } from './runtime.ts';
@@ -107,6 +114,8 @@ export type Container = {
readonly toolInvoker: ToolInvoker;
readonly message: MessageService;
readonly channel: ChannelService;
/** 邮件收发(ADR-013):账号 / 轮询 / 订阅 / 发信 */
readonly mail: MailService;
readonly humanTool: HumanToolService;
readonly interaction: InteractionService;
readonly registry: ActivityRegistryService;
@@ -172,9 +181,6 @@ const sfeelEvaluator = (expression: string, scope: Readonly<Record<string, unkno
}
};
/** 第一方工具实现 —— 由 app 注册进 ToolRuntime;活动只见调用契约 */
export const firstPartyToolHandlers = new Map<string, ToolHandler>(FIRST_PARTY_TOOLS);
/** 集成测试的注入缝:验签与名册来源都是外界,测试给替身,不联网 */
export type ContainerOverrides = {
readonly tokenVerifier?: TokenVerifier;
@@ -369,9 +375,36 @@ export async function createContainer(config: AppConfig, overrides: ContainerOve
ids,
});
/*
* 邮件收发(ADR-013)—— L1,只要库 / 密封 / 时钟;先于工具运行时装配,`mail.send` 的处理器要闭包持它。
* 变更经 ChangeNotifier 接到广播:轮询器不走 /api/gov/* 中间件,新来信 / 拉取出错得自己说一声。
* 附件落 sys.upload —— 与上传路由同一段存取(8MB 上限、文本抽取、下载路由一并复用)。
*/
const mail = createMailService({
accounts: createMailAccountStore(db),
inbound: createInboundStore(db),
outbound: createOutboundStore(db),
attachments: {
put: async (file) => ({
ref: (await storeUpload(db, { name: file.name, contentType: file.contentType, bytes: file.bytes, uploadedBy: null, note: file.note })).ref,
}),
get: (ref) => readUpload(db, ref),
},
providers: { 'imap-smtp': createImapSmtpProvider(), wecom: createWecomProvider() },
mime: createPostalMimeParser(),
secrets,
clock: systemClock,
ids,
notify: { changed: (reason) => broadcaster.publish({ kind: 'system', id: 'mail', reason }) },
});
const toolRegistry = createToolRegistry(db);
/** 第一方工具实现 —— `mail.send` 真发信,处理器在这儿闭包持 mail(放行时经 ToolInvoker 执行) */
// 导入的自定义工具走通用 HTTP 实现:契约申报 endpoint,运行时只把那一个连接目标交出去
const toolRuntime = createToolRuntime(firstPartyToolHandlers, httpToolHandler);
const toolRuntime = createToolRuntime(
new Map<string, ToolHandler>([...FIRST_PARTY_TOOLS, ['mail.send', mailSendTool(mail)]]),
httpToolHandler,
);
const schemas = createSchemaValidator();
const toolbox = createToolboxService({
registry: toolRegistry,
@@ -399,11 +432,13 @@ export async function createContainer(config: AppConfig, overrides: ContainerOve
recordOutbound: (ref) => event.recordOutbound(ref),
},
ledger: { systemWrite: (input) => ledger.systemWrite(input) },
mail: createSimulatedMailTransport(db, ids),
mail: channelTransport(mail),
archive: createFileAttachmentArchive(config.attachmentRoot),
clock: systemClock,
ids,
});
// channel 是第一个订阅方:来信归一进事件流,活动经触发器 / 监听收到;账号没绑通道的信留在运营面
mail.subscribe('channel', channelSubscriber(channel));
/* ── L3/L4:交互、人类工具、活动库、放行门 ─────────────────────── */
@@ -775,6 +810,7 @@ export async function createContainer(config: AppConfig, overrides: ContainerOve
toolInvoker,
message,
channel,
mail,
humanTool,
interaction,
registry,