# 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 配置列表、编辑、测试、测试后保存。
- `server/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，并能在调用日志中定位。
