在 OncallAgent 中,HTTP 与 SSE 不是两套互不相干的传输实现,而是前端、后端和 Agent 生命周期之间的公共语言。共享契约位于 packages/api-contracts,覆盖响应 envelope、错误码、认证与领域 DTO、OpenAPI 路径以及流事件。这个包的价值不只是让 TypeScript 编译通过,更重要的是把“成功、失败、流式进度和恢复信息”变成可审查、可测试的产品行为。
📷 [图片 token=QGrkbz8DHoFPVGxV0N2cpeGjnpc(未能下载,见飞书原文)]
AI Native 应用比普通 CRUD 更需要稳定契约。一次聊天可能先输出内容增量,再触发工具调用、产生知识引用,最后完成或失败;一次 AIOps 诊断还会包含任务状态与报告。若每个页面或服务临时设计 payload,前端很快会依赖隐含顺序,错误也容易泄露上游异常。OncallAgent 用带判别字段的联合类型、统一错误消息和受保护 OpenAPI 表面来降低这种漂移。
📷 [图片 token=HkQPb5CLsozo2vxnKnDcWpVOnhh(未能下载,见飞书原文)]
共享契约并不自动保证 Python 实现正确。后端仍需在 apps/backend/src/super_ai/api/responses.py、apps/backend/src/super_ai/error_catalog.py 和 SSE 事件构造中手工对齐 TypeScript 形状。因此理解契约需要同时看定义、消费端、后端序列化与跨层测试,而不能只读一个类型文件。
📷 [图片 token=ThOHbDDYcofTltxBX1lcUjQHnPW(未能下载,见飞书原文)]
学习目标
掌握统一 HTTP 成功与错误 envelope 的判别方式及 request ID 作用。
理解错误目录如何统一 category、HTTP 状态与安全默认消息。
能区分 OpenAPI 的路径级安全声明与运行时认证、授权检查。
能追踪聊天或诊断 SSE 从后端事件到前端异步迭代器的完整链路。
识别流开始前的 HTTP 错误与流开始后的
error事件这两种失败表面。
📷 [图片 token=WMlzbrUwyoKLuBxk86wcFAuknOh(未能下载,见飞书原文)]
功能入口与完整调用链
普通 HTTP 请求从 apps/frontend/src/api/apiClient.ts 的 createApiClient 开始。buildTransportHeaders 设置 Accept,在非 FormData 请求中补 Content-Type,并从回调读取 token 后加入 Authorization。响应交给 readResponseEnvelope:只有包含 ok 判别字段且形状可识别时才继续;ok: false 被包装为 ApiClientError,调用页面可以统一读取 code、category、httpStatus 和 message。
📷 [图片 token=LO0ObktiyovPHixihvyczeW4nZc(未能下载,见飞书原文)]
后端由 apps/backend/src/super_ai/api/app.py 的请求中间件生成或接收 x-request-id,并在响应头写回 X-Request-ID。成功路由调用 success_response,业务或权限失败抛出 ApiErrorException;全局异常处理器再调用 error_response。Pydantic 的 RequestValidationError 也被归一成 VALIDATION_INVALID_ARGUMENT,客户端无需理解 FastAPI 默认验证 payload。
📷 [图片 token=GxDHbqhn0op3MMxOJAJcLqCXnBd(未能下载,见飞书原文)]
流式请求由 apps/frontend/src/api/sseClient.ts 的 createSseClient 发起。若 HTTP 状态不是成功,仍按统一 HTTP 错误解析;只有拿到响应 body 后,才逐块读取字节、用 TextDecoder 拼接缓冲区,以空行拆分 SSE frame,并解析每个 data: JSON。后端聊天路由返回 StreamingResponse,apps/backend/src/super_ai/chat/streaming.py 的 encode_sse 把共享事件写成帧。浏览器得到的不是任意 JSON,而是至少含 id、type、channel 和 timestamp 的事件。
📷 [图片 token=UiV2bObYvoKBxBxuFxDcLIEhnze(未能下载,见飞书原文)]
请求前:共享 DTO → JSON 或 FormData → bearer HTTP
普通响应:success_response / error_response → ApiResponse → ApiClient
流响应:领域事件 → 共享 SseEvent → encode_sse → SSE frame → SseClient
流开始前失败:HTTP ApiErrorResponse
流开始后失败:type 为 error 的结构化 SseEvent
📷 [图片 token=FV5HbP8aKoQ64BxoLNGc5Uu5ngh(未能下载,见飞书原文)]
以聊天为例,POST /chat/sessions/{sessionId}/messages:stream 在 OpenAPI 中声明 bearer 认证、请求体和 text/event-stream 响应。运行时先完成认证和 owner-scoped 会话查询,所以缺少 token 或跨 tenant 会话会在建流前返回 401 或 403。建流后,ChatStreamingService.stream_message 可发出 content.delta、reasoning.delta、tool.call、reference.source、complete 或 error。这一区分很重要:前端既要处理 fetch 失败,也要处理合法流中的错误事件。
📷 [图片 token=ENQ2bscLyop4uRxnfVNcU5ZFnJg(未能下载,见飞书原文)]
核心源码地图
| 源码位置 | 关键符号 | 职责 |
|---|---|---|
packages/api-contracts/src/responses.ts | ApiResponse、buildSuccessResponse、buildErrorResponse | 定义成功/错误联合类型、metadata 与构造函数。 |
packages/api-contracts/src/errors.ts | API_ERROR_CODES、ApiErrorCode | 维护稳定错误码、类别、HTTP 状态和默认消息。 |
packages/api-contracts/src/sse.ts | SSE_EVENT_TYPES、SseEvent | 定义聊天与 AIOps 共用的带判别字段事件联合。 |
packages/api-contracts/src/openapi.ts | OPENAPI_CONTRACT、protectedErrorResponses | 描述路径、请求/响应 schema、bearer 安全方案与 401/403。 |
apps/backend/src/super_ai/error_catalog.py | ERROR_DEFINITIONS | 后端错误码到 category、status、message 的运行时映射。 |
apps/backend/src/super_ai/api/responses.py | success_response、error_response、ApiErrorException | 生成与共享契约对齐的 JSON envelope。 |
apps/backend/src/super_ai/chat/streaming.py | encode_sse、_sse_event、_error_event | 构造聊天事件并编码 SSE frame。 |
apps/frontend/src/api/apiClient.ts | readApiError、readResponseEnvelope | 严格解析 HTTP envelope并提供安全 fallback。 |
apps/frontend/src/api/sseClient.ts | createSseClient、parseSseFrames | 处理流式 fetch、增量缓冲、事件解析与坏帧错误。 |
packages/api-contracts/tests/api-contracts.test.ts | HTTP response contracts、SSE event contracts、OpenAPI contract | 锁定跨层契约的结构、覆盖面和安全声明。 |
📷 [图片 token=HkwBbLj9coSzgzxyYGicztZNn8c(未能下载,见飞书原文)]
📷 [图片 token=FSXubgxlEoQjaQxVI3BcyuSOnJb(未能下载,见飞书原文)]
📷 [图片 token=L19ybw7wDosIdtxEhm3cCETUn0g(未能下载,见飞书原文)]
📷 [图片 token=Jqu5bcTWMoykoVxkGafcd1TSnne(未能下载,见飞书原文)]
代码调用流程图
契约并不是单个类型文件,而是一条从前端传输层、FastAPI 路由到普通 HTTP 或 SSE 消费端的双分支链路。

