T4: AnySearch 改为全局固定配置 + 修复外部服务弹窗 UI
This commit is contained in:
1 parent
3c9d9518b1
commit
97125d7b65
6 files changed
+634
-441
No files matched your search
@@ -1,118 +1,179 @@
|
||||
"""
|
||||
AnySearch 联网检索客户端
|
||||
======================
|
||||
调用 AnySearch 网页/领域搜索 API (https://api.anysearch.com/v1/search),
|
||||
为 AI 聊天注入实时联网检索结果,亦可被后端端点 / Agent 工具复用。
|
||||
|
||||
约束:平台容器存在智能路由代理,对外部 API 偶发重置;本客户端使用
|
||||
trust_env=False 直连 (等价于 NO_PROXY=api.anysearch.com),规避代理重置。
|
||||
失败一律返回空/降级结果,保证聊天链路不中断。
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
logger = logging.getLogger("aiflow.anysearch")
|
||||
|
||||
DEFAULT_API_URL = "https://api.anysearch.com/v1/search"
|
||||
DEFAULT_ZONE = "cn"
|
||||
REQUEST_TIMEOUT = 8.0
|
||||
|
||||
|
||||
async def anysearch_search(
|
||||
api_key: str,
|
||||
api_url: str = DEFAULT_API_URL,
|
||||
query: str = "",
|
||||
top_k: int = 5,
|
||||
zone: str = DEFAULT_ZONE,
|
||||
freshness: Optional[str] = None,
|
||||
timeout: float = REQUEST_TIMEOUT,
|
||||
) -> List[Dict]:
|
||||
"""调用 AnySearch 搜索,返回结构化结果列表。
|
||||
|
||||
每个结果:
|
||||
{
|
||||
"content": snippet 摘要文本,
|
||||
"score": 0.9..0.5 递减,
|
||||
"source": "web_anysearch",
|
||||
"metadata": {"url", "title", "source_domain", "zone"}
|
||||
}
|
||||
失败时返回 [] (不抛异常)。
|
||||
"""
|
||||
if not api_key:
|
||||
return []
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
logger.warning("httpx not available for AnySearch")
|
||||
return []
|
||||
|
||||
payload = {
|
||||
"query": query,
|
||||
"zone": zone,
|
||||
"top_k": max(1, min(int(top_k), 20)),
|
||||
}
|
||||
if freshness:
|
||||
payload["freshness"] = freshness
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
try:
|
||||
# trust_env=False -> 忽略容器内 HTTP(S)_PROXY,直连 api.anysearch.com
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=timeout) as client:
|
||||
resp = await client.post(api_url, json=payload, headers=headers)
|
||||
if resp.status_code != 200:
|
||||
logger.warning(f"AnySearch HTTP {resp.status_code}: {resp.text[:200]}")
|
||||
return []
|
||||
data = resp.json()
|
||||
if data.get("code") != 0:
|
||||
logger.warning(f"AnySearch biz error: {data.get('message')}")
|
||||
return []
|
||||
results = (data.get("data") or {}).get("results") or []
|
||||
hits = []
|
||||
for idx, r in enumerate(results[:top_k]):
|
||||
title = r.get("title", "") or ""
|
||||
url = r.get("url", "") or ""
|
||||
snippet = r.get("snippet") or r.get("content") or ""
|
||||
if not snippet and not title:
|
||||
continue
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
domain = urlparse(url).netloc
|
||||
except Exception:
|
||||
domain = ""
|
||||
hits.append({
|
||||
"content": snippet,
|
||||
"score": max(0.4, 0.9 - idx * 0.08),
|
||||
"source": "web_anysearch",
|
||||
"metadata": {
|
||||
"url": url,
|
||||
"title": title,
|
||||
"source_domain": domain,
|
||||
"zone": zone,
|
||||
},
|
||||
})
|
||||
return hits
|
||||
except Exception as e:
|
||||
logger.warning(f"AnySearch search failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def anysearch_health(api_key: str, api_url: str = DEFAULT_API_URL, timeout: float = REQUEST_TIMEOUT) -> Dict:
|
||||
"""连通性探测:返回 {"status": "connected"/"disconnected", "detail": ...}。"""
|
||||
if not api_key:
|
||||
return {"status": "disconnected", "detail": "未配置 API Key", "connected": False, "usable": False}
|
||||
try:
|
||||
import httpx
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=timeout) as client:
|
||||
resp = await client.post(
|
||||
api_url,
|
||||
json={"query": "ping", "zone": DEFAULT_ZONE, "top_k": 1},
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return {"status": "connected", "detail": "AnySearch 连接正常", "connected": True, "usable": True}
|
||||
return {"status": "error", "detail": f"HTTP {resp.status_code}", "connected": False, "usable": False}
|
||||
except Exception as e:
|
||||
return {"status": "error", "detail": str(e)[:200], "connected": False, "usable": False}
|
||||
"""
|
||||
AnySearch 联网检索客户端
|
||||
======================
|
||||
调用 AnySearch 网页/领域搜索 API (https://api.anysearch.com/v1/search),
|
||||
为 AI 聊天注入实时联网检索结果,亦可被后端端点 / Agent 工具复用。
|
||||
|
||||
约束:平台容器存在智能路由代理,对外部 API 偶发重置;本客户端使用
|
||||
trust_env=False 直连 (等价于 NO_PROXY=api.anysearch.com),规避代理重置。
|
||||
失败一律返回空/降级结果,保证聊天链路不中断。
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
logger = logging.getLogger("aiflow.anysearch")
|
||||
|
||||
DEFAULT_API_URL = "https://api.anysearch.com/v1/search"
|
||||
DEFAULT_ZONE = "cn"
|
||||
REQUEST_TIMEOUT = 8.0
|
||||
|
||||
|
||||
async def anysearch_search(
|
||||
api_key: str,
|
||||
api_url: str = DEFAULT_API_URL,
|
||||
query: str = "",
|
||||
top_k: int = 5,
|
||||
zone: str = DEFAULT_ZONE,
|
||||
freshness: Optional[str] = None,
|
||||
timeout: float = REQUEST_TIMEOUT,
|
||||
) -> List[Dict]:
|
||||
"""调用 AnySearch 搜索,返回结构化结果列表。
|
||||
|
||||
每个结果:
|
||||
{
|
||||
"content": snippet 摘要文本,
|
||||
"score": 0.9..0.5 递减,
|
||||
"source": "web_anysearch",
|
||||
"metadata": {"url", "title", "source_domain", "zone"}
|
||||
}
|
||||
失败时返回 [] (不抛异常)。
|
||||
"""
|
||||
if not api_key:
|
||||
return []
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
logger.warning("httpx not available for AnySearch")
|
||||
return []
|
||||
|
||||
payload = {
|
||||
"query": query,
|
||||
"zone": zone,
|
||||
"top_k": max(1, min(int(top_k), 20)),
|
||||
}
|
||||
if freshness:
|
||||
payload["freshness"] = freshness
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
try:
|
||||
# trust_env=False -> 忽略容器内 HTTP(S)_PROXY,直连 api.anysearch.com
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=timeout) as client:
|
||||
resp = await client.post(api_url, json=payload, headers=headers)
|
||||
if resp.status_code != 200:
|
||||
logger.warning(f"AnySearch HTTP {resp.status_code}: {resp.text[:200]}")
|
||||
return []
|
||||
data = resp.json()
|
||||
if data.get("code") != 0:
|
||||
logger.warning(f"AnySearch biz error: {data.get('message')}")
|
||||
return []
|
||||
results = (data.get("data") or {}).get("results") or []
|
||||
hits = []
|
||||
for idx, r in enumerate(results[:top_k]):
|
||||
title = r.get("title", "") or ""
|
||||
url = r.get("url", "") or ""
|
||||
snippet = r.get("snippet") or r.get("content") or ""
|
||||
if not snippet and not title:
|
||||
continue
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
domain = urlparse(url).netloc
|
||||
except Exception:
|
||||
domain = ""
|
||||
hits.append({
|
||||
"content": snippet,
|
||||
"score": max(0.4, 0.9 - idx * 0.08),
|
||||
"source": "web_anysearch",
|
||||
"metadata": {
|
||||
"url": url,
|
||||
"title": title,
|
||||
"source_domain": domain,
|
||||
"zone": zone,
|
||||
},
|
||||
})
|
||||
return hits
|
||||
except Exception as e:
|
||||
logger.warning(f"AnySearch search failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def anysearch_health(api_key: str, api_url: str = DEFAULT_API_URL, timeout: float = REQUEST_TIMEOUT) -> Dict:
|
||||
"""连通性探测:返回 {"status": "connected"/"disconnected", "detail": ...}。"""
|
||||
if not api_key:
|
||||
return {"status": "disconnected", "detail": "未配置 API Key", "connected": False, "usable": False}
|
||||
try:
|
||||
import httpx
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=timeout) as client:
|
||||
resp = await client.post(
|
||||
api_url,
|
||||
json={"query": "ping", "zone": DEFAULT_ZONE, "top_k": 1},
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return {"status": "connected", "detail": "AnySearch 连接正常", "connected": True, "usable": True}
|
||||
return {"status": "error", "detail": f"HTTP {resp.status_code}", "connected": False, "usable": False}
|
||||
except Exception as e:
|
||||
return {"status": "error", "detail": str(e)[:200], "connected": False, "usable": False}
|
||||
|
||||
|
||||
def load_anysearch_config(db_session) -> Dict:
|
||||
"""从 site_settings 读取全局 AnySearch 配置(失败返回空字典)。"""
|
||||
if db_session is None:
|
||||
return {}
|
||||
try:
|
||||
from app.models import SiteSetting
|
||||
row = db_session.query(SiteSetting).filter(SiteSetting.key == "anysearch_config").first()
|
||||
if not row or not row.value:
|
||||
return {}
|
||||
cfg = json.loads(row.value)
|
||||
if not isinstance(cfg, dict):
|
||||
return {}
|
||||
return cfg
|
||||
except Exception as e:
|
||||
logger.warning(f"load_anysearch_config failed: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def save_anysearch_config(
|
||||
db_session,
|
||||
api_key: str,
|
||||
is_enabled: bool = True,
|
||||
api_url: str = DEFAULT_API_URL,
|
||||
zone: str = DEFAULT_ZONE,
|
||||
freshness: Optional[str] = None,
|
||||
top_k: int = 5,
|
||||
) -> bool:
|
||||
"""保存/更新全局 AnySearch 配置到 site_settings。"""
|
||||
if db_session is None:
|
||||
return False
|
||||
try:
|
||||
from app.models import SiteSetting
|
||||
from datetime import datetime
|
||||
cfg = {
|
||||
"api_key": api_key.strip(),
|
||||
"is_enabled": bool(is_enabled),
|
||||
"api_url": api_url.strip() or DEFAULT_API_URL,
|
||||
"zone": zone or DEFAULT_ZONE,
|
||||
"freshness": freshness,
|
||||
"top_k": max(1, min(int(top_k), 20)),
|
||||
}
|
||||
row = db_session.query(SiteSetting).filter(SiteSetting.key == "anysearch_config").first()
|
||||
if row:
|
||||
row.value = json.dumps(cfg, ensure_ascii=False)
|
||||
row.updated_at = datetime.now()
|
||||
else:
|
||||
from app.models import gen_uuid
|
||||
row = SiteSetting(id=gen_uuid(), key="anysearch_config", value=json.dumps(cfg, ensure_ascii=False))
|
||||
db_session.add(row)
|
||||
db_session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"save_anysearch_config failed: {e}")
|
||||
try:
|
||||
db_session.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
Reference in new issue
Block a user