Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
限流、幂等和服务生命周期契约。
- [Telegram 长轮询 Adapter](telegram.md):Issue #31 的文档先行契约,固定单 Binding、Bot
身份校验、普通文本映射、Dispatch 聚合回复和生命周期边界。
- [企业微信自建应用 Text Webhook](wecom.md):Issue #60 的文档先行契约,固定 callback
验签/AES 解密、可信 Binding 路由、文本入站和可靠回复边界
- [企业微信自建应用 Channel Adapter](wecom.md):Issue #60/#98 的契约,固定 callback
验签/AES 解密、可信 Binding 路由、文本/媒体入站和可靠回复边界
- [Telegram live E2E 示例](https://github.com/XnLemon/trpc-agent-service/tree/main/examples/telegram-e2e):
Issue #33 的真实 Bot API 传输冒烟测试和手动 CI 运行说明。
- [PostgreSQL 控制面与启动装配](postgresql-control-plane.md):Issue #37 的实现契约,
Expand Down Expand Up @@ -76,7 +76,7 @@ cd trpc-agent-service
- [数据模型](data-model.md) — 核心表结构、Session/Event/Memory/Summary/Audit 和租户约束
- [Channel Binding](channel-binding.md) — 租户级通道绑定、候选发现与可信入站路由
- [Telegram 长轮询 Adapter](telegram.md) — 单 Binding Telegram long polling、文本映射与安全边界
- [企业微信自建应用 Text Webhook](wecom.md) — 自建应用 callback、文本入站与回复 Outbox
- [企业微信自建应用 Channel Adapter](wecom.md) — 自建应用 callback、文本/媒体入站与回复 Outbox
- [Gateway、Execution Plan 与 HTTP/SSE](gateway.md) — 可信主体、固定执行计划、Runner Registry、
Dispatch、健康检查、优雅停机和普通/流式 API
- [PostgreSQL 控制面与启动装配](postgresql-control-plane.md) — 六类控制面表的 migration 顺序、
Expand Down
83 changes: 83 additions & 0 deletions docs/docs/issue-98-native-media.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Issue #98:原生媒体附件与富回复

Issue #98 在 Issue #77 的通道能力基础上补齐协议中立附件契约。目标不是让通道 payload
绕过 Gateway,而是在验证后的 channel 边界内下载、限流、校验并持久化媒体,然后只把
安全的 `attachment.Reference` 和可加载的 `ContentPart` 交给 Runner。

## MVP 范围

- 入站附件类型固定为 `image`、`video`、`audio`、`document`,引用包含 ID、MIME、
大小、SHA-256、原始文件名和 provider 文件身份。
- Telegram 入站会保留图片、文档、音频和视频的 provider file id,使用受控
`MediaDownloader` 下载后立即转存到租户隔离 attachment store。
- WeCom 入站会在签名、AES 解密、`ReceiveID` 和 `AgentID` 全部通过后处理
`image`、`file`、`voice`、`video`;未知类型和缺少 `MediaId` 的回调 fail closed。
- Gateway 将附件绑定到 durable `message_event` 后,Runner 才能通过 tenant/event/reference
读取内容;Runner 不接触 Telegram 下载链接、WeCom `media_id` 下载 URL、access token 或
channel secret。
- 出站 Outbox 支持结构化 `kind + attachment ref + fallback`;Telegram 和 WeCom 对
`image`、`document` 走原生发送,`audio`、`video` 或不支持能力时使用确定性文本 fallback。

## 入站生命周期

```text
channel callback/update
-> 验签、解密、绑定候选校验
-> 识别 provider media/file id
-> 受控 downloader 下载,限制大小和响应形态
-> attachment store 写入 bytes + metadata
-> Gateway 记录 message_event 并绑定 attachment
-> Runner 按模型能力读取 ContentParts
```

附件 ID 使用 `(tenant, binding, external_message_id, ordinal/provider_id)` 语义生成,保证
重复回调和重试不会产生新的逻辑附件。attachment store 必须返回 defensive copy,并按
tenant/event/reference 验证读取;未绑定到 durable event 的附件不能被 Runner 加载。

## 模型边界

`trpc-agent-go` 已支持 `ContentParts`,本仓库的 Responses 适配器按模型 profile 的显式输入
能力转换:

| 附件 | Runner 内容 | OpenAI Responses 映射 |
| --- | --- | --- |
| text | `ContentPart{Type:text}` | `input_text` |
| image | image content part | `input_image` |
| document/file | file content part | `input_file` |
| mp3/wav audio | audio content part | `input_audio` |
| video | 保留附件和 fallback 文本 | 不声明视频理解 |

视频可以安全收发和存储,但当前不承诺模型视频理解。抽帧、OCR、ASR 和视频分析是后续独立
能力,不属于 #98 MVP。

## 出站语义

`ReplyOutbox` 的媒体段是不可变结构:`Kind` 表示协议中立类型,`Attachment` 指向已验证内容,
`Payload` 可作为 caption,`Fallback` 是目标通道无法表达原生媒体时必须使用的确定性文本。

- Telegram 使用 attachment reader 构造 SDK upload,图片走 `SendPhoto`,文档走
`SendDocument`;视频和音频先保守发送 fallback。
- WeCom 图片和文档先上传临时素材,再分别发送 `image` 或 `file` 应用消息;未配置 reader 或
不支持的 kind 发送 fallback。
- Provider 成功 receipt 继续写回 Outbox 状态机;token、provider URL、原始响应和消息正文不进入
日志或审计。

## 存储与上线

当前 in-memory 和 PostgreSQL runtime store 都实现了 attachment store。PostgreSQL 版本复用
现有 object boundary,但二进制内容仍落在数据库内,适合受限 MVP 和 deterministic E2E,不适合
长期生产视频流量。生产视频或大文件上线前应补 S3/COS 一类流式对象存储实现,并明确:

- 每租户数量、单文件大小、总容量和 MIME allow-list;
- 保留期、引用计数、清理任务和 dead-letter 后的处置;
- downloader 超时、取消、重试和限速;
- 跨 tenant/binding 的读取拒绝和 provider secret 脱敏;
- 部署文档中的对象存储 endpoint、凭据轮换和迁移策略。

## 验证证据

- Telegram 入站、原生图片/文档出站、fallback 和 cancellation 使用 fake SDK/reader 测试。
- WeCom 入站媒体回调使用加密 XML 和 fake downloader 测试,证明不使用 `PicUrl`。
- WeCom 出站图片/文档使用 `httptest` 验证临时素材上传和发送 payload。
- Gateway、in-memory/PostgreSQL runtime store 和 migrations 覆盖 tenant/event/reference 绑定、
幂等和清理。
41 changes: 33 additions & 8 deletions docs/docs/ops.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
# 运维、可观测性与生产风险

> 本页把 [生产架构设计](architecture.md) 转成可执行的发布、监控、恢复和风险检查表。
> 当前仓库只有控制面领域模型、快照和最小 Runner spine;Gateway、队列、真实 IM/Storage
> Adapter、Dashboard 和告警规则仍是后续平台实现,不应把本页当作已经部署的运行手册。
> 本页同时记录已在仓库实现的运行时能力和仍待生产化的能力。代码、测试或示例可验证
> 不代表相应的基础设施已经按本页要求部署并通过容量或灾备验收。

## 当前实现状态

下表区分“有领域契约或设计”与“默认运行链路已经可用”。发布和答辩应以此表为准,避免把
规划中的架构能力表述为已完成的生产部署。

| 能力 | 状态 | 当前边界与验证 | 生产化缺口或后续门禁 |
| --- | --- | --- | --- |
| Gateway、Execution Plan、HTTP Chat/SSE 与 Agent Worker | 已实现 | 可信租户路由、版本化执行快照、限流和幂等均在服务运行链路中 | 仍需按部署规模验证跨节点容量和 SLO |
| 控制面与运行时存储 | 已实现 | PostgreSQL 控制面;InMemory 和 PostgreSQL Runtime Store 覆盖 Session/Event、Memory、Knowledge、Vector、Artifact 和 Reply Outbox | PostgreSQL 向量/知识检索尚未替代为专用索引服务;InMemory 仅适合单进程开发/测试 |
| 可靠回复投递 | 已实现 | Reply Outbox 有租约、fencing、指数退避、dead-letter 与 reconcile 测试 | 尚未形成通用的供应商 `Retry-After` 解析和按 chat/provider 限频队列 |
| Telegram 与企业微信通道 | 已实现,默认装配范围有限 | 两个通道已有入站处理、附件/媒体 MVP、图片和文档出站以及 fallback;企业微信环境 bootstrap 仍是单个静态身份配置 | 需要按目标供应商补齐更丰富交互、平台级多账号装配和限频/429 演练 |
| Trace、metrics、Prometheus 与审计 | 已实现 | OpenTelemetry、Prometheus 导出、脱敏字段和审计事件均有代码、文档或示例验证 | 告警阈值、保留策略和真实 exporter 背压容量尚未完成生产验收 |
| 灰度和回滚 | 部分实现 | Tenant/App 级 canary revision 与版本化回滚指针已可用 | 不含百分比 rollout、稳定分桶、指标自动止损或自动回滚 |
| 故障注入 | 部分实现 | deterministic E2E 覆盖 Gateway、Worker、Outbox lease/retry/restart 和并发边界 | 未完成真实数据库故障转移、OTel 背压、IM 429/Retry-After 与模型超时的演练 |
| Redis、对象存储与独立向量库 | 未实现 | Backend Profile 和 Runtime Capability 已定义可路由边界 | 需要真实 provider、租户装配、迁移、隔离契约和运行验证;当前优先跟踪 Redis Runtime Store |
| Admin 身份与工具治理 | 部分实现 | Admin 使用静态 Bearer Token;工具支持 allow/deny/approval 策略,运行时记录调用上限和审计数据 | 需要 OIDC/JWT、RBAC、token rotation、参数级策略、主体授权和可扣减预算 |
| 容量、备份与灾备 | 未实现 | 已有容量模型、部署示例和故障注入测试 | 需要可重复压测、备份恢复、容量基线和真实基础设施演练 |

本页的 runbook 描述目标运行约束;当表中能力尚未实现时,它是后续实现的验收条件,而不是
已经可由默认环境保证的行为。

## 运行边界与值班目标

Expand Down Expand Up @@ -207,10 +228,14 @@ backpressure。高峰保护使用租户级 token bucket、全局队列上限、
| 回复重试风暴 | IM 429/5xx、固定间隔重试、无 per-chat 限速 | 供应商封禁、用户刷屏、队列雪崩 | retry multiplier、429、DLQ、outbox age | 指数退避+jitter、解析 Retry-After、按通道/chat 分桶、最大预算和 DLQ |
| goroutine/事件泄漏 | context 未传递、Runner Event channel 未排空、consumer 无关闭边界 | Worker 内存上涨、滚动发布卡住、重复消费 | goroutine、FD、channel backlog、shutdown duration 持续上升 | owner 明确;context deadline;有界 drain;supervisor/health check;超时交给幂等重投递 |

## 当前实现状态与后续门禁
## 后续实现门禁

本仓库目前可以验证 Tenant、Agent App/Revision、Model Profile、Backend Profile、无密钥
Execution Plan、Runner policy 和 Tenant-scoped Session 的模型/边界测试;不能验证真实 IM
验签、跨节点 CAS、队列至少一次投递、SQL/Redis 迁移或生产告警。后续实现每落地一个 Adapter
都必须补充:双租户隔离测试、重复/乱序/验签失败测试、provider 一致性契约测试、故障注入、
审计字段检查和 `mkdocs build --strict`。
当前仓库已经覆盖 Tenant、Agent App/Revision、Model Profile、Backend Profile、无密钥
Execution Plan、Runner policy、Tenant-scoped PostgreSQL/内存 Runtime Store、Telegram/企业微信
入站验证与可靠回复投递的模型、边界或 E2E 测试。它们不替代跨节点容量、真实基础设施故障
恢复或外部后端迁移的生产验收。

后续每落地一个外部 Adapter 或生产运行能力,都必须补充:双租户隔离测试、重复/乱序/验签
失败测试、provider 一致性契约测试、目标基础设施故障注入、审计字段检查和
`mkdocs build --strict`。涉及数据后端的变更还必须证明 migration、shadow read、回滚窗口和
provider 不可用时的失败语义。
27 changes: 17 additions & 10 deletions docs/docs/telegram.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Telegram 长轮询 Channel Adapter

> Issue #31 的基础实现;Issue #77 在此之上补充 webhook、媒体/rich update 与统一回复渲染。
> Issue #31 的基础实现;Issue #77 在此之上补充 webhook、媒体/rich update 与统一回复渲染,Issue #98 补齐受控附件入站和图片/文档原生出站

## 1. 交付边界

Expand All @@ -9,13 +9,13 @@ Telegram 适配器是一个绑定级别的协议入口,不创建第二套租

```text
Telegram getUpdates
-> Update.Message 校验和规范化
-> Update.Message 校验和规范化为文本或媒体附件
-> 已验证的 channels.RoutingTarget
-> gateway.Channel Principal
-> gateway.DispatchService
-> 完整消费脱敏 DispatchEvent
-> 聚合并分段
-> Telegram sendMessage
-> Telegram sendMessage / SendPhoto / SendDocument
```

long polling 与 webhook 共用同一个 Adapter、幂等和 Gateway 边界;命令、回调仍 fail closed。
Expand All @@ -38,6 +38,9 @@ Webhook 由调用方拥有 HTTP listener,`telegram.Webhook` 只负责精确 pa
| `Workers` | 零值为 1;大于 1 必须由调用方显式配置,并由 SDK 同步处理 handler 生命周期 |
| `ErrorHook` | 只接收稳定的适配器错误类别,不接收 SDK/provider 原始错误、token 或 endpoint 凭据 |
| `Factory` | 注入式 Bot factory;生产实现才依赖 `github.com/go-telegram/bot`,测试不创建网络 client |
| `Attachments` | 可选 runtime attachment store;配置后媒体 bytes 在进入 Runner 前先按 tenant/event/reference 持久化 |
| `MediaDownloader` | 可选受控下载器;未配置时生产 SDK client 会在具备 `GetFile` 能力时创建默认下载器 |
| `MaxAttachmentBytes` | 单附件大小上限,零值使用协议中立默认限制 |

构造函数接收 Context,先创建带默认 update handler 的 client,再调用 `getMe`,把返回的 Bot user
ID 规范化为十进制字符串并与 `Target.ProviderAccountID` 精确比较。创建失败、`getMe` 失败或身份
Expand All @@ -58,7 +61,7 @@ Bot factory 对 SDK 使用以下固定策略:

## 3. 入站规范化与幂等

第一版只接受 `Update.Message` 中的普通文本
基础文本字段仍按以下规则规范化

| Telegram 字段 | Gateway 字段 | 规则 |
| --- | --- | --- |
Expand All @@ -69,8 +72,12 @@ Bot factory 对 SDK 使用以下固定策略:
| `Message.MessageThreadID` | `ExternalThreadID` | 大于零时保留;发送回复时原样作为 forum thread |
| `Update.ID` + trusted `BindingID` | `ExternalMessageID` / `RequestID` | 使用长度前缀编码生成稳定、无碰撞的 binding-aware ID |

编辑消息、channel post、callback/inline、service update、无 sender/chat/text、未知 chat 类型和
媒体-only update 都以稳定的非敏感原因忽略或拒绝,且不得进入 Dispatch。所有合法消息先用固定
Issue #98 之后,图片、文档、音频和视频会保留 provider file id,经受控 downloader 下载、限流、
校验并写入 attachment store;没有 attachment store 时仍保留兼容的 caption 或 `[telegram media]`
文本标记,不把 provider 下载 URL 或 token 传入 Runner。

编辑消息、channel post、callback/inline、service update、无 sender/chat、未知 chat 类型和
不支持的 rich/service update 都以稳定的非敏感原因忽略或拒绝,且不得进入 Dispatch。所有合法消息先用固定
principal 调用 `IdempotencyStore.Begin`:

- pending duplicate 不再次调用 Dispatch,也不启动隐藏 retry;
Expand Down Expand Up @@ -141,8 +148,8 @@ README 和 MkDocs 状态应明确区分已交付与后续能力:
- [x] `getMe` 身份校验、tenant/Binding/Runner 隔离、普通文本映射和 binding-aware 幂等通过测试;
- [x] Dispatch 完整消费、单逻辑回复、4096 code point 分段、forum thread 路由和失败脱敏通过测试;
- [x] cancellation、polling error、send failure、duplicate delivery 和资源生命周期通过测试;
- [x] Telegram long polling 已实现;Webhook、持久化幂等/outbox、媒体、跨节点 ownership
和其他 rich update 明确保持未勾选
- [x] Telegram long polling、webhook、持久化 outbox、媒体附件入站、图片/文档原生出站和 fallback
已实现;其他 rich update、音频/视频原生出站和视频理解保持非目标或后续能力

参考:[Telegram Bot API](https://core.telegram.org/bots/api)、
[getUpdates](https://core.telegram.org/bots/api#getting-updates)、
Expand All @@ -160,7 +167,7 @@ trace 或错误。CI 使用受保护的 `telegram-e2e` Environment,至少配
`TELEGRAM_SENDER_BOT_TOKEN`。一个 Bot Token 不能模拟普通用户向自己发送入站消息,
所以当前 workflow 必须显式配置第二个受控测试 Bot;本地人工运行可以不配置发送者。

示例和 CI 都只验证普通文本;命令、媒体、rich update、Webhook、持久化 outbox 和
生产模型供应商仍不属于该 E2E 范围。详见
示例和 CI 都只验证普通文本;命令、媒体和 rich update 不属于该 live E2E 范围,媒体行为由
deterministic fake 测试覆盖。详见
[Telegram live E2E example](https://github.com/XnLemon/trpc-agent-service/tree/main/examples/telegram-e2e)
和 Issue #33。
Loading
Loading