# AI 场景路由配置中心设计 日期:2026-05-22 ## 背景 当前后台 `web-admin` 已有 AI 配置管理功能,后端使用 `t_ai_config` 保存 Coze 相关配置。该表同时承载服务商凭证、接口 URL、Bot/Workflow ID、业务场景、请求参数、测试保存等多种职责。随着短片小说生成、剧本生成迁移到自建 Dify 平台,且后续可能继续接入更多 AI 服务商,现有单表模型难以支持灵活切换和统一测试。 目标是将后台升级为 AI 场景路由配置中心:管理员可以配置服务商、接口/工作流、业务场景绑定,并让所有需要使用 AI 的业务都通过场景编码调用。后台切换场景绑定后,不需要发版,下一次调用立即生效。 ## 现状 现有实现包含: - `web-admin/src/views/aiconfig/AiConfigList.vue`:AI 配置列表、编辑、测试、测试后保存。 - `backend-single/src/main/java/com/emotion/entity/AiConfig.java`:对应 `t_ai_config`。 - `AiConfigController`、`AiConfigService`、`AiConfigServiceImpl`:配置 CRUD、启用禁用、默认配置、测试后更新。 - `AiChatServiceImpl`:包含大量 Coze 专用调用、请求组装、SSE 解析、工作流调用和日志记录逻辑。 - `docs/dify平台接口.md`:Dify 平台接口文档,当前重点使用 `/chat-messages`。 现有 AI 业务入口包括: - 对话 / WebSocket 对话。 - 剧本生成。 - 短片小说生成。 - 日记总结 / AI 评论。 - 情绪总结 / 情绪分析。 - 人生事件疗愈回复。 - 后续新增 AI 场景。 ## 设计目标 1. 后台可配置所有 AI 服务商,例如 Dify、Coze、未来 OpenAI 或自定义 HTTP 服务。 2. 后台可配置具体接口或工作流,例如 Dify `/chat-messages`、Coze workflow。 3. 后台可列出所有业务场景,并为每个场景选择当前生效的接口配置。 4. 场景绑定修改后立即对后续调用生效。 5. 支持按服务商和按场景测试,测试结果可用于排错和保存配置。 6. 保留现有 Coze 能力,并支持短片小说生成、剧本生成优先绑定 Dify。 7. 后续新增 AI 场景时,业务代码只新增稳定 `sceneCode`,不直接依赖服务商。 8. 所有面向用户的 AI 业务调用必须流式输出,用户端必须逐段显示生成内容;后台不可等完整结果生成后再一次性返回。 ## 非目标 - 本期不做复杂流量权重、AB 实验和多配置灰度。 - 本期不删除旧 `t_ai_config`,它作为迁移来源和兼容兜底保留。 - 本期不要求所有历史 Coze 调用代码一次性完全清理,但新调用入口必须具备统一路由能力。 - 本期不把 blocking 响应作为用户端正式输出方式。blocking 只允许用于后台健康检查、兼容旧接口排障或服务商不支持流式时的禁用前诊断,不能绑定为已启用用户场景。 ## 总体架构 采用三层配置模型: 1. `ai_provider`:服务商配置。 2. `ai_endpoint_config`:接口/工作流配置。 3. `ai_scene_binding`:业务场景绑定。 运行时新增 `AiRuntimeService`,业务服务只传 `sceneCode` 和输入参数: ```java 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.invokeStream(sceneCode, inputs, userId, streamConsumer) -> 查询 ai_scene_binding 当前启用绑定 -> 加载 ai_endpoint_config -> 加载 ai_provider -> 根据 provider_type 选择 DifyProviderAdapter / CozeProviderAdapter -> 按请求模板组包并以流式模式调用外部服务 -> 将服务商事件转换为统一 AiStreamEvent -> 边接收边推送到用户端 -> 完成后汇总最终文本和元数据 -> 写 ai_call_log ``` ### 模块边界 后端新增以下核心单元,避免继续把服务商差异堆在 `AiChatServiceImpl`: | 单元 | 职责 | 不负责 | | --- | --- | --- | | `AiRuntimeService` | 按 `sceneCode` 查询绑定、选择接口配置、执行 fallback、返回统一结果 | 不关心 Dify/Coze 请求细节 | | `AiProviderAdapter` | 服务商适配器接口,定义组包、调用、解析的统一契约 | 不读取业务场景 | | `DifyProviderAdapter` | 处理 Dify `/chat-messages` 的 streaming 请求、SSE 解析和事件转换 | 不处理 Coze 格式 | | `CozeProviderAdapter` | 迁移现有 Coze workflow 请求、SSE 解析和错误处理 | 不处理 Dify 格式 | | `AiTemplateRenderer` | 渲染请求模板,合并场景输入、endpoint 默认值和运行时变量 | 不发送 HTTP | | `AiCallLogService` | 统一写入调用日志和测试日志,敏感字段脱敏 | 不决定业务结果 | | `AiStreamGateway` | 将统一流式事件转发到用户端 SSE 或 WebSocket 连接 | 不直接调用 Dify/Coze | 业务服务只依赖 `AiRuntimeService`,不直接依赖 `DifyProviderAdapter`、`CozeProviderAdapter` 或数据库配置表。 ## 数据模型 所有新增表都遵循项目已有公共字段约定: ```text id create_by create_time update_by update_time is_deleted remarks ``` 删除操作使用逻辑删除。查询默认过滤 `is_deleted = 0`。 ### ai_provider 服务商账号和基础能力配置。 | 字段 | 说明 | | --- | --- | | `id` | 主键 | | `provider_code` | 唯一编码,如 `dify_default`、`coze_prod` | | `provider_name` | 显示名称 | | `provider_type` | `dify`、`coze`、`openai`、`custom` | | `base_url` | 服务商基础地址,如 `http://49.232.138.53/v1` | | `auth_type` | `bearer`、`api_key`、`oauth`、`none` | | `api_key` | 服务商级 API Key | | `client_id` | OAuth Client ID | | `client_secret` | OAuth Client Secret | | `grant_type` | OAuth 授权类型 | | `default_headers` | JSON,默认请求头 | | `health_check_url` | 健康检查地址 | | `timeout_ms` | 默认超时时间 | | `retry_count` | 默认重试次数 | | `retry_delay_ms` | 默认重试延迟 | | `environment` | `development`、`testing`、`production` | | `is_enabled` | 是否启用 | | `description` | 描述 | 约束和索引: ```text UNIQUE(provider_code) INDEX(provider_type, is_enabled) INDEX(environment, is_enabled) ``` 同一 `provider_code` 逻辑删除后不可直接复用,避免历史调用日志无法追溯。需要复用时通过恢复或显式改名处理。 ### ai_endpoint_config 具体接口、工作流或模型调用配置。 | 字段 | 说明 | | --- | --- | | `id` | 主键 | | `provider_id` | 关联 `ai_provider.id` | | `endpoint_code` | 唯一编码,如 `dify.short_story.chat_messages` | | `endpoint_name` | 显示名称 | | `endpoint_type` | `chat`、`workflow`、`completion`、`audio`、`custom` | | `http_method` | `GET`、`POST` 等 | | `path` | 接口路径,如 `/chat-messages` | | `full_url_override` | 特殊接口完整 URL 覆盖 | | `app_id` | Dify App 或未来扩展 | | `bot_id` | Coze Bot ID | | `workflow_id` | Dify 或 Coze Workflow ID | | `model_name` | 模型名称 | | `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` | 接口级重试次数 | | `retry_delay_ms` | 接口级重试延迟 | | `first_token_timeout_ms` | 首个文本片段超时时间 | | `stream_idle_timeout_ms` | 流式空闲超时时间 | | `max_output_chars` | 单次输出最大字符数 | | `last_test_status` | 最近一次测试状态 | | `last_test_time` | 最近一次测试时间 | | `last_test_trace_id` | 最近一次测试链路 ID | | `is_enabled` | 是否启用 | | `description` | 描述 | 约束和索引: ```text UNIQUE(endpoint_code) INDEX(provider_id, is_enabled) INDEX(endpoint_type, is_enabled) ``` `path` 与 `full_url_override` 的优先级: 1. 如果 `full_url_override` 非空,直接使用该 URL。 2. 否则使用 `ai_provider.base_url + ai_endpoint_config.path`。 `timeout_ms`、`retry_count`、`retry_delay_ms` 的优先级: ```text 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 业务场景到接口配置的绑定。 | 字段 | 说明 | | --- | --- | | `id` | 主键 | | `scene_code` | 稳定业务场景编码,如 `chat`、`script_generate` | | `scene_name` | 场景名称 | | `scene_category` | `chat`、`content`、`analysis`、`healing` | | `endpoint_config_id` | 当前生效接口配置 | | `fallback_endpoint_id` | 兜底接口配置,可为空 | | `environment` | 环境 | | `is_enabled` | 是否启用 | | `priority` | 预留优先级 | | `input_schema` | JSON,说明入参结构 | | `test_payload` | JSON,后台场景测试默认入参 | | `last_test_status` | 最近一次场景测试状态 | | `last_test_time` | 最近一次场景测试时间 | | `last_test_trace_id` | 最近一次场景测试链路 ID | | `description` | 描述 | 约束和索引: ```text UNIQUE(scene_code, environment) INDEX(endpoint_config_id) INDEX(fallback_endpoint_id) INDEX(scene_category, is_enabled) ``` 同一环境内一个 `scene_code` 只能有一条生效绑定。后续如果需要灰度或权重路由,另行增加路由规则表,不在本期混入 `ai_scene_binding`。 本期不单独新增场景字典表,`ai_scene_binding` 同时承担“场景清单”和“环境绑定”职责。初始化时为每个支持的 `scene_code` 按环境创建一条记录,`endpoint_config_id` 可先为空,`is_enabled = 0`。后台场景页必须展示这些未绑定场景,管理员完成 endpoint 绑定、测试通过并启用后才参与运行时路由。 ### ai_call_log 统一调用日志,替代后续新增场景对 `t_coze_api_call` 的依赖。 | 字段 | 说明 | | --- | --- | | `id` | 主键 | | `trace_id` | 链路追踪 ID | | `scene_code` | 场景编码 | | `provider_id` | 服务商 ID | | `provider_type` | 服务商类型 | | `endpoint_config_id` | 接口配置 ID | | `fallback_used` | 是否使用兜底 | | `request_url` | 请求 URL | | `request_headers` | 脱敏后的请求头 | | `request_body` | 请求体 | | `response_status` | HTTP 状态码 | | `response_body` | 原始响应 | | `parsed_result` | 解析后的结果 | | `stream_chunk_count` | 流式片段数量 | | `first_chunk_duration_ms` | 首个片段返回耗时 | | `completed_at` | 流式完成时间 | | `output_preview` | 输出预览,保存前后截断后的文本 | | `duration_ms` | 耗时 | | `status` | `success`、`failed` | | `error_code` | 错误码 | | `error_message` | 错误信息 | 索引: ```text INDEX(trace_id) INDEX(scene_code, create_time) INDEX(provider_type, create_time) INDEX(endpoint_config_id, create_time) INDEX(status, create_time) ``` 日志中 `request_headers` 必须脱敏 `Authorization`、`api-key`、`x-api-key`、`client_secret` 等敏感字段。流式响应内容可能很长,`response_body` 和 `parsed_result` 不保存无限完整文本;默认保存元数据、错误上下文、`output_preview`、`stream_chunk_count` 和首包耗时。业务表需要完整结果时,由具体业务在 `done` 后保存最终文本。 ## 初始场景编码 | sceneCode | 场景 | | --- | --- | | `chat` | 对话 | | `script_generate` | 剧本生成 | | `short_story_generate` | 短片小说生成 | | `diary_summary` | 日记总结 / AI 评论 | | `emotion_summary` | 情绪总结 | | `emotion_analysis` | 情绪分析 | | `life_healing` | 人生事件疗愈回复 | 后续新增场景只需要新增 `ai_scene_binding` 和业务调用点,不需要新增服务商专用字段。 新增场景的规则: 1. 先在代码中定义稳定 `sceneCode` 常量,业务调用只引用该常量。 2. 数据初始化脚本为所有环境创建对应 `ai_scene_binding` 记录,默认禁用且不绑定 endpoint。 3. 后台展示未绑定场景,并允许管理员选择 endpoint、填写 `test_payload`、执行场景测试。 4. 只有绑定的 endpoint、provider 都启用,且场景本身启用后,运行时才会调用。 5. 如果业务调用了未初始化的 `sceneCode`,返回 `AI_SCENE_NOT_FOUND` 并写入失败日志。 ## Dify 适配 依据 `docs/dify平台接口.md`,本期优先支持: ```text POST /chat-messages ``` Dify 请求体模板应支持: ```json { "inputs": {}, "query": "{{input}}", "response_mode": "streaming", "conversation_id": "{{conversationId}}", "user": "{{userId}}", "workflow_id": "{{workflowId}}" } ``` 后台排障用 blocking 响应解析规则: ```json { "mode": "json_path", "answerPath": "$.answer" } ``` 流式响应是本期用户场景的强制输出方式。Dify endpoint 如果要绑定到用户场景,`support_stream`、`stream_required` 必须同时为 `true`,请求模板必须使用 `response_mode = streaming`。Dify SSE 解析规则如下: ```json { "mode": "sse", "eventField": "event", "contentEvents": ["message"], "contentPath": "$.answer", "endEvents": ["message_end"], "errorEvents": ["error"] } ``` `DifyProviderAdapter` 的正式运行时必须使用 `response_mode = streaming`。适配器收到 Dify SSE 后,逐个事件转换为统一 `AiStreamEvent` 并立即下发给用户端;同时在后端累积完整文本,用于业务落库和 `ai_call_log.parsed_result`。遇到 `event=error` 时,立即发送错误事件、标记调用失败并写入 `ai_call_log.error_message`。 blocking 解析能力只保留给后台排障:例如验证某个 Dify 应用是否还能返回 `$.answer`。后台排障结果不能作为用户端输出,也不能让该 endpoint 通过用户场景启用校验。 ## Coze 适配 现有 Coze 工作流能力迁移到 `CozeProviderAdapter`: - 组装 `bot_id`、`workflow_id`、`user_id`、`stream`、`additional_messages`、`parameters`。 - 保留现有 SSE 解析能力。 - 保留现有重试和异常处理策略。 - 新日志写入 `ai_call_log`,旧 `t_coze_api_call` 可在过渡期继续写入。 Coze 默认请求模板: ```json { "bot_id": "{{botId}}", "workflow_id": "{{workflowId}}", "user_id": "{{userId}}", "stream": true, "additional_messages": [ { "role": "user", "content": "{{input}}", "content_type": "text", "type": "question" } ], "parameters": "{{parameters}}" } ``` Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑,但运行时必须边解析边输出 `AiStreamEvent.delta`。如果某个 Coze workflow 只能在 End 节点一次性返回 `content.output`,该 workflow 不能作为用户场景 endpoint 启用,需要先调整 Coze 工作流或更换 provider,使其能产生可展示文本片段。`message` 阶段事件可以辅助展示进度,但不能替代文本 `delta`;迁移完成后该逻辑归属 `CozeProviderAdapter`。 ## 请求模板与响应解析 ### 模板变量 `request_template` 使用简单占位符语法,避免引入复杂脚本执行: ```text {{input}} 单文本输入 {{userId}} 当前用户 ID {{conversationId}} 会话 ID,可为空 {{workflowId}} endpoint.workflow_id {{botId}} endpoint.bot_id {{parameters}} 运行时参数对象 {{inputs.xxx}} 运行时参数中的 xxx 字段 ``` 渲染规则: 1. 先加载 endpoint 的 `request_template`。 2. 注入 endpoint 字段,例如 `workflowId`、`botId`。 3. 注入运行时参数 `inputs`。 4. 注入系统参数,例如 `userId`、`conversationId`、`traceId`。 5. 渲染完成后必须校验为合法 JSON。 如果某个必填占位符缺失,测试和运行时调用都直接失败,并返回明确错误,例如 `AI_TEMPLATE_VARIABLE_MISSING: workflowId`。 ### 响应解析 `response_parser` 支持三类模式: ```json { "mode": "json_path", "answerPath": "$.answer" } ``` ```json { "mode": "sse", "contentEvents": ["message"], "contentPath": "$.answer", "endEvents": ["message_end"], "errorEvents": ["error"] } ``` ```json { "mode": "raw_text" } ``` 用户场景只能启用流式解析模式。`json_path` 和 `raw_text` 仅用于后台诊断、旧数据兼容或非用户端离线任务;如果把这两类解析器绑定到用户场景,启用时必须失败并返回 `AI_ENDPOINT_STREAM_REQUIRED`。 解析失败时: 1. `ai_call_log.status = failed`。 2. `error_code = AI_RESPONSE_PARSE_ERROR`。 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` 之后;如果中途失败,保存已输出片段和失败原因,便于排查。 流式完成判定: 1. 成功调用必须至少发送一个 `start`、一个非空文本 `delta`、一个 `done`。 2. 如果服务商正常结束但没有任何非空 `delta`,调用按失败处理,错误码为 `AI_STREAM_NO_DELTA`。 3. 如果首个 `delta` 超过 endpoint 的首包超时阈值,发送 `error` 并记录 `AI_STREAM_TIMEOUT`。 4. 如果服务商中途断流,发送 `error`,保留已输出文本,记录 `AI_STREAM_INTERRUPTED`。 5. 如果用户端主动断开,后端取消上游请求或停止读取,并记录 `AI_STREAM_CLIENT_DISCONNECTED`。 统一错误码: | 错误码 | 场景 | | --- | --- | | `AI_SCENE_NOT_FOUND` | 未找到当前环境的场景配置 | | `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` | 请求模板缺少必填变量 | | `AI_TEMPLATE_INVALID_JSON` | 请求模板渲染后不是合法 JSON | | `AI_PROVIDER_REQUEST_FAILED` | 外部服务调用失败 | | `AI_RESPONSE_PARSE_ERROR` | 响应解析失败 | | `AI_STREAM_NO_DELTA` | 服务商连接成功但没有产生可展示文本片段 | | `AI_STREAM_TIMEOUT` | 流式首包或整体输出超时 | | `AI_STREAM_CLIENT_DISCONNECTED` | 用户端连接中断 | | `AI_STREAM_INTERRUPTED` | 服务商流式输出中断 | | `AI_STREAM_OUTPUT_LIMIT` | 输出超过 endpoint 配置的最大字符数 | | `AI_FALLBACK_FAILED` | 主配置和兜底配置都失败 | 业务层收到错误时,只处理统一错误码和用户可见提示,不直接解析 Dify 或 Coze 的原始错误结构。 ## 后端接口 新增管理 API 前缀: ```text /ai/providers /ai/endpoints /ai/scenes /ai/runtime/test /ai/runtime/stream /ai/call-logs ``` ### 服务商接口 ```text GET /ai/providers/page GET /ai/providers/detail?id= POST /ai/providers/create PUT /ai/providers/update DELETE /ai/providers/delete?id= PUT /ai/providers/enable?id= PUT /ai/providers/disable?id= POST /ai/providers/test ``` ### 接口/工作流接口 ```text GET /ai/endpoints/page GET /ai/endpoints/detail?id= POST /ai/endpoints/create PUT /ai/endpoints/update DELETE /ai/endpoints/delete?id= POST /ai/endpoints/test ``` ### 场景绑定接口 ```text GET /ai/scenes/page GET /ai/scenes/detail?id= POST /ai/scenes/create PUT /ai/scenes/update PUT /ai/scenes/bind POST /ai/scenes/test ``` ### 运行时测试接口 `POST /ai/runtime/test` 支持: - 按 endpoint 测试:验证某个接口配置能否调通。 - 按 scene 测试:验证某个业务场景当前绑定是否能返回结果。 `GET /ai/runtime/stream` 或 `POST /ai/runtime/stream` 用于后台和用户端统一流式调试,返回 SSE 或 WebSocket 消息。测试页面必须显示: - 首个片段耗时。 - 实时片段内容。 - 最终拼接文本。 - chunk 数量。 - 错误事件。 - traceId 和调用日志入口。 后台测试通过条件: 1. 必须收到 `start`。 2. 必须在 `first_token_timeout_ms` 内收到至少一个非空 `delta`。 3. 必须收到 `done`,或在失败时收到 `error` 并能展示统一错误码。 4. `chunkCount` 必须大于 0。 5. 调用日志必须能通过 traceId 查到 provider、endpoint、scene、耗时、chunk 数量和错误上下文。 所有返回 AI 生成内容的业务接口都必须改造为流式接口,或通过现有 WebSocket 通道输出统一 `AiStreamEvent`。旧的一次性响应接口在迁移期只能作为内部兼容入口,不能继续作为用户端调用入口;如果某个页面仍调用一次性 AI 接口,该页面不算完成迁移。 ## 后台页面 保留现有菜单 `AI配置管理`,内部升级为四个 Tab 或子路由: 1. 服务商配置。 2. 接口/工作流配置。 3. 场景绑定。 4. 调用日志。 ### 服务商配置页 列表展示: - 服务商名称。 - 类型。 - Base URL。 - 环境。 - 状态。 - 健康状态。 - 更新时间。 操作: - 新增。 - 编辑。 - 测试连接。 - 启用/禁用。 - 查看关联接口。 ### 接口/工作流配置页 列表展示: - 接口名称。 - 编码。 - 服务商。 - 类型。 - 路径 / Workflow。 - 是否流式。 - 状态。 - 最近测试结果。 表单包含: - 服务商。 - HTTP 方法。 - 接口路径或完整 URL 覆盖。 - `botId`、`workflowId`、`appId`。 - 请求模板 JSON。 - 响应解析规则 JSON。 - 超时和重试。 - 流式、文件上传等能力开关。 ### 场景绑定页 列表展示: - 场景名称。 - `sceneCode`。 - 当前绑定接口。 - 服务商。 - 兜底接口。 - 环境。 - 状态。 操作: - 切换接口配置。 - 配置兜底接口。 - 场景测试。 - 启用/禁用。 - 查看调用日志。 ### 调用日志页 列表展示: - 时间。 - 场景。 - 服务商。 - 接口配置。 - 状态。 - 耗时。 - 错误信息。 详情展示: - 请求 URL。 - 请求头,敏感字段脱敏。 - 请求体。 - 原始响应。 - 解析结果。 - 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 功能在上线前必须提供用户端流式展示验证;如果只能一次性显示完整文本,则不能进入验收。 ### 用户端场景迁移清单 每个 AI 场景迁移时都要同时完成后端路由、业务接口和用户端渲染,不能只改后台配置: | sceneCode | 用户端入口 | 输出要求 | | --- | --- | --- | | `chat` | 对话页 / WebSocket 对话 | 用户发送后立即进入生成中,逐段追加回复 | | `script_generate` | 剧本生成页 | 章节、片段或正文逐段显示,最终文本在 `done` 后保存 | | `short_story_generate` | 短篇小说生成页 | 正文逐段显示,失败时保留已输出内容 | | `diary_summary` | 日记总结 / AI 评论入口 | 总结文本逐段显示,不等待完整总结 | | `emotion_summary` | 情绪总结入口 | 总结文本逐段显示 | | `emotion_analysis` | 情绪分析入口 | 分析文本逐段显示,结构化结果在 `done` 后落库 | | `life_healing` | 人生事件疗愈入口 | 回复文本逐段显示 | 迁移完成的判定是用户端页面实际消费 `AiStreamEvent`,并能在弱网或长文本生成时持续更新。如果只是在后端拿到流式内容再拼成完整文本返回给页面,不算完成迁移。 ## 运行时行为 ### 立即生效 每次 `AiRuntimeService.invokeStream` 都从数据库读取当前启用的场景绑定、接口配置和服务商配置。因此以下修改在保存后立即影响下一次调用: - 服务商 Token。 - 服务商 Base URL。 - 接口路径。 - Workflow ID。 - 请求模板。 - 响应解析规则。 - 场景绑定的 endpoint。 - 启用/禁用状态。 - 兜底配置。 本期不做长期缓存。后续如增加缓存,必须基于 `update_time` 或版本号自动失效。 后台保存配置时不需要重启服务。运行时读取顺序固定为: ```text scene_code + environment -> enabled ai_scene_binding -> enabled ai_endpoint_config -> enabled ai_provider ``` 任一层不存在或禁用,调用失败并写日志,不静默降级到旧配置。 运行环境来源固定为后端当前环境配置,例如 Spring Profile 或项目已有环境变量。业务代码不从前端请求参数读取 environment,避免用户侧伪造环境导致误路由。 ### 流式连接控制 流式调用需要显式处理连接生命周期: 1. 后端为每次调用创建 `traceId`,并在 `start` 事件中返回。 2. 后端向 Dify/Coze 发起请求后,不能在当前线程同步等待完整响应;必须边读取边转发。 3. 用户端断开 SSE/WebSocket 时,后端应停止继续向用户端写入,并尽量取消上游请求。 4. 超过 `stream_idle_timeout_ms` 没有任何新事件时,后端发送 `error` 并结束连接。 5. 超过 `max_output_chars` 时,后端发送 `error` 并记录 `AI_STREAM_OUTPUT_LIMIT`,避免无限输出拖垮页面和日志。 6. 同一用户同时发起多个 AI 场景请求时,前端按 `traceId` 区分输出容器,不能串流到错误页面区域。 ### 配置生效与切换保护 立即生效只针对已启用配置。新增或编辑 provider、endpoint、scene 时,保存动作只落库;是否进入运行时由 `is_enabled` 控制。 生产环境建议采用以下切换流程: 1. 保存 provider 或 endpoint 后先保持禁用。 2. 在后台执行 provider 测试、endpoint 测试或 scene 测试。 3. 测试成功后再启用 endpoint 或 scene binding。 4. 切换场景绑定时,后台记录切换前 endpoint、切换后 endpoint、操作者、时间和测试 traceId。 5. 如果切换后出现失败,可直接把 scene 绑定切回上一条 endpoint 配置,下一次调用立即生效。 后台可以先实现为确认弹窗和审计日志,不强制做审批流。若后续生产切换风险升高,再增加“双人复核”或“待发布配置”状态。 ### 兜底策略 ```text 主配置成功 -> 返回主配置结果 主配置失败且存在 fallback_endpoint_id -> 调用兜底配置 主配置失败且不存在 fallback_endpoint_id -> 返回业务可识别错误 ``` 调用日志必须记录主配置失败原因和是否发生 fallback。 兜底配置仍然必须流式输出。运行时在调用 fallback 前再次校验 `support_stream`、`stream_required` 和流式解析器;如果 fallback 不满足流式要求,直接返回 `AI_ENDPOINT_STREAM_REQUIRED`,不能退回 blocking 输出。 ### 禁用策略 - 服务商禁用:其下接口不可被调用。 - 接口配置禁用:不可作为主配置或兜底配置调用。 - 场景禁用:业务调用直接返回明确错误。 - 后台绑定时应提示禁用配置不可绑定,或允许保存但不可启用。 ### 安全与审计 - 管理后台编辑页允许输入完整 Key,但列表页和详情页默认脱敏展示。 - 只有显式“显示密钥”操作才展示完整 Key,并记录管理员操作日志。 - `ai_call_log` 永远不保存明文 Token。 - 测试请求中的请求头展示也必须脱敏。 - 所有配置新增、修改、启用、禁用、场景切换都写入后台操作审计日志。 - 不允许前端直接调用外部 Dify/Coze URL 进行正式测试;正式测试走后端 `/ai/runtime/test`,避免 Token 暴露到浏览器。 ## 迁移策略 第一期保留 `t_ai_config`。 迁移脚本将现有 Coze 配置拆分为: ```text t_ai_config.provider/apiBaseUrl/apiToken -> ai_provider t_ai_config.botId/workflowId/customParams -> ai_endpoint_config t_ai_config.usageScenario/configKey -> ai_scene_binding 初始绑定 ``` Dify 初始化: - 从 `docs/dify平台接口.md` 的基础地址创建 `dify_default` provider。 - 为短片小说生成创建 Dify endpoint。 - 为剧本生成创建 Dify endpoint。 - 分别创建 `short_story_generate`、`script_generate` 的 scene 绑定。 旧配置键到新场景建议映射: | 旧配置键 | 新场景 | | --- | --- | | `coze.chat.default` | `chat` | | `coze.course.life.generate` | `script_generate` | | `coze.user.dairy.summary` | `diary_summary` 或 `life_healing` | | `coze.emotion_analysis.default` | `emotion_analysis` | | `coze.summary.default` | `emotion_summary` | 迁移后,业务代码逐步从 `callWorkflowByConfigKey(configKey, ...)` 迁移到 `invokeStream(sceneCode, ...)`。 迁移后的兼容规则: 1. 新场景优先走 `AiRuntimeService`。 2. 尚未迁移的旧调用继续走 `AiChatServiceImpl` 和 `t_ai_config`。 3. 如果新路由调用失败,不自动回退到旧 `t_ai_config`,除非该场景显式配置了 fallback endpoint。 4. 每迁移一个业务场景,都要删除该业务中的硬编码 `coze.*` 配置键引用。 回滚方式: 1. 保留旧 `t_ai_config` 和旧调用代码直到所有关键场景验收通过。 2. 如果某个场景新路由异常,可在后台将该 `sceneCode` 绑定切回 Coze endpoint。 3. 如果新 runtime 服务整体异常,可通过配置开关让指定业务临时回退旧方法;该开关只作为过渡保护,不作为长期设计。 ## 实施顺序 1. 新增数据库表、实体、Mapper、DTO、Service、Controller。 2. 新增 `AiRuntimeService` 和 provider adapter 接口。 3. 实现 `DifyProviderAdapter` 和 `CozeProviderAdapter`。 4. 实现 endpoint 测试和 scene 测试。 5. 升级 web-admin AI 配置管理为四个页面。 6. 编写迁移脚本和初始数据。 7. 将短片小说生成、剧本生成迁到 `sceneCode` 调用。 8. 将对话、日记总结、情绪分析、疗愈等旧 Coze 场景逐步迁到 `sceneCode`。 9. 保留旧 `AiConfig` 页面或提供只读迁移视图,确认稳定后再下线。 每迁移一个场景,都必须同时完成以下交付: 1. 后台存在该 `sceneCode` 的启用绑定。 2. endpoint 支持流式并通过后台流式测试。 3. 业务接口改为 `invokeStream` 或 WebSocket 流式转发。 4. 用户端页面消费 `AiStreamEvent` 并逐段渲染。 5. 调用日志能查到该场景的 traceId、chunk 数、首包耗时和最终状态。 每一步交付都应保持系统可启动、已有 Coze 对话不回归。 ## 测试计划 后端测试: - `AiRuntimeServiceTest`:验证场景路由、禁用状态、fallback。 - `DifyProviderAdapterTest`:验证 Dify 请求体强制生成 `response_mode = streaming`,并能转换统一流式事件。 - `DifyProviderAdapterStreamTest`:验证 Dify SSE 的 `message` 拼接、`message_end` 结束和 `error` 失败处理。 - `CozeProviderAdapterTest`:验证 Coze 请求体生成和 SSE 解析。 - `AiRuntimeStreamTest`:验证 `invokeStream` 会按 `start`、`delta`、`done` 顺序输出事件,且不会等待完整响应才返回。 - `AiStreamGatewayTest`:验证 SSE/WebSocket 转发格式、错误事件、断连处理和 traceId 透传。 - `AiStreamLifecycleTest`:验证首包超时、空闲超时、用户端断连、无 delta、输出过长等异常都会产生统一错误码和调用日志。 - `AiTemplateRendererTest`:验证占位符替换、对象注入、缺失变量报错和 JSON 校验。 - 迁移脚本测试:验证旧 `t_ai_config` 能拆成 provider、endpoint、scene。 前端测试: - 服务商配置可创建、编辑、启用、禁用和测试连接。 - 接口配置可按服务商生成默认请求模板。 - 场景绑定切换后,场景测试走新的接口配置。 - 禁用配置后不能被正常调用。 - 测试请求不在浏览器暴露明文 Token。 - 用户端和后台测试页都能逐段显示 AI 输出,不出现等待接口完成后一次性渲染的行为。 - 用户端断网、返回、重复发起请求时,不会把不同 traceId 的输出串到同一个展示区域。 集成验收: - 短片小说生成绑定 Dify 后能返回可解析结果。 - 剧本生成绑定 Dify 后能返回可解析结果。 - 对话绑定 Coze 后保持现有可用。 - 将剧本生成从 Dify 切回 Coze 后,不发版,下一次调用立即生效。 - 主配置失败且配置了兜底时,调用日志记录 fallback。 - 用户端对话、剧本生成、短篇小说生成、总结分析等 AI 场景都能看到流式文本逐段输出。 - 任一场景出现服务商无文本片段、超时或断流时,用户端能看到错误状态,后台能查到统一错误码和 traceId。 ## 风险与缓解 | 风险 | 缓解 | | --- | --- | | Dify 与 Coze 响应结构不同 | 使用 provider adapter 和 response_parser 隔离差异 | | 请求模板过于自由导致配置错误 | 后台提供 Dify、Coze 默认模板,并在测试时校验 JSON | | 立即生效可能导致误操作影响线上 | 增加测试按钮、启用状态、环境隔离和调用日志 | | 旧业务调用链较长 | 分阶段迁移,旧 `t_ai_config` 保留兼容 | | Token 暴露风险 | 管理后台可编辑但日志和普通详情默认脱敏 | | 请求模板配置错误导致线上失败 | 保存时校验 JSON,测试通过后再允许启用场景绑定 | | 场景切换误操作影响生产 | 环境隔离、操作审计、场景测试和明确启用开关 | | 服务商连接成功但没有输出文本 | 以 `AI_STREAM_NO_DELTA` 判定失败,禁止作为用户场景验收通过 | | 长文本流式输出拖垮页面或日志 | 设置 `max_output_chars`,日志保存预览和统计,不保存无限完整流 | | 用户端断连导致后台继续消耗服务商资源 | 检测 SSE/WebSocket 断开并取消上游请求,记录 `AI_STREAM_CLIENT_DISCONNECTED` | ## 验收标准 1. 管理员可以在后台创建 Dify 和 Coze 服务商。 2. 管理员可以创建 Dify `/chat-messages` endpoint 和 Coze workflow endpoint。 3. 管理员可以在场景绑定页将 `script_generate`、`short_story_generate`、`chat` 绑定到任意启用 endpoint。 4. 后台场景测试可以展示原始响应和解析结果。 5. 业务调用只依赖 `sceneCode`,不再需要知道当前场景使用 Dify 还是 Coze。 6. 修改场景绑定后,下一次业务调用立即走新配置。 7. 调用日志能区分服务商、接口配置、场景、成功/失败和 fallback。 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、有调用日志、有用户端可见输出”,不能出现接口成功但页面无输出的情况。 14. 后台流式测试必须校验首包耗时、chunk 数、最终状态和 traceId 日志入口。 15. 用户端断连、服务商断流、无 delta、首包超时等异常都必须结束 loading,并能在调用日志中定位。