From 33dee5c32930f4b886efd41215e0e63cba0679b5 Mon Sep 17 00:00:00 2001 From: jingrow Date: Wed, 29 Oct 2025 21:58:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eapp=5Fmanager.py=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=B4=9F=E8=B4=A3app=E7=9A=84=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=8D=B8=E8=BD=BD=E6=B3=A8=E5=86=8C=E7=AD=89=E7=94=9F=E5=91=BD?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jingrow/api/local_app_installer.py | 16 ++ apps/jingrow/jingrow/services/whitelist.py | 32 +--- apps/jingrow/jingrow/utils/app_installer.py | 4 + apps/jingrow/jingrow/utils/app_manager.py | 148 ++++++++++++++++++ 4 files changed, 170 insertions(+), 30 deletions(-) create mode 100644 apps/jingrow/jingrow/utils/app_manager.py diff --git a/apps/jingrow/jingrow/api/local_app_installer.py b/apps/jingrow/jingrow/api/local_app_installer.py index 809655d..6602910 100644 --- a/apps/jingrow/jingrow/api/local_app_installer.py +++ b/apps/jingrow/jingrow/api/local_app_installer.py @@ -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 Def(custom)记录(上传安装) 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 Def(custom)记录(本地扫描安装) 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 Def(custom)记录 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 Def(custom)记录(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} 卸载成功' diff --git a/apps/jingrow/jingrow/services/whitelist.py b/apps/jingrow/jingrow/services/whitelist.py index a89f9a5..2964710 100644 --- a/apps/jingrow/jingrow/services/whitelist.py +++ b/apps/jingrow/jingrow/services/whitelist.py @@ -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/) - 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]) diff --git a/apps/jingrow/jingrow/utils/app_installer.py b/apps/jingrow/jingrow/utils/app_installer.py index 373d894..2ee7ca8 100644 --- a/apps/jingrow/jingrow/utils/app_installer.py +++ b/apps/jingrow/jingrow/utils/app_installer.py @@ -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 { diff --git a/apps/jingrow/jingrow/utils/app_manager.py b/apps/jingrow/jingrow/utils/app_manager.py new file mode 100644 index 0000000..31dca5a --- /dev/null +++ b/apps/jingrow/jingrow/utils/app_manager.py @@ -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/) + 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() +