Ver código fonte

feat(backend): 新增三个代理服务脚本

1. 新增p04.py: 通用HTTP反向代理服务,可转发请求到指定后端
2. 新增qwen2.py: Qwen3.6大模型流式代理服务,支持思维链返回
3. 新增qwen.py: Qwen模型思维链展示API,实时返回思考过程
Stclair 1 mês atrás
pai
commit
41cc66b51d
3 arquivos alterados com 463 adições e 0 exclusões
  1. 102 0
      backend/p04.py
  2. 219 0
      backend/qwen.py
  3. 142 0
      backend/qwen2.py

+ 102 - 0
backend/p04.py

@@ -0,0 +1,102 @@
+"""
+FastAPI HTTP 代理服务
+代理 HTTP://192.168.0.42:8005/mcp 接口
+"""
+import httpx
+from fastapi import FastAPI, Request, Response
+from fastapi.responses import StreamingResponse
+import json
+
+app = FastAPI(
+    title="HTTP Proxy Service",
+    description="代理 MCP 接口到 192.168.0.42:8005",
+    version="1.0.0"
+)
+
+# 目标服务地址
+TARGET_BASE_URL = "http://192.168.0.42:8005"
+
+
+def get_forwarded_headers(request: Request) -> dict:
+    """获取需要转发的 headers"""
+    headers = {}
+    # 转发所有 headers,跳过一些不需要的
+    skip_headers = {"host", "content-length", "transfer-encoding"}
+    
+    for key, value in request.headers.items():
+        if key.lower() not in skip_headers:
+            headers[key] = value
+    
+    return headers
+
+
+async def proxy_request(path: str, request: Request):
+    """代理请求到目标服务"""
+    url = f"{TARGET_BASE_URL}{path}"
+    method = request.method
+    headers = get_forwarded_headers(request)
+    
+    # 获取请求体
+    body = await request.body()
+    
+    async with httpx.AsyncClient(timeout=60.0) as client:
+        try:
+            # 发送请求
+            response = await client.request(
+                method=method,
+                url=url,
+                headers=headers,
+                content=body if body else None,
+                params=request.query_params
+            )
+            
+            # 构建响应头
+            response_headers = {}
+            for key, value in response.headers.items():
+                if key.lower() not in {"content-length", "transfer-encoding", "connection"}:
+                    response_headers[key] = value
+            
+            # 返回响应
+            return Response(
+                content=response.content,
+                status_code=response.status_code,
+                headers=response_headers
+            )
+            
+        except httpx.TimeoutException:
+            return Response(
+                content=json.dumps({"error": "请求超时"}).encode("utf-8"),
+                status_code=504,
+                media_type="application/json"
+            )
+        except httpx.ConnectError:
+            return Response(
+                content=json.dumps({"error": "无法连接到目标服务"}).encode("utf-8"),
+                status_code=503,
+                media_type="application/json"
+            )
+        except Exception as e:
+            return Response(
+                content=json.dumps({"error": f"代理请求失败: {str(e)}"}).encode("utf-8"),
+                status_code=500,
+                media_type="application/json"
+            )
+
+
+@app.api_route("/mcp/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
+async def proxy_mcp(path: str, request: Request):
+    """代理 /mcp 路径下的所有请求"""
+    target_path = f"/mcp/{path}"
+    return await proxy_request(target_path, request)
+
+
+@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
+async def proxy_all(path: str, request: Request):
+    """代理所有其他路径的请求"""
+    return await proxy_request(f"/{path}", request)
+
+
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run("p04:app", host="0.0.0.0", port=8000, reload=True)

+ 219 - 0
backend/qwen.py

@@ -0,0 +1,219 @@
+"""
+FastAPI SSE 接口 - Qwen 模型思维链展示
+实时返回模型的思考过程(think process)
+"""
+import asyncio
+import json
+from fastapi import FastAPI, Request
+from fastapi.responses import StreamingResponse
+from pydantic import BaseModel
+import httpx
+
+app = FastAPI(
+    title="Qwen Think Process API",
+    description="展示 Qwen 模型的思维链(Think Process),实时返回思考过程",
+    version="1.0.0"
+)
+
+# Qwen API 配置(预留配置参数)
+QWEN_API_URL = "http://localhost:8000/v1/chat/completions"  # Qwen 服务地址
+QWEN_API_KEY = "your-api-key-here"  # API Key 预留
+QWEN_MODEL = "Qwen/Qwen2-7B-Instruct"  # 模型名称
+
+
+class ThinkProcessRequest(BaseModel):
+    """请求体模型"""
+    question: str
+    max_tokens: int = 500
+    model: str = QWEN_MODEL
+    api_key: str = None  # 允许通过请求覆盖 API Key
+
+
+async def call_qwen_api(question: str, model: str = None, api_key: str = None):
+    """
+    真实调用 Qwen API 获取思维链
+    
+    Args:
+        question: 用户问题
+        model: 模型名称(可选,默认使用配置值)
+        api_key: API Key(可选,默认使用配置值)
+    """
+    # 使用传入值或默认配置
+    target_model = model or QWEN_MODEL
+    target_api_key = api_key or QWEN_API_KEY
+    
+    # 构建提示词,引导模型输出思维链
+    think_prompt = f"""
+    请用中文详细展示你的思考过程来回答以下问题:
+    
+    问题:{question}
+    
+    思考过程格式:
+    1. 分析问题核心需求
+    2. 检索相关知识
+    3. 构建推理链条
+    4. 验证逻辑正确性
+    5. 生成最终回答
+    
+    请按步骤详细说明你的思考过程。
+    """
+    
+    payload = {
+        "model": target_model,
+        "messages": [
+            {
+                "role": "system",
+                "content": "你是一个擅长展示思考过程的AI助手,请详细展示你的思考步骤。"
+            },
+            {
+                "role": "user",
+                "content": think_prompt
+            }
+        ],
+        "stream": True,
+        "max_tokens": 1000,
+        "temperature": 0.7
+    }
+    
+    headers = {
+        "Content-Type": "application/json"
+    }
+    
+    # 如果有 API Key,添加认证头
+    if target_api_key and target_api_key != "your-api-key-here":
+        headers["Authorization"] = f"Bearer {target_api_key}"
+    
+    try:
+        async with httpx.AsyncClient(timeout=120.0) as client:
+            async with client.stream(
+                "POST",
+                QWEN_API_URL,
+                json=payload,
+                headers=headers
+            ) as response:
+                if response.status_code != 200:
+                    try:
+                        error_data = await response.json()
+                        error_msg = error_data.get("error", {}).get("message", f"API返回错误: {response.status_code}")
+                    except Exception:
+                        error_msg = f"API请求失败: {response.status_code}"
+                    
+                    yield {"type": "error", "content": error_msg}
+                    yield {"type": "completed", "content": ""}
+                    return
+                
+                # 解析流式响应
+                thinking_content = ""
+                async for chunk in response.aiter_bytes(chunk_size=64):
+                    try:
+                        chunk_text = chunk.decode("utf-8", errors="ignore")
+                        # 解析 SSE 格式
+                        for line in chunk_text.split("\n"):
+                            if line.startswith("data: "):
+                                data_str = line[6:].strip()
+                                if data_str == "[DONE]":
+                                    continue
+                                try:
+                                    data = json.loads(data_str)
+                                    choices = data.get("choices", [])
+                                    if choices:
+                                        delta = choices[0].get("delta", {})
+                                        content = delta.get("content", "")
+                                        if content:
+                                            thinking_content += content
+                                            yield {
+                                                "type": "thinking",
+                                                "content": content
+                                            }
+                                except json.JSONDecodeError:
+                                    continue
+                    except Exception as e:
+                        continue
+                
+                # 最终回答汇总
+                if thinking_content:
+                    yield {"type": "answer", "content": thinking_content}
+                
+                yield {"type": "completed", "content": ""}
+    
+    except httpx.TimeoutException:
+        yield {"type": "error", "content": "API请求超时"}
+        yield {"type": "completed", "content": ""}
+    except httpx.ConnectError:
+        yield {"type": "error", "content": "无法连接到Qwen服务"}
+        yield {"type": "completed", "content": ""}
+    except Exception as e:
+        yield {"type": "error", "content": f"API调用异常: {str(e)}"}
+        yield {"type": "completed", "content": ""}
+
+
+@app.post("/api/v1/think")
+async def think_stream(request: Request):
+    """
+    SSE 接口:实时返回 Qwen 模型的思维链
+    
+    请求体:
+    {
+        "question": "您的问题",
+        "max_tokens": 500,
+        "model": "Qwen/Qwen2-7B-Instruct",  # 可选,覆盖默认模型
+        "api_key": "your-key"  # 可选,覆盖默认API Key
+    }
+    
+    响应格式(SSE):
+    - thinking: 思考过程(流式返回)
+    - answer: 最终回答汇总
+    - error: 错误信息
+    - completed: 完成标记
+    """
+    # 解析请求体
+    body = await request.body()
+    try:
+        data = json.loads(body.decode("utf-8"))
+        question = data.get("question", "")
+        model = data.get("model", None)
+        api_key = data.get("api_key", None)
+    except Exception:
+        question = "用户提问"
+        model = None
+        api_key = None
+    
+    async def stream_generator():
+        async for step in call_qwen_api(question, model, api_key):
+            yield f"data: {json.dumps(step, ensure_ascii=False)}\n\n"
+    
+    return StreamingResponse(
+        stream_generator(),
+        media_type="text/event-stream",
+        headers={
+            "Cache-Control": "no-cache",
+            "Connection": "keep-alive",
+            "Access-Control-Allow-Origin": "*",
+        }
+    )
+
+
+@app.get("/")
+async def root():
+    return {
+        "service": "Qwen Think Process API",
+        "version": "1.0.0",
+        "endpoints": {
+            "think": "POST /api/v1/think (SSE)"
+        },
+        "description": "实时展示 Qwen 模型的思维链过程",
+        "config": {
+            "api_url": QWEN_API_URL,
+            "default_model": QWEN_MODEL
+        }
+    }
+
+
+@app.get("/health")
+async def health():
+    return {"status": "healthy"}
+
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run("qwen:app", host="0.0.0.0", port=8000, reload=True)

+ 142 - 0
backend/qwen2.py

@@ -0,0 +1,142 @@
+
+import os
+import json
+import asyncio
+import httpx
+from typing import AsyncGenerator, Optional
+from fastapi import FastAPI, Request, HTTPException
+from fastapi.responses import StreamingResponse
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+
+# 初始化 FastAPI 应用
+app = FastAPI(title="Qwen3.6 Thinking Stream Proxy")
+
+# 配置 CORS
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# Qwen3.6 服务配置
+# 假设使用兼容 OpenAI 格式的接口,如本地部署或 DashScope
+QWEN_BASE_URL = os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
+QWEN_API_KEY = os.getenv("QWEN_API_KEY", "your_api_key_here")
+QWEN_MODEL = os.getenv("QWEN_MODEL", "qwen-plus") # 或 qwen-max, qwen-turbo 等支持思考的模型
+
+class ChatRequest(BaseModel):
+    message: str
+    model: Optional[str] = QWEN_MODEL
+    enable_thinking: bool = True # 默认开启思考模式
+
+async def event_generator(request_body: dict, client: httpx.AsyncClient) -> AsyncGenerator[str, None]:
+    """
+    异步生成器:从 Qwen 接口获取 SSE 流并转发给前端
+    """
+    headers = {
+        "Authorization": f"Bearer {QWEN_API_KEY}",
+        "Content-Type": "application/json",
+        "Accept": "text/event-stream"
+    }
+    
+    # 构建符合 OpenAI 兼容格式的请求体
+    # 注意:enable_thinking 通常放在 extra_body 或特定参数中,具体取决于服务商实现
+    # 这里假设通过 extra_body 传递,或者如果是 DashScope 原生接口需调整结构
+    # 以下以 OpenAI 兼容格式为例,若使用 DashScope 原生 SDK 需调整 payload 结构
+    payload = {
+        "model": request_body.get("model", QWEN_MODEL),
+        "messages": [
+            {"role": "user", "content": request_body["message"]}
+        ],
+        "stream": True,
+        "stream_options": {"include_usage": False} # 可选
+    }
+    
+    # 如果使用的是 DashScope 兼容模式且支持 extra_body 传递 thinking 参数
+    if request_body.get("enable_thinking", True):
+        payload["extra_body"] = {"enable_thinking": True}
+
+    try:
+        async with client.stream(
+            "POST", 
+            f"{QWEN_BASE_URL}/chat/completions", 
+            json=payload, 
+            headers=headers,
+            timeout=None
+        ) as response:
+            if response.status_code != 200:
+                error_detail = await response.aread()
+                raise HTTPException(status_code=response.status_code, detail=f"Upstream error: {error_detail.decode()}")
+            
+            async for line in response.aiter_lines():
+                if not line:
+                    continue
+                
+                # 处理 SSE 结束标记
+                if line.strip() == "[DONE]":
+                    yield "data: [DONE]\n\n"
+                    break
+                
+                # 解析上游返回的 JSON 数据
+                if line.startswith("data: "):
+                    json_str = line[6:]
+                    try:
+                        data = json.loads(json_str)
+                        
+                        # 提取内容片段
+                        # Qwen 的思考过程通常在 delta.content 或 delta.reasoning_content 中
+                        # 具体字段名取决于模型版本和接口定义
+                        choices = data.get("choices", [])
+                        if choices:
+                            delta = choices.get("delta", {})
+                            
+                            # 尝试获取思考内容 (reasoning_content) 和普通内容 (content)
+                            reasoning = delta.get("reasoning_content", "")
+                            content = delta.get("content", "")
+                            
+                            # 构造前端易于区分的数据格式
+                            # 例如:{ "type": "thinking", "data": "..." } 或 { "type": "answer", "data": "..." }
+                            if reasoning:
+                                output_data = json.dumps({"type": "thinking", "content": reasoning}, ensure_ascii=False)
+                                yield f"data: {output_data}\n\n"
+                            
+                            if content:
+                                output_data = json.dumps({"type": "answer", "content": content}, ensure_ascii=False)
+                                yield f"data: {output_data}\n\n"
+                                
+                    except json.JSONDecodeError:
+                        # 如果不是 JSON,直接透传(以防万一)
+                        yield f"data: {json_str}\n\n"
+                else:
+                    # 非 data: 开头的行,可能是注释或其他,忽略或按需处理
+                    pass
+
+    except httpx.RequestError as exc:
+        yield f"data: {{\"type\": \"error\", \"content\": \"Connection error: {str(exc)}\"}}\n\n"
+    except Exception as exc:
+        yield f"data: {{\"type\": \"error\", \"content\": \"Internal error: {str(exc)}\"}}\n\n"
+
+@app.post("/api/chat/stream")
+async def chat_stream(request: ChatRequest):
+    """
+    代理接口:接收前端提问,转发给 Qwen3.6,流式返回思考和回答
+    """
+    upstream_payload = request.dict()
+    
+    async with httpx.AsyncClient() as client:
+        return StreamingResponse(
+            event_generator(upstream_payload, client),
+            media_type="text/event-stream",
+            headers={
+                "Cache-Control": "no-cache",
+                "Connection": "keep-alive",
+                "X-Accel-Buffering": "no"
+            }
+        )
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run(app, host="0.0.0.0", port=8000)