diff --git a/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md b/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md index ff5d2f8..dd17592 100644 --- a/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md +++ b/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md @@ -37,12 +37,14 @@ 5. 支持按服务商和按场景测试,测试结果可用于排错和保存配置。 6. 保留现有 Coze 能力,并支持短片小说生成、剧本生成优先绑定 Dify。 7. 后续新增 AI 场景时,业务代码只新增稳定 `sceneCode`,不直接依赖服务商。 +8. 所有面向用户的 AI 业务调用必须流式输出,用户端必须逐段显示生成内容;后台不可等完整结果生成后再一次性返回。 ## 非目标 - 本期不做复杂流量权重、AB 实验和多配置灰度。 - 本期不删除旧 `t_ai_config`,它作为迁移来源和兼容兜底保留。 - 本期不要求所有历史 Coze 调用代码一次性完全清理,但新调用入口必须具备统一路由能力。 +- 本期不把 blocking 响应作为用户端正式输出方式。blocking 只允许用于后台健康检查、兼容旧接口排障或服务商不支持流式时的禁用前诊断,不能绑定为已启用用户场景。 ## 总体架构 @@ -55,23 +57,25 @@ 运行时新增 `AiRuntimeService`,业务服务只传 `sceneCode` 和输入参数: ```java -aiRuntimeService.invoke("chat", params, userId); -aiRuntimeService.invoke("script_generate", params, userId); -aiRuntimeService.invoke("short_story_generate", params, userId); -aiRuntimeService.invoke("diary_summary", params, userId); +aiRuntimeService.invokeStream("chat", params, userId, streamConsumer); +aiRuntimeService.invokeStream("script_generate", params, userId, streamConsumer); +aiRuntimeService.invokeStream("short_story_generate", params, userId, streamConsumer); +aiRuntimeService.invokeStream("diary_summary", params, userId, streamConsumer); ``` 调用链: ```text 业务服务 - -> AiRuntimeService.invoke(sceneCode, inputs, userId) + -> AiRuntimeService.invokeStream(sceneCode, inputs, userId, streamConsumer) -> 查询 ai_scene_binding 当前启用绑定 -> 加载 ai_endpoint_config -> 加载 ai_provider -> 根据 provider_type 选择 DifyProviderAdapter / CozeProviderAdapter - -> 按请求模板组包并调用外部服务 - -> 按响应解析规则提取结果 + -> 按请求模板组包并以流式模式调用外部服务 + -> 将服务商事件转换为统一 AiStreamEvent + -> 边接收边推送到用户端 + -> 完成后汇总最终文本和元数据 -> 写 ai_call_log ``` @@ -83,10 +87,11 @@ aiRuntimeService.invoke("diary_summary", params, userId); | --- | --- | --- | | `AiRuntimeService` | 按 `sceneCode` 查询绑定、选择接口配置、执行 fallback、返回统一结果 | 不关心 Dify/Coze 请求细节 | | `AiProviderAdapter` | 服务商适配器接口,定义组包、调用、解析的统一契约 | 不读取业务场景 | -| `DifyProviderAdapter` | 处理 Dify `/chat-messages` 的 blocking/streaming 请求和响应解析 | 不处理 Coze 格式 | +| `DifyProviderAdapter` | 处理 Dify `/chat-messages` 的 streaming 请求、SSE 解析和事件转换 | 不处理 Coze 格式 | | `CozeProviderAdapter` | 迁移现有 Coze workflow 请求、SSE 解析和错误处理 | 不处理 Dify 格式 | | `AiTemplateRenderer` | 渲染请求模板,合并场景输入、endpoint 默认值和运行时变量 | 不发送 HTTP | | `AiCallLogService` | 统一写入调用日志和测试日志,敏感字段脱敏 | 不决定业务结果 | +| `AiStreamGateway` | 将统一流式事件转发到用户端 SSE 或 WebSocket 连接 | 不直接调用 Dify/Coze | 业务服务只依赖 `AiRuntimeService`,不直接依赖 `DifyProviderAdapter`、`CozeProviderAdapter` 或数据库配置表。 @@ -162,6 +167,8 @@ INDEX(environment, is_enabled) | `request_template` | JSON,请求模板 | | `response_parser` | JSON,响应解析规则 | | `support_stream` | 是否支持流式 | +| `stream_protocol` | 服务商流式协议,`sse`、`websocket`、`chunked`、`custom` | +| `stream_required` | 是否强制流式。面向用户的 AI 场景必须为 `true` | | `support_file_upload` | 是否支持文件上传 | | `timeout_ms` | 接口级超时时间 | | `retry_count` | 接口级重试次数 | @@ -191,6 +198,14 @@ INDEX(endpoint_type, is_enabled) endpoint 配置值 > provider 默认值 > 系统默认值 ``` +用户场景启用约束: + +1. 绑定到 `ai_scene_binding` 且 `is_enabled = 1` 的 endpoint 必须满足 `support_stream = 1`、`stream_required = 1`。 +2. 后台保存或启用场景绑定时,如果 endpoint 不支持流式,直接拒绝启用并提示 `AI_ENDPOINT_STREAM_REQUIRED`。 +3. 用户场景 endpoint 的 `response_parser.mode` 必须使用流式解析,例如 `sse`、`websocket` 或等价自定义流式解析器。 +4. `fallback_endpoint_id` 如果存在,也必须满足同样的流式要求,不能用非流式接口作为用户场景兜底。 +5. 非流式 endpoint 只能用于后台连接诊断、迁移排障或未来非用户端离线任务,不允许承接对话、剧本生成、短篇小说生成、总结分析等用户可见 AI 场景。 + ### ai_scene_binding 业务场景到接口配置的绑定。 @@ -245,6 +260,9 @@ INDEX(scene_category, is_enabled) | `response_status` | HTTP 状态码 | | `response_body` | 原始响应 | | `parsed_result` | 解析后的结果 | +| `stream_chunk_count` | 流式片段数量 | +| `first_chunk_duration_ms` | 首个片段返回耗时 | +| `completed_at` | 流式完成时间 | | `duration_ms` | 耗时 | | `status` | `success`、`failed` | | `error_code` | 错误码 | @@ -298,14 +316,14 @@ Dify 请求体模板应支持: { "inputs": {}, "query": "{{input}}", - "response_mode": "blocking", + "response_mode": "streaming", "conversation_id": "{{conversationId}}", "user": "{{userId}}", "workflow_id": "{{workflowId}}" } ``` -响应解析规则: +后台排障用 blocking 响应解析规则: ```json { @@ -314,7 +332,7 @@ Dify 请求体模板应支持: } ``` -流式响应本期定义为可测试、可解析,但业务是否启用由 endpoint 的 `support_stream` 和请求模板共同决定。Dify SSE 解析规则如下: +流式响应是本期用户场景的强制输出方式。Dify endpoint 如果要绑定到用户场景,`support_stream`、`stream_required` 必须同时为 `true`,请求模板必须使用 `response_mode = streaming`。Dify SSE 解析规则如下: ```json { @@ -327,7 +345,9 @@ Dify 请求体模板应支持: } ``` -`DifyProviderAdapter` 在 blocking 模式下返回 `$.answer`,在 streaming 模式下拼接所有 `event=message` 的 `answer` 内容;遇到 `event=error` 时标记调用失败并写入 `ai_call_log.error_message`。 +`DifyProviderAdapter` 的正式运行时必须使用 `response_mode = streaming`。适配器收到 Dify SSE 后,逐个事件转换为统一 `AiStreamEvent` 并立即下发给用户端;同时在后端累积完整文本,用于业务落库和 `ai_call_log.parsed_result`。遇到 `event=error` 时,立即发送错误事件、标记调用失败并写入 `ai_call_log.error_message`。 + +blocking 解析能力只保留给后台排障:例如验证某个 Dify 应用是否还能返回 `$.answer`。后台排障结果不能作为用户端输出,也不能让该 endpoint 通过用户场景启用校验。 ## Coze 适配 @@ -358,7 +378,7 @@ Coze 默认请求模板: } ``` -Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑,提取 End 节点 `content.output` 或文本回答;迁移完成后该逻辑归属 `CozeProviderAdapter`。 +Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑,但运行时必须边解析边输出 `AiStreamEvent.delta`。如果 Coze 只在 End 节点返回 `content.output`,适配器至少要在收到 End 前持续转发可用文本片段或阶段事件,不能让用户端一直空白等待;迁移完成后该逻辑归属 `CozeProviderAdapter`。 ## 请求模板与响应解析 @@ -413,6 +433,8 @@ Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑, } ``` +用户场景只能启用流式解析模式。`json_path` 和 `raw_text` 仅用于后台诊断、旧数据兼容或非用户端离线任务;如果把这两类解析器绑定到用户场景,启用时必须失败并返回 `AI_ENDPOINT_STREAM_REQUIRED`。 + 解析失败时: 1. `ai_call_log.status = failed`。 @@ -420,6 +442,34 @@ Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑, 3. 日志保留原始响应。 4. 如果场景配置了 fallback,则继续调用 fallback。 +### 统一流式事件 + +所有 provider adapter 必须输出统一事件,运行时和用户端只处理该统一协议: + +```json +{ + "traceId": "string", + "sceneCode": "chat", + "type": "start|delta|message|error|done", + "content": "本次新增文本片段", + "metadata": { + "providerType": "dify", + "endpointCode": "dify.short_story.chat_messages", + "conversationId": "string" + } +} +``` + +事件规则: + +1. `start`:服务商请求建立后立即发送,用于用户端进入生成中状态。 +2. `delta`:每个文本片段到达后立即发送,用户端追加显示,不等待完整响应。 +3. `message`:可选结构化消息,例如引用、工具调用状态或阶段提示。 +4. `error`:调用失败时发送,包含统一错误码和可展示提示。 +5. `done`:服务商正常结束后发送,包含最终统计信息,例如 `durationMs`、`chunkCount`。 + +后端可以在内部累积完整文本,但不能因为落库、解析或审核而阻塞 `delta` 下发。业务落库发生在 `done` 之后;如果中途失败,保存已输出片段和失败原因,便于排查。 + 统一错误码: | 错误码 | 场景 | @@ -428,6 +478,7 @@ Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑, | `AI_SCENE_DISABLED` | 场景被禁用 | | `AI_ENDPOINT_NOT_FOUND` | endpoint 不存在或已删除 | | `AI_ENDPOINT_DISABLED` | endpoint 被禁用 | +| `AI_ENDPOINT_STREAM_REQUIRED` | 用户场景绑定的 endpoint 不支持流式输出 | | `AI_PROVIDER_NOT_FOUND` | provider 不存在或已删除 | | `AI_PROVIDER_DISABLED` | provider 被禁用 | | `AI_TEMPLATE_VARIABLE_MISSING` | 请求模板缺少必填变量 | @@ -447,6 +498,7 @@ Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑, /ai/endpoints /ai/scenes /ai/runtime/test +/ai/runtime/stream /ai/call-logs ``` @@ -492,6 +544,17 @@ POST /ai/scenes/test - 按 endpoint 测试:验证某个接口配置能否调通。 - 按 scene 测试:验证某个业务场景当前绑定是否能返回结果。 +`GET /ai/runtime/stream` 或 `POST /ai/runtime/stream` 用于后台和用户端统一流式调试,返回 SSE 或 WebSocket 消息。测试页面必须显示: + +- 首个片段耗时。 +- 实时片段内容。 +- 最终拼接文本。 +- chunk 数量。 +- 错误事件。 +- traceId 和调用日志入口。 + +所有返回 AI 生成内容的业务接口都必须改造为流式接口,或通过现有 WebSocket 通道输出统一 `AiStreamEvent`。旧的一次性响应接口在迁移期只能作为内部兼容入口,不能继续作为用户端调用入口;如果某个页面仍调用一次性 AI 接口,该页面不算完成迁移。 + ## 后台页面 保留现有菜单 `AI配置管理`,内部升级为四个 Tab 或子路由: @@ -586,11 +649,42 @@ POST /ai/scenes/test - 解析结果。 - fallback 信息。 +## 用户端流式显示 + +所有用户可见 AI 场景都必须走流式链路: + +```text +用户端发起 AI 场景请求 + -> 后端创建 traceId + -> AiRuntimeService.invokeStream + -> ProviderAdapter 接收 Dify/Coze 流式事件 + -> AiStreamGateway 转为用户端 SSE 或 WebSocket 消息 + -> 用户端逐段追加文本 + -> done 后用户端结束 loading,并展示最终内容 +``` + +用户端要求: + +1. 对话、剧本生成、短篇小说生成、日记总结、情绪总结、情绪分析、人生事件疗愈等所有 AI 输出都逐段显示。 +2. 用户端收到 `start` 后立即展示生成中状态,收到第一个 `delta` 后显示正文。 +3. 用户端收到多个 `delta` 时只追加新增内容,不重复覆盖整段文本。 +4. 用户端收到 `done` 后关闭生成中状态,保留最终文本。 +5. 用户端收到 `error` 后停止生成中状态,展示可理解错误提示,并保留已输出内容。 +6. 网络中断时允许用户重新发起同一场景请求;后端通过 `traceId` 和调用日志排查失败原因。 + +协议选择: + +- 已有 WebSocket 场景继续使用 WebSocket 下发统一 `AiStreamEvent`。 +- 普通 HTTP 页面优先使用 SSE。若小程序运行环境不稳定支持 SSE,则通过现有 WebSocket 通道承载同样的事件结构。 +- 前端不直接连接 Dify 或 Coze,所有流式消息都由后端转发,避免 Token 暴露。 + +任何新 AI 功能在上线前必须提供用户端流式展示验证;如果只能一次性显示完整文本,则不能进入验收。 + ## 运行时行为 ### 立即生效 -每次 `AiRuntimeService.invoke` 都从数据库读取当前启用的场景绑定、接口配置和服务商配置。因此以下修改在保存后立即影响下一次调用: +每次 `AiRuntimeService.invokeStream` 都从数据库读取当前启用的场景绑定、接口配置和服务商配置。因此以下修改在保存后立即影响下一次调用: - 服务商 Token。 - 服务商 Base URL。 @@ -641,6 +735,8 @@ scene_code + environment 调用日志必须记录主配置失败原因和是否发生 fallback。 +兜底配置仍然必须流式输出。运行时在调用 fallback 前再次校验 `support_stream`、`stream_required` 和流式解析器;如果 fallback 不满足流式要求,直接返回 `AI_ENDPOINT_STREAM_REQUIRED`,不能退回 blocking 输出。 + ### 禁用策略 - 服务商禁用:其下接口不可被调用。 @@ -686,7 +782,7 @@ Dify 初始化: | `coze.emotion_analysis.default` | `emotion_analysis` | | `coze.summary.default` | `emotion_summary` | -迁移后,业务代码逐步从 `callWorkflowByConfigKey(configKey, ...)` 迁移到 `invoke(sceneCode, ...)`。 +迁移后,业务代码逐步从 `callWorkflowByConfigKey(configKey, ...)` 迁移到 `invokeStream(sceneCode, ...)`。 迁移后的兼容规则: @@ -720,9 +816,11 @@ Dify 初始化: 后端测试: - `AiRuntimeServiceTest`:验证场景路由、禁用状态、fallback。 -- `DifyProviderAdapterTest`:验证 Dify 请求体生成和 `answer` 解析。 +- `DifyProviderAdapterTest`:验证 Dify 请求体强制生成 `response_mode = streaming`,并能转换统一流式事件。 - `DifyProviderAdapterStreamTest`:验证 Dify SSE 的 `message` 拼接、`message_end` 结束和 `error` 失败处理。 - `CozeProviderAdapterTest`:验证 Coze 请求体生成和 SSE 解析。 +- `AiRuntimeStreamTest`:验证 `invokeStream` 会按 `start`、`delta`、`done` 顺序输出事件,且不会等待完整响应才返回。 +- `AiStreamGatewayTest`:验证 SSE/WebSocket 转发格式、错误事件、断连处理和 traceId 透传。 - `AiTemplateRendererTest`:验证占位符替换、对象注入、缺失变量报错和 JSON 校验。 - 迁移脚本测试:验证旧 `t_ai_config` 能拆成 provider、endpoint、scene。 @@ -733,6 +831,7 @@ Dify 初始化: - 场景绑定切换后,场景测试走新的接口配置。 - 禁用配置后不能被正常调用。 - 测试请求不在浏览器暴露明文 Token。 +- 用户端和后台测试页都能逐段显示 AI 输出,不出现等待接口完成后一次性渲染的行为。 集成验收: @@ -741,6 +840,7 @@ Dify 初始化: - 对话绑定 Coze 后保持现有可用。 - 将剧本生成从 Dify 切回 Coze 后,不发版,下一次调用立即生效。 - 主配置失败且配置了兜底时,调用日志记录 fallback。 +- 用户端对话、剧本生成、短篇小说生成、总结分析等 AI 场景都能看到流式文本逐段输出。 ## 风险与缓解 @@ -763,6 +863,9 @@ Dify 初始化: 5. 业务调用只依赖 `sceneCode`,不再需要知道当前场景使用 Dify 还是 Coze。 6. 修改场景绑定后,下一次业务调用立即走新配置。 7. 调用日志能区分服务商、接口配置、场景、成功/失败和 fallback。 -8. Dify blocking 和 streaming 两种响应都能被后台测试解析。 +8. Dify、Coze 的用户场景调用都通过统一流式事件输出,后台测试页能实时显示 `start`、`delta`、`done` 或 `error`。 9. 管理后台和调用日志不泄露明文 Token。 10. 旧 Coze 对话在迁移期间保持可用。 +11. 对话、剧本生成、短篇小说生成、总结分析、疗愈等所有 AI 用户端入口都能逐段显示输出,不能一次性等完整结果后再渲染。 +12. 非流式 endpoint 不能启用到用户场景,启用时必须返回 `AI_ENDPOINT_STREAM_REQUIRED`。 +13. 每个已迁移 AI 场景都必须满足“有 start、有 delta、有 done 或 error、有调用日志、有用户端可见输出”,不能出现接口成功但页面无输出的情况。