mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-08 14:56:03 +00:00
chore: Add PyPI package support for uvx/pip installation (#1764)
* Initial plan * Add package structure and resource path utilities - Created langbot/ package with __init__.py and __main__.py entry point - Added paths utility to find frontend and resource files from package installation - Updated config loading to use resource paths - Updated frontend serving to use resource paths - Added MANIFEST.in for package data inclusion - Updated pyproject.toml with build system and entry points Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Add PyPI publishing workflow and update license - Created GitHub Actions workflow to build frontend and publish to PyPI - Added license field to pyproject.toml to fix deprecation warning - Updated .gitignore to exclude build artifacts - Tested package building successfully Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Add PyPI installation documentation - Created PYPI_INSTALLATION.md with detailed installation and usage instructions - Updated README.md to feature uvx/pip installation as recommended method - Updated README_EN.md with same changes for English documentation Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Address code review feedback - Made package-data configuration more specific to langbot package only - Improved path detection with caching to avoid repeated file I/O - Removed sys.path searching which was incorrect for package data - Removed interactive input() call for non-interactive environment compatibility - Simplified error messages for version check Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Fix code review issues - Use specific exception types instead of bare except - Fix misleading comments about directory levels - Remove redundant existence check before makedirs with exist_ok=True - Use context manager for file opening to ensure proper cleanup Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Simplify package configuration and document behavioral differences - Removed redundant package-data configuration, relying on MANIFEST.in - Added documentation about behavioral differences between package and source installation - Clarified that include-package-data=true uses MANIFEST.in for data files Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * chore: update pyproject.toml * chore: try pack templates in langbot/ * chore: update * chore: update * chore: update * chore: update * chore: update * chore: adjust dir structure * chore: fix imports * fix: read default-pipeline-config.json * fix: read default-pipeline-config.json * fix: tests * ci: publish pypi * chore: bump version 4.6.0-beta.1 for testing * chore: add templates/** * fix: send adapters and requesters icons * chore: bump version 4.6.0b2 for testing * chore: add platform field for docker-compose.yaml --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
211
src/langbot/pkg/utils/version.py
Normal file
211
src/langbot/pkg/utils/version.py
Normal file
@@ -0,0 +1,211 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import typing
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from ..core import app
|
||||
from . import constants
|
||||
|
||||
|
||||
class VersionManager:
|
||||
"""版本管理器"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
def get_current_version(self) -> str:
|
||||
current_tag = constants.semantic_version
|
||||
|
||||
return current_tag
|
||||
|
||||
async def get_release_list(self) -> list:
|
||||
"""获取发行列表"""
|
||||
try:
|
||||
rls_list_resp = requests.get(
|
||||
url='https://api.github.com/repos/langbot-app/LangBot/releases',
|
||||
proxies=self.ap.proxy_mgr.get_forward_proxies(),
|
||||
timeout=5,
|
||||
)
|
||||
rls_list_resp.raise_for_status() # 检查请求是否成功
|
||||
rls_list = rls_list_resp.json()
|
||||
return rls_list
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f'获取发行列表失败: {e}')
|
||||
pass
|
||||
return []
|
||||
|
||||
async def update_all(self):
|
||||
"""检查更新并下载源码"""
|
||||
|
||||
current_tag = self.get_current_version()
|
||||
|
||||
rls_list = await self.get_release_list()
|
||||
|
||||
latest_rls = {}
|
||||
rls_notes = []
|
||||
latest_tag_name = ''
|
||||
for rls in rls_list:
|
||||
rls_notes.append(rls['name']) # 使用发行名称作为note
|
||||
if latest_tag_name == '':
|
||||
latest_tag_name = rls['tag_name']
|
||||
|
||||
if rls['tag_name'] == current_tag:
|
||||
break
|
||||
|
||||
if latest_rls == {}:
|
||||
latest_rls = rls
|
||||
self.ap.logger.info('更新日志: {}'.format(rls_notes))
|
||||
|
||||
if latest_rls == {} and not self.is_newer(latest_tag_name, current_tag): # 没有新版本
|
||||
return False
|
||||
|
||||
# 下载最新版本的zip到temp目录
|
||||
self.ap.logger.info('开始下载最新版本: {}'.format(latest_rls['zipball_url']))
|
||||
|
||||
zip_url = latest_rls['zipball_url']
|
||||
zip_resp = requests.get(url=zip_url, proxies=self.ap.proxy_mgr.get_forward_proxies())
|
||||
zip_data = zip_resp.content
|
||||
|
||||
# 检查temp/updater目录
|
||||
if not os.path.exists('temp'):
|
||||
os.mkdir('temp')
|
||||
if not os.path.exists('temp/updater'):
|
||||
os.mkdir('temp/updater')
|
||||
with open('temp/updater/{}.zip'.format(latest_rls['tag_name']), 'wb') as f:
|
||||
f.write(zip_data)
|
||||
|
||||
self.ap.logger.info('下载最新版本完成: {}'.format('temp/updater/{}.zip'.format(latest_rls['tag_name'])))
|
||||
|
||||
# 解压zip到temp/updater/<tag_name>/
|
||||
import zipfile
|
||||
|
||||
# 检查目标文件夹
|
||||
if os.path.exists('temp/updater/{}'.format(latest_rls['tag_name'])):
|
||||
import shutil
|
||||
|
||||
shutil.rmtree('temp/updater/{}'.format(latest_rls['tag_name']))
|
||||
os.mkdir('temp/updater/{}'.format(latest_rls['tag_name']))
|
||||
with zipfile.ZipFile('temp/updater/{}.zip'.format(latest_rls['tag_name']), 'r') as zip_ref:
|
||||
zip_ref.extractall('temp/updater/{}'.format(latest_rls['tag_name']))
|
||||
|
||||
# 覆盖源码
|
||||
source_root = ''
|
||||
# 找到temp/updater/<tag_name>/中的第一个子目录路径
|
||||
for root, dirs, files in os.walk('temp/updater/{}'.format(latest_rls['tag_name'])):
|
||||
if root != 'temp/updater/{}'.format(latest_rls['tag_name']):
|
||||
source_root = root
|
||||
break
|
||||
|
||||
# 覆盖源码
|
||||
import shutil
|
||||
|
||||
for root, dirs, files in os.walk(source_root):
|
||||
# 覆盖所有子文件子目录
|
||||
for file in files:
|
||||
src = os.path.join(root, file)
|
||||
dst = src.replace(source_root, '.')
|
||||
if os.path.exists(dst):
|
||||
os.remove(dst)
|
||||
|
||||
# 检查目标文件夹是否存在
|
||||
if not os.path.exists(os.path.dirname(dst)):
|
||||
os.makedirs(os.path.dirname(dst))
|
||||
# 检查目标文件是否存在
|
||||
if not os.path.exists(dst):
|
||||
# 创建目标文件
|
||||
open(dst, 'w').close()
|
||||
|
||||
shutil.copy(src, dst)
|
||||
|
||||
# 把current_tag写入文件
|
||||
current_tag = latest_rls['tag_name']
|
||||
with open('current_tag', 'w') as f:
|
||||
f.write(current_tag)
|
||||
|
||||
# TODO statistics
|
||||
|
||||
async def is_new_version_available(self) -> bool:
|
||||
"""检查是否有新版本"""
|
||||
# 从github获取release列表
|
||||
rls_list = await self.get_release_list()
|
||||
if rls_list is None:
|
||||
return False
|
||||
|
||||
# 获取当前版本
|
||||
current_tag = self.get_current_version()
|
||||
|
||||
# 检查是否有新版本
|
||||
latest_tag_name = ''
|
||||
for rls in rls_list:
|
||||
if latest_tag_name == '':
|
||||
latest_tag_name = rls['tag_name']
|
||||
break
|
||||
|
||||
return self.is_newer(latest_tag_name, current_tag)
|
||||
|
||||
def is_newer(self, new_tag: str, old_tag: str):
|
||||
"""判断版本是否更新,忽略第四位版本和第一位版本"""
|
||||
if new_tag == old_tag:
|
||||
return False
|
||||
|
||||
new_tag = new_tag.split('.')
|
||||
old_tag = old_tag.split('.')
|
||||
|
||||
# 判断主版本是否相同
|
||||
if new_tag[0] != old_tag[0]:
|
||||
return False
|
||||
|
||||
if len(new_tag) < 4:
|
||||
return True
|
||||
|
||||
# 合成前三段,判断是否相同
|
||||
new_tag = '.'.join(new_tag[:3])
|
||||
old_tag = '.'.join(old_tag[:3])
|
||||
|
||||
return new_tag != old_tag
|
||||
|
||||
def compare_version_str(v0: str, v1: str) -> int:
|
||||
"""比较两个版本号"""
|
||||
|
||||
# 删除版本号前的v
|
||||
if v0.startswith('v'):
|
||||
v0 = v0[1:]
|
||||
if v1.startswith('v'):
|
||||
v1 = v1[1:]
|
||||
|
||||
v0: list = v0.split('.')
|
||||
v1: list = v1.split('.')
|
||||
|
||||
# 如果两个版本号节数不同,把短的后面用0补齐
|
||||
if len(v0) < len(v1):
|
||||
v0.extend(['0'] * (len(v1) - len(v0)))
|
||||
elif len(v0) > len(v1):
|
||||
v1.extend(['0'] * (len(v0) - len(v1)))
|
||||
|
||||
# 从高位向低位比较
|
||||
for i in range(len(v0)):
|
||||
if int(v0[i]) > int(v1[i]):
|
||||
return 1
|
||||
elif int(v0[i]) < int(v1[i]):
|
||||
return -1
|
||||
|
||||
return 0
|
||||
|
||||
async def show_version_update(self) -> typing.Tuple[str, int]:
|
||||
try:
|
||||
if await self.ap.ver_mgr.is_new_version_available():
|
||||
return (
|
||||
'New version available:\n有新版本可用,根据文档更新: \nhttps://docs.langbot.app/zh/deploy/update.html',
|
||||
logging.INFO,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return f'Error checking version update: {e}', logging.WARNING
|
||||
Reference in New Issue
Block a user