Files
archived-MoviePilot-Plugins/plugins.v2/chatgpt/__init__.py
2025-06-09 14:15:19 +08:00

491 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Any, List, Dict, Tuple
from app.core.config import settings
from app.core.event import eventmanager, Event
from app.log import logger
from app.plugins import _PluginBase
from app.plugins.chatgpt.openai import OpenAi
from app.schemas.types import EventType, ChainEventType
from app.schemas import NotificationType
class ChatGPT(_PluginBase):
# 插件名称
plugin_name = "ChatGPT"
# 插件描述
plugin_desc = "消息交互支持与ChatGPT对话。"
# 插件图标
plugin_icon = "Chatgpt_A.png"
# 插件版本
plugin_version = "2.1.6"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
author_url = "https://github.com/jxxghp"
# 插件配置项ID前缀
plugin_config_prefix = "chatgpt_"
# 加载顺序
plugin_order = 15
# 可使用的用户级别
auth_level = 1
# 私有属性
openai = None
_enabled = False
_proxy = False
_compatible = False
_recognize = False
_openai_url = None
_openai_key = None
_model = None
# 存储多个API密钥
_api_keys = []
# 当前使用的密钥索引
_current_key_index = 0
# 密钥失效状态
_key_status = {}
# 是否发送通知
_notify = False
# 自定义提示词
_customize_prompt = '接下来我会给你一个电影或电视剧的文件名你需要识别文件名中的名称、版本、分段、年份、分瓣率、季集等信息并按以下JSON格式返回{"name":string,"version":string,"part":string,"year":string,"resolution":string,"season":number|null,"episode":number|null}特别注意返回结果需要严格附合JSON格式不需要有任何其它的字符。如果中文电影或电视剧的文件名中存在谐音字或字母替代的情况请还原最有可能的结果。'
def init_plugin(self, config: dict = None):
if config:
self._enabled = config.get("enabled")
self._proxy = config.get("proxy")
self._compatible = config.get("compatible")
self._recognize = config.get("recognize")
self._openai_url = config.get("openai_url")
self._openai_key = config.get("openai_key")
self._model = config.get("model")
self._notify = config.get("notify")
self._customize_prompt = config.get("customize_prompt")
# 处理多个API密钥
if self._openai_key:
self._api_keys = [key.strip() for key in self._openai_key.split(',') if key.strip()]
# 初始化密钥状态
self._key_status = {key: True for key in self._api_keys}
logger.info(f"ChatGPT插件加载了 {len(self._api_keys)} 个API密钥")
if self._openai_url and self._api_keys:
# 使用第一个密钥初始化
self._current_key_index = 0
self.init_openai(self._api_keys[self._current_key_index])
def init_openai(self, api_key):
"""
初始化OpenAI客户端
"""
if self._openai_url and api_key:
self.openai = OpenAi(api_key=api_key, api_url=self._openai_url,
proxy=settings.PROXY if self._proxy else None,
model=self._model, compatible=bool(self._compatible), customize_prompt=self._customize_prompt)
logger.info(f"ChatGPT插件初始化API客户端成功")
return True
return False
def switch_to_next_key(self, failed_key):
"""
切换到下一个可用的API密钥
:return: (is_switched, error_message) 元组,表示是否切换成功及错误信息
"""
# 标记当前密钥为失效
self._key_status[failed_key] = False
# 寻找下一个可用的密钥
original_index = self._current_key_index
while True:
self._current_key_index = (self._current_key_index + 1) % len(self._api_keys)
next_key = self._api_keys[self._current_key_index]
# 如果密钥标记为可用或者已经尝试了所有密钥,则使用该密钥
if self._key_status.get(next_key, True) or self._current_key_index == original_index:
break
# 检查是否所有密钥都失效
if all(not status for status in self._key_status.values()):
logger.error("所有API密钥均已失效")
return False, "所有API密钥均已失效请检查配置"
# 使用新密钥重新初始化客户端
next_key = self._api_keys[self._current_key_index]
logger.info(f"切换到下一个API密钥 {next_key}")
success = self.init_openai(next_key)
return success, ""
def get_state(self) -> bool:
return self._enabled
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""
拼装插件配置页面需要返回两块数据1、页面配置2、数据结构
"""
return [
{
'component': 'VForm',
'content': [
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'enabled',
'label': '启用插件',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'proxy',
'label': '使用代理服务器',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'compatible',
'label': '兼容模式',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'recognize',
'label': '辅助识别',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'notify',
'label': '开启通知',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'openai_url',
'label': 'OpenAI API Url',
'placeholder': 'https://api.openai.com',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'openai_key',
'label': 'API密钥 (多个密钥以逗号分隔)',
'placeholder': 'sk-xxx,sk-yyy'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'model',
'label': '自定义模型',
'placeholder': 'gpt-3.5-turbo',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {'cols': 12},
'content': [
{
'component': 'VTextarea',
'props': {
'rows': 2,
'auto-grow': True,
'model': 'customize_prompt',
'label': '辅助识别提示词',
'hint': '在辅助识别时的给AI的提示词',
'clearable': True,
'persistent-hint': True,
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '开启插件后,消息交互时使用请[问帮你]开头或者以号结尾或者超过10个汉字/单词则会触发ChatGPT回复。'
'开启辅助识别后,内置识别功能无法正常识别种子/文件名称时将使用ChatGTP进行AI辅助识别可以提升动漫等非规范命名的识别成功率。'
'支持输入多个API密钥以逗号分隔在密钥调用失败时将自动切换到下一个可用密钥。'
'开启通知选项后将在API密钥调用失败时发送系统通知。'
}
}
]
}
]
}
]
}
], {
"enabled": False,
"proxy": False,
"compatible": False,
"recognize": False,
"notify": False,
"openai_url": "https://api.openai.com",
"openai_key": "",
"model": "gpt-3.5-turbo",
"customize_prompt": '接下来我会给你一个电影或电视剧的文件名你需要识别文件名中的名称、版本、分段、年份、分瓣率、季集等信息并按以下JSON格式返回{"name":string, '
'"version":string,"part":string,"year":string,"resolution":string,"season":number|null,"episode":number|null}特别注意返回结果需要严格附合JSON格式不需要有任何其它的字符。如果中文电影或电视剧的文件名中存在谐音字或字母替代的情况请还原最有可能的结果。'
}
def get_page(self) -> List[dict]:
pass
@staticmethod
def is_api_error(response):
"""
判断响应是否表示API错误
:param response: API响应
:return: (is_error, error_message) 元组,表示是否错误及错误信息
"""
# 检查响应是否为字典且包含errorMsg
if isinstance(response, dict) and response.get("errorMsg"):
return True, response.get("errorMsg")
# 检查响应是否为字符串且包含错误信息
if isinstance(response, str) and "请求ChatGPT出现错误" in response:
return True, response
# 如果没有错误信息,则表示调用成功
return False, ""
@eventmanager.register(EventType.UserMessage)
def talk(self, event: Event):
"""
监听用户消息获取ChatGPT回复
"""
if not self._enabled:
return
if not self.openai:
return
text = event.event_data.get("text")
userid = event.event_data.get("userid")
channel = event.event_data.get("channel")
if not text:
return
if text.startswith("http") or text.startswith("magnet") or text.startswith("ftp"):
return
# 尝试获取响应失败时切换API密钥
retry_count = 0
max_retries = len(self._api_keys)
while retry_count < max_retries:
response = self.openai.get_response(text=text, userid=userid)
# 判断响应是否正常
is_error, error_msg = self.is_api_error(response)
logger.info(f"ChatGPT返回结果{response}")
if is_error:
current_key = self._api_keys[self._current_key_index]
switched, switch_error = self.switch_to_next_key(current_key)
# 发送密钥失效通知
if self._notify:
message = f"API密钥 {current_key} 调用失败: {error_msg}"
self.post_message(channel=channel, title=message, userid=userid)
# 如果所有密钥都失效,发送额外通知
if not switched:
message = switch_error
self.post_message(mtype=NotificationType.Plugin, title="ChatGpt", text=message)
if not switched:
# 所有密钥都失效,发送消息并退出
return
retry_count += 1
else:
# 成功获取响应
self.post_message(channel=channel, title=response, userid=userid)
return
# 所有重试都失败
if self._notify:
self.post_message(channel=channel,
title="无法获取ChatGPT响应所有API密钥都已失效",
userid=userid)
@eventmanager.register(ChainEventType.NameRecognize)
def recognize(self, event: Event):
"""
监听识别事件使用ChatGPT辅助识别名称
"""
if not self.openai:
return
if not self._recognize:
return
if not event.event_data:
return
title = event.event_data.get("title")
if not title:
return
# 尝试获取媒体名称失败时切换API密钥
retry_count = 0
max_retries = len(self._api_keys)
while retry_count < max_retries:
response = self.openai.get_media_name(filename=title)
logger.info(f"ChatGPT返回结果{response}")
# 判断响应是否正常
is_error, error_msg = self.is_api_error(response)
# 如果不是错误但返回字典中没有name字段也视为错误
if not is_error and isinstance(response, dict) and not response.get("name"):
is_error = True
error_msg = "未返回有效识别结果"
if is_error:
# 发生错误,尝试切换密钥
current_key = self._api_keys[self._current_key_index]
switched, switch_error = self.switch_to_next_key(current_key)
# 发送密钥失效通知 (通过系统通知,因为这里没有用户交互)
if self._notify:
message = f"API密钥 {current_key} 调用失败: {error_msg}"
self.post_message(mtype=NotificationType.Plugin, title="ChatGpt", text=message)
# 如果所有密钥都失效,发送额外通知
if not switched:
message = switch_error
self.post_message(mtype=NotificationType.Plugin, title="ChatGpt", text=message)
if not switched:
# 所有密钥都失效
return
retry_count += 1
else:
# 成功获取结果
event.event_data = {
'title': title,
'name': response.get("name"),
'year': response.get("year"),
'season': response.get("season"),
'episode': response.get("episode")
}
return
# 所有重试都失败
if self._notify:
logger.error(f"无法识别标题 {title}所有API密钥都已失效")
self.post_message(mtype=NotificationType.Plugin,
title="ChatGpt",
text=f"无法识别标题 {title}所有API密钥都已失效")
def stop_service(self):
"""
退出插件
"""
pass