关键实现拆解
HTTP envelope 与 request metadata
ApiSuccessResponse 固定为 ok: true、类型化 data 和 meta;ApiErrorResponse 固定为 ok: false、error 与同样的 meta。这比依赖 HTTP status 猜测 body 更稳定:202 的索引任务仍是成功 envelope,503 的 readiness 降级也可以携带完整组件诊断数据;业务失败则同时有传输状态和机器可读错误码。
📷 [图片 token=LRQ2bG6kOoSRo3x47cscAKUunod(未能下载,见飞书原文)]
后端 _request_id 优先使用中间件写入 request state 的 ID,其次读取请求头,最后生成新 ID。request ID 不代表分布式追踪已经完整实现,但它让浏览器错误、服务日志和 API 响应有稳定的关联键。共享类型还允许可选 traceId,当前 Python helper 只写 requestId,因此不应把可选字段描述为已在每个响应中产生。
📷 [图片 token=NuMKbk3spoORW5xy7VscKkg9nTc(未能下载,见飞书原文)]
看什么:共享类型把 ok 设为字面量判别字段,成功与失败都强制携带同一种 metadata。
// 1. ok 为 true 时,消费者可以安全收窄到 data。
export interface ApiSuccessResponse<TData> {
readonly ok: true;
readonly data: TData;
readonly meta: ApiResponseMeta;
}
// 2. ok 为 false 时,机器错误在 error,关联信息仍在 meta。
export interface ApiErrorResponse {
readonly ok: false;
readonly error: ApiErrorMessage;
readonly meta: ApiResponseMeta;
}
export type ApiResponse<TData> = ApiSuccessResponse<TData> | ApiErrorResponse;
📷 [图片 token=HWT1bsMVbohfyIxX1GjcmDsnnMb(未能下载,见飞书原文)]
代码证明客户端不需要通过“有没有 data 字段”猜测分支;202、503 等状态码也不改变 envelope 形状。边界在于 TypeScript 只约束编译期消费者,Python 返回值和线上异常仍需运行时构造与测试对齐,且可选 traceId 当前并非每次都生成。
📷 [图片 token=CY4zbPDVmoXwhWxGuwhcosIzn5e(未能下载,见飞书原文)]
看什么:request ID 从进入中间件到响应 meta 的关联路径,可解释浏览器报错如何回到一条服务端请求记录。

