feat: AI 场景路由、ASR 服务及前后端全链路同步

- 新增 AI 场景路由控制器和管理接口
- 新增 ASR 语音识别服务及前后端集成
- 同步 AI Runtime 客户端到 Web/小程序/Life-Script
- 完善 AI 配置测试修复和管理后台路由配置
- 新增数据库迁移脚本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 13:25:21 +08:00
parent d77090aa5e
commit 89fc42819d
72 changed files with 4584 additions and 383 deletions
@@ -0,0 +1,64 @@
# Mini Program ASR Service Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Deploy a private Chinese ASR service on `101.200.208.45` and wire the mini program voice orb to transcribe speech into the wish input.
**Architecture:** A local FastAPI ASR service listens on `127.0.0.1:19120` and uses FunASR/SenseVoiceSmall when available. The Java backend exposes `/api/asr/transcribe` as the authenticated upload proxy, and the mini program records audio with `uni.getRecorderManager()` then uploads it through the existing request service.
**Tech Stack:** FunASR/SenseVoiceSmall, Python 3.11, FastAPI, Spring Boot multipart upload, uni-app recorder/upload APIs.
---
### Task 1: ASR Service
**Files:**
- Create: `backend-single/asr-service/app.py`
- Create: `backend-single/asr-service/requirements.txt`
- Create: `backend-single/asr-service/emotion-museum-asr.service`
- Create: `backend-single/asr-service/README.md`
- [x] Add a FastAPI service with `/health` and `/transcribe`.
- [x] Accept an uploaded audio file, save it under `/tmp/emotion-museum-asr`, run the ASR model, and return JSON with `success`, `text`, `language`, `durationMs`, `engine`, and `errorMessage`.
- [x] Keep the service bound to `127.0.0.1:19120`.
### Task 2: Java Backend Proxy
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/response/asr/AsrTranscribeResponse.java`
- Create: `backend-single/src/main/java/com/emotion/service/AsrService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/AsrServiceImpl.java`
- Create: `backend-single/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `backend-single/src/main/resources/application-prod.yml`
- [x] Add `emotion.asr` config for `enabled`, `engine-url`, `max-file-size`, and `allowed-types`.
- [x] Validate upload presence, size, and suffix.
- [x] Forward multipart audio to the local ASR service.
- [x] Return a normal `Result<AsrTranscribeResponse>` to the mini program.
### Task 3: Mini Program Recording
**Files:**
- Create: `mini-program/src/services/asr.js`
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- [x] Use `uni.getRecorderManager()` to start recording on press.
- [x] Stop recording on release/cancel.
- [x] Upload the recorded temp file to `/asr/transcribe`.
- [x] Fill `wishText` with recognized text and keep the current visual theme.
- [x] Track success/failure analytics.
### Task 4: Deploy And Verify
**Commands:**
- `npm run build:mp-weixin`
- `mvn -DskipTests package`
- Deploy backend jar to `101.200.208.45`.
- Install/start `emotion-museum-asr`.
- Check `curl http://127.0.0.1:19120/health`.
**Acceptance:**
- Pressing and releasing the mini program voice orb sends audio to backend ASR.
- Recognized Chinese text appears in the wish input.
- Server ASR and TTS services both remain active.
@@ -0,0 +1,226 @@
# AI 配置管理接口测试修复实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 删除废弃的旧 AI 配置页面,在新系统测试对话框中增加非流式测试功能
**Architecture:** 路由已正确指向新系统 `AiRoutingList.vue`,只需删除旧文件残留并增强测试对话框。后端接口 `testAiRuntime` 已存在,只需在 API 层和 UI 层连接。
**Tech Stack:** Vue 3, Element Plus, TypeScript
---
### 前置确认:检查生产数据库配置
- [ ] 在生产数据库上执行以下查询,确认 Dify provider 和 endpoint 状态
```sql
-- 检查 Dify provider
SELECT id, provider_code, provider_name, provider_type, base_url, is_enabled
FROM t_ai_provider WHERE provider_code = 'dify_default';
-- 检查 Dify endpoints
SELECT id, endpoint_code, endpoint_name, provider_id, api_path, support_stream, is_enabled
FROM t_ai_endpoint_config WHERE endpoint_code LIKE 'dify.%';
-- 检查场景绑定
SELECT id, scene_code, scene_name, endpoint_id, is_enabled, required_stream
FROM t_ai_scene_binding WHERE is_enabled = 1;
```
- [ ] 如果 Dify provider `is_enabled = 0`,执行:
```sql
UPDATE t_ai_provider SET is_enabled = 1 WHERE provider_code = 'dify_default';
```
- [ ] 如果 Dify endpoints `is_enabled = 0`,执行:
```sql
UPDATE t_ai_endpoint_config SET is_enabled = 1 WHERE endpoint_code LIKE 'dify.%';
```
### Task 1: 删除废弃的旧 AI 配置页面
**Files:**
- Delete: `web-admin/src/views/aiconfig/AiConfigList.vue`
- [ ] 删除旧页面文件
```bash
rm web-admin/src/views/aiconfig/AiConfigList.vue
```
### Task 2: 在 API 层导出非流式测试函数
**Files:**
- Modify: `web-admin/src/api/aiconfig.ts`
- [ ] 确认 `testAiRuntime` 已存在(已确认在第 263 行)
```typescript
export function testAiRuntime(data: AiRuntimeRequest) {
return request({ url: '/ai/runtime/test', method: 'post', data })
}
```
已存在,无需修改。但需要确认 `AiRoutingList.vue` 中是否 import 了这个函数。
检查 `web-admin/src/views/aiconfig/AiRoutingList.vue` 的 import 语句(当前第 269-281 行):
- 当前 import 了 `streamAiRuntime`**没有 import** `testAiRuntime`
- 需要在 import 列表中添加 `testAiRuntime`
### Task 3: 在测试对话框中增加非流式测试按钮
**Files:**
- Modify: `web-admin/src/views/aiconfig/AiRoutingList.vue`
- [ ] 在 import 中添加 `testAiRuntime`
修改第 269-281 行的 import 语句:
```typescript
import {
deleteAiEndpoint,
deleteAiProvider,
deleteAiScene,
listAiCallLogs,
listAiEndpoints,
listAiProviders,
listAiScenes,
saveAiEndpoint,
saveAiProvider,
saveAiScene,
streamAiRuntime,
testAiRuntime // ← 新增
} from '@/api/aiconfig'
```
- [ ] 在测试对话框 footer 中增加「非流式测试」按钮
找到第 257-261 行的 `<template #footer>`,修改为:
```vue
<template #footer>
<el-button @click="testDialog = false">关闭</el-button>
<el-button :loading="testing" @click="submitNonStreamTest">非流式测试</el-button>
<el-button type="primary" :loading="testing" @click="submitRuntimeTest">流式测试</el-button>
</template>
```
- [ ] 添加 `nonStreamResult` 响应式变量
`testResult` 声明后面(第 296 行)添加:
```typescript
const nonStreamResult = ref<AiRuntimeTestResponse | null>(null)
```
- [ ] 实现 `submitNonStreamTest` 函数
`submitRuntimeTest` 函数(第 451 行)之前添加:
```typescript
async function submitNonStreamTest() {
let inputs: Record<string, any>
try {
inputs = JSON.parse(testInputsJson.value || '{}')
} catch (error) {
ElMessage.error('入参 JSON 格式不正确')
return
}
testing.value = true
try {
const res = await testAiRuntime({ sceneCode: testForm.sceneCode, inputs })
nonStreamResult.value = res.data as AiRuntimeTestResponse
if (nonStreamResult.value.status === 'success') {
ElMessage.success('非流式测试成功')
} else {
ElMessage.error(`测试失败: ${nonStreamResult.value.errorMessage || nonStreamResult.value.errorCode}`)
}
await loadAll()
} catch (error: any) {
nonStreamResult.value = {
sceneCode: testForm.sceneCode,
status: 'failed',
errorMessage: error?.message || '非流式测试失败'
} as AiRuntimeTestResponse
ElMessage.error(error?.message || '非流式测试失败')
} finally {
testing.value = false
}
}
```
- [ ] 在测试对话框中展示非流式测试结果
`submitRuntimeTest` 相关的 `<el-alert>``<pre>` 展示块(第 250-256 行)之前,添加非流式测试结果的展示:
```vue
<el-alert
v-if="nonStreamResult"
:type="nonStreamResult.status === 'success' ? 'success' : 'error'"
:title="nonStreamResult.status === 'success' ? '非流式测试成功' : '非流式测试失败'"
show-icon
:closable="false"
style="margin-bottom: 12px;"
/>
<pre v-if="nonStreamResult" class="test-output">{{ nonStreamResult.output || nonStreamResult.errorMessage || '暂无输出' }}</pre>
<el-alert
v-if="testResult"
:type="testResult.status === 'success' ? 'success' : 'error'"
:title="testResult.status === 'success' ? '流式测试成功' : '流式测试失败'"
show-icon
/>
<pre v-if="testResult" class="test-output">{{ testResult.output || testResult.errorMessage || '暂无输出' }}</pre>
```
- [ ] 在打开测试对话框时清空非流式结果
`openRuntimeTest` 函数(第 400-404 行)中添加:
```typescript
function openRuntimeTest() {
testForm.sceneCode = scenes.value.find(item => item.isEnabled === 1 && item.endpointId)?.sceneCode || scenes.value[0]?.sceneCode || ''
testResult.value = null
nonStreamResult.value = null // ← 新增
testDialog.value = true
}
```
- [ ] 在 TypeScript 类型定义中确保 `AiRuntimeTestResponse` 包含所有必要字段
检查 `web-admin/src/types/aiconfig.ts` 中的 `AiRuntimeTestResponse` 定义,确保包含:
```typescript
export interface AiRuntimeTestResponse {
sceneCode: string
status: 'success' | 'failed'
output?: string
streamChunks?: number
durationMs?: number
errorCode?: string
errorMessage?: string
}
```
### Task 4: 浏览器验证
- [ ] 启动 web-admin 开发服务器
```bash
cd web-admin && npm run dev
```
- [ ] 在浏览器中访问管理后台 `http://localhost:5174/emotion-museum-admin/`
- [ ] 导航到「AI 配置管理」页面,确认旧页面已删除、新页面正常显示
- [ ] 在「场景绑定」Tab 中点击「流式测试」按钮,确认测试对话框正常打开
- [ ] 确认测试对话框中有两个按钮:「非流式测试」和「流式测试」
- [ ] 选择一个场景(如 `script_generate`),输入入参 `{ "prompt": "请用一句中文回复测试成功。" }`,点击「非流式测试」,确认返回成功结果
- [ ] 点击「流式测试」,确认流式输出正常
- [ ] 确认浏览器 Console 中没有任何错误
@@ -0,0 +1,58 @@
# AI Routing Admin Runtime Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the AI configuration center match the existing admin theme, use Chinese copy everywhere, show existing provider/workflow bindings from the database, and expose a single user-facing streaming runtime entry point driven by scene code plus JSON inputs.
**Architecture:** Keep the existing provider/endpoint/scene/log model. The admin page uses the current Element Plus dark glass theme and existing list-page conventions. Runtime calls accept `sceneCode` and `inputs` JSON, enrich inputs with the logged-in user context on the backend, resolve the scene binding from the database, then stream provider output back to the user.
**Tech Stack:** Spring Boot, MyBatis-Plus, FastJSON, Vue 3, Element Plus, uni-app chunked streaming.
---
### Task 1: Admin Page Theme And Chinese Copy
**Files:**
- Modify: `web-admin/src/views/aiconfig/AiRoutingList.vue`
- [x] Replace English page title, tabs, table columns, dialogs, buttons, alerts, empty states, and confirm text with Chinese.
- [x] Replace the standalone white panel styles with the project page-header, glass card, table card, and dark Element Plus table styles.
- [x] Add summary cards for providers, endpoints, scenes, and logs so the page matches existing management pages.
### Task 2: User Runtime Request Shape
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- [x] Accept user calls as `sceneCode` plus `inputs` JSON.
- [x] Keep user identity server-side by reading `UserContextHolder`.
- [x] Enrich runtime inputs with safe user context keys before provider invocation.
- [x] Keep SSE stream events as the only formal output path for user-facing AI calls.
### Task 3: Seed Existing Configurations Into Routing Tables
**Files:**
- Modify: `sql/2026-05-22-ai-scene-routing.sql`
- [x] Insert a Coze provider from existing `t_ai_config` rows when missing.
- [x] Insert endpoint rows from current enabled Coze workflow configurations.
- [x] Bind existing scenes to known workflow configs where a safe mapping exists.
- [x] Keep unknown scenes visible but disabled until explicitly bound in the admin page.
### Task 4: Verify End To End
**Commands:**
- `mvn test`
- `mvn -DskipTests clean package`
- `npm run build`
- `npm run build:mp-weixin`
- Remote SQL migration execution.
- Remote health checks for backend and ASR.
- [x] Backend tests pass.
- [x] Backend package succeeds.
- [x] Admin build succeeds.
- [x] Mini program build succeeds.
- [x] Remote routing tables contain provider, endpoints, and scene bindings.
@@ -0,0 +1,66 @@
# AI Runtime Client Sync Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Synchronize AI calls in web, web-admin, life-script, and mini-program so user-facing AI output uses the backend scene routing runtime and streams text progressively.
**Architecture:** The backend remains the single trust boundary for provider credentials, scene bindings, user context enrichment, and workflow selection. Browser and mini-program clients call `/ai/runtime/stream` with `sceneCode` and `inputs`, while existing WebSocket chat is bridged through `AiRuntimeService` so chat also follows the configured `chat` scene. Existing business save APIs remain responsible for persistence after a stream completes.
**Tech Stack:** Spring Boot, STOMP WebSocket, FastJSON, Vue 3, React/Vite, uni-app chunked request, Element Plus.
---
### Task 1: Backend Chat Bridge To Runtime
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java`
- [x] Route chat generation through `AiRuntimeService` with `sceneCode=chat`.
- [x] Push `start`, `delta`, `done`, and `error` events over the existing user WebSocket topic.
- [x] Save the final assistant message after `done` using the accumulated stream output.
- [x] Preserve the existing user-message persistence and conversation ID behavior.
### Task 2: Shared Runtime Clients
**Files:**
- Create: `web/src/services/aiRuntime.ts`
- Create: `life-script/src/services/aiRuntime.js`
- Modify: `mini-program/src/services/aiRuntime.js`
- Modify: `web-admin/src/api/aiconfig.ts`
- Modify: `web-admin/src/views/aiconfig/AiRoutingList.vue`
- [x] Add browser SSE client helpers that parse unified AI stream events.
- [x] Keep mini-program chunk parsing but localize error messages and flush trailing frames.
- [x] Add a web-admin stream test helper and make the test dialog render output progressively.
### Task 3: Client Scene Migration
**Files:**
- Modify: `life-script/src/services/ai.js`
- Modify: `life-script/src/views/ScriptView.jsx`
- Modify: `life-script/src/views/TimelineView.jsx`
- Modify: `life-script/src/views/PathView.jsx`
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- Inspect: `web/src/stores/chat.ts`
- [x] Remove direct OpenRouter calls and hard-coded client API keys.
- [x] Route `script_generate`, `life_healing`, and path generation through `streamAiScene`.
- [x] Ensure pages append `delta` content instead of waiting for complete text.
- [x] Keep final persistence through existing business save APIs after stream completion.
### Task 4: Verification
**Commands:**
- `mvn test`
- `mvn -DskipTests clean package`
- `npm run build` in `web`
- `npm run build` in `web-admin`
- `npm run build` in `life-script`
- `npm run build:mp-weixin` in `mini-program`
- [x] Backend tests pass.
- [x] Backend package succeeds.
- [x] All four frontend builds succeed.
- [x] Source scan shows no client-side external AI provider key or direct Dify/Coze/OpenRouter call.
- [x] Database scene bindings are enabled with streaming endpoints for `chat`, `script_generate`, `short_story_generate`, `diary_summary`, `emotion_summary`, `emotion_analysis`, and `life_healing`.