فهرست منبع

feat: 优化AI控制器和音频服务,添加开发计划文档

MyFramework User 5 ماه پیش
والد
کامیت
59a781cd79

+ 93 - 2
client/src/pages/ai/index.vue

@@ -15,6 +15,29 @@
 
     <!-- 主内容区 -->
     <view class="main-content">
+      <!-- 模型选择 -->
+      <view class="card model-card">
+        <view class="card-header">
+          <text class="card-title">📡 选择模型</text>
+        </view>
+        <view class="model-select">
+          <picker
+            :value="modelIndex"
+            :range="modelList"
+            range-key="name"
+            @change="onModelChange"
+          >
+            <view class="picker-content">
+              <text class="picker-text">{{ modelList[modelIndex]?.name || '请选择模型' }}</text>
+              <text class="picker-arrow">▼</text>
+            </view>
+          </picker>
+        </view>
+        <view class="model-hint">
+          <text>优先使用免费模型:{{ modelList[0]?.name }}</text>
+        </view>
+      </view>
+
       <!-- 输入提示词 -->
       <view class="card input-card">
         <view class="card-header">
@@ -79,7 +102,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { ref, onMounted } from 'vue';
 import { post } from '../../utils/request';
 
 const prompt = ref('');
@@ -87,14 +110,47 @@ const generating = ref(false);
 const generatedText = ref('');
 const historyList = ref<{ prompt: string; text: string }[]>([]);
 
+// 模型列表
+const modelList = ref<{ id: string; name: string }[]>([
+  { id: 'tongyi-xiaomi-analysis-pro', name: '通义小米分析专业版 (免费)' },
+  { id: 'qwen3.5-122b-a10b', name: 'Qwen3.5-122B-A10B (免费)' },
+  { id: 'qwen-plus', name: 'Qwen-Plus (付费)' },
+]);
+const modelIndex = ref(0);
+
+// 页面加载时获取可用模型
+onMounted(async () => {
+  try {
+    const result = await post<{ models: string[]; default: string }>('/ai/models', {});
+    if (result.models) {
+      modelList.value = result.models.map((m: string) => ({
+        id: m,
+        name: m.includes('xiaomi') ? '通义小米分析专业版 (免费)' 
+          : m.includes('122b') ? 'Qwen3.5-122B-A10B (免费)'
+          : 'Qwen-Plus (付费)',
+      }));
+      // 默认选择第一个免费模型
+      modelIndex.value = 0;
+    }
+  } catch (e) {
+    console.error('获取模型列表失败:', e);
+  }
+});
+
+// 模型切换
+function onModelChange(e: any) {
+  modelIndex.value = e.detail.value;
+}
+
 // 生成文本
 async function handleGenerate() {
   if (!prompt.value.trim() || generating.value) return;
 
   generating.value = true;
   try {
-    const result = await post<{ text: string }>('/ai/generate', {
+    const result = await post<{ text: string; model: string }>('/ai/generate', {
       prompt: prompt.value,
+      model: modelList.value[modelIndex.value]?.id,
     });
 
     generatedText.value = result.text;
@@ -213,6 +269,41 @@ function goBack() {
   margin-bottom: 24rpx;
 }
 
+.model-card {
+  margin-bottom: 24rpx;
+}
+
+.model-select {
+  background: #f9fafb;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+
+.picker-content {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.picker-text {
+  font-size: 28rpx;
+  color: #1f2937;
+}
+
+.picker-arrow {
+  font-size: 22rpx;
+  color: #9ca3af;
+}
+
+.model-hint {
+  margin-top: 16rpx;
+}
+
+.model-hint text {
+  font-size: 22rpx;
+  color: #10b981;
+}
+
 .card-title {
   font-size: 32rpx;
   font-weight: 600;

+ 28 - 2
server/src/modules/ai/ai.controller.ts

@@ -5,10 +5,18 @@ import { config } from '../../config';
 
 const router = new Router();
 
+// 免费优先的模型列表
+const FREE_MODELS = [
+  'tongyi-xiaomi-analysis-pro',
+  'qwen3.5-122b-a10b',
+  'qwen-plus',
+];
+
 // AI 生成文本
 router.post('/generate', async (ctx: Context) => {
-  const { prompt } = ctx.request.body as {
+  const { prompt, model } = ctx.request.body as {
     prompt: string;
+    model?: string;
   };
 
   if (!prompt || prompt.trim().length === 0) {
@@ -27,11 +35,16 @@ router.post('/generate', async (ctx: Context) => {
       return;
     }
 
+    // 优先使用指定的模型,否则使用免费的模型列表
+    const selectedModel = model || FREE_MODELS[0];
+    
+    console.log(`🤖 使用模型: ${selectedModel}`);
+
     // 调用阿里云百炼文本生成 API
     const response = await axios.post(
       'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
       {
-        model: 'qwen-plus',
+        model: selectedModel,
         input: {
           prompt: prompt,
         },
@@ -63,6 +76,7 @@ router.post('/generate', async (ctx: Context) => {
       message: 'success',
       data: {
         text: generatedText,
+        model: selectedModel,
       },
     };
   } catch (error: any) {
@@ -75,4 +89,16 @@ router.post('/generate', async (ctx: Context) => {
   }
 });
 
+// 获取可用模型列表
+router.get('/models', async (ctx: Context) => {
+  ctx.body = {
+    code: 0,
+    message: 'success',
+    data: {
+      models: FREE_MODELS,
+      default: FREE_MODELS[0],
+    },
+  };
+});
+
 export default router;

+ 24 - 1
server/src/modules/audio/audio.controller.ts

@@ -9,11 +9,12 @@ const router = new Router();
 // 获取音频列表
 router.get('/list', optionalAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId;
-  const { page = 1, pageSize = 10, isFavorite, keyword } = ctx.query as {
+  const { page = 1, pageSize = 10, isFavorite, keyword, category } = ctx.query as {
     page?: string;
     pageSize?: string;
     isFavorite?: string;
     keyword?: string;
+    category?: string;
   };
 
   const result = await AudioService.getAudioList(userId || '', {
@@ -21,6 +22,28 @@ router.get('/list', optionalAuth, async (ctx: Context) => {
     pageSize: Number(pageSize) || 10,
     isFavorite: isFavorite === 'true' ? true : isFavorite === 'false' ? false : undefined,
     keyword,
+    category,
+  });
+
+  ctx.body = {
+    code: 0,
+    message: 'success',
+    data: result,
+  };
+});
+
+// 获取收藏列表
+router.get('/favorites', optionalAuth, async (ctx: Context) => {
+  const userId = ctx.state.user?.userId;
+  const { page = 1, pageSize = 20 } = ctx.query as {
+    page?: string;
+    pageSize?: string;
+  };
+
+  const result = await AudioService.getAudioList(userId || '', {
+    page: Number(page) || 1,
+    pageSize: Number(pageSize) || 20,
+    isFavorite: true,
   });
 
   ctx.body = {

+ 39 - 1
server/src/modules/audio/audio.service.ts

@@ -1,6 +1,7 @@
 import { Audio, AudioDocument } from '../../models/Audio';
 import { User } from '../../models/User';
 import { PaginationResult } from '../../types';
+import mongoose from 'mongoose';
 
 // 获取用户音频列表
 export async function getAudioList(
@@ -10,9 +11,21 @@ export async function getAudioList(
     pageSize?: number;
     isFavorite?: boolean;
     keyword?: string;
+    category?: string;
   }
 ): Promise<PaginationResult<AudioDocument>> {
-  const { page = 1, pageSize = 10, isFavorite, keyword } = options;
+  const { page = 1, pageSize = 10, isFavorite, keyword, category } = options;
+
+  // 验证 userId 是否为有效的 ObjectId
+  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+    return {
+      list: [],
+      total: 0,
+      page,
+      pageSize,
+      totalPages: 0,
+    };
+  }
 
   const query: Record<string, unknown> = { userId };
 
@@ -27,6 +40,10 @@ export async function getAudioList(
     ];
   }
 
+  if (category) {
+    query.category = category;
+  }
+
   const total = await Audio.countDocuments(query);
   const totalPages = Math.ceil(total / pageSize);
 
@@ -46,17 +63,29 @@ export async function getAudioList(
 
 // 获取单个音频
 export async function getAudioById(audioId: string, userId: string): Promise<AudioDocument | null> {
+  // 验证 userId 是否为有效的 ObjectId
+  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+    return null;
+  }
   return Audio.findOne({ _id: audioId, userId });
 }
 
 // 删除音频
 export async function deleteAudio(audioId: string, userId: string): Promise<boolean> {
+  // 验证 userId 是否为有效的 ObjectId
+  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+    return false;
+  }
   const result = await Audio.findOneAndDelete({ _id: audioId, userId });
   return !!result;
 }
 
 // 切换收藏状态
 export async function toggleFavorite(audioId: string, userId: string): Promise<AudioDocument | null> {
+  // 验证 userId 是否为有效的 ObjectId
+  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+    return null;
+  }
   const audio = await Audio.findOne({ _id: audioId, userId });
   if (!audio) return null;
 
@@ -71,6 +100,10 @@ export async function updateAudio(
   userId: string,
   data: { title?: string; tags?: string[] }
 ): Promise<AudioDocument | null> {
+  // 验证 userId 是否为有效的 ObjectId
+  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+    return null;
+  }
   const audio = await Audio.findOne({ _id: audioId, userId });
   if (!audio) return null;
 
@@ -88,6 +121,11 @@ export async function getUserStats(userId: string): Promise<{
   totalWords: number;
   favoriteCount: number;
 }> {
+  // 验证 userId 是否为有效的 ObjectId
+  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+    return { totalAudios: 0, totalDuration: 0, totalWords: 0, favoriteCount: 0 };
+  }
+
   const stats = await Audio.aggregate([
     { $match: { userId: userId } },
     {

+ 103 - 0
开发计划.md

@@ -452,6 +452,109 @@ c:/Users/caoyg/test/audio/
 - 音频编辑功能
 - 导出多种格式
 
+---
+
+## 第二期开发计划(体验增强)
+
+基于用户反馈和产品优化,新增以下功能:
+
+### 一、搜索与发现
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 🔍 全局搜索 | P1 | 搜索有声书标题、历史记录 |
+| 🏷️ 内容分类 | P2 | 按类型筛选(故事/小说/新闻/学习) |
+
+### 二、播放体验优化
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| ⏰ 定时关闭 | P1 | 睡眠定时、15/30/60分钟自动停止 |
+| 🎚️ 播放速度 | P1 | 0.5x-2x速听,记忆用户偏好 |
+| 🔊 音质选择 | P2 | 高/标准音质切换 |
+| 🎵 AI音色库 | P2 | 多种AI主播声音可选 |
+
+### 三、数据同步
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 📊 播放记录 | P1 | 记录播放进度,支持续播 |
+| ⭐ 收藏夹 | P1 | 收藏喜欢的音频 |
+| 💾 离线缓存 | P2 | 下载到本地离线听 |
+
+### 四、社交与分享
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 📤 分享功能 | P1 | 分享给微信好友/朋友圈 |
+| 💬 评论打分 | P2 | 对内容评论评分 |
+| 🔔 消息通知 | P3 | 新内容推送、会员优惠 |
+
+### 五、UI/UX优化
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 🌙 夜间模式 | P2 | 护眼深色主题 |
+| 🎨 个性化主题 | P3 | 自定义主题色 |
+| 📱 首页改版 | P2 | 更直观的文本输入体验 |
+
+### 六、技术优化
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| ⚡ 加载优化 | P1 | 骨架屏、缓存优化 |
+| 📡 断点续传 | P2 | 大文件下载断点续传 |
+| 🔄 数据同步 | P2 | 多端数据实时同步 |
+
+---
+
+### 第二期页面规划
+
+新增页面:
+
+```
+pages/
+├── search/          # 搜索页面
+│   └── index.vue
+├── favorites/        # 收藏页面
+│   └── index.vue
+└── settings/         # 设置页面
+    └── index.vue
+```
+
+修改页面:
+
+```
+pages/
+├── index/           # 首页增加分类、推荐
+├── player/          # 播放器增加定时、速度、音质
+├── history/        # 增加搜索、收藏入口
+└── mine/           # 增加设置、收藏入口
+```
+
+### 第二期实施顺序
+
+**第一阶段(P1 - 核心体验)**
+1. 播放记录 + 续播
+2. 收藏功能
+3. 定时关闭
+4. 播放速度记忆
+5. 分享功能
+
+**第二阶段(P2 - 丰富功能)**
+6. 全局搜索
+7. 内容分类
+8. 夜间模式
+9. 音质选择
+10. 离线缓存
+
+**第三阶段(P3 - 完善优化)**
+11. 评论打分
+12. 消息通知
+13. 个性化主题
+
+---
+
 ## Agent Extensions
 
 ### Skill