diff --git a/apps/jingrow/frontend/src/shared/api/system.ts b/apps/jingrow/frontend/src/shared/api/system.ts new file mode 100644 index 0000000..40ed945 --- /dev/null +++ b/apps/jingrow/frontend/src/shared/api/system.ts @@ -0,0 +1,86 @@ +import axios from 'axios' +import { get_session_api_headers } from './auth' + +const jingrowServerUrl = import.meta.env.VITE_JINGROW_SERVER_URL || '' + +export interface EnvironmentConfig { + jingrow_server_url: string + jingrow_api_key: string + jingrow_api_secret: string + jingrow_session_cookie: string + jingrow_cloud_url: string + jingrow_cloud_api_url: string + jingrow_cloud_api_key: string + jingrow_cloud_api_secret: string + jingrow_db_host: string + jingrow_db_port: string + jingrow_db_name: string + jingrow_db_user: string + jingrow_db_password: string + jingrow_db_type: string + qdrant_host: string + qdrant_port: number + run_mode: string + environment: string + log_level: string + backend_host: string + backend_port: number + backend_reload: boolean + worker_processes: number + worker_threads: number + watch: boolean +} + +// 获取环境配置 +export const getEnvironmentConfig = async (): Promise<{ success: boolean; data?: EnvironmentConfig; message?: string }> => { + try { + const response = await axios.get( + `${jingrowServerUrl}/jingrow/system/environment-config`, + { + headers: get_session_api_headers(), + withCredentials: true + } + ) + + if (response.data?.success) { + return { success: true, data: response.data.data } + } + return { success: false, message: response.data?.message || '获取环境配置失败' } + } catch (error: any) { + if (error.response?.status === 403) { + return { success: false, message: '仅系统管理员可以访问此功能' } + } + if (error.response?.status === 401) { + return { success: false, message: '认证失败,请重新登录' } + } + return { success: false, message: error.response?.data?.detail || error.message || '获取环境配置失败' } + } +} + +// 更新环境配置 +export const updateEnvironmentConfig = async (config: Partial): Promise<{ success: boolean; message?: string }> => { + try { + const response = await axios.post( + `${jingrowServerUrl}/jingrow/system/environment-config`, + config, + { + headers: get_session_api_headers(), + withCredentials: true + } + ) + + if (response.data?.success) { + return { success: true, message: response.data.message || '环境配置已更新' } + } + return { success: false, message: response.data?.message || '更新环境配置失败' } + } catch (error: any) { + if (error.response?.status === 403) { + return { success: false, message: '仅系统管理员可以访问此功能' } + } + if (error.response?.status === 401) { + return { success: false, message: '认证失败,请重新登录' } + } + return { success: false, message: error.response?.data?.detail || error.message || '更新环境配置失败' } + } +} + diff --git a/apps/jingrow/frontend/src/views/Settings.vue b/apps/jingrow/frontend/src/views/Settings.vue index 38e0742..f0f48a0 100644 --- a/apps/jingrow/frontend/src/views/Settings.vue +++ b/apps/jingrow/frontend/src/views/Settings.vue @@ -57,16 +57,164 @@ + + + + + + {{ t('Only system administrators can view and edit environment configuration') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('Save Environment Configuration') }} + + + {{ t('Refresh') }} + + + + + + diff --git a/apps/jingrow/jingrow/api/system.py b/apps/jingrow/jingrow/api/system.py new file mode 100644 index 0000000..3a00871 --- /dev/null +++ b/apps/jingrow/jingrow/api/system.py @@ -0,0 +1,267 @@ +# Copyright (c) 2025, JINGROW and contributors +# For license information, please see license.txt + +""" +系统环境配置管理 API +仅限系统管理员用户可以访问 +""" + +from fastapi import APIRouter, HTTPException, Request +from typing import Dict, Any, Optional +import logging +from jingrow.utils.jingrow_api import get_logged_user +from jingrow.config import Config, get_settings + +logger = logging.getLogger(__name__) +router = APIRouter() + + +def check_admin_permission(request: Request) -> str: + """ + 检查用户是否为系统管理员(Administrator) + 返回当前用户名,如果不是管理员则抛出异常 + """ + session_cookie = request.cookies.get('sid') + if not session_cookie: + raise HTTPException(status_code=401, detail="未提供认证信息") + + user = get_logged_user(session_cookie) + if not user: + raise HTTPException(status_code=401, detail="认证失败") + + if user != "Administrator": + raise HTTPException(status_code=403, detail="仅系统管理员可以访问此功能") + + return user + + +@router.get("/jingrow/system/environment-config") +async def get_environment_config(request: Request): + """ + 获取环境配置信息 + 仅限系统管理员访问 + """ + try: + # 检查管理员权限 + check_admin_permission(request) + + # 获取当前配置 + settings = get_settings() + + # 返回完整配置信息(仅管理员可以访问,因此返回完整信息) + config_dict = settings.model_dump() + + return { + "success": True, + "data": config_dict + } + except HTTPException: + raise + except Exception as e: + logger.error(f"获取环境配置失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"获取环境配置失败: {str(e)}") + + +@router.post("/jingrow/system/environment-config") +async def update_environment_config(request: Request, config_data: Dict[str, Any]): + """ + 更新环境配置信息 + 仅限系统管理员访问 + """ + try: + # 检查管理员权限 + check_admin_permission(request) + + # 验证配置数据 + if not config_data: + raise HTTPException(status_code=400, detail="配置数据不能为空") + + # 获取当前配置 + settings = get_settings() + + # 可更新的配置字段列表(从 config.py 中的 Settings 类定义) + # 按照 config.py 中的注释分组 + field_groups = { + 'Jingrow API配置': [ + 'jingrow_server_url', + 'jingrow_api_key', + 'jingrow_api_secret', + 'jingrow_session_cookie', + ], + 'Jingrow Cloud API配置': [ + 'jingrow_cloud_url', + 'jingrow_cloud_api_url', + 'jingrow_cloud_api_key', + 'jingrow_cloud_api_secret', + ], + '数据库配置': [ + 'jingrow_db_host', + 'jingrow_db_port', + 'jingrow_db_name', + 'jingrow_db_user', + 'jingrow_db_password', + 'jingrow_db_type', + ], + 'Qdrant 向量数据库配置': [ + 'qdrant_host', + 'qdrant_port', + ], + '运行模式': [ + 'run_mode', + ], + '环境:development/production(控制启动模式、热重载等)': [ + 'environment', + ], + '日志级别:DEBUG/INFO/WARNING/ERROR/CRITICAL(全局默认级别)': [ + 'log_level', + ], + '本地后端主机配置': [ + 'backend_host', + 'backend_port', + 'backend_reload', + ], + 'Dramatiq 任务队列': [ + 'worker_processes', + 'worker_threads', + 'watch', + ], + } + + # 生成所有允许的字段列表 + allowed_fields = [] + for fields in field_groups.values(): + allowed_fields.extend(fields) + + # 更新允许的字段 + updated_fields = [] + env_file_path = settings.model_config.get('env_file') + + # 读取现有的 .env 文件内容(如果存在) + env_lines = [] + if env_file_path: + try: + from pathlib import Path + env_path = Path(env_file_path) + if env_path.exists(): + env_lines = env_path.read_text(encoding='utf-8').split('\n') + except Exception as e: + logger.warning(f"读取 .env 文件失败: {e}") + + # 构建更新的配置字典 + env_updates = {} + for field, value in config_data.items(): + if field in allowed_fields: + env_updates[field.upper()] = str(value) if value is not None else '' + updated_fields.append(field) + + # 更新 .env 文件 + if env_file_path and env_updates: + try: + from pathlib import Path + env_path = Path(env_file_path) + + # 收集所有已存在的键值对(包括未更新的) + existing_config = {} + header_comments = [] # 保存文件开头的注释(分组注释之前的) + + # 解析现有文件内容 + found_first_config = False + for line in env_lines: + stripped = line.strip() + if not stripped: + if not found_first_config: + header_comments.append('') + continue + + if stripped.startswith('#'): + # 检查是否是分组注释 + is_group_comment = any(group in stripped for group in field_groups.keys()) + if not found_first_config and not is_group_comment: + # 文件开头的非分组注释,保留 + header_comments.append(line.rstrip()) + continue + + if '=' in stripped: + found_first_config = True + key, value = stripped.split('=', 1) + key = key.strip() + existing_config[key.upper()] = (key, value.strip()) + + # 更新配置字典 + for key_upper, new_value in env_updates.items(): + # 保持原有的键名格式(大小写) + if key_upper in existing_config: + original_key, _ = existing_config[key_upper] + existing_config[key_upper] = (original_key, new_value) + else: + # 新字段,使用大写键名 + existing_config[key_upper] = (key_upper, new_value) + + # 按照分组顺序生成新的 .env 文件内容 + new_lines = [] + + # 添加文件开头的注释 + if header_comments: + new_lines.extend(header_comments) + # 如果最后一行不是空行,添加一个空行用于分隔 + if header_comments and header_comments[-1].strip(): + new_lines.append('') + + # 按照分组顺序添加配置项 + has_previous_group = False + for group_name, fields in field_groups.items(): + # 检查该组是否有配置项 + group_configs = [] + for field in fields: + field_upper = field.upper() + if field_upper in existing_config: + key, value = existing_config[field_upper] + group_configs.append(f'{key}={value}') + + # 如果有配置项,添加分组注释和配置 + if group_configs: + if has_previous_group: + new_lines.append('') # 组之间添加空行 + new_lines.append(f'# {group_name}') + new_lines.extend(group_configs) + has_previous_group = True + + # 添加剩余的未分组配置项(如果有) + remaining_keys = set(existing_config.keys()) + for fields in field_groups.values(): + for field in fields: + remaining_keys.discard(field.upper()) + + if remaining_keys: + if has_previous_group: + new_lines.append('') # 与其他分组之间添加空行 + new_lines.append('# 其他配置') + for key_upper in sorted(remaining_keys): + key, value = existing_config[key_upper] + new_lines.append(f'{key}={value}') + + # 写入文件(移除末尾的多个空行) + content = '\n'.join(new_lines).rstrip() + if content: + env_path.write_text(content + '\n', encoding='utf-8') + else: + env_path.write_text('\n', encoding='utf-8') + + except Exception as e: + logger.error(f"更新 .env 文件失败: {e}") + raise HTTPException(status_code=500, detail=f"更新配置文件失败: {str(e)}") + + logger.info(f"环境配置已更新,字段: {updated_fields}") + + return { + "success": True, + "message": "环境配置已更新", + "updated_fields": updated_fields + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"更新环境配置失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"更新环境配置失败: {str(e)}") + diff --git a/apps/jingrow/jingrow/config.py b/apps/jingrow/jingrow/config.py index f351ef9..b897940 100644 --- a/apps/jingrow/jingrow/config.py +++ b/apps/jingrow/jingrow/config.py @@ -11,7 +11,6 @@ class Settings(BaseSettings): jingrow_api_key: str = '' jingrow_api_secret: str = '' jingrow_session_cookie: str = '' - jingrow_site: str = '' # Jingrow Cloud API配置 jingrow_cloud_url: str = 'https://cloud.jingrow.com' @@ -43,7 +42,7 @@ class Settings(BaseSettings): backend_port: int = 9001 backend_reload: bool = True - # 异步任务队列(Dramatiq)配置 + # Dramatiq 任务队列 worker_processes: int = 1 worker_threads: int = 1 watch: bool = True