Compare commits

..

15 Commits

Author SHA1 Message Date
Soulter 5c2e7099fc Update README.md 2024-05-26 21:38:32 +08:00
Soulter 1fd1d55895 Update config.py 2024-05-26 21:31:26 +08:00
Soulter 5ce4137e75 fix: 修复model指令 2024-05-26 21:15:33 +08:00
Soulter d49179541e feat: 给插件的init方法传入 ctx 2024-05-26 21:10:19 +08:00
Soulter 676f258981 perf: 重启后终止子进程 2024-05-26 21:09:23 +08:00
Soulter fa44749240 fix: 修复相对路径导致的windows启动器无法安装依赖的问题 2024-05-26 18:15:25 +08:00
Soulter 6c856f9da2 fix(typo): 修复插件注册器的一个typo导致无法注册消息平台插件的问题 2024-05-26 18:07:07 +08:00
Soulter e8773cea7f fix: 修复配置文件没有有效迁移的问题 2024-05-25 20:59:37 +08:00
Soulter 4d36ffcb08 fix: 优化插件的结果处理 2024-05-25 18:46:38 +08:00
Soulter c653e492c4 Merge pull request #164 from Soulter/stat-upload-perf
/models 指令优化
2024-05-25 18:35:56 +08:00
Soulter f08de1f404 perf: 添加 models 指令到帮助中 2024-05-25 18:34:08 +08:00
Soulter 1218691b61 perf: model 指令放宽限制,支持输入自定义模型。设置模型后持久化保存。 2024-05-25 18:29:01 +08:00
Soulter 61fc27ff79 Merge pull request #163 from Soulter/stat-upload-perf
优化统计记录数据结构
2024-05-25 18:28:08 +08:00
Soulter 123ee24f7e fix: stat perf 2024-05-25 18:01:16 +08:00
Soulter 52c9045a28 feat: 优化了统计信息数据结构 2024-05-25 17:47:41 +08:00
21 changed files with 210 additions and 96 deletions
+3 -1
View File
@@ -133,7 +133,7 @@
- `LLMS`: https://github.com/Soulter/llms | Claude, HuggingChat 大语言模型接入。
- `GoodPlugins`: https://github.com/Soulter/goodplugins | 随机动漫图片、搜番、喜报生成器等
- `GoodPlugins`: https://github.com/Soulter/goodplugins | 随机动漫图片、搜番、喜报生成器等
- `sysstat`: https://github.com/Soulter/sysstatqcbot | 查看系统状态
@@ -141,6 +141,8 @@
- `liferestart`: https://github.com/Soulter/liferestart | 人生重开模拟器
- `astrbot_plugin_aiocqhttp`: https://github.com/Soulter/astrbot_plugin_aiocqhttp | aiocqhttp 适配器,支持接入支持反向 WS 的 OneBot 协议实现,如 Lagrange.OneBotShamrock 等。
<img width="900" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/824d1ff3-7b85-481c-b795-8e62dedb9fd7">
+2 -3
View File
@@ -16,7 +16,7 @@ from persist.session import dbConn
from type.register import RegisteredPlugin
from typing import List
from util.cmd_config import CmdConfig
from util.updator import check_update, update_project, request_release_info
from util.updator import check_update, update_project, request_release_info, _reboot
from SparkleLogging.utils.core import LogManager
from logging import Logger
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
@@ -344,8 +344,7 @@ class AstrBotDashBoard():
def shutdown_bot(self, delay_s: int):
time.sleep(delay_s)
py = sys.executable
os.execl(py, py, *sys.argv)
_reboot()
def _get_configs(self, namespace: str):
if namespace == "":
+9 -26
View File
@@ -22,6 +22,7 @@ from util.cmd_config import init_astrbot_config_items
from type.types import GlobalObject
from type.register import *
from type.message import AstrBotMessage
from type.config import *
from addons.dashboard.helper import DashBoardHelper
from addons.dashboard.server import DashBoardData
from persist.session import dbConn
@@ -38,9 +39,6 @@ frequency_time = 60
# 计数默认值
frequency_count = 10
# 版本
version = '3.1.13'
# 语言模型
OPENAI_OFFICIAL = 'openai_official'
NONE_LLM = 'none_llm'
@@ -56,13 +54,9 @@ baidu_judge = None
# CLI
PLATFORM_CLI = 'cli'
init_astrbot_config_items()
# 全局对象
_global_object: GlobalObject = None
# 语言模型选择
def privider_chooser(cfg):
l = []
@@ -70,21 +64,16 @@ def privider_chooser(cfg):
l.append('openai_official')
return l
'''
初始化机器人
'''
def init():
'''
初始化机器人
'''
global llm_instance, llm_command_instance
global baidu_judge, chosen_provider
global frequency_count, frequency_time
global _global_object
# 迁移旧配置
gu.try_migrate_config()
# 使用新配置
init_astrbot_config_items()
cfg = cc.get_all()
_event_loop = asyncio.new_event_loop()
@@ -92,9 +81,10 @@ def init():
# 初始化 global_object
_global_object = GlobalObject()
_global_object.version = version
_global_object.version = VERSION
_global_object.base_config = cfg
logger.info("AstrBot v"+version)
_global_object.logger = logger
logger.info("AstrBot v" + VERSION)
if 'reply_prefix' in cfg:
# 适配旧版配置
@@ -182,7 +172,7 @@ def init():
logger.info("正在载入插件...")
# 加载插件
_command = Command(None, _global_object)
ok, err = putil.plugin_reload(_global_object.cached_plugins)
ok, err = putil.plugin_reload(_global_object)
if ok:
logger.info(
f"成功载入 {len(_global_object.cached_plugins)} 个插件")
@@ -319,7 +309,6 @@ async def record_message(platform: str, session_id: str):
db_inst.increment_stat_session(platform, session_id, 1)
db_inst.increment_stat_message(curr_ts, 1)
db_inst.increment_stat_platform(curr_ts, platform, 1)
_global_object.cnt_total += 1
async def oper_msg(message: AstrBotMessage,
@@ -453,12 +442,6 @@ async def oper_msg(message: AstrBotMessage,
return
command = command_result[2]
if command == "update latest r":
def update_restart():
py = sys.executable
os.execl(py, py, *sys.argv)
return MessageResult(command_result[1] + "\n\n即将自动重启。", callback=update_restart)
if not command_result[0]:
return MessageResult(f"指令调用错误: \n{str(command_result[1])}")
+6 -7
View File
@@ -5,12 +5,11 @@ import warnings
import traceback
import threading
from logging import Formatter, Logger
from util.cmd_config import CmdConfig, try_migrate_config
warnings.filterwarnings("ignore")
abs_path = os.path.dirname(os.path.realpath(sys.argv[0])) + '/'
logger: Logger = None
logo_tmpl = """
___ _______.___________..______ .______ ______ .___________.
/ \ / | || _ \ | _ \ / __ \ | |
@@ -34,9 +33,11 @@ def update_dept():
'''
# 获取 Python 可执行文件路径
py = sys.executable
requirements_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
print(requirements_path)
# 更新依赖库
mirror = "https://mirrors.aliyun.com/pypi/simple/"
os.system(f"{py} -m pip install -r requirements.txt -i {mirror}")
os.system(f"{py} -m pip install -r {requirements_path} -i {mirror}")
def main():
try:
@@ -77,9 +78,9 @@ def check_env():
exit()
if __name__ == "__main__":
update_dept()
# 设置代理
from util.cmd_config import CmdConfig
try_migrate_config()
cc = CmdConfig()
http_proxy = cc.get("http_proxy")
https_proxy = cc.get("https_proxy")
@@ -88,8 +89,6 @@ if __name__ == "__main__":
if https_proxy:
os.environ['HTTPS_PROXY'] = https_proxy
os.environ['NO_PROXY'] = 'https://api.sgroup.qq.com'
update_dept()
from SparkleLogging.utils.core import LogManager
logger = LogManager.GetLogger(
+3
View File
@@ -64,6 +64,8 @@ class Command:
result = await plugin.plugin_instance.run(ame)
else:
result = await asyncio.to_thread(plugin.plugin_instance.run, ame)
if not result:
continue
if isinstance(result, CommandResult):
hit = result.hit
res = result._result_tuple()
@@ -73,6 +75,7 @@ class Command:
else:
raise TypeError("插件返回值格式错误。")
if hit:
plugin.trig()
logger.debug("hit plugin: " + plugin.metadata.plugin_name)
return True, res
except TypeError as e:
+17 -19
View File
@@ -84,6 +84,7 @@ class CommandOpenAIOfficial(Command):
for model in models:
ret += f"\n{i}. {model.id}"
i += 1
ret += "\nTips: 使用 /model 模型名/编号,即可实时更换模型。如目标模型不存在于上表,请输入模型名。"
logger.debug(ret)
return True, ret, "models"
@@ -93,31 +94,28 @@ class CommandOpenAIOfficial(Command):
if len(l) == 1:
return True, "请输入 /model 模型名/编号", "model"
model = str(l[1])
models = await self.get_models()
models = list(models)
if model.isdigit() and int(model) <= len(models) and int(model) >= 1:
model = models[int(model)-1]
if model.isdigit():
models = await self.get_models()
models = list(models)
if int(model) <= len(models) and int(model) >= 1:
model = models[int(model)-1]
self.provider.set_model(model.id)
return True, f"模型已设置为 {model.id}", "model"
else:
f = False
for m in models:
if model == m.id:
f = True
break
if not f:
return True, "模型不存在或输入非法", "model"
self.provider.set_model(model)
return True, f"模型已设置为 {model} (自定义)", "model"
self.provider.set_model(model.id)
return True, f"模型已设置为 {model.id}", "model"
async def help(self):
commands = super().general_commands()
commands[''] = '调用 OpenAI DallE 模型生成图片'
commands['set'] = '人格设置面板'
commands['status'] = '查看 Api Key 状态和配置信息'
commands['token'] = '查看本轮会话 token'
commands['reset'] = '重置当前与 LLM 的会话,但保留人格(system prompt'
commands['reset p'] = '重置当前与 LLM 的会话,并清除人格。'
commands['/set'] = '人格设置面板'
commands['/status'] = '查看 Api Key 状态和配置信息'
commands['/token'] = '查看本轮会话 token'
commands['/reset'] = '重置当前与 LLM 的会话,但保留人格(system prompt'
commands['/reset p'] = '重置当前与 LLM 的会话,并清除人格。'
commands['/models'] = '获取当前可用的模型'
commands['/model'] = '更换模型'
return True, await super().help_messager(commands, self.platform, self.global_object.cached_plugins), "help"
+10 -4
View File
@@ -14,34 +14,40 @@ class Platform():
初始化平台的各种接口
'''
self.message_handler = message_handler
self.cnt_receive = 0
self.cnt_reply = 0
pass
@abc.abstractmethod
async def handle_msg():
async def handle_msg(self):
'''
处理到来的消息
'''
self.cnt_receive += 1
pass
@abc.abstractmethod
async def reply_msg():
async def reply_msg(self):
'''
回复消息(被动发送)
'''
self.cnt_reply += 1
pass
@abc.abstractmethod
async def send_msg(target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
async def send_msg(self, target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
'''
发送消息(主动发送)
'''
self.cnt_reply += 1
pass
@abc.abstractmethod
async def send(target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
async def send(self, target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
'''
发送消息(主动发送)同 send_msg()
'''
self.cnt_reply += 1
pass
def parse_message_outline(self, message: Union[GuildMessage, GroupMessage, FriendMessage, str, list]) -> str:
+4
View File
@@ -104,6 +104,7 @@ class QQGOCQ(Platform):
self.client.run()
async def handle_msg(self, message: AstrBotMessage):
await super().handle_msg()
logger.info(
f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}")
@@ -176,6 +177,7 @@ class QQGOCQ(Platform):
async def reply_msg(self,
message: Union[AstrBotMessage, GuildMessage, GroupMessage, FriendMessage],
result_message: list):
await super().reply_msg()
"""
插件开发者请使用send方法, 可以不用直接调用这个方法。
"""
@@ -254,6 +256,7 @@ class QQGOCQ(Platform):
提供给插件的发送QQ消息接口。
参数说明:第一个参数可以是消息对象,也可以是QQ群号。第二个参数是消息内容(消息内容可以是消息链列表,也可以是纯文字信息)。
'''
await super().reply_msg()
try:
await self.reply_msg(message, result_message)
except BaseException as e:
@@ -265,6 +268,7 @@ class QQGOCQ(Platform):
'''
同 send_msg()
'''
await super().reply_msg()
await self.reply_msg(to, res)
def create_text_image(title: str, text: str, max_width=30, font_size=20):
+2
View File
@@ -102,6 +102,7 @@ class QQOfficial(Platform):
)
async def handle_msg(self, message: AstrBotMessage):
await super().handle_msg()
assert isinstance(message.raw_message, (botpy.message.Message,
botpy.message.GroupMessage, botpy.message.DirectMessage))
is_group = message.type != MessageType.FRIEND_MESSAGE
@@ -154,6 +155,7 @@ class QQOfficial(Platform):
'''
回复频道消息
'''
await super().reply_msg()
if isinstance(message, AstrBotMessage):
source = message.raw_message
else:
+5
View File
@@ -73,6 +73,7 @@ class ProviderOpenAIOfficial(Provider):
base_url=self.base_url
)
self.model_configs: Dict = cfg['chatGPTConfigs']
super().set_curr_model(self.model_configs['model'])
self.image_generator_model_configs: Dict = self.cc.get('openai_image_generate', None)
self.session_memory: Dict[str, List] = {} # 会话记忆
self.session_memory_lock = threading.Lock()
@@ -289,6 +290,7 @@ class ProviderOpenAIOfficial(Provider):
extra_conf: Dict = None,
**kwargs
) -> str:
super().accu_model_stat()
if not session_id:
session_id = "unknown"
if "unknown" in self.session_memory:
@@ -421,6 +423,7 @@ class ProviderOpenAIOfficial(Provider):
'''
retry = 0
conf = self.image_generator_model_configs
super().accu_model_stat(model=conf['model'])
if not conf:
logger.error("OpenAI 图片生成模型配置不存在。")
raise Exception("OpenAI 图片生成模型配置不存在。")
@@ -481,6 +484,8 @@ class ProviderOpenAIOfficial(Provider):
def set_model(self, model: str):
self.model_configs['model'] = model
self.cc.put_by_dot_str("openai.chatGPTConfigs.model", model)
super().set_curr_model(model)
def get_configs(self):
return self.model_configs
+26 -3
View File
@@ -1,4 +1,27 @@
from collections import defaultdict
class Provider:
def __init__(self) -> None:
self.model_stat = defaultdict(int) # 用于记录 LLM Model 使用数据
self.curr_model_name = "unknown"
def reset_model_stat(self):
self.model_stat.clear()
def set_curr_model(self, model_name: str):
self.curr_model_name = model_name
def get_curr_model(self):
'''
返回当前正在使用的 LLM
'''
return self.curr_model_name
def accu_model_stat(self, model: str = None):
if not model:
model = self.get_curr_model()
self.model_stat[model] += 1
async def text_chat(self,
prompt: str,
session_id: str,
@@ -18,7 +41,7 @@ class Provider:
extra_conf: 额外配置
default_personality: 默认人格
'''
raise NotImplementedError
raise NotImplementedError()
async def image_generate(self, prompt, session_id, **kwargs) -> str:
'''
@@ -26,10 +49,10 @@ class Provider:
prompt: 提示词
session_id: 会话id
'''
raise NotImplementedError
raise NotImplementedError()
async def forget(self, session_id=None) -> bool:
'''
重置会话
'''
raise NotImplementedError
raise NotImplementedError()
+1 -1
View File
@@ -18,7 +18,7 @@ class CommandResult():
用于在Command中返回多个值
'''
def __init__(self, hit: bool, success: bool, message_chain: list, command_name: str = "unknown_command") -> None:
def __init__(self, hit: bool, success: bool = False, message_chain: list = [], command_name: str = "unknown_command") -> None:
self.hit = hit
self.success = success
self.message_chain = message_chain
+1
View File
@@ -0,0 +1 @@
VERSION = '3.2.3'
+1 -1
View File
@@ -2,7 +2,7 @@ from enum import Enum
from dataclasses import dataclass
class PluginType(Enum):
PLATFORM = 'platfrom' # 平台类插件。
PLATFORM = 'platform' # 平台类插件。
LLM = 'llm' # 大语言模型类插件
COMMON = 'common' # 其他插件
+7
View File
@@ -15,6 +15,13 @@ class RegisteredPlugin:
module_path: str
module: ModuleType
root_dir_name: str
trig_cnt: int = 0
def reset_trig_cnt(self):
self.trig_cnt = 0
def trig(self):
self.trig_cnt += 1
def __str__(self) -> str:
return f"RegisteredPlugin({self.metadata}, {self.module_path}, {self.root_dir_name})"
+4 -2
View File
@@ -1,5 +1,7 @@
from type.register import *
from typing import List
from logging import Logger
class GlobalObject:
'''
@@ -15,9 +17,10 @@ class GlobalObject:
web_search: bool # 是否开启了网页搜索
reply_prefix: str # 回复前缀
unique_session: bool # 是否开启了独立会话
cnt_total: int # 总消息数
default_personality: dict
dashboard_data = None
logger: Logger = None
def __init__(self):
self.nick = None # gocq 的昵称
@@ -26,7 +29,6 @@ class GlobalObject:
self.web_search = False # 是否开启了网页搜索
self.reply_prefix = None
self.unique_session = False
self.cnt_total = 0
self.platforms = []
self.llms = []
self.default_personality = None
+26
View File
@@ -1,5 +1,6 @@
import os
import json
import yaml
from typing import Union
cpath = "data/cmd_config.json"
@@ -117,3 +118,28 @@ def init_astrbot_config_items():
cc.init_attributes("https_proxy", "")
cc.init_attributes("dashboard_username", "")
cc.init_attributes("dashboard_password", "")
def try_migrate_config():
'''
将 cmd_config.json 迁移至 data/cmd_config.json
'''
print("try migrate configs")
if os.path.exists("cmd_config.json"):
with open("cmd_config.json", "r", encoding="utf-8-sig") as f:
data = json.load(f)
with open("data/cmd_config.json", "w", encoding="utf-8-sig") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
try:
os.remove("cmd_config.json")
except Exception as e:
pass
if not os.path.exists("cmd_config.json") and not os.path.exists("data/cmd_config.json"):
# 从 configs/config.yaml 上拿数据
configs_pth = os.path.abspath(os.path.join(os.path.abspath(__file__), "../../configs/config.yaml"))
with open(configs_pth, encoding='utf-8') as f:
data = yaml.load(f, Loader=yaml.Loader)
print(data)
with open("data/cmd_config.json", "w", encoding="utf-8-sig") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
+33 -22
View File
@@ -422,21 +422,6 @@ def create_markdown_image(text: str):
raise e
def try_migrate_config():
'''
将 cmd_config.json 迁移至 data/cmd_config.json
'''
if os.path.exists("cmd_config.json"):
with open("cmd_config.json", "r", encoding="utf-8-sig") as f:
data = json.load(f)
with open("data/cmd_config.json", "w", encoding="utf-8-sig") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
try:
os.remove("cmd_config.json")
except Exception as e:
pass
def get_local_ip_addresses():
ip = ''
try:
@@ -466,15 +451,41 @@ def get_sys_info(global_object: GlobalObject):
def upload(_global_object: GlobalObject):
'''
上传相关非敏感统计数据
'''
time.sleep(10)
while True:
addr_ip = ''
platform_stats = {}
llm_stats = {}
plugin_stats = {}
for platform in _global_object.platforms:
platform_stats[platform.platform_name] = {
"cnt_receive": platform.platform_instance.cnt_receive,
"cnt_reply": platform.platform_instance.cnt_reply
}
for llm in _global_object.llms:
stat = llm.llm_instance.model_stat
for k in stat:
llm_stats[llm.llm_name + "#" + k] = stat[k]
llm.llm_instance.reset_model_stat()
for plugin in _global_object.cached_plugins:
plugin_stats[plugin.metadata.plugin_name] = {
"metadata": plugin.metadata,
"trig_cnt": plugin.trig_cnt
}
plugin.reset_trig_cnt()
try:
res = {
"version": _global_object.version,
"count": _global_object.cnt_total,
"ip": addr_ip,
"sys": sys.platform,
"admin": "null",
"stat_version": "moon",
"version": _global_object.version, # 版本号
"platform_stats": platform_stats, # 过去 30 分钟各消息平台交互消息数
"llm_stats": llm_stats,
"plugin_stats": plugin_stats,
"sys": sys.platform, # 系统版本
}
resp = requests.post(
'https://api.soulter.top/upload', data=json.dumps(res), timeout=5)
@@ -484,7 +495,7 @@ def upload(_global_object: GlobalObject):
_global_object.cnt_total = 0
except BaseException as e:
pass
time.sleep(10*60)
time.sleep(30*60)
def retry(n: int = 3):
'''
+1 -1
View File
@@ -1,5 +1,5 @@
from astrbot.core import oper_msg
from type.message import AstrMessageEvent, AstrBotMessage
from type.message import *
from type.command import CommandResult
from model.platform._message_result import MessageResult
+21 -4
View File
@@ -16,6 +16,7 @@ from type.plugin import *
from type.register import *
from SparkleLogging.utils.core import LogManager
from logging import Logger
from type.types import GlobalObject
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
@@ -91,7 +92,19 @@ def check_plugin_dept_update(cached_plugins: RegisteredPlugins, target_plugin: s
update_plugin_dept(os.path.join(plugin_path, "requirements.txt"))
def plugin_reload(cached_plugins: RegisteredPlugins):
def has_init_param(cls, param_name):
try:
# 获取 __init__ 方法的签名
init_signature = inspect.signature(cls.__init__)
# 检查参数名是否在签名中
return param_name in init_signature.parameters
except (AttributeError, ValueError):
# 如果类没有 __init__ 方法或者无法获取签名
return False
def plugin_reload(ctx: GlobalObject):
cached_plugins = ctx.cached_plugins
plugins = get_plugin_modules()
if plugins is None:
return False, "未找到任何插件模块"
@@ -113,7 +126,12 @@ def plugin_reload(cached_plugins: RegisteredPlugins):
root_dir_name + "." + p, fromlist=[p])
cls = get_classes(p, module)
obj = getattr(module, cls[0])()
try:
# 尝试传入 ctx
obj = getattr(module, cls[0])(ctx=ctx)
except:
obj = getattr(module, cls[0])()
metadata = None
try:
@@ -125,8 +143,7 @@ def plugin_reload(cached_plugins: RegisteredPlugins):
else:
metadata = PluginMetadata(
plugin_name=info['name'],
plugin_type=PluginType.COMMON if 'plugin_type' not in info else PluginType(
info['plugin_type']),
plugin_type=PluginType.COMMON if 'plugin_type' not in info else PluginType(info['plugin_type']),
author=info['author'],
desc=info['desc'],
version=info['version'],
+28 -2
View File
@@ -6,9 +6,35 @@ except BaseException as e:
has_git = False
import sys, os
import requests
import psutil
from type.config import VERSION
from SparkleLogging.utils.core import LogManager
from logging import Logger
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
def terminate_child_processes():
try:
parent = psutil.Process(os.getpid())
children = parent.children(recursive=True)
logger.info(f"正在终止 {len(children)} 个子进程。")
for child in children:
logger.info(f"正在终止子进程 {child.pid}")
child.terminate()
try:
child.wait(timeout=3)
except psutil.NoSuchProcess:
continue
except psutil.TimeoutExpired:
logger.info(f"子进程 {child.pid} 没有被正常终止, 正在强行杀死。")
child.kill()
except psutil.NoSuchProcess:
pass
def _reboot():
py = sys.executable
terminate_child_processes()
os.execl(py, py, *sys.argv)
def find_repo() -> Repo:
@@ -78,11 +104,11 @@ def check_update() -> str:
print(f"当前版本: {curr_commit}")
print(f"最新版本: {new_commit}")
if curr_commit.startswith(new_commit):
return "当前已经是最新版本"
return f"当前已经是最新版本: v{VERSION}"
else:
update_info = f"""有新版本可用。
=== 当前版本 ===
{curr_commit}
v{VERSION}
=== 新版本 ===
{update_data[0]['version']}