图中的关联键方便排障,但不等同于完整分布式 trace;外部 Qwen、Milvus 或 MCP 是否传播同一 ID 不能由此推断。若处理器在统一 helper 之外崩溃,还要依赖框架错误路径和中间件日志,不能假设一定得到相同 envelope。
📷 [图片 token=LNaubNFwYo4l8IxlPbhcRlyRnMf(未能下载,见飞书原文)]
错误目录与安全消息
API_ERROR_CODES 当前覆盖认证、业务冲突或未找到、验证、系统不可用与内部错误。认证中特别区分 AUTH_UNAUTHENTICATED 的 401 和 AUTH_FORBIDDEN 的 403;登录失败统一为 AUTH_INVALID_CREDENTIALS,不告诉调用者是邮箱不存在还是密码错误。ErrorSseEvent 直接复用 ApiErrorMessage,避免流式失败另造一套含原始异常的 payload。
📷 [图片 token=IZsnbCckfozRNox1vC8cZuTZnFb(未能下载,见飞书原文)]
当前 error_response 支持 code 和可选 message,但没有把 FastAPI 参数级错误细节写入 details。共享契约允许 details,主规格也要求参数级验证信息;阅读当前实现时应如实区分“类型预留”与“运行时已经填充”。同理,后端全局只注册了业务错误和请求验证异常处理器;未预期异常仍由框架形成 500,日志中间件只记录异常类别。对外部 provider 的错误脱敏主要在 provider 与具体服务边界完成。
📷 [图片 token=Vv92bOs9golDEwx7rqjcw1S2nNe(未能下载,见飞书原文)]
看什么:Python helper 不复制 HTTP status 规则,而是从后端错误目录取得 category、status 和默认消息,再写成与共享类型相同的字段。
📷 [图片 token=MMSrb5v6koiox3xiXwscx1Zwnxb(未能下载,见飞书原文)]
# 1. code 决定 category、HTTP status 和默认安全消息。
def error_response(request: Request, code: str, *, message: str | None = None) -> JSONResponse:
category, http_status, default_message = ERROR_DEFINITIONS[code]
return JSONResponse(
status_code=http_status,
content={
"ok": False,
"error": {
"code": code,
"category": category,
"httpStatus": http_status,
"message": message or default_message,
},
# 2. requestId 关联客户端错误与服务端请求日志。
"meta": {"requestId": _request_id(request)},
},
)
📷 [图片 token=VnUibHpdFoFjmcx5Bt8cGmu3nZb(未能下载,见飞书原文)]
片段证明错误码同时控制传输与业务语义,并且原始异常不会自动进入响应。边界也很明确:调用者传入的自定义 message 必须先在领域边界完成脱敏;这个 helper 当前没有输出 details,因此不能把类型中的可选验证明细描述成已普遍实现。
📷 [图片 token=ID8Fbe4WIoxDPyxm4dPctv5knch(未能下载,见飞书原文)]
判别联合与事件生命周期
SSE_EVENT_TYPES 包含八种类型。content.delta 与 reasoning.delta 携带顺序号;tool.call 的状态是 started、delta、completed 或 failed;reference.source 可携带 chunk、文档、知识库、来源、向量/BM25/RRF/rerank 排名与分数;task.status 和 report 服务于长任务;complete 携带最终结果;error 携带统一错误。
📷 [图片 token=B0JsbsC6uoNCAIxlPEzcWwAknSh(未能下载,见飞书原文)]
聊天实现不会要求每条流都出现全部类型。没有工具调用就没有 tool.call,没有知识命中就没有引用。最终内容当前按单字符 content.delta 发出,非内容事件保持原粒度。工具调用 started 会先创建 owner-scoped 审计,terminal 状态再完成对应审计;审计持久化不应吞掉最终聊天输出。前端消费应根据 type 分派,而不是依赖事件位置或假定固定数量。
📷 [图片 token=F71gbo5s0oSvKyxUEzLcsrqxn7c(未能下载,见飞书原文)]
看什么:事件联合不仅区分内容与任务,还让终止事件和 HTTP 共享同一个 ApiErrorMessage。
// 1. complete 与 error 都是显式事件类型。
export interface CompleteSseEvent extends SseEventBase<"complete"> {
readonly result?: unknown;
}
export interface ErrorSseEvent extends SseEventBase<"error"> {
readonly error: ApiErrorMessage;
}
// 2. 消费者必须按 type 分派,不能依赖固定事件顺序。
export type SseEvent =
| ContentDeltaSseEvent
| ReasoningDeltaSseEvent
| ToolCallSseEvent
| ReferenceSourceSseEvent
| TaskStatusSseEvent
| ReportSseEvent
| CompleteSseEvent
| ErrorSseEvent;
📷 [图片 token=VdHbblIYwoM57fxgztucKRnYnle(未能下载,见飞书原文)]
这段联合证明内容、推理、工具、引用、任务和终态共享同一判别入口;没有某类事件仍是合法流。失败边界是:error 代表业务已知终止,而 TCP 断开、浏览器取消或坏 frame 只是客户端无法确认终态,需要回读持久状态。
看什么:工具调用有自己的局部生命周期,而整条流只能走向完成、业务错误或连接未知三类终局。

图中工具失败不必然等于整条流失败,最终由 Agent 与服务边界决定是否还能回答;而连接未知不能伪装成 error。工具审计使用 toolCall ID 配对,流终态使用事件 type,二者不能互换。
📷 [图片 token=FxYEb4njOofxCqxzUThc1uSHn9W(未能下载,见飞书原文)]
OpenAPI 是安全表面的机器可读索引
OPENAPI_CONTRACT 使用 OpenAPI 3.1,集中定义 health、认证、聊天、知识文档、索引、后台任务、反馈、MCP 与 AIOps 路径。bearerSecurity 和 protectedErrorResponses 被复用于受保护操作,使 401、403、验证、冲突和系统错误在文档层保持一致。文档上传策略来自 DOCUMENT_UPLOAD_POLICY,减少前端提示、OpenAPI 与后端校验之间的魔法数字。
📷 [图片 token=VcUzbqjALod0UZxMek7ciJmFnIc(未能下载,见飞书原文)]
但 OpenAPI 的 security 只是声明,不执行认证。真正的校验在 FastAPI Depends(_current_user)、_bearer_token 和 owner-scoped Repository 查询。反过来,路由已实现也不代表共享 OpenAPI 必然覆盖;契约测试中的 covers required backend surfaces 和 marks protected paths with bearer auth and unified 401/403 errors 正是用来防止两边分离。
📷 [图片 token=EpagbNQ0coUwVvx79WYcTI4inxf(未能下载,见飞书原文)]
看什么:受保护响应和 bearer security 被定义为可复用常量,路径声明不必重复手写 401/403 结构。
const unauthenticatedResponse = {
description: API_ERROR_CODES.AUTH_UNAUTHENTICATED.message,
content: jsonContent("#/components/schemas/ApiErrorResponse")
} as const;
const forbiddenResponse = {
description: API_ERROR_CODES.AUTH_FORBIDDEN.message,
content: jsonContent("#/components/schemas/ApiErrorResponse")
} as const;
// 1. 每个受保护操作复用统一 401、403 和通用错误。
const protectedErrorResponses = {
"401": unauthenticatedResponse,
"403": forbiddenResponse,
...errorResponses
} as const;
// 2. 安全声明引用同一个 bearerAuth scheme。
const bearerSecurity = [{ bearerAuth: [] }] as const;
📷 [图片 token=KqStbztF8oC7aWxMGN4cicsinjd(未能下载,见飞书原文)]
📷 [图片 token=C5hcbmJD2oZEm7xDhlWcaMn5nHf(未能下载,见飞书原文)]
代码证明 OpenAPI 能稳定表达安全表面,并让契约测试检查路径是否同时声明 bearer、401 和 403。它不执行身份验证,也不验证 Repository 是否真的按 owner 查询;因此 API 测试仍要用两个用户实际访问同一资源 ID,确认运行时返回统一 403。
📷 [图片 token=E8ekbYPYroMMSdxFAsZcDRCInfc(未能下载,见飞书原文)]
客户端恢复策略来自契约语义
契约不仅决定数据怎样解析,也决定失败后该做什么。401 表示当前认证不可继续,认证 store 可以移除 token、清空受保护数据并引导重新登录;403 表示调用者身份有效但目标资源不属于其范围,界面不应通过反复登录掩盖授权问题;409 通常提示资源状态冲突,例如重复文档需要用户明确覆盖;400 或 422 要把注意力放在输入;503 表示依赖暂不可用,可以保留用户输入并允许稍后重试。恢复策略应依据稳定 code 与 category,而不是对 message 做字符串匹配。
📷 [图片 token=OiZmblmtlohJOOxaSXic0sYYnde(未能下载,见飞书原文)]
createApiClient 对格式错误的成功响应也会抛系统错误,因为无法证明其中的 data 满足协议。createSseClient 对没有 body、坏 JSON 或缺少基础事件字段采取相同保守策略。这样的“严格消费”能尽早暴露后端漂移;如果客户端默默忽略未知结构,页面可能看似运行却丢失完成、引用或错误状态。
📷 [图片 token=OElvbO9YyoImrZxqTTacfHQHn6b(未能下载,见飞书原文)]
看什么:通用客户端先解析 envelope,再使用 ok 分支;格式错误不会被当作成功数据继续传播。
return {
async request<TData>(path: string, init: RequestInit = {}): Promise<TData> {
const response = await fetchImpl(`${baseUrl}${path}`, {
...init,
headers: buildTransportHeaders({
accept: "application/json",
body: init.body,
headers: init.headers,
token: options.getAccessToken()
})
});
// 1. JSON 与 envelope 形状都在统一入口检查。
const payload = await readResponseEnvelope<TData>(response);
if (!payload.ok) {
// 2. 恢复策略读取稳定 code/status,而非 message 文本。
throw new ApiClientError(payload.error, response.status);
}
return payload.data;
}
};
📷 [图片 token=KwcZbNl0eoEm2tx5jiEcA7FInih(未能下载,见飞书原文)]
片段证明 bearer header、envelope 校验和类型化错误集中在同一传输层,页面无需复制解析逻辑。边界是 isApiResponse 只做基础结构检查,并不会完整验证每个领域 DTO;高风险字段仍要由领域测试和组件状态处理覆盖。
终止事件与幂等观察
流式界面需要明确区分草稿和事实。content.delta 只是当前连接看到的增量,complete 才表示后端完成最终持久化并可给出结果。error 表示流在业务层终止;连接在两者之前断开则是第三种“不知道最终状态”的情况。聊天前端应重新读取会话历史协调屏幕草稿,AIOps 前端应依赖持久任务和事件恢复,而不是把最后收到的进度当作最终报告。
📷 [图片 token=QoGabX5pFok2sHx6lftcPezVncd(未能下载,见飞书原文)]
事件 id、任务 ID、消息 ID 和工具调用 ID 各自服务不同关联关系。工具调用的 started 与 completed/failed 通过稳定 toolCall ID 配对,引用用 chunk 或来源 ID 标识,任务状态用 task ID。不能把 SSE frame 的 id 当成业务对象主键,也不能仅凭 sequence 跨连接去重所有事件。当前契约为这些标识留出了明确位置,具体恢复仍由聊天历史、后台任务 repository 和诊断事件记录完成。
📷 [图片 token=DHvlbon8Wotz3nxT0dkcHGUkn2b(未能下载,见飞书原文)]
看什么:SSE 客户端保留不完整 frame 的 remainder,只有完整分隔后才解析和产出共享事件。
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
for (;;) {
const { done, value } = await reader.read();
if (done) {
break;
}
// 1. 网络 chunk 不等于 SSE frame,先累计再解析。
buffer += decoder.decode(value, { stream: true });
const parsed = parseSseFrames(buffer);
buffer = parsed.remainder;
yield* parsed.events;
}
// 2. 流结束后刷新 decoder,但不会凭空补 complete。
buffer += decoder.decode();
const parsed = parseSseFrames(buffer);
yield* parsed.events;
📷 [图片 token=TU2kbtgfJo7BGRxGagKcxJ6unIb(未能下载,见飞书原文)]
代码证明客户端不会因为 TCP 分块而截断 JSON,也不会在连接结束时自动生成终止事件。若最后没有 complete 或 error,调用者必须把结果视为未知并回读会话或后台任务;仅重播屏幕上的 delta 不能证明服务端最终持久化成功。
📷 [图片 token=V0IvbalKIolpfvxwKsXcCDzfn0g(未能下载,见飞书原文)]
看什么:把“所见增量”和“可确认事实”分开,明确每种终止后应该读取哪个持久来源。

这条恢复链只保证客户端不把未知状态误报为成功;幂等与断点读取仍依赖具体领域。聊天历史、后台 job event sequence 和诊断任务各有自己的业务 ID,不能用一个通用 SSE event ID 替代。
契约演进的检查方法
增加字段时,优先把非必需展示信息设计为可选,并确保旧消费者仍能根据判别字段工作;改变字段含义、错误 code 或事件 type 则是更高风险变更,需要同步 OpenAPI、后端构造、前端分派和测试。删除一个 type 前还要检查持久化历史中是否可能存在该事件。共享包的类型检查只能发现编译期消费者,Python 字典和数据库中保存的 JSON 还必须由运行时测试覆盖。
📷 [图片 token=ZsuybSfnfolJjkxjsg5cc6r6nFf(未能下载,见飞书原文)]
尤其不要在某个页面为临时需求复制一份局部接口。局部复制会让上传策略、索引状态或引用分数字段出现两种解释。正确入口是修改 packages/api-contracts 的相应领域模块,从 packages/api-contracts/src/index.ts 导出,再让前端 client 和后端 serializer 同时对齐。这样代码审查能看到协议影响范围,而不是在页面 diff 中猜测后端行为。
📷 [图片 token=ArOTbbMZwoY5XOxEdBBcjnD3nke(未能下载,见飞书原文)]
看什么:下面的检查图把一次字段或事件变更拆成必须同步的五个落点,任何一处缺失都会产生可观察漂移。

图中的共享包是入口而不是全部证明:TypeScript 无法检查 Python 字典或数据库历史 JSON。高风险变更还应覆盖旧事件、终止顺序、权限响应和安全消息;若删除字段或 type,必须先确认持久化数据与旧客户端的兼容策略。
数据、契约与状态
错误 category 只有 auth、business、validation 和 system。category 适合界面选择呈现与恢复策略,code 适合精确业务分支,httpStatus 仍用于传输语义。例如 token 失效可以清理认证状态,409 冲突可以提示覆盖或刷新,503 可以提示稍后重试;不能只比较英文 message。
📷 [图片 token=GXvSbB6idoApRYx2CgkcQaGKngf(未能下载,见飞书原文)]
OpenAPI schema 之外,领域模块继续提供静态 DTO。packages/api-contracts/src/auth.ts 定义注册、登录、用户和 token;packages/api-contracts/src/documents.ts 定义上传策略、文档状态与 chunk 预览;packages/api-contracts/src/indexing.ts 定义索引任务状态;packages/api-contracts/src/chat.ts 与 packages/api-contracts/src/retrieval.ts 描述消息、完成结果和知识命中。这些类型从 packages/api-contracts/src/index.ts 统一导出,前端 client 直接导入,而不是在页面内复制接口。
📷 [图片 token=BIftbnXCboxjdUxXZCucF4SknBd(未能下载,见飞书原文)]
SSE 的 channel 是 chat 或 aiops,同一个 type 因而可以在不同业务流中复用。id 和 timestamp 支持事件标识与展示;sequence 只在增量事件内表达顺序。当前前端 parser 验证的是基础字段,不会逐类型深度校验所有 payload,因此后端事件构造与契约测试仍是防止字段漂移的主要保证。
📷 [图片 token=AQOqb1ymtoY2iKxuh0jcQmCpnMb(未能下载,见飞书原文)]
权限、安全与失败边界
认证与授权错误必须在建流之前尽早发生。stream_chat_message 先解析 bearer session,再按 owner_user_id 获取会话;跨 tenant ID 返回 AUTH_FORBIDDEN,Agent runner 未被调用。知识文档、索引任务和诊断路径同样先做 owner-scoped 查询。OpenAPI 中所有这类路径同时声明 bearer、401 与 403,避免调用者把“没登录”和“无权访问该对象”混为一谈。
📷 [图片 token=NlUsbCBGlokj4nxlskVcx8s5ndB(未能下载,见飞书原文)]
错误 payload 不应包含密码、token、API key、Authorization header、连接串、堆栈、用户消息、工具参数值或文档正文。apps/backend/src/super_ai/observability.py 的 _redact 清理敏感键,LLM readiness 对错误中的 API key 做替换,/ready 与 /config/check 将组件失败归一为安全消息。前端遇到非 JSON、非法 envelope、空流或坏 SSE 帧时也生成通用系统错误,不把解析细节直接显示给用户。
📷 [图片 token=DzFabqHlRoLTXXxgbIhcAaCFn7g(未能下载,见飞书原文)]
网络中断还有一个容易忽略的边界:HTTP 成功且已经开始读流后,连接可能在 complete 前断开。聊天服务的规格要求失败时不持久化部分 assistant 消息;前端则应保留可见失败状态并重新读取后端历史,而不是把屏幕上的草稿视为已完成事实。AIOps 长任务另有 durable job 和持久化事件恢复路径,不能仅依赖某一条浏览器连接。
📷 [图片 token=CATybokKsoTqETxkew7cDiF1nXf(未能下载,见飞书原文)]
阅读顺序与小结
先读
packages/api-contracts/src/responses.ts与packages/api-contracts/src/errors.ts,掌握 HTTP 判别字段和错误语义。再读
packages/api-contracts/src/sse.ts,按事件 type 建立流式状态机,而不是记页面代码。浏览
packages/api-contracts/src/openapi.ts的路径、安全与 schema 复用方式。对照
apps/backend/src/super_ai/api/responses.py、apps/backend/src/super_ai/error_catalog.py和apps/backend/src/super_ai/chat/streaming.py检查 Python 序列化。最后追踪
apps/frontend/src/api/apiClient.ts与apps/frontend/src/api/sseClient.ts,确认失败如何从服务端到达用户界面。
这套共享契约的核心不是“多写一份类型”,而是让普通请求、长连接、工具生命周期、引用和错误都具有可预测形状。开发新 API 或事件时,应先更新共享定义与 OpenAPI,再同步后端序列化、前端消费和双方测试;安全上则始终区分 HTTP 建流失败、流内错误和连接中断。这样 Agent 的非确定执行才能被包在确定的工程协议里。
📷 [图片 token=HzpAbUWQEomLmpxXwN4c7jPlnqg(未能下载,见飞书原文)]