Files
happy-life-star/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md
T

940 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,并能在调用日志中定位。