新增app_manager.py文件负责app的安装卸载注册等生命周期管理

This commit is contained in:
jingrow 2025-10-29 21:58:05 +08:00
parent e06566c3fc
commit 33dee5c329
4 changed files with 170 additions and 30 deletions

View File

@ -24,6 +24,7 @@ from jingrow.utils.jingrow_api import get_jingrow_api_headers
from jingrow.utils.auth import get_jingrow_cloud_url, get_jingrow_cloud_api_headers, get_jingrow_cloud_api_url, get_jingrow_api_headers
from jingrow.utils.export_app_package import export_app_package_from_local
from jingrow.config import Config
from jingrow.utils.app_manager import update_apps_txt
logger = logging.getLogger(__name__)
router = APIRouter()
@ -151,6 +152,9 @@ async def install_app_from_upload(
# 额外:确保创建 Package 与 Module Defcustom记录上传安装
ensure_package_and_module(app_name_result)
# 更新 apps.txt确保路由生效
update_apps_txt(app_name_result, add=True)
# 2. 调用 sync_app_files 同步文件到数据库
if app_dir:
current = Path(__file__).resolve()
@ -396,6 +400,9 @@ async def install_local_app(request: Request, app_name: str):
# 确保创建 Package 与 Module Defcustom记录本地扫描安装
ensure_package_and_module(app_name)
# 更新 apps.txt确保路由生效
update_apps_txt(app_name, add=True)
try:
api_url = f"{Config.jingrow_server_url}/api/action/jingrow.ai.utils.jlocal.sync_app_files"
requests.post(
@ -599,6 +606,9 @@ async def install_from_git(repo_url: str = Form(...)):
# 确保创建 Package 与 Module Defcustom记录
ensure_package_and_module(app_name)
# 更新 apps.txt确保路由生效
update_apps_txt(app_name, add=True)
except Exception as e:
logger.error(f"Failed to register app: {e}")
@ -690,6 +700,9 @@ async def install_from_url(url: str = Form(...)):
# 额外:确保创建 Package 与 Module Defcustom记录URL安装
ensure_package_and_module(app_name_result)
# 更新 apps.txt确保路由生效
update_apps_txt(app_name_result, add=True)
# 2. 调用 sync_app_files 同步文件到数据库
if app_dir:
current = Path(__file__).resolve()
@ -820,6 +833,9 @@ async def uninstall_app(request: Request, app_name: str):
shutil.rmtree(app_dir)
await _remove_from_database(app_name)
# 更新 apps.txt确保路由生效
update_apps_txt(app_name, add=False)
return {
'success': True,
'message': f'应用 {app_name} 卸载成功'

View File

@ -8,44 +8,16 @@ Jingrow 白名单路由服务
from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import JSONResponse
import importlib
import sys
from pathlib import Path
from typing import Any, Dict
import logging
from jingrow import get_whitelisted_function, is_whitelisted
from jingrow.utils.auth import get_jingrow_api_headers, get_session_api_headers
from jingrow.utils.jingrow_api import get_logged_user
from jingrow.utils.app_manager import ensure_apps_on_sys_path
logger = logging.getLogger(__name__)
router = APIRouter()
# 确保各 app 根目录在 sys.path 中(仅初始化一次)
_apps_path_initialized = False
def _ensure_apps_on_sys_path():
"""确保各 app 根目录在 sys.path 中,支持跨 app 导入"""
global _apps_path_initialized
if _apps_path_initialized:
return
try:
project_root = Path(__file__).resolve().parents[4]
apps_dir = project_root / "apps"
# 读取 apps.txt添加各 app 的根目录apps/<app>
apps_txt = apps_dir / "apps.txt"
if apps_txt.exists():
for app_name in apps_txt.read_text().splitlines():
app_name = app_name.strip()
if app_name:
app_root_dir = apps_dir / app_name
if app_root_dir.exists() and str(app_root_dir) not in sys.path:
sys.path.insert(0, str(app_root_dir))
except Exception:
pass
finally:
_apps_path_initialized = True
async def authenticate_request(request: Request, allow_guest: bool) -> bool:
"""
认证请求支持两种认证方式
@ -98,7 +70,7 @@ async def _process_whitelist_call(request: Request, full_module_path: str):
return {}
# 确保 apps 目录在 sys.path 中(支持跨 app 导入)
_ensure_apps_on_sys_path()
ensure_apps_on_sys_path()
# 解析路径并导入
modulename = ".".join(full_module_path.split('.')[:-1])

View File

@ -18,6 +18,7 @@ import logging
from jingrow.config import Config
from jingrow.utils.jingrow_api import get_jingrow_api_headers
from jingrow.utils.jingrow_api import get_record_id, create_record
from jingrow.utils.app_manager import update_apps_txt
logger = logging.getLogger(__name__)
@ -349,6 +350,9 @@ def install_app(uploaded_file_path: str, app_name: str = None) -> Dict[str, Any]
# 忽略该步骤错误,不影响安装完成
pass
# 更新 apps.txt确保路由生效
update_apps_txt(app_name, add=True)
# 清理临时文件
cleanup_temp_dir(temp_dir)
return {

View File

@ -0,0 +1,148 @@
# Copyright (c) 2025, JINGROW and contributors
# For license information, please see license.txt
"""
Jingrow App 生命周期管理
统一管理 app 的安装卸载路由注册等生命周期操作
"""
import sys
import logging
from pathlib import Path
from typing import Set
logger = logging.getLogger(__name__)
# 确保各 app 根目录在 sys.path 中(仅初始化一次)
_apps_path_initialized = False
def get_project_root() -> Path:
"""获取项目根目录路径"""
# 从当前文件位置计算apps/jingrow/jingrow/utils/app_manager.py -> 向上4级
return Path(__file__).resolve().parents[4]
def get_apps_dir() -> Path:
"""获取 apps 目录路径"""
return get_project_root() / "apps"
def get_apps_txt_path() -> Path:
"""获取 apps.txt 文件路径"""
return get_apps_dir() / "apps.txt"
def ensure_apps_on_sys_path():
"""
确保各 app 根目录在 sys.path 支持跨 app 导入
在路由处理时自动调用确保新安装的 app 路由可以生效
"""
global _apps_path_initialized
if _apps_path_initialized:
return
try:
apps_dir = get_apps_dir()
apps_txt = get_apps_txt_path()
# 读取 apps.txt添加各 app 的根目录apps/<app>
if apps_txt.exists():
for app_name in apps_txt.read_text(encoding='utf-8').splitlines():
app_name = app_name.strip()
if app_name:
app_root_dir = apps_dir / app_name
if app_root_dir.exists() and str(app_root_dir) not in sys.path:
sys.path.insert(0, str(app_root_dir))
logger.debug(f"已添加 app 路径到 sys.path: {app_root_dir}")
except Exception as e:
logger.warning(f"初始化 app 路径失败: {e}")
finally:
_apps_path_initialized = True
def reset_apps_path_cache():
"""
重置 apps path 缓存强制下次请求时重新初始化
在安装/卸载 app 后调用确保路由立即生效
"""
global _apps_path_initialized
_apps_path_initialized = False
logger.debug("已重置 apps path 缓存")
def get_apps_from_txt() -> Set[str]:
"""
apps.txt 读取已注册的 app 列表
Returns:
Set[str]: app 名称集合
"""
apps_txt = get_apps_txt_path()
apps = set()
if apps_txt.exists():
for line in apps_txt.read_text(encoding='utf-8').splitlines():
line = line.strip()
if line:
apps.add(line)
return apps
def update_apps_txt(app_name: str, add: bool = True) -> bool:
"""
更新 apps.txt 文件保持原有顺序新增仅在末尾追加
Args:
app_name: 应用名称
add: True 为添加False 为删除
Returns:
bool: 是否成功更新
"""
try:
apps_txt = get_apps_txt_path()
apps_txt.parent.mkdir(parents=True, exist_ok=True)
# 读取现有的 app 列表(保持顺序)
if apps_txt.exists():
lines = [ln.strip() for ln in apps_txt.read_text(encoding='utf-8').splitlines()]
lines = [ln for ln in lines if ln]
else:
lines = []
if add:
if app_name not in lines:
lines.append(app_name)
logger.info(f"添加应用到 apps.txt: {app_name}")
else:
new_lines = [ln for ln in lines if ln != app_name]
if len(new_lines) != len(lines):
logger.info(f"从 apps.txt 移除应用: {app_name}")
lines = new_lines
# 写回文件(保持顺序,统一以单个换行分隔,并以换行结尾)
apps_txt.write_text('\n'.join(lines) + ('\n' if lines else ''), encoding='utf-8')
# 重置缓存,确保下次请求时重新加载
reset_apps_path_cache()
return True
except Exception as e:
logger.error(f"更新 apps.txt 失败: {e}", exc_info=True)
return False
def is_app_registered(app_name: str) -> bool:
"""
检查 app 是否已注册到 apps.txt
Args:
app_name: 应用名称
Returns:
bool: 是否已注册
"""
return app_name in get_apps_from_txt()