157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
"""
|
||
Kimi (月之暗面) 平台适配器 - 使用 Moonshot AI API 获取真实AI回答
|
||
|
||
API文档: https://platform.moonshot.cn/docs
|
||
使用 Moonshot v1 chat completion API
|
||
"""
|
||
|
||
import asyncio
|
||
import logging
|
||
import time
|
||
from typing import Optional
|
||
|
||
import httpx
|
||
|
||
from app.config import settings
|
||
from app.workers.platforms.base import BasePlatformAdapter
|
||
from app.workers.platforms.search_engine import fetch_search_content
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 模块级频率限制器
|
||
_last_request_time: float = 0.0
|
||
_min_interval: float = 6.0 # 每分钟10次 -> 6秒间隔
|
||
|
||
|
||
async def _rate_limit_wait():
|
||
"""确保请求间隔不低于最小间隔"""
|
||
global _last_request_time
|
||
now = time.monotonic()
|
||
elapsed = now - _last_request_time
|
||
if elapsed < _min_interval:
|
||
wait_time = _min_interval - elapsed
|
||
logger.debug(f"Kimi 频率限制等待 {wait_time:.1f}s")
|
||
await asyncio.sleep(wait_time)
|
||
_last_request_time = time.monotonic()
|
||
|
||
|
||
class KimiAdapter(BasePlatformAdapter):
|
||
"""Kimi 平台适配器 - 使用 Moonshot AI API 获取真实AI回答"""
|
||
|
||
platform_name = "kimi"
|
||
platform_url = "https://kimi.moonshot.cn"
|
||
_api_base = "https://api.moonshot.cn/v1"
|
||
_model = "moonshot-v1-8k"
|
||
|
||
def __init__(self):
|
||
self._api_key: Optional[str] = None
|
||
self._client: Optional[httpx.AsyncClient] = None
|
||
|
||
@property
|
||
def api_key(self) -> Optional[str]:
|
||
if self._api_key is None:
|
||
self._api_key = settings.MOONSHOT_API_KEY
|
||
return self._api_key
|
||
|
||
@property
|
||
def is_configured(self) -> bool:
|
||
return bool(self.api_key and self.api_key.strip())
|
||
|
||
def _get_client(self) -> httpx.AsyncClient:
|
||
if self._client is None or self._client.is_closed:
|
||
self._client = httpx.AsyncClient(
|
||
timeout=httpx.Timeout(60.0, connect=10.0),
|
||
headers={
|
||
"Content-Type": "application/json",
|
||
"Authorization": f"Bearer {self.api_key}",
|
||
},
|
||
)
|
||
return self._client
|
||
|
||
async def query(self, keyword: str) -> str:
|
||
"""在 Kimi 查询关键词,返回原始响应文本"""
|
||
last_error = None
|
||
for attempt in range(3):
|
||
try:
|
||
return await self._do_query(keyword)
|
||
except Exception as e:
|
||
last_error = e
|
||
logger.warning(f"Kimi 查询第 {attempt + 1} 次尝试失败: {e}")
|
||
if attempt < 2:
|
||
await asyncio.sleep(2 ** attempt)
|
||
|
||
# 所有尝试失败后回退到搜索引擎
|
||
logger.warning(f"Kimi API 调用全部失败,回退到搜索引擎: {last_error}")
|
||
return await self._fallback_search(keyword)
|
||
|
||
async def _do_query(self, keyword: str) -> str:
|
||
"""通过 Moonshot API 获取真实AI回答"""
|
||
if not self.is_configured:
|
||
logger.warning("Kimi API Key 未配置,回退到搜索引擎")
|
||
return await self._fallback_search(keyword)
|
||
|
||
await _rate_limit_wait()
|
||
|
||
client = self._get_client()
|
||
payload = {
|
||
"model": self._model,
|
||
"messages": [
|
||
{
|
||
"role": "system",
|
||
"content": (
|
||
"你是一个专业的AI搜索助手。请基于你的知识,"
|
||
"详细回答用户的问题。如果引用了外部来源,"
|
||
"请在回答中标注来源URL或出处名称。"
|
||
),
|
||
},
|
||
{
|
||
"role": "user",
|
||
"content": keyword,
|
||
},
|
||
],
|
||
"temperature": 0.7,
|
||
"max_tokens": 2000,
|
||
}
|
||
|
||
response = await client.post(
|
||
f"{self._api_base}/chat/completions",
|
||
json=payload,
|
||
)
|
||
|
||
if response.status_code == 429:
|
||
retry_after = int(response.headers.get("Retry-After", "10"))
|
||
logger.warning(f"Kimi API 限流,等待 {retry_after}s 后重试")
|
||
await asyncio.sleep(retry_after)
|
||
raise RuntimeError(f"Kimi API 限流,等待 {retry_after}s")
|
||
|
||
if response.status_code != 200:
|
||
error_body = response.text[:500]
|
||
raise RuntimeError(
|
||
f"Kimi API 返回错误 {response.status_code}: {error_body}"
|
||
)
|
||
|
||
data = response.json()
|
||
choices = data.get("choices", [])
|
||
if not choices:
|
||
raise RuntimeError("Kimi API 返回空 choices")
|
||
|
||
content = choices[0].get("message", {}).get("content", "")
|
||
if not content:
|
||
raise RuntimeError("Kimi API 返回空内容")
|
||
|
||
# 标记数据来源为AI平台
|
||
result = f"[data_source: ai_platform]\n{content}"
|
||
logger.info(f"Kimi API 调用成功,返回 {len(content)} 字符")
|
||
return result
|
||
|
||
async def _fallback_search(self, keyword: str) -> str:
|
||
"""回退到搜索引擎模式,标记数据来源"""
|
||
content = await fetch_search_content(self.platform_name, keyword)
|
||
return f"[data_source: search_engine]\n{content}"
|
||
|
||
async def close(self):
|
||
"""清理资源"""
|
||
if self._client and not self._client.is_closed:
|
||
await self._client.aclose()
|
||
self._client = None
|