فهرست منبع

feat: 更新配置和前端界面,优化音频生成和搜索功能

JJSun 4 ماه پیش
والد
کامیت
072456e4bb

+ 1 - 1
Co-creation-projects/JJason-DeepCastAgent/README.md

@@ -106,7 +106,7 @@ cp env.example .env
 
 ```bash
 cd backend
-python src/main.py
+uv run src/main.py
 ```
 
 **启动前端**:

+ 1 - 1
Co-creation-projects/JJason-DeepCastAgent/backend/env.example

@@ -8,7 +8,7 @@ FETCH_FULL_PAGE=True
 LLM_PROVIDER=custom
 LLM_MODEL_ID=ecnu-max
 SMART_LLM_MODEL=ecnu-reasoner
-FAST_LLM_MODEL=ecnu-plus
+FAST_LLM_MODEL=ecnu-max
 LLM_API_KEY=your_ecnu_api_key_here
 LLM_BASE_URL=your_openai_api_key_here
 LLM_TIMEOUT=60

+ 3 - 3
Co-creation-projects/JJason-DeepCastAgent/backend/scripts/verify_ffmpeg.py

@@ -1,5 +1,5 @@
-import os
-import sys
+import os  # noqa: D100
+
 from pydub import AudioSegment
 
 """
@@ -20,7 +20,7 @@ def test_ffmpeg():
     if not os.path.exists(ffmpeg_path):
         print(f"❌ Warning: ffmpeg executable not found at {ffmpeg_path}")
     else:
-        print(f"✅ ffmpeg executable found.")
+        print("✅ ffmpeg executable found.")
     
     try:
         # 创建 1 秒的静音片段

+ 2 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/scripts/verify_search.py

@@ -1,5 +1,6 @@
 import os
 import sys
+
 from dotenv import load_dotenv
 
 # Add src to path
@@ -11,6 +12,7 @@ load_dotenv(os.path.join(os.path.dirname(__file__), '../.env'))
 from config import Configuration
 from services.search import get_global_search_tool
 
+
 def test_search_configuration():
     print("Testing search configuration...")
     

+ 6 - 3
Co-creation-projects/JJason-DeepCastAgent/backend/src/agent.py

@@ -415,14 +415,17 @@ class DeepResearchAgent:
         script_turns = len(script) if script else 0
         yield {"type": "log", "message": f"✓ 脚本生成完成,共 {script_turns} 轮对话"}
         
-        if script_turns == 0:
-            yield {"type": "log", "message": "⚠️ 警告:脚本为空,可能是解析失败,请检查后端日志"}
-        
         yield {
             "type": "podcast_script",
             "script": script,
             "turns": script_turns,
         }
+        
+        # 脚本为空时跳过音频生成
+        if script_turns == 0:
+            yield {"type": "log", "message": "⚠️ 警告:脚本为空,跳过音频生成"}
+            yield {"type": "done"}
+            return
 
         # 检查取消
         if self.is_cancelled():

+ 16 - 20
Co-creation-projects/JJason-DeepCastAgent/backend/src/config.py

@@ -1,7 +1,7 @@
 import os
 from enum import Enum
 from pathlib import Path
-from typing import Any, Optional
+from typing import Any
 
 from pydantic import BaseModel, Field, field_validator
 
@@ -9,10 +9,8 @@ from pydantic import BaseModel, Field, field_validator
 BACKEND_ROOT = Path(__file__).resolve().parent.parent
 
 class SearchAPI(Enum):
-    """可用的搜索 API 提供商。"""
+    """搜索 API 提供商(仅支持混合搜索:Tavily + SerpApi)。"""
     HYBRID = "hybrid"
-    TAVILY = "tavily"
-    SERPAPI = "serpapi"
 
 
 class Configuration(BaseModel):
@@ -31,7 +29,7 @@ class Configuration(BaseModel):
     search_api: SearchAPI = Field(
         default=SearchAPI.HYBRID,
         title="搜索 API",
-        description="使用的网络搜索 API (hybrid, tavily, serpapi)",
+        description="使用混合搜索引擎 (Tavily + SerpApi)",
     )
     enable_notes: bool = Field(
         default=True,
@@ -58,32 +56,32 @@ class Configuration(BaseModel):
         title="使用工具调用",
         description="使用工具调用而非 JSON 模式进行结构化输出",
     )
-    llm_api_key: Optional[str] = Field(
+    llm_api_key: str | None = Field(
         default=None,
         title="LLM API 密钥",
         description="使用自定义 OpenAI 兼容服务时的可选 API 密钥",
     )
-    llm_base_url: Optional[str] = Field(
+    llm_base_url: str | None = Field(
         default=None,
         title="LLM 基础 URL",
         description="使用自定义 OpenAI 兼容服务时的可选基础 URL",
     )
-    llm_model_id: Optional[str] = Field(
+    llm_model_id: str | None = Field(
         default=None,
         title="LLM 模型 ID",
         description="自定义 OpenAI 兼容服务的可选模型标识符",
     )
-    smart_llm_model: Optional[str] = Field(
+    smart_llm_model: str | None = Field(
         default="ecnu-reasoner",
         title="Smart LLM Model",
-        description="Model ID for complex reasoning tasks (e.g. Planning, Reporting)",
+        description="复杂推理任务使用的模型 ID (e.g. Planning, Reporting)",
     )
-    fast_llm_model: Optional[str] = Field(
+    fast_llm_model: str | None = Field(
         default="ecnu-max",
         title="Fast LLM Model",
-        description="Model ID for simple/fast tasks (e.g. Summarization)",
+        description="快速响应任务使用的模型 ID (e.g. Web Research, Script Generation)",
     )
-    tts_api_key: Optional[str] = Field(
+    tts_api_key: str | None = Field(
         default=None,
         title="TTS API 密钥",
         description="TTS 服务的 API 密钥",
@@ -103,17 +101,17 @@ class Configuration(BaseModel):
         title="音频输出目录",
         description="保存生成的音频文件的目录",
     )
-    ffmpeg_path: Optional[str] = Field(
+    ffmpeg_path: str | None = Field(
         default=None,
         title="FFmpeg 路径",
         description="ffmpeg 可执行文件的路径",
     )
-    tavily_api_key: Optional[str] = Field(
+    tavily_api_key: str | None = Field(
         default=None,
         title="Tavily API 密钥",
         description="Tavily 搜索的 API 密钥",
     )
-    serpapi_api_key: Optional[str] = Field(
+    serpapi_api_key: str | None = Field(
         default=None,
         title="SerpApi 密钥",
         description="SerpApi 的 API 密钥",
@@ -131,7 +129,7 @@ class Configuration(BaseModel):
         return v
 
     @classmethod
-    def from_env(cls, overrides: Optional[dict[str, Any]] = None) -> "Configuration":
+    def from_env(cls, overrides: dict[str, Any] | None = None) -> "Configuration":
         """
         使用环境变量和覆盖项创建配置对象。
         
@@ -141,7 +139,6 @@ class Configuration(BaseModel):
         Returns:
             初始化的配置对象。
         """
-
         raw_values: dict[str, Any] = {}
 
         # 基于字段名从环境变量加载值
@@ -192,7 +189,6 @@ class Configuration(BaseModel):
 
         return cls(**raw_values)
 
-    def resolved_model(self) -> Optional[str]:
+    def resolved_model(self) -> str | None:
         """尽力解析要使用的模型标识符。"""
-
         return self.llm_model_id

+ 3 - 12
Co-creation-projects/JJason-DeepCastAgent/backend/src/main.py

@@ -19,7 +19,7 @@ from loguru import logger
 from pydantic import BaseModel, Field
 
 from agent import DeepResearchAgent
-from config import Configuration, SearchAPI
+from config import Configuration
 
 # 添加控制台日志处理程序
 logger.add(
@@ -43,10 +43,6 @@ class ResearchRequest(BaseModel):
     """触发研究运行的负载。"""
 
     topic: str = Field(..., description="用户提供的研究主题")
-    search_api: SearchAPI | None = Field(
-        default=None,
-        description="覆盖通过环境变量配置的默认搜索后端",
-    )
 
 class PodcastScript(BaseModel):
     """播客脚本内容模型。"""
@@ -81,12 +77,7 @@ def _mask_secret(value: str | None, visible: int = 4) -> str:
 
 
 def _build_config(payload: ResearchRequest) -> Configuration:
-    overrides: dict[str, Any] = {}
-
-    if payload.search_api is not None:
-        overrides["search_api"] = payload.search_api
-
-    return Configuration.from_env(overrides=overrides)
+    return Configuration.from_env()
 
 
 def create_app() -> FastAPI:
@@ -121,7 +112,7 @@ def create_app() -> FastAPI:
             config.llm_provider,
             config.resolved_model() or "unset",
             config.llm_base_url or "unset",
-            (config.search_api.value if isinstance(config.search_api, SearchAPI) else config.search_api),
+            config.search_api.value,
             config.max_web_research_loops,
             config.fetch_full_page,
             config.use_tool_calling,

+ 122 - 4
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/script_generator.py

@@ -47,8 +47,8 @@ class ScriptGenerationService:
             api_key=config.llm_api_key,
             base_url=config.llm_base_url,
         )
-        # 使用 fast_llm_model(ecnu-plus)进行脚本生成,它支持结构化输出
-        self._model = config.fast_llm_model or "ecnu-plus"
+        # 使用 fast_llm_model(ecnu-max)进行脚本生成,它支持结构化输出
+        self._model = config.fast_llm_model or "ecnu-max"
 
     def generate_script(self, state: SummaryState) -> list[dict[str, str]]:
         """基于结构化报告生成播客脚本(使用结构化输出)。"""
@@ -86,8 +86,11 @@ class ScriptGenerationService:
                 logger.error("Empty response from LLM")
                 return []
             
-            # 直接解析 JSON(结构化输出保证格式正确)
-            script = json.loads(content)
+            # 尝试解析 JSON(处理各种格式问题)
+            script = self._parse_script_json(content)
+            
+            if script is None:
+                return []
             
             if not isinstance(script, list):
                 logger.error("Script output is not a list: %s", type(script))
@@ -115,3 +118,118 @@ class ScriptGenerationService:
         except Exception as e:
             logger.error("Script generation failed: %s", e)
             return []
+
+    def _parse_script_json(self, content: str) -> list | None:
+        """
+        尝试多种方式解析脚本 JSON。
+        
+        Args:
+            content: LLM 返回的原始内容
+            
+        Returns:
+            解析后的列表,失败返回 None
+        """
+        import re
+        
+        # 1. 直接尝试解析
+        try:
+            return json.loads(content)
+        except json.JSONDecodeError as e:
+            logger.debug("Direct JSON parse failed at char %d: %s", e.pos, e.msg)
+        
+        # 2. 尝试从 markdown 代码块中提取
+        json_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', content)
+        if json_match:
+            try:
+                result = json.loads(json_match.group(1).strip())
+                logger.info("Extracted JSON from markdown code block")
+                return result
+            except json.JSONDecodeError:
+                pass
+        
+        # 3. 提取 JSON 数组部分
+        start_idx = content.find('[')
+        end_idx = content.rfind(']')
+        if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
+            json_str = content[start_idx:end_idx + 1]
+            
+            # 3a. 直接尝试
+            try:
+                return json.loads(json_str)
+            except json.JSONDecodeError as e:
+                logger.debug("Array extraction failed at char %d: %s", e.pos, e.msg)
+                # 记录出错位置附近的内容
+                error_start = max(0, e.pos - 50)
+                error_end = min(len(json_str), e.pos + 50)
+                logger.debug("Content around error: ...%s...", json_str[error_start:error_end])
+            
+            # 3b. 尝试修复常见问题
+            fixed_json = self._fix_json_issues(json_str)
+            try:
+                result = json.loads(fixed_json)
+                logger.info("Parsed JSON after fixing common issues")
+                return result
+            except json.JSONDecodeError:
+                pass
+        
+        # 4. 最后尝试:逐个对象解析
+        result = self._parse_objects_individually(content)
+        if result:
+            logger.info("Parsed %d objects individually", len(result))
+            return result
+        
+        logger.error("Could not parse JSON from response. First 500 chars: %s", content[:500])
+        return None
+    
+    def _fix_json_issues(self, json_str: str) -> str:
+        """尝试修复常见的 JSON 格式问题。"""
+        import re
+        
+        fixed = json_str
+        
+        # 替换中文引号为英文引号
+        fixed = fixed.replace('"', '"').replace('"', '"')
+        fixed = fixed.replace(''', "'").replace(''', "'")
+        
+        # 移除可能的 BOM 或其他不可见字符
+        fixed = fixed.strip('\ufeff\u200b\u200c\u200d')
+        
+        # 修复未转义的换行符(在字符串值内)
+        # 这是一个简化的修复,可能不完美
+        def escape_newlines_in_strings(match):
+            return match.group(0).replace('\n', '\\n').replace('\r', '\\r')
+        
+        # 匹配 JSON 字符串值
+        fixed = re.sub(r'"[^"]*"', escape_newlines_in_strings, fixed)
+        
+        return fixed
+    
+    def _parse_objects_individually(self, content: str) -> list | None:
+        """
+        尝试逐个解析 JSON 对象。
+        
+        当整体解析失败时,尝试提取每个 {role, content} 对象。
+        """
+        import re
+        
+        results = []
+        
+        # 匹配 {"role": "...", "content": "..."} 模式
+        # 使用非贪婪匹配
+        pattern = r'\{\s*"role"\s*:\s*"(Host|Guest)"\s*,\s*"content"\s*:\s*"((?:[^"\\]|\\.)*)"\s*\}'
+        
+        for match in re.finditer(pattern, content, re.DOTALL):
+            role = match.group(1)
+            # 处理转义字符
+            content_text = match.group(2)
+            try:
+                # 使用 json.loads 来正确处理转义
+                content_text = json.loads(f'"{content_text}"')
+            except Exception:
+                pass
+            results.append({"role": role, "content": content_text})
+        
+        if results:
+            return results
+        
+        return None

+ 0 - 2
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/search.py

@@ -48,7 +48,6 @@ def dispatch_search(
     Returns:
         元组 (原始负载, 通知列表, 答案文本, 后端标签)。
     """
-
     search_api = get_config_value(config.search_api)
     search_tool = get_global_search_tool(config)
 
@@ -116,7 +115,6 @@ def prepare_research_context(
     Returns:
         元组 (来源摘要列表, 详细上下文文本)。
     """
-
     sources_summary = format_sources(search_result)
     context = deduplicate_and_format_sources(
         search_result or {"results": []},

+ 1 - 4
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/summarizer.py

@@ -54,10 +54,7 @@ class SummarizationService:
         agent = self._agent_factory()
 
         def flush_visible() -> Iterator[str]:
-            """
-            处理缓冲区,提取并 yield 所有不在 <think>...</think> 块中的可见文本。
-            如果遇到不完整的 <think> 标签,会暂停输出等待更多数据。
-            """  # noqa: D205
+            """处理缓冲区,提取并 yield 所有不在 <think>...</think> 块中的可见文本。如果遇到不完整的 <think> 标签,会暂停输出等待更多数据。"""
             nonlocal emit_index, raw_buffer
             while True:
                 start = raw_buffer.find("<think>", emit_index)

+ 85 - 93
Co-creation-projects/JJason-DeepCastAgent/frontend/src/App.vue

@@ -24,16 +24,9 @@
           </div>
 
           <div class="settings-row">
-            <div class="setting-item">
-              <label>搜索引擎</label>
-              <div class="select-wrapper">
-                <select v-model="form.searchApi">
-                  <option value="hybrid">混合搜索 (Tavily + SerpApi)</option>
-                  <option value="tavily">仅 Tavily</option>
-                  <option value="serpapi">仅 SerpApi</option>
-                </select>
-                <span class="select-arrow">▼</span>
-              </div>
+            <div class="setting-item search-hint">
+              <span class="hint-icon">🔍</span>
+              <span class="hint-text">使用混合搜索引擎 (Tavily + SerpApi)</span>
             </div>
           </div>
 
@@ -49,20 +42,18 @@
         <div class="production-content">
           <!-- 顶部:标题和控制 -->
           <header class="production-header">
-            <div class="header-left">
-              <h2>正在制作您的播客</h2>
-              <p class="production-topic">「{{ form.topic }}」</p>
-            </div>
-            <button class="cancel-btn" @click="cancelProduction">取消</button>
+            <h2>{{ podcastReady ? '🎉 播客制作完成!' : '正在制作您的播客' }}</h2>
+            <button v-if="!podcastReady" class="cancel-btn" @click="cancelProduction">取消</button>
           </header>
+          <p class="production-topic">「{{ form.topic }}」</p>
 
           <!-- 阶段进度指示器 -->
-          <div class="stage-monitor">
+          <div class="stage-monitor" v-if="!podcastReady">
             <div class="stage-step" :class="{ active: productionStage === 'research', completed: isStageCompleted('research') }">
               <div class="step-icon">🔍</div>
               <div class="step-label">深度研究</div>
-              <div class="step-progress" v-if="productionStage === 'research' && totalTasks > 0">
-                {{ completedTasks }}/{{ totalTasks }}
+              <div class="step-progress" v-if="productionStage === 'research' && (todoList.length > 0 || totalTasks > 0)">
+                {{ completedTasks }}/{{ todoList.length || totalTasks }}
               </div>
             </div>
             <div class="stage-line" :class="{ active: isStageCompleted('research') }"></div>
@@ -81,7 +72,7 @@
           </div>
 
           <!-- 当前执行状态卡片 -->
-          <div class="current-status-card" v-if="currentStatusMessage">
+          <div class="current-status-card" v-if="currentStatusMessage && !podcastReady">
             <div class="status-indicator"></div>
             <span class="status-text">{{ currentStatusMessage }}</span>
           </div>
@@ -226,8 +217,7 @@ interface PodcastMessage {
 const currentView = ref<ViewState>("setup");
 const productionStage = ref<ProductionStage>("research");
 const form = reactive({
-  topic: "",
-  searchApi: "hybrid"
+  topic: ""
 });
 
 const logs = ref<LogEntry[]>([]);
@@ -374,7 +364,7 @@ async function startProduction() {
 
   try {
     await runResearchStream(
-      { topic: form.topic, search_api: form.searchApi },
+      { topic: form.topic },
       handleStreamEvent,
       { signal: abortController.signal }
     );
@@ -396,6 +386,13 @@ function handleStreamEvent(event: ResearchStreamEvent) {
   if (event.type === "log") {
     const msg = String((event as any).message || "");
     addLog(`INFO: ${msg}`);
+    
+    // 解析 TTS 成功日志来更新进度 (格式: [TTS 6/13] ✓ Host 语音生成成功)
+    const ttsMatch = msg.match(/\[TTS (\d+)\/(\d+)\] ✓/);
+    if (ttsMatch) {
+      audioProgress.current = parseInt(ttsMatch[1], 10);
+      audioProgress.total = parseInt(ttsMatch[2], 10);
+    }
     return;
   }
 
@@ -497,24 +494,47 @@ function handleStreamEvent(event: ResearchStreamEvent) {
   // 4. Research Updates
   if (event.type === "task_status") {
     const payload = event as any;
+    const taskId = payload.task_id;
+    const status = payload.status;
+    const title = payload.title || "";
+    const query = payload.query || "";
+    
+    // 如果 todoList 为空但收到了 task_status,动态添加任务
+    if (todoList.value.length === 0 || !todoList.value.find(t => t.id === taskId)) {
+      todoList.value.push({
+        id: taskId,
+        title: title,
+        status: status,
+        query: query,
+        intent: payload.intent || "",
+      });
+      // 更新总任务数(基于已知的最大 task_id)
+      if (taskId > totalTasks.value) {
+        totalTasks.value = taskId;
+      }
+    }
+    
     // 更新内部状态
-    const taskIndex = todoList.value.findIndex(t => t.id === payload.task_id);
+    const taskIndex = todoList.value.findIndex(t => t.id === taskId);
     if (taskIndex !== -1) {
-      todoList.value[taskIndex].status = payload.status;
+      todoList.value[taskIndex].status = status;
       if (payload.summary) {
         todoList.value[taskIndex].summary = payload.summary;
       }
     }
     
-    const taskId = payload.task_id;
-    const status = payload.status;
-    const title = payload.title || "";
-    const query = payload.query || "";
-    
     // 截断长查询内容
     const truncateText = (text: string, max: number) => 
       text.length > max ? text.slice(0, max) + "..." : text;
     
+    // 获取当前已知的总任务数
+    const getTotal = () => {
+      if (todoList.value.length > 0) return todoList.value.length;
+      if (totalTasks.value > 0) return totalTasks.value;
+      // 根据 task_id 推断(假设 task_id 是从 1 开始的连续数字)
+      return Math.max(taskId, completedTasks.value + 1);
+    };
+    
     if (status === "in_progress") {
       currentTask.value = payload;
       addLog(`🚀 [TASK ${taskId}] status=in_progress title="${title}"`);
@@ -526,13 +546,13 @@ function handleStreamEvent(event: ResearchStreamEvent) {
       }
     } else if (status === "completed") {
       completedTasks.value++;
-      addLog(`✅ [TASK ${taskId}] status=completed (${completedTasks.value}/${totalTasks.value}) title="${title}"`);
+      addLog(`✅ [TASK ${taskId}] status=completed (${completedTasks.value}/${getTotal()}) title="${title}"`);
     } else if (status === "skipped") {
       completedTasks.value++;
-      addLog(`⏭️ [TASK ${taskId}] status=skipped title="${title}"`);
+      addLog(`⏭️ [TASK ${taskId}] status=skipped (${completedTasks.value}/${getTotal()}) title="${title}"`);
     } else if (status === "failed") {
       completedTasks.value++;
-      addLog(`❌ [TASK ${taskId}] status=failed title="${title}" error="${payload.detail || 'unknown'}"`);
+      addLog(`❌ [TASK ${taskId}] status=failed (${completedTasks.value}/${getTotal()}) title="${title}" error="${payload.detail || 'unknown'}"`);
     }
   }
   
@@ -572,10 +592,7 @@ function handleStreamEvent(event: ResearchStreamEvent) {
       ? `脚本完成,准备生成 ${turns} 段语音` 
       : "脚本为空,跳过音频生成";
     addLog(`🎙️ [SCRIPT] status=completed turns=${turns}`);
-    
-    if (turns === 0) {
-      addLog(`⚠️ [WARNING] script is empty, JSON parsing may have failed`);
-    }
+    // 不再输出额外的警告,后端会通过 log 事件发送
   }
 
   // 6.5. Audio Start
@@ -593,7 +610,7 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     audioProgress.total = payload.total;
     audioProgress.role = payload.role;
     currentStatusMessage.value = `TTS ${payload.current}/${payload.total}: ${payload.role}`;
-    addLog(`🎤 [TTS ${payload.current}/${payload.total}] role=${payload.role} status=generating`);
+    // 不再输出 generating 日志,后端会在生成成功后发送 log 事件
   }
 
   // 8. Audio Generation Complete
@@ -616,25 +633,27 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     const filename = String(payload.file).split(/[\\/]/).pop();
     if (filename) {
       const baseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
-      audioUrl.value = `${baseUrl}/output/${filename}`;
+      audioUrl.value = `${baseUrl}/output/audio/${filename}`;
       podcastReady.value = true;
+      productionStage.value = "done";
       currentStatusMessage.value = "🎉 播客制作完成!";
       addLog(`🎉 [PODCAST] status=ready file=${filename}`);
-      productionStage.value = "done";
+      // 停止等待动画
+      stopWaitingAnimation();
     }
   }
 
   // 10. Done event
   if (event.type === "done") {
-    currentStatusMessage.value = "全部完成";
     addLog(`✅ [DONE] all tasks completed`);
-  }
-  
-  // 11. Backend Log event (直接显示后端日志)
-  if (event.type === "log") {
-    const payload = event as any;
-    const msg = payload.message || "";
-    addLog(`💬 INFO: ${msg}`);
+    stopWaitingAnimation();
+    productionStage.value = "done";
+    // 如果播客已就绪,确保状态正确
+    if (podcastReady.value) {
+      currentStatusMessage.value = "🎉 播客制作完成!";
+    } else {
+      currentStatusMessage.value = "全部完成";
+    }
   }
 }
 
@@ -836,30 +855,24 @@ h1 {
   margin: 1.5rem 0;
 }
 
-.select-wrapper {
-  position: relative;
-}
-
-select {
-  width: 100%;
-  appearance: none;
-  background: rgba(15, 23, 42, 0.6);
-  border: 1px solid rgba(255, 255, 255, 0.1);
-  color: #fff;
+/* 搜索引擎提示样式 */
+.search-hint {
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
   padding: 0.75rem 1rem;
+  background: rgba(96, 165, 250, 0.1);
+  border: 1px solid rgba(96, 165, 250, 0.2);
   border-radius: 8px;
-  font-size: 0.95rem;
-  cursor: pointer;
 }
 
-.select-arrow {
-  position: absolute;
-  right: 1rem;
-  top: 50%;
-  transform: translateY(-50%);
-  color: #64748b;
-  pointer-events: none;
-  font-size: 0.8rem;
+.hint-icon {
+  font-size: 1rem;
+}
+
+.hint-text {
+  color: #94a3b8;
+  font-size: 0.9rem;
 }
 
 .cta-button {
@@ -908,11 +921,11 @@ select {
 .production-header {
   display: flex;
   justify-content: space-between;
-  align-items: flex-start;
-  margin-bottom: 1rem;
+  align-items: center;
+  margin-bottom: 0.5rem;
 }
 
-.header-left h2 {
+.production-header h2 {
   font-size: 1.5rem;
   margin: 0;
 }
@@ -920,7 +933,8 @@ select {
 .production-topic {
   color: #94a3b8;
   font-size: 1rem;
-  margin-top: 0.25rem;
+  margin-top: 0;
+  margin-bottom: 1rem;
   font-style: italic;
 }
 
@@ -1209,28 +1223,6 @@ select {
   border-radius: 4px;
 }
 
-.production-header {
-  width: 100%;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 3rem;
-}
-
-.production-header h2 {
-  font-size: 1.5rem;
-  margin: 0;
-}
-
-.cancel-btn {
-  background: transparent;
-  border: 1px solid rgba(239, 68, 68, 0.5);
-  color: #fca5a5;
-  padding: 0.5rem 1rem;
-  border-radius: 6px;
-  cursor: pointer;
-}
-
 .stage-monitor {
   display: flex;
   align-items: center;

+ 0 - 1
Co-creation-projects/JJason-DeepCastAgent/frontend/src/services/api.ts

@@ -3,7 +3,6 @@ const baseURL =
 
 export interface ResearchRequest {
   topic: string;
-  search_api?: string;
 }
 
 export interface ResearchStreamEvent {