Просмотр исходного кода

feat: 增强施工方案编写 API — 内容补全与大纲生成优化

优化 content_completion 和 outline_views 接口,新增 regenerate_views 重新生成功能。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WangXuMing 1 неделя назад
Родитель
Сommit
bdaab204c0

+ 5 - 4
views/construction_review/launch_review.py

@@ -343,7 +343,7 @@ async def launch_review_sse(request_data: LaunchReviewRequest):
                 last_progress = 10
                 last_progress_data = None
                 no_change_count = 0
-                max_no_data_count = 18000  # 1小时 = 3600s / 0.2s
+                max_no_data_count = 50  # 10 秒 = 50 × 0.2s
 
                 while True:
                     try:
@@ -351,12 +351,13 @@ async def launch_review_sse(request_data: LaunchReviewRequest):
                         progress_data = await progress_manager.get_progress(callback_task_id)
 
                         if progress_data is None:
-                            # Redis 中没有数据(异常情况),最多等待 1 小时
-                            logger.warning(f"Redis中未找到进度,可能任务已完成: {callback_task_id}")
                             no_change_count += 1
+                            # Redis key 不存在时快速退出,避免 shutdown 后长时间空转
                             if no_change_count >= max_no_data_count:
-                                logger.info(f"长时间未获取到进度,结束SSE: {callback_task_id}")
+                                logger.info(f"进度数据已过期,结束SSE: {callback_task_id}")
                                 break
+                            if no_change_count == 1:
+                                logger.debug(f"Redis中未找到进度: {callback_task_id}")
                             await asyncio.sleep(0.2)
                             continue
 

+ 69 - 37
views/construction_write/content_completion.py

@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 """
-上下文生成接口 - 极速版 (DashScope Aliyun Optimized)
-目标平台:阿里云 DashScope (兼容模式)
-API URL: https://dashscope.aliyuncs.com/compatible-mode/v1
-模型:qwen3-30b-a3b-instruct-2507
+上下文生成接口 - 极速版 (Shutian Optimized)
+目标平台:蜀天算力 Qwen3.5-122B-A10B
+API: 蜀天算力 (通过统一配置管理)
+模型:Qwen3.5-122B-A10B
 """
 
 import uuid
@@ -35,14 +35,14 @@ async def init_global_resources():
     global GLOBAL_HTTP_SESSION, GLOBAL_REDIS_CLIENT
     
     if GLOBAL_HTTP_SESSION is None or GLOBAL_HTTP_SESSION.closed:
-        # 增加 DNS 缓存和连接复用,针对阿里云域名优化
+        # 增加 DNS 缓存和连接复用,针对蜀天算力域名优化
         connector = aiohttp.TCPConnector(limit=100, limit_per_host=20, ttl_dns_cache=300, force_close=False)
         GLOBAL_HTTP_SESSION = aiohttp.ClientSession(
             timeout=aiohttp.ClientTimeout(total=120, connect=10, sock_read=10), # 连接超时稍长以防网络波动
             connector=connector,
-            headers={"User-Agent": "FastAPI-DashScope-Optimized/2.0"}
+            headers={"User-Agent": "FastAPI-Shutian-Optimized/2.0"}
         )
-        logger.info("✅ 全局 HTTP 连接池已初始化 (DashScope Ready)")
+        logger.info("✅ 全局 HTTP 连接池已初始化 (Shutian Ready)")
 
     if GLOBAL_REDIS_CLIENT is None:
         try:
@@ -74,38 +74,70 @@ async def get_redis_client():
 
 # ==================== 3. 文件操作工具 ====================
 
-# ==================== 4. 自定义 API 配置 (阿里云 DashScope) ====================
+# ==================== 4. 自定义 API 配置 (蜀天算力 Qwen3.5-122B) ====================
 
 class CustomAPIConfig:
-    # 【关键修改】阿里云 DashScope 兼容模式地址
-    # 注意:必须包含 /chat/completions 后缀
-    DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
-    DASHSCOPE_CHAT_URL = f"{DASHSCOPE_BASE_URL}/chat/completions"
-    
-    # 【关键修改】您的 API Key
-    DASHSCOPE_API_KEY = "sk-ae805c991b6a4a8da3a09351c34963a5"
-    
-    # 【关键修改】目标模型
-    DEFAULT_MODEL_NAME = "qwen3-30b-a3b-instruct-2507"
-    
+    # model_setting.yaml 中的功能名称
+    FUNCTION_NAME = "write_content_generate"
+
+    # 兜底默认值(蜀天 Qwen3.5-122B-A10B)
+    SHUTIAN_SERVER_URL_DEFAULT = "http://183.220.37.46:25423/v1"
+    SHUTIAN_API_KEY_DEFAULT = "lq123456"
+    DEFAULT_MODEL_NAME = "/model/Qwen3.5-122B-A10B"
+
+    @staticmethod
+    def _resolve_from_model_handler():
+        """通过 model_handler 统一解析模型配置(url, api_key, model_id)"""
+        try:
+            from foundation.ai.models.model_handler import model_handler
+            llm = model_handler.get_model_by_function(CustomAPIConfig.FUNCTION_NAME)
+            url = getattr(llm, 'base_url', None) or getattr(llm, 'openai_api_base', '')
+            url = str(url) if url else ''
+            model_id = getattr(llm, 'model_name', None) or getattr(llm, 'model', '')
+            model_id = str(model_id) if model_id else ''
+            api_key = getattr(llm, 'openai_api_key', None)
+            if api_key:
+                api_key = api_key.get_secret_value() if hasattr(api_key, 'get_secret_value') else str(api_key)
+            else:
+                api_key = ''
+            if url and api_key:
+                return url, api_key, model_id
+        except Exception:
+            pass
+        return None, None, None
+
     @staticmethod
     def get_api_url() -> str:
-        # 优先使用硬编码的阿里云地址
-        return CustomAPIConfig.DASHSCOPE_CHAT_URL
-    
+        configured_url = config_handler.get("custom_api", "API_URL", "")
+        if configured_url:
+            return configured_url
+        url, _, _ = CustomAPIConfig._resolve_from_model_handler()
+        if url:
+            return url
+        return config_handler.get("shutian", "SHUTIAN_122B_SERVER_URL", CustomAPIConfig.SHUTIAN_SERVER_URL_DEFAULT)
+
     @staticmethod
     def get_api_key() -> str:
-        return CustomAPIConfig.DASHSCOPE_API_KEY
-    
+        configured_key = config_handler.get("custom_api", "API_KEY", "")
+        if configured_key:
+            return configured_key
+        _, api_key, _ = CustomAPIConfig._resolve_from_model_handler()
+        if api_key:
+            return api_key
+        return config_handler.get("shutian", "SHUTIAN_122B_API_KEY", CustomAPIConfig.SHUTIAN_API_KEY_DEFAULT)
+
     @staticmethod
     def get_model_name() -> str:
-        # 允许配置覆盖,否则使用默认
         configured_model = config_handler.get("custom_api", "MODEL_NAME", "")
-        return configured_model if configured_model else CustomAPIConfig.DEFAULT_MODEL_NAME
-    
+        if configured_model:
+            return configured_model
+        _, _, model_id = CustomAPIConfig._resolve_from_model_handler()
+        if model_id:
+            return model_id
+        return config_handler.get("shutian", "SHUTIAN_122B_MODEL_ID", CustomAPIConfig.DEFAULT_MODEL_NAME)
+
     @staticmethod
     def is_enabled() -> bool:
-        # 只要 Key 不为空即启用
         return bool(CustomAPIConfig.get_api_key()) and bool(CustomAPIConfig.get_api_url())
 
 # ==================== 5. 极速流式调用 (核心优化) ====================
@@ -119,9 +151,9 @@ async def call_custom_api_stream(
     model_name = CustomAPIConfig.get_model_name()
     api_key = CustomAPIConfig.get_api_key()
     
-    logger.debug(f"[{trace_id}] 正在调用阿里云 DashScope: {model_name} @ {api_url}")
+    logger.debug(f"[{trace_id}] 正在调用蜀天算力: {model_name} @ {api_url}")
 
-    # 截断过长的 Prompt (阿里云对输入长度有限制,且为了速度)
+    # 截断过长的 Prompt (服务端对输入长度有限制,且为了速度)
     max_prompt_len = 10000
     if len(prompt) > max_prompt_len:
         prompt = prompt[-max_prompt_len:]
@@ -136,7 +168,7 @@ async def call_custom_api_stream(
         "max_tokens": max_tokens,
         "temperature": temperature,
         "stream": True,
-        "incremental_output": True # 阿里云兼容模式可能支持此参数,优化流式体验
+        "incremental_output": True # 蜀天算力兼容模式支持此参数,优化流式体验
     }
     
     headers = {
@@ -151,7 +183,7 @@ async def call_custom_api_stream(
     session = await get_http_session()
     
     try:
-        # 阿里云 HTTPS 连接,保持 read_bufsize=1 以获取最快首字
+        # 蜀天算力 HTTP 连接,保持 read_bufsize=1 以获取最快首字
         async with session.post(api_url, json=payload, headers=headers, read_bufsize=1) as response:
             if response.status != 200:
                 error_text = await response.text()
@@ -176,7 +208,7 @@ async def call_custom_api_stream(
                             
                             try:
                                 event_data = json.loads(data)
-                                # 处理阿里云可能的错误格式
+                                # 处理服务端可能的错误格式
                                 if "error" in event_data:
                                     err_msg = event_data["error"].get("message", "Unknown Error")
                                     logger.error(f"[{trace_id}] 流式数据中包含错误: {err_msg}")
@@ -303,13 +335,13 @@ async def generate_content_stream(callback_task_id, source_task_id, user_id, req
 
         yield format_sse_event("generating", json.dumps({
             "status": "generating", 
-            "message": f"正在调用阿里云 Qwen3 ({CustomAPIConfig.get_model_name()})...", 
+            "message": f"正在调用蜀天 Qwen3.5-122B ({CustomAPIConfig.get_model_name()})...",
             "timestamp": int(time.time())
         }, ensure_ascii=False))
 
         # 执行生成
         if CustomAPIConfig.is_enabled():
-            logger.info(f"[{callback_task_id}] 使用阿里云 DashScope API (模型:{CustomAPIConfig.get_model_name()})")
+            logger.info(f"[{callback_task_id}] 使用蜀天算力 API (模型:{CustomAPIConfig.get_model_name()})")
             async for content, ftl in call_custom_api_stream(
                 prompt=user_prompt,
                 system_prompt=CONTENT_COMPLETION_SYSTEM_PROMPT,
@@ -406,7 +438,7 @@ async def content_completion(request: ContentCompletionRequest):
 async def health_check():
     return {
         "status": "healthy",
-        "provider": "Aliyun DashScope",
+        "provider": "Shutian",
         "current_model": CustomAPIConfig.get_model_name(),
         "api_url_prefix": "https://dashscope.aliyuncs.com/compatible-mode/v1"
     }
@@ -426,7 +458,7 @@ async def get_api_status():
         code=200, message="success", 
         data={
             "enabled": enabled, 
-            "provider": "Aliyun DashScope",
+            "provider": "Shutian",
             "model": CustomAPIConfig.get_model_name()
         }
     )

+ 79 - 33
views/construction_write/outline_views.py

@@ -277,14 +277,14 @@ async def init_global_resources():
     global GLOBAL_HTTP_SESSION, GLOBAL_REDIS_CLIENT
     
     if GLOBAL_HTTP_SESSION is None or GLOBAL_HTTP_SESSION.closed:
-        # 增加 DNS 缓存和连接复用,针对阿里云域名优化
+        # 增加 DNS 缓存和连接复用,针对蜀天算力域名优化
         connector = aiohttp.TCPConnector(limit=100, limit_per_host=20, ttl_dns_cache=300, force_close=False)
         GLOBAL_HTTP_SESSION = aiohttp.ClientSession(
             timeout=aiohttp.ClientTimeout(total=120, connect=10, sock_read=10), # 连接超时稍长以防网络波动
             connector=connector,
-            headers={"User-Agent": "FastAPI-DashScope-Optimized/2.0"}
+            headers={"User-Agent": "FastAPI-Shutian-Optimized/2.0"}
         )
-        logger.info("✅ 全局 HTTP 连接池已初始化 (DashScope Ready)")
+        logger.info("✅ 全局 HTTP 连接池已初始化 (Shutian Ready)")
 
     if GLOBAL_REDIS_CLIENT is None:
         try:
@@ -314,38 +314,70 @@ async def get_redis_client():
         await init_global_resources()
     return GLOBAL_REDIS_CLIENT
 
-# ==================== 自定义 API 配置 (阿里云 DashScope) ====================
+# ==================== 自定义 API 配置 (蜀天算力 Qwen3.5-122B) ====================
 
 class CustomAPIConfig:
-    # 【关键修改】阿里云 DashScope 兼容模式地址
-    # 注意:必须包含 /chat/completions 后缀
-    DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
-    DASHSCOPE_CHAT_URL = f"{DASHSCOPE_BASE_URL}/chat/completions"
-    
-    # 【关键修改】您的 API Key
-    DASHSCOPE_API_KEY = "sk-ae805c991b6a4a8da3a09351c34963a5"
-    
-    # 【关键修改】目标模型
-    DEFAULT_MODEL_NAME = "qwen3-30b-a3b-instruct-2507"
-    
+    # model_setting.yaml 中的功能名称
+    FUNCTION_NAME = "write_outline_generate"
+
+    # 兜底默认值(蜀天 Qwen3.5-122B-A10B)
+    SHUTIAN_SERVER_URL_DEFAULT = "http://183.220.37.46:25423/v1"
+    SHUTIAN_API_KEY_DEFAULT = "lq123456"
+    DEFAULT_MODEL_NAME = "/model/Qwen3.5-122B-A10B"
+
+    @staticmethod
+    def _resolve_from_model_handler():
+        """通过 model_handler 统一解析模型配置(url, api_key, model_id)"""
+        try:
+            from foundation.ai.models.model_handler import model_handler
+            llm = model_handler.get_model_by_function(CustomAPIConfig.FUNCTION_NAME)
+            url = getattr(llm, 'base_url', None) or getattr(llm, 'openai_api_base', '')
+            url = str(url) if url else ''
+            model_id = getattr(llm, 'model_name', None) or getattr(llm, 'model', '')
+            model_id = str(model_id) if model_id else ''
+            api_key = getattr(llm, 'openai_api_key', None)
+            if api_key:
+                api_key = api_key.get_secret_value() if hasattr(api_key, 'get_secret_value') else str(api_key)
+            else:
+                api_key = ''
+            if url and api_key:
+                return url, api_key, model_id
+        except Exception:
+            pass
+        return None, None, None
+
     @staticmethod
     def get_api_url() -> str:
-        # 优先使用硬编码的阿里云地址
-        return CustomAPIConfig.DASHSCOPE_CHAT_URL
-    
+        configured_url = config_handler.get("custom_api", "API_URL", "")
+        if configured_url:
+            return configured_url
+        url, _, _ = CustomAPIConfig._resolve_from_model_handler()
+        if url:
+            return url
+        return config_handler.get("shutian", "SHUTIAN_122B_SERVER_URL", CustomAPIConfig.SHUTIAN_SERVER_URL_DEFAULT)
+
     @staticmethod
     def get_api_key() -> str:
-        return CustomAPIConfig.DASHSCOPE_API_KEY
-    
+        configured_key = config_handler.get("custom_api", "API_KEY", "")
+        if configured_key:
+            return configured_key
+        _, api_key, _ = CustomAPIConfig._resolve_from_model_handler()
+        if api_key:
+            return api_key
+        return config_handler.get("shutian", "SHUTIAN_122B_API_KEY", CustomAPIConfig.SHUTIAN_API_KEY_DEFAULT)
+
     @staticmethod
     def get_model_name() -> str:
-        # 允许配置覆盖,否则使用默认
         configured_model = config_handler.get("custom_api", "MODEL_NAME", "")
-        return configured_model if configured_model else CustomAPIConfig.DEFAULT_MODEL_NAME
-    
+        if configured_model:
+            return configured_model
+        _, _, model_id = CustomAPIConfig._resolve_from_model_handler()
+        if model_id:
+            return model_id
+        return config_handler.get("shutian", "SHUTIAN_122B_MODEL_ID", CustomAPIConfig.DEFAULT_MODEL_NAME)
+
     @staticmethod
     def is_enabled() -> bool:
-        # 只要 Key 不为空即启用
         return bool(CustomAPIConfig.get_api_key()) and bool(CustomAPIConfig.get_api_url())
 
 # ==================== 极速流式调用 (核心优化) ====================
@@ -359,9 +391,9 @@ async def call_custom_api_stream(
     model_name = CustomAPIConfig.get_model_name()
     api_key = CustomAPIConfig.get_api_key()
     
-    logger.debug(f"[{trace_id}] 正在调用阿里云 DashScope: {model_name} @ {api_url}")
+    logger.debug(f"[{trace_id}] 正在调用蜀天算力: {model_name} @ {api_url}")
 
-    # 截断过长的 Prompt (阿里云对输入长度有限制,且为了速度)
+    # 截断过长的 Prompt (服务端对输入长度有限制,且为了速度)
     max_prompt_len = 10000
     if len(prompt) > max_prompt_len:
         prompt = prompt[-max_prompt_len:]
@@ -376,7 +408,7 @@ async def call_custom_api_stream(
         "max_tokens": max_tokens,
         "temperature": temperature,
         "stream": True,
-        "incremental_output": True # 阿里云兼容模式可能支持此参数,优化流式体验
+        "incremental_output": True # 蜀天算力兼容模式支持此参数,优化流式体验
     }
     
     headers = {
@@ -391,7 +423,7 @@ async def call_custom_api_stream(
     session = await get_http_session()
     
     try:
-        # 阿里云 HTTPS 连接,保持 read_bufsize=1 以获取最快首字
+        # 蜀天算力 HTTP 连接,保持 read_bufsize=1 以获取最快首字
         async with session.post(api_url, json=payload, headers=headers, read_bufsize=1) as response:
             if response.status != 200:
                 error_text = await response.text()
@@ -416,7 +448,7 @@ async def call_custom_api_stream(
                             
                             try:
                                 event_data = json.loads(data)
-                                # 处理阿里云可能的错误格式
+                                # 处理服务端可能的错误格式
                                 if "error" in event_data:
                                     err_msg = event_data["error"].get("message", "Unknown Error")
                                     logger.error(f"[{trace_id}] 流式数据中包含错误: {err_msg}")
@@ -510,13 +542,13 @@ async def generate_content_stream(callback_task_id, source_task_id, user_id, req
 
         yield format_sse_event("generating", json.dumps({
             "status": "generating", 
-            "message": f"正在调用阿里云 Qwen3 ({CustomAPIConfig.get_model_name()})...", 
+            "message": f"正在调用蜀天 Qwen3.5-122B ({CustomAPIConfig.get_model_name()})...",
             "timestamp": int(time.time())
         }, ensure_ascii=False))
 
         # 执行生成
         if CustomAPIConfig.is_enabled():
-            logger.info(f"[{callback_task_id}] 使用阿里云 DashScope API (模型:{CustomAPIConfig.get_model_name()})")
+            logger.info(f"[{callback_task_id}] 使用蜀天算力 API (模型:{CustomAPIConfig.get_model_name()})")
             async for content, ftl in call_custom_api_stream(
                 prompt=user_prompt,
                 system_prompt=CONTEXT_GENERATE_SYSTEM_PROMPT,
@@ -615,7 +647,7 @@ async def context_generate(request: ContextGenerateRequest):
 async def health_check():
     return {
         "status": "healthy",
-        "provider": "Aliyun DashScope",
+        "provider": "Shutian",
         "current_model": CustomAPIConfig.get_model_name(),
         "api_url_prefix": "https://dashscope.aliyuncs.com/compatible-mode/v1"
     }
@@ -635,7 +667,7 @@ async def get_api_status():
         code=200, message="success", 
         data={
             "enabled": enabled, 
-            "provider": "Aliyun DashScope",
+            "provider": "Shutian",
             "model": CustomAPIConfig.get_model_name()
         }
     )
@@ -802,6 +834,7 @@ async def generating_outline(request: OutlineGenerationRequest):
             last_event_type = "processing"
             last_message = ""
             no_change_count = 0
+            no_data_count = 0
 
             while True:
                 try:
@@ -835,6 +868,7 @@ async def generating_outline(request: OutlineGenerationRequest):
                     progress_data = await progress_manager.get_progress(callback_task_id)
 
                     if progress_data:
+                        no_data_count = 0
                         current_progress = progress_data.get("current", last_progress)
                         current_event_type = progress_data.get("event_type", "processing")
                         current_message = progress_data.get("message", "")
@@ -887,6 +921,11 @@ async def generating_outline(request: OutlineGenerationRequest):
                                 last_progress_data = stream_data
                                 yield format_sse_event("processing", json.dumps(stream_data, ensure_ascii=False))
                             break
+                    else:
+                        no_data_count += 1
+                        if no_data_count >= 60:  # 30 秒无数据
+                            logger.info(f"[{callback_task_id}] 进度数据丢失,结束SSE")
+                            break
 
                     await asyncio.sleep(0.5)
 
@@ -1306,6 +1345,7 @@ async def regenerate_outline(request: RegenerateOutlineRequest):
             last_event_type = "processing"
             last_message = ""
             no_change_count = 0
+            no_data_count = 0
 
             while True:
                 try:
@@ -1340,6 +1380,7 @@ async def regenerate_outline(request: RegenerateOutlineRequest):
                     progress_data = await progress_manager.get_progress(new_callback_task_id)
 
                     if progress_data:
+                        no_data_count = 0
                         current_progress = progress_data.get("current", last_progress)
                         current_event_type = progress_data.get("event_type", "processing")
                         current_message = progress_data.get("message", "")
@@ -1392,6 +1433,11 @@ async def regenerate_outline(request: RegenerateOutlineRequest):
                                 last_progress_data = stream_data
                                 yield format_sse_event("processing", json.dumps(stream_data, ensure_ascii=False))
                             break
+                    else:
+                        no_data_count += 1
+                        if no_data_count >= 60:  # 30 秒无数据
+                            logger.info(f"[{new_callback_task_id}] 进度数据丢失,结束SSE")
+                            break
 
                     await asyncio.sleep(0.5)
 

+ 7 - 0
views/construction_write/regenerate_views.py

@@ -408,6 +408,7 @@ async def regenerate_outline(request: RegenerateOutlineRequest):
             last_event_type = "processing"
             last_message = ""
             no_change_count = 0
+            no_data_count = 0
 
             while True:
                 try:
@@ -442,6 +443,7 @@ async def regenerate_outline(request: RegenerateOutlineRequest):
                     progress_data = await progress_manager.get_progress(new_callback_task_id)
 
                     if progress_data:
+                        no_data_count = 0
                         current_progress = progress_data.get("current", last_progress)
                         current_event_type = progress_data.get("event_type", "processing")
                         current_message = progress_data.get("message", "")
@@ -494,6 +496,11 @@ async def regenerate_outline(request: RegenerateOutlineRequest):
                                 last_progress_data = stream_data
                                 yield format_sse_event("processing", json.dumps(stream_data, ensure_ascii=False))
                             break
+                    else:
+                        no_data_count += 1
+                        if no_data_count >= 60:  # 30 秒无数据
+                            logger.info(f"[{new_callback_task_id}] 进度数据丢失,结束SSE")
+                            break
 
                     await asyncio.sleep(0.5)