初始化多平台机器人部署项目 - Telegram/Discord/QQ资源搜索机器人
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""AI 意图理解客户端。
|
||||
|
||||
对接任意 OpenAI 兼容接口(可自定义 base_url),将用户的自然语言输入解析为
|
||||
结构化的搜索意图,供机器人选择合适的爬虫 / API 进行资源搜索。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
SYSTEM_PROMPT = """你是一个 Telegram 资源搜索机器人的「意图理解」模块。用户会用自然语言描述想要的资源,可能包括:
|
||||
- AV 番号(如 ABP-123、FC2-PPV-12345)
|
||||
- 演员名字
|
||||
- 电影 / 电视剧名称
|
||||
- 一段剧情、台词或影片描述
|
||||
- 电影类型 / 题材(如「悬疑电影」「科幻片」)
|
||||
|
||||
请解析用户输入,并只输出一个 JSON 对象(不要输出任何其他文字),字段如下:
|
||||
{
|
||||
"intent": "av_id | actor | genre | keyword | plot | general",
|
||||
"target": "解析后的搜索目标(番号 / 演员名 / 关键词 / 推测出的片名)",
|
||||
"source": "javdb | javbus | sukebei | dmm | apibay | auto",
|
||||
"explain": "用一句话说明你理解到的搜索目标"
|
||||
}
|
||||
|
||||
规则:
|
||||
- intent=av_id:输入是明确的番号;target 保持番号原样(英文字母大写、连字符保留)
|
||||
- intent=actor:仅当输入是明确的日本 AV 演员 / 演员名(例如「波多野结衣」「桥本有菜」「三上悠亜」);target 必须是最可能的正式演员名,不要直接原样照抄用户输入;如果用户把演员名打错、同音字、错别字、简写、日文假名近似、中文音译偏差,优先纠正成真正的日本 AV 演员名;如果用户说的是普通成人明星、非 AV 演员、或无法确定是日本 AV 演员,则不要使用 actor,改用 keyword / general
|
||||
- intent=genre:输入是电影类型 / 题材;target 为该类型关键词(如「悬疑」)
|
||||
- intent=plot:输入是剧情 / 台词 / 描述;target 为推测出的最可能片名(不确定也给一个最接近的)
|
||||
- intent=keyword:输入是电影 / 电视剧名称或其他明确关键词;target 为精简后的关键词
|
||||
- intent=general:无法明确分类时使用;target 为原始输入的精简版
|
||||
- source 表示最合适的数据源:AV 相关用 javdb/javbus/sukebei/dmm,电影 / 电视剧 / 通用关键词用 apibay,拿不准用 auto
|
||||
- target 尽量精简,去掉语气词和多余描述
|
||||
- 特别注意:演员搜索是“纠错 + 归一化”任务,不要因为用户拼错字或音近字就保留错误名字;例如“波多野结衣”可能写成“波多野结衣”“娑多野结衣”,应修正到正确正式名;“三上悠亚”可能写成“三上悠亜/三上悠亚”都应统一成正式名
|
||||
"""
|
||||
|
||||
|
||||
class AiClient:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str = "",
|
||||
api_key: str = "",
|
||||
model: str = "gpt-4o-mini",
|
||||
proxy: str = "",
|
||||
timeout: int = 60,
|
||||
):
|
||||
self.base_url = (base_url or "").rstrip("/")
|
||||
self.api_key = api_key or ""
|
||||
self.model = model or "gpt-4o-mini"
|
||||
self.proxy = proxy or ""
|
||||
self.proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None
|
||||
self.timeout = timeout
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return bool(self.base_url and self.api_key)
|
||||
|
||||
def chat(self, messages, temperature=0.2, max_tokens=800):
|
||||
if not self.enabled:
|
||||
return None
|
||||
url = f"{self.base_url}/chat/completions"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
try:
|
||||
resp = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
proxies=self.proxies,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error(f"AI request failed: {e}")
|
||||
return None
|
||||
if resp.status_code != 200:
|
||||
LOG.error(
|
||||
f"AI request failed with status {resp.status_code}: {resp.text[:300]}"
|
||||
)
|
||||
return None
|
||||
try:
|
||||
data = resp.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
except Exception as e:
|
||||
LOG.error(f"AI response parse failed: {e}")
|
||||
return None
|
||||
|
||||
def understand(self, text: str):
|
||||
"""将自然语言输入解析为搜索意图 dict,失败返回 None。"""
|
||||
if not self.enabled:
|
||||
return None
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": text},
|
||||
]
|
||||
content = self.chat(messages)
|
||||
if not content:
|
||||
return None
|
||||
LOG.info(f"AI 原始返回: {content}")
|
||||
intent = self._parse_json(content)
|
||||
LOG.info(f"AI 解析意图: {intent}")
|
||||
return intent
|
||||
|
||||
@staticmethod
|
||||
def _parse_json(content: str):
|
||||
content = (content or "").strip()
|
||||
content = re.sub(r"^```(?:json)?\s*", "", content)
|
||||
content = re.sub(r"\s*```$", "", content)
|
||||
try:
|
||||
return json.loads(content)
|
||||
except Exception:
|
||||
pass
|
||||
start = content.find("{")
|
||||
end = content.rfind("}")
|
||||
if start == -1 or end == -1 or end <= start:
|
||||
LOG.error(f"AI returned non-JSON content: {content[:300]}")
|
||||
return None
|
||||
try:
|
||||
return json.loads(content[start : end + 1])
|
||||
except Exception as e:
|
||||
LOG.error(f"AI JSON parse failed: {e}, content: {content[:300]}")
|
||||
return None
|
||||
Reference in New Issue
Block a user