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
+35
View File
@@ -0,0 +1,35 @@
# module: channel
职责: 通道实例:配置(归属键提取规则 / 载荷映射 / 默认认领岗位)、入站归一
(解析 → 系统写 → 数据事件)、出站发送与出站关联记录登记
不负责: 归属解析与三级消费判定(见 event,经 `EventGateway` 端口)、
台账写入的实现(见 ledger,经 `LedgerSystemWriter` 端口)、消息兜底的呈现(见 message)
公开出口: index.ts
- 类型:`ChannelService` `ChannelInstanceConfig` `ExtractRule` `LedgerMapping`
`NormalizedWrite` `InboundOutcome` `OutboundMail`
- 错误:`ChannelNotFoundError` `ChannelDisabledError`
- 端口类型:`ChannelRepository` `EventGateway` `LedgerSystemWriter` `MailTransport`
`AttachmentArchive` `Clock` `IdGenerator`
- 用例工厂:`createChannelService(deps: ChannelServiceDeps): ChannelService`
- 纯函数:`extractFields` `normalize`
- adapter:`createChannelRepository` `createSimulatedMailTransport` `createFileAttachmentArchive`
- Drizzle 表:`channelTable` `outboundMailTable`
层级: L2(编码规范 §4)
依赖: @contracts;@shared/db;port:`EventGateway`(app 注入 event)、
`LedgerSystemWriter`(app 注入 ledger)、`MailTransport`(模拟通道)
被依赖: release(出站发送)、app(治理台 · 系统管理 / ingest 路由)
不变量:
- **活动认数据,不认通道**:入站一律归一为域.对象.字段的写入,通道对活动的前提不可见
- 入站归一走 **系统写**(登记世界已发生的事实,免门、即时抛数据事件),
绝不走草稿写、也不绕过 `ledger` 出口(§12.5#9)
- 归属解析**不在本模块**:通道只交事实,判定归 event —— 匹配次序与降级口径一处实现
- **无主不写台账**:仅有主入站才归一为系统写;无主入站只入事件流并走消息兜底
- 无主键的映射结果丢弃(无从定位),由调用方按提取失败处置
- 出站即登记关联记录:线程 ID 与往来地址是归属解析①③级的对表来源
- 邮件通道是**外界**:MVP 用模拟通道(出站落库不真发,入站经 ingest 注入);
系统内部机制一律真跑,不得 mock
影响半径: index/contract/port 未变 → 对外零影响
@@ -0,0 +1,81 @@
/** 入站归一的载荷映射 —— domain 测试零 mock(OS 6.2) */
import { describe, expect, it } from 'vitest';
import { extractFields, normalize } from '../internal/domain/payload-mapping.ts';
import type { ExtractRule, LedgerMapping } from '../internal/domain/payload-mapping.ts';
const fields = {
subject: '回复:报价 Q202601',
body: 'PN: LM358DR,我方接受单价 1.85 元',
from: 'buyer@acme.com',
to: ['bd@we.com'],
};
const rules: ExtractRule[] = [
{ key: 'quoteNo', regex: 'Q\\d{6}', source: 'subject' },
{ key: 'pn', regex: 'PN:\\s*([A-Z0-9]+)', source: 'body' },
{ key: 'sender', regex: '.+', source: 'from' },
];
describe('extractFields', () => {
it('按来源分别提取,捕获组优先', () => {
expect(extractFields(fields, rules)).toEqual({
quoteNo: 'Q202601',
pn: 'LM358DR',
sender: 'buyer@acme.com',
});
});
it('不命中即不产出该键', () => {
expect(extractFields({ ...fields, subject: '你好' }, [rules[0]!])).toEqual({});
});
it('both 同时看主题与正文', () => {
expect(
extractFields({ ...fields, subject: '你好' }, [{ key: 'pn', regex: 'LM\\d+DR', source: 'both' }]),
).toEqual({ pn: 'LM358DR' });
});
});
describe('normalize —— 提取字段 → 域.对象.字段', () => {
const mapping: LedgerMapping[] = [
{ from: 'quoteNo', domain: 'quote', object: 'reply', field: 'quote_no', primaryKey: true },
{ from: 'pn', domain: 'quote', object: 'reply', field: 'pn', primaryKey: true },
{ from: 'sender', domain: 'quote', object: 'reply', field: 'sender' },
{ from: '', domain: 'quote', object: 'reply', field: 'channel', constant: 'mail' },
];
it('同一对象的字段合并成一次写入,主键进 key', () => {
const writes = normalize(extractFields(fields, rules), mapping);
expect(writes).toEqual([
{
domain: 'quote',
object: 'reply',
key: { quote_no: 'Q202601', pn: 'LM358DR' },
values: { sender: 'buyer@acme.com', channel: 'mail' },
},
]);
});
it('常量映射不依赖提取结果', () => {
const writes = normalize({}, [{ from: 'x', domain: 'd', object: 'o', field: 'k', primaryKey: true, constant: 'C' }]);
expect(writes[0]?.key).toEqual({ k: 'C' });
});
it('无主键的写入丢弃 —— 无从定位', () => {
expect(normalize({ sender: 'a@b.com' }, [{ from: 'sender', domain: 'd', object: 'o', field: 'f' }])).toEqual([]);
});
it('缺提取值的映射跳过,不写 undefined', () => {
const writes = normalize({ quoteNo: 'Q1' }, mapping);
expect(writes[0]?.values).toEqual({ channel: 'mail' });
expect(writes[0]?.key).toEqual({ quote_no: 'Q1' });
});
it('多对象各成一次写入', () => {
const writes = normalize({ a: '1', b: '2' }, [
{ from: 'a', domain: 'd1', object: 'o1', field: 'k', primaryKey: true },
{ from: 'b', domain: 'd2', object: 'o2', field: 'k', primaryKey: true },
]);
expect(writes).toHaveLength(2);
});
});
+57
View File
@@ -0,0 +1,57 @@
/**
* module: channel —— 对外类型(编码规范 §10.1)。
*/
import type { InboundMail, OwnershipResolution, TaskId } from '@contracts';
import type {
ChannelInstanceConfig,
ExtractRule,
LedgerMapping,
NormalizedWrite,
} from './internal/domain/payload-mapping.ts';
export type { ChannelInstanceConfig, ExtractRule, InboundMail, LedgerMapping, NormalizedWrite, OwnershipResolution };
export class ChannelNotFoundError extends Error {
constructor(readonly channelId: string) {
super(`通道实例不存在:${channelId}`);
this.name = 'ChannelNotFoundError';
}
}
export class ChannelDisabledError extends Error {
constructor(readonly channelId: string) {
super(`通道实例已停用:${channelId}`);
this.name = 'ChannelDisabledError';
}
}
export type InboundOutcome = {
readonly eventId: string;
readonly resolution: OwnershipResolution;
readonly writes: readonly NormalizedWrite[];
readonly listenersHit: number;
readonly triggersHit: number;
readonly fellBackToMessage: boolean;
};
export type OutboundMail = {
readonly taskId: TaskId;
readonly to: readonly string[];
readonly cc?: readonly string[];
readonly subject: string;
readonly body: string;
readonly threadId?: string;
readonly attachments?: ReadonlyArray<{ name: string; ref: string }>;
};
export type ChannelService = {
list(): Promise<ChannelInstanceConfig[]>;
upsert(config: ChannelInstanceConfig): Promise<ChannelInstanceConfig>;
/**
* 入站:归属解析 → 归一为**系统写** → 数据事件。
* 「环外回音经归属解析归一为数据写入后,以数据事件抛出 —— 活动认数据,不认通道。」
*/
ingestMail(channelId: string, mail: InboundMail): Promise<InboundOutcome>;
/** 出站:触达世界的动作;发送即登记出站关联记录(归属解析①③级的对表来源) */
sendMail(channelId: string, mail: OutboundMail): Promise<{ messageId: string; threadId: string }>;
};
+36
View File
@@ -0,0 +1,36 @@
/**
* module: channel —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type {
ChannelInstanceConfig,
ChannelService,
ExtractRule,
InboundMail,
InboundOutcome,
LedgerMapping,
NormalizedWrite,
OutboundMail,
} from './contract.ts';
export { ChannelDisabledError, ChannelNotFoundError } from './contract.ts';
export type {
AttachmentArchive,
ChannelRepository,
Clock,
EventGateway,
IdGenerator,
LedgerSystemWriter,
MailTransport,
} from './port.ts';
export type { ChannelServiceDeps } from './internal/usecase/channel-service.ts';
export { createChannelService } from './internal/usecase/channel-service.ts';
export { extractFields, normalize } from './internal/domain/payload-mapping.ts';
export {
createChannelRepository,
createFileAttachmentArchive,
createSimulatedMailTransport,
} from './internal/adapter/channel-store.ts';
export { channelTable, outboundMailTable } from './internal/adapter/channel-schema.ts';
@@ -0,0 +1,35 @@
/**
* channel 的 sys schema 表(编码规范 §7 / ADR-002)。
* 表定义单独成文件:drizzle.config.ts 以 `*-schema.ts` glob 收集。
*/
import { boolean, integer, jsonb, text, timestamp } from 'drizzle-orm/pg-core';
import { sys } from '@shared/db.ts';
export const channelTable = sys.table('channel_instance', {
id: text('id').primaryKey(),
name: text('name').notNull(),
kind: text('kind').notNull(),
address: text('address').notNull(),
enabled: boolean('enabled').notNull().default(true),
/** 归属键提取规则(声明式,OS 6.2) */
extract: jsonb('extract').notNull().default([]),
/** 载荷映射:提取字段 → 域.对象.字段,与边映射同一形式 */
mapping: jsonb('mapping').notNull().default([]),
/** 无主入站的默认认领岗位 */
fallbackPosition: text('fallback_position'),
addressWindowMs: integer('address_window_ms').notNull().default(604_800_000),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});
/** 出站存档:模拟通道把「已发出的信」落库,供演示与回放查看 */
export const outboundMailTable = sys.table('outbound_mail', {
id: text('id').primaryKey(),
threadId: text('thread_id').notNull(),
fromAddr: text('from_addr').notNull(),
toAddrs: jsonb('to_addrs').notNull(),
ccAddrs: jsonb('cc_addrs').notNull().default([]),
subject: text('subject').notNull(),
body: text('body').notNull(),
attachments: jsonb('attachments').notNull().default([]),
sentAt: timestamp('sent_at', { withTimezone: true }).notNull().defaultNow(),
});
@@ -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}`;
},
};
}
@@ -0,0 +1,100 @@
/**
* 通道入站的载荷映射 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 6.2)。
*
* 「有主入站归一为**数据写入**:写入目标由通道实例配置的载荷映射给定
* (提取字段 → 域.对象.字段,**与边映射同一形式**),原件存档为附件对象,以数据事件抛出。」
*
* 活动认数据,不认通道 —— 通道对活动的前提不可见。
*/
import type { PositionId } from '@contracts';
export type ExtractRule = {
readonly key: string;
readonly regex: string;
readonly source: 'subject' | 'body' | 'both' | 'from' | 'to';
};
/** 提取字段 → 域.对象.字段 */
export type LedgerMapping = {
readonly from: string;
readonly domain: string;
readonly object: string;
readonly field: string;
/** 该字段是否参与主键 */
readonly primaryKey?: boolean;
/** 常量:from 留空时用它 */
readonly constant?: string;
};
export type ChannelInstanceConfig = {
readonly id: string;
readonly name: string;
readonly kind: 'mail' | 'im';
readonly address: string;
readonly enabled: boolean;
/** 归属键提取规则(声明式) */
readonly extract: readonly ExtractRule[];
/** 载荷映射 */
readonly mapping: readonly LedgerMapping[];
/** 无主入站的默认认领岗位 */
readonly fallbackPosition?: PositionId | undefined;
readonly addressWindowMs: number;
};
export type InboundFields = {
readonly subject: string;
readonly body: string;
readonly from: string;
readonly to: readonly string[];
};
export function extractFields(fields: InboundFields, rules: readonly ExtractRule[]): Record<string, string> {
const out: Record<string, string> = {};
for (const rule of rules) {
const haystack =
rule.source === 'subject'
? fields.subject
: rule.source === 'body'
? fields.body
: rule.source === 'from'
? fields.from
: rule.source === 'to'
? fields.to.join(',')
: `${fields.subject}\n${fields.body}`;
const match = new RegExp(rule.regex).exec(haystack);
const captured = match?.[1] ?? match?.[0];
if (captured !== undefined) out[rule.key] = captured;
}
return out;
}
export type NormalizedWrite = {
readonly domain: string;
readonly object: string;
readonly key: Record<string, string>;
readonly values: Record<string, string>;
};
/**
* 归一:提取结果 + 映射 → 一组「域.对象」的写入。
* 同一对象的多个字段合并成一次写入;主键字段进 key,其余进 values。
*/
export function normalize(
extracted: Readonly<Record<string, string>>,
mapping: readonly LedgerMapping[],
): NormalizedWrite[] {
const byObject = new Map<string, { domain: string; object: string; key: Record<string, string>; values: Record<string, string> }>();
for (const rule of mapping) {
const value = rule.constant ?? extracted[rule.from];
if (value === undefined) continue;
const objectKey = `${rule.domain}.${rule.object}`;
const bucket = byObject.get(objectKey) ?? { domain: rule.domain, object: rule.object, key: {}, values: {} };
if (rule.primaryKey === true) bucket.key[rule.field] = value;
else bucket.values[rule.field] = value;
byObject.set(objectKey, bucket);
}
// 无主键的写入无从定位,丢弃并由调用方按「提取失败」处置
return [...byObject.values()].filter((w) => Object.keys(w.key).length > 0);
}
@@ -0,0 +1,135 @@
/**
* channel 用例 —— 入站归一与出站发送(编码规范 §5.3;语义出处 OS 6.2 / 07)。
*/
import type { InboundMail, TaskId } from '@contracts';
import type { ChannelService, InboundOutcome, OutboundMail } from '../../contract.ts';
import { ChannelDisabledError, ChannelNotFoundError } from '../../contract.ts';
import type {
AttachmentArchive,
ChannelRepository,
Clock,
EventGateway,
IdGenerator,
LedgerSystemWriter,
MailTransport,
} from '../../port.ts';
import { extractFields, normalize } from '../domain/payload-mapping.ts';
export type ChannelServiceDeps = {
readonly channels: ChannelRepository;
readonly events: EventGateway;
readonly ledger: LedgerSystemWriter;
readonly mail: MailTransport;
readonly archive: AttachmentArchive;
readonly clock: Clock;
readonly ids: IdGenerator;
};
export function createChannelService(deps: ChannelServiceDeps): ChannelService {
async function require(channelId: string) {
const config = await deps.channels.get(channelId);
if (config === undefined) throw new ChannelNotFoundError(channelId);
if (!config.enabled) throw new ChannelDisabledError(channelId);
return config;
}
return {
async list() {
return deps.channels.list();
},
async upsert(config) {
return deps.channels.upsert(config);
},
async ingestMail(channelId, mail: InboundMail): Promise<InboundOutcome> {
const config = await require(channelId);
const receivedAt = new Date(mail.receivedAt);
// 原件存档为附件对象 —— 信封带原件指针,不带内容本身
const originRef = await deps.archive.put(
`inbound/${mail.messageId}.eml`,
JSON.stringify({ ...mail }, null, 2),
);
const extracted = extractFields(
{ subject: mail.subject, body: mail.body, from: mail.from, to: mail.to },
config.extract,
);
// 归属解析 + 三级消费判定归 event(L1);channel 只把通道事实交上去
const result = await deps.events.ingestInbound({
facts: {
...(mail.threadId === undefined ? {} : { threadId: mail.threadId }),
from: mail.from,
to: mail.to,
subject: mail.subject,
body: mail.body,
receivedAt,
},
rules: {
patterns: config.extract
.filter((r) => r.source === 'subject' || r.source === 'body' || r.source === 'both')
.map((r) => ({ key: r.key, regex: r.regex, source: r.source as 'subject' | 'body' | 'both' })),
addressWindowMs: config.addressWindowMs,
fallbackPosition: config.fallbackPosition,
},
draft: {
type: 'data',
correlation: { cause: { kind: 'inbound', ref: mail.messageId }, keys: extracted },
payload: { data: { ...extracted, subject: mail.subject, from: mail.from }, originRef },
occurredAt: receivedAt.toISOString(),
source: { kind: 'channel', detail: config.name },
},
});
// 有主入站归一为**系统写**:登记世界已发生的事实,免门、即时抛数据事件
const writes = normalize(extracted, config.mapping);
if (result.resolution.outcome === 'owned') {
for (const write of writes) {
await deps.ledger.systemWrite({
address: { domain: write.domain, object: write.object },
key: write.key,
values: write.values,
cause: { kind: 'inbound', ref: mail.messageId },
taskId: result.resolution.taskId,
});
}
}
return {
eventId: result.eventId,
resolution: result.resolution,
writes,
listenersHit: result.plan.taskTargets.length,
triggersHit: result.plan.triggerTargets.length,
fellBackToMessage: result.plan.messageFallback,
};
},
async sendMail(channelId, mail: OutboundMail) {
const config = await require(channelId);
const sent = await deps.mail.send({
from: config.address,
to: mail.to,
cc: mail.cc ?? [],
subject: mail.subject,
body: mail.body,
...(mail.threadId === undefined ? {} : { threadId: mail.threadId }),
attachments: mail.attachments ?? [],
});
// 出站即登记关联记录 —— 归属解析①线程与③地址+时间窗的对表来源
const sentAt = deps.clock.now();
for (const address of mail.to) {
await deps.events.recordOutbound({
taskId: mail.taskId as TaskId,
threadId: sent.threadId,
address,
sentAt,
});
}
return sent;
},
};
}
@@ -0,0 +1,7 @@
{
"name": "@modules/channel",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": { ".": "./index.ts" }
}
+71
View File
@@ -0,0 +1,71 @@
/**
* module: channel —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
import type { EnvelopeDraft, OwnershipResolution, PositionId, TaskId } from '@contracts';
import type { ChannelInstanceConfig } from './internal/domain/payload-mapping.ts';
export type Clock = { now(): Date };
export type IdGenerator = { next(): string };
export type ChannelRepository = {
list(): Promise<ChannelInstanceConfig[]>;
get(channelId: string): Promise<ChannelInstanceConfig | undefined>;
upsert(config: ChannelInstanceConfig): Promise<ChannelInstanceConfig>;
};
/** 事件入口 —— app 注入 `event`(L1)的出口 */
export type EventGateway = {
ingestInbound(input: {
facts: {
threadId?: string;
from: string;
to: readonly string[];
subject: string;
body: string;
receivedAt: Date;
};
rules: {
patterns: ReadonlyArray<{ key: string; regex: string; source: 'subject' | 'body' | 'both' }>;
addressWindowMs: number;
fallbackPosition?: PositionId | undefined;
};
draft: EnvelopeDraft;
}): Promise<{
eventId: string;
resolution: OwnershipResolution;
plan: { taskTargets: readonly string[]; triggerTargets: readonly string[]; messageFallback: boolean };
}>;
recordOutbound(ref: { taskId: TaskId; threadId?: string; address: string; sentAt: Date }): Promise<void>;
};
/**
* 台账系统写 —— app 注入 `ledger`(L1)的 `systemWrite`。
* 入站归一是「登记世界已发生的事实」,走**系统写**:不过放行门,写正式版并即时抛数据事件(OS 09)。
*/
export type LedgerSystemWriter = {
systemWrite(input: {
address: { domain: string; object: string };
key: Record<string, string | number>;
values: Record<string, unknown>;
cause: { kind: 'inbound'; ref: string };
taskId?: TaskId;
}): Promise<void>;
};
/** 邮件收发 —— 外界。MVP 允许模拟通道 / 录制回放替身(README MVP 完成判据) */
export type MailTransport = {
send(input: {
readonly from: string;
readonly to: readonly string[];
readonly cc: readonly string[];
readonly subject: string;
readonly body: string;
readonly threadId?: string;
readonly attachments: ReadonlyArray<{ name: string; ref: string }>;
}): Promise<{ messageId: string; threadId: string }>;
};
/** 原件存档 —— 附件对象的存放;返回存档地址(信封的原件指针) */
export type AttachmentArchive = {
put(name: string, content: string): Promise<string>;
};