379 lines
5.4 KiB
Python
379 lines
5.4 KiB
Python
# Auto-extracted from chat.py during Phase 1 refactor (Strangler Fig).
|
||
# Source of truth: scripts/phase1_extract.py. Do not hand-merge back.
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import and_, desc, asc, func
|
||
from pydantic import BaseModel, Field
|
||
from typing import Optional
|
||
import uuid
|
||
from datetime import datetime, timezone, timedelta
|
||
import re
|
||
import json
|
||
import asyncio
|
||
import math
|
||
import httpx
|
||
import os
|
||
import logging
|
||
from ..database import get_db, SessionLocal
|
||
from ..models import (
|
||
|
||
|
||
|
||
User, Conversation, Message, AIModel, ModelProvider, TokenUsageLog,
|
||
|
||
|
||
|
||
KnowledgeBase, KnowledgeDocument, KnowledgeDocumentChunk, EvolutionSkill,
|
||
|
||
|
||
|
||
EvolutionTrajectory, MarketApp, AppPurchase, AppInstallation,
|
||
|
||
|
||
|
||
MCPServer,
|
||
|
||
|
||
|
||
gen_uuid
|
||
|
||
|
||
|
||
)
|
||
from ..auth import get_current_user
|
||
from ..platform_knowledge import get_platform_knowledge
|
||
from ..mcp_client import list_mcp_tools, call_mcp_tool
|
||
from ..middleware.log_service import LogService
|
||
from ..capability_registry import resolve_auto_model
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
CST_TZ = timezone(timedelta(hours=8))
|
||
|
||
def cst_now():
|
||
|
||
|
||
|
||
"""Get current time in CST timezone"""
|
||
|
||
|
||
|
||
return datetime.now(CST_TZ)
|
||
|
||
def _html_escape(s: str) -> str:
|
||
|
||
|
||
|
||
return (str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
||
|
||
|
||
|
||
.replace('"', """).replace("'", "'"))
|
||
|
||
def _outbound_proxy() -> str:
|
||
|
||
|
||
|
||
"""出网代理地址(兜底)。默认经 172.18.0.1:7890 智能路由代理;
|
||
|
||
|
||
|
||
运行时若未注入代理环境变量则回退到该地址。"""
|
||
|
||
|
||
|
||
import os
|
||
|
||
|
||
|
||
return os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") or "http://172.18.0.1:7890"
|
||
|
||
_HK_PROXY = "http://qyyfw:XHR521yhf@103.242.14.208:11495"
|
||
|
||
def _proxy_for_url(url: str) -> str | None:
|
||
|
||
|
||
|
||
"""按目标域名选择是否走代理。
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
2026-07-15 调整:AGNES(apihub.agnes-ai.com / platform-outputs.agnes-ai.space)
|
||
|
||
|
||
|
||
经用户实测走香港代理 103.242.14.208:11495 延迟更低、更稳定,故改为走 HK 代理
|
||
|
||
|
||
|
||
(带鉴权),不再直连,也不经智能路由代理(172.18.0.1:7890 对 AGNES 返 502)。
|
||
|
||
|
||
|
||
其余国内/平台域名直连,真正的境外域名走 _outbound_proxy()。
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
注意:httpx 默认 trust_env=True 会忽略 proxy 参数而改用环境变量 HTTP_PROXY,
|
||
|
||
|
||
|
||
故调用方必须同时传 trust_env=False 才能真正走此处指定的代理。
|
||
|
||
|
||
|
||
"""
|
||
|
||
|
||
|
||
import os
|
||
|
||
|
||
|
||
from urllib.parse import urlparse
|
||
|
||
|
||
|
||
host = (urlparse(url).hostname or "").lower()
|
||
|
||
|
||
|
||
if "agnes-ai.com" in host or "agnes-ai.space" in host:
|
||
|
||
|
||
|
||
return _HK_PROXY
|
||
|
||
|
||
|
||
# 平台自有域名(含自建 Dify/n8n 等)必须直连,不经出网代理。
|
||
|
||
|
||
|
||
if "qihuyun.com" in host:
|
||
|
||
|
||
|
||
return None
|
||
|
||
|
||
|
||
# 尊重 NO_PROXY:国内域名(硅基流动/DeepSeek/小米MoE 等)直连更稳定,
|
||
|
||
|
||
|
||
# 避免经易波动的智能路由代理 172.18.0.1:7890(对国内域名偶发连接重置)。
|
||
|
||
|
||
|
||
_no_proxy = [h.strip().lower() for h in os.environ.get("NO_PROXY", "").split(",") if h.strip()]
|
||
|
||
|
||
|
||
for _np in _no_proxy:
|
||
|
||
|
||
|
||
if host == _np or host.endswith("." + _np) or _np in host:
|
||
|
||
|
||
|
||
return None
|
||
|
||
|
||
|
||
return _outbound_proxy()
|
||
|
||
def _extract_text_from_json(obj) -> str:
|
||
|
||
|
||
|
||
"""从 n8n / 通用 webhook 的任意 JSON 响应中尽力抽取文本回答。"""
|
||
|
||
|
||
|
||
if obj is None:
|
||
|
||
|
||
|
||
return ""
|
||
|
||
|
||
|
||
if isinstance(obj, str):
|
||
|
||
|
||
|
||
return obj
|
||
|
||
|
||
|
||
if isinstance(obj, (int, float, bool)):
|
||
|
||
|
||
|
||
return str(obj)
|
||
|
||
|
||
|
||
if isinstance(obj, list):
|
||
|
||
|
||
|
||
# 取第一个可抽取到文本的元素
|
||
|
||
|
||
|
||
for it in obj:
|
||
|
||
|
||
|
||
t = _extract_text_from_json(it)
|
||
|
||
|
||
|
||
if t:
|
||
|
||
|
||
|
||
return t
|
||
|
||
|
||
|
||
return ""
|
||
|
||
|
||
|
||
if isinstance(obj, dict):
|
||
|
||
|
||
|
||
# 常见回答字段优先级
|
||
|
||
|
||
|
||
for k in ("output", "text", "answer", "response", "reply",
|
||
|
||
|
||
|
||
"message", "content", "result", "data"):
|
||
|
||
|
||
|
||
if k in obj:
|
||
|
||
|
||
|
||
t = _extract_text_from_json(obj[k])
|
||
|
||
|
||
|
||
if t:
|
||
|
||
|
||
|
||
return t
|
||
|
||
|
||
|
||
# 兜底:整体序列化
|
||
|
||
|
||
|
||
try:
|
||
|
||
|
||
|
||
return json.dumps(obj, ensure_ascii=False)
|
||
|
||
|
||
|
||
except Exception:
|
||
|
||
|
||
|
||
return str(obj)
|
||
|
||
|
||
|
||
return str(obj)
|
||
|
||
class CreateConversationRequest(BaseModel):
|
||
|
||
|
||
|
||
title: str = Field(default="新对话")
|
||
|
||
|
||
|
||
model_id: Optional[str] = None
|
||
|
||
class UpdateConversationRequest(BaseModel):
|
||
|
||
|
||
|
||
title: Optional[str] = None
|
||
|
||
|
||
|
||
status: Optional[str] = None # active, archived
|
||
|
||
|
||
|
||
pinned: Optional[bool] = None
|
||
|
||
|
||
|
||
agent_ids: Optional[list] = None
|
||
|
||
|
||
|
||
skill_ids: Optional[list] = None
|
||
|
||
|
||
|
||
knowledge_base_ids: Optional[list] = None
|
||
|
||
class SendMessageRequest(BaseModel):
|
||
|
||
|
||
|
||
content: str
|
||
|
||
|
||
|
||
model_id: Optional[str] = None
|
||
|
||
|
||
|
||
skill_ids: Optional[list] = None
|
||
|
||
|
||
|
||
agent_ids: Optional[list] = None
|
||
|
||
|
||
|
||
knowledge_base_ids: Optional[list] = None
|
||
|
||
|
||
|
||
mcp_ids: Optional[list] = None
|
||
|
||
|
||
|
||
attachments: Optional[list] = None
|
||
|
||
|
||
|
||
use_tools: Optional[bool] = False
|