重构设置页面,实现config前端可视化配置

This commit is contained in:
jingrow 2025-11-01 23:53:41 +08:00
parent e2a09d0dde
commit c59355dcae
4 changed files with 586 additions and 5 deletions

View File

@ -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<EnvironmentConfig>): 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 || '更新环境配置失败' }
}
}

View File

@ -57,16 +57,164 @@
</n-form>
</n-card>
</n-grid-item>
<!-- 环境配置仅系统管理员可见 -->
<n-grid-item v-if="isAdmin">
<n-card :title="t('Environment Configuration')">
<n-alert type="warning" style="margin-bottom: 16px">
{{ t('Only system administrators can view and edit environment configuration') }}
</n-alert>
<n-form
:model="envConfig"
label-placement="left"
label-width="180px"
:loading="envConfigLoading"
>
<n-collapse>
<n-collapse-item name="jingrow" :title="t('Jingrow API Configuration')">
<n-form-item label="Jingrow Server URL">
<n-input v-model:value="envConfig.jingrow_server_url" placeholder="https://example.jingrow.com" />
</n-form-item>
<n-form-item label="Jingrow API Key">
<n-input v-model:value="envConfig.jingrow_api_key" type="password" show-password-on="click" />
</n-form-item>
<n-form-item label="Jingrow API Secret">
<n-input v-model:value="envConfig.jingrow_api_secret" type="password" show-password-on="click" />
</n-form-item>
<n-form-item label="Jingrow Session Cookie">
<n-input v-model:value="envConfig.jingrow_session_cookie" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="cloud" :title="t('Jingrow Cloud Configuration')">
<n-form-item label="Cloud URL">
<n-input v-model:value="envConfig.jingrow_cloud_url" />
</n-form-item>
<n-form-item label="Cloud API URL">
<n-input v-model:value="envConfig.jingrow_cloud_api_url" />
</n-form-item>
<n-form-item label="Cloud API Key">
<n-input v-model:value="envConfig.jingrow_cloud_api_key" type="password" show-password-on="click" />
</n-form-item>
<n-form-item label="Cloud API Secret">
<n-input v-model:value="envConfig.jingrow_cloud_api_secret" type="password" show-password-on="click" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="database" :title="t('Database Configuration')">
<n-form-item label="DB Host">
<n-input v-model:value="envConfig.jingrow_db_host" />
</n-form-item>
<n-form-item label="DB Port">
<n-input v-model:value="envConfig.jingrow_db_port" />
</n-form-item>
<n-form-item label="DB Name">
<n-input v-model:value="envConfig.jingrow_db_name" />
</n-form-item>
<n-form-item label="DB User">
<n-input v-model:value="envConfig.jingrow_db_user" />
</n-form-item>
<n-form-item label="DB Password">
<n-input v-model:value="envConfig.jingrow_db_password" type="password" show-password-on="click" />
</n-form-item>
<n-form-item label="DB Type">
<n-select v-model:value="envConfig.jingrow_db_type" :options="dbTypeOptions" style="width: 200px" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="qdrant" :title="t('Qdrant Configuration')">
<n-form-item label="Qdrant Host">
<n-input v-model:value="envConfig.qdrant_host" />
</n-form-item>
<n-form-item label="Qdrant Port">
<n-input-number v-model:value="envConfig.qdrant_port" :min="1" :max="65535" />
</n-form-item>
</n-collapse-item>
<n-collapse-item name="runtime" :title="t('Runtime Configuration')">
<n-form-item label="Run Mode">
<n-select v-model:value="envConfig.run_mode" :options="runModeOptions" style="width: 200px" />
</n-form-item>
<n-form-item label="Environment">
<n-select v-model:value="envConfig.environment" :options="environmentOptions" style="width: 200px" />
</n-form-item>
<n-form-item label="Log Level">
<n-select v-model:value="envConfig.log_level" :options="logLevelOptions" style="width: 200px" />
</n-form-item>
<n-form-item label="Backend Host">
<n-input v-model:value="envConfig.backend_host" />
</n-form-item>
<n-form-item label="Backend Port">
<n-input-number v-model:value="envConfig.backend_port" :min="1" :max="65535" />
</n-form-item>
<n-form-item label="Backend Reload">
<n-switch v-model:value="envConfig.backend_reload" />
</n-form-item>
<n-form-item label="Worker Processes">
<n-input-number v-model:value="envConfig.worker_processes" :min="1" :max="32" />
</n-form-item>
<n-form-item label="Worker Threads">
<n-input-number v-model:value="envConfig.worker_threads" :min="1" :max="32" />
</n-form-item>
<n-form-item label="Watch">
<n-switch v-model:value="envConfig.watch" />
</n-form-item>
</n-collapse-item>
</n-collapse>
<n-form-item style="margin-top: 16px">
<n-space>
<n-button type="primary" :loading="envConfigSaving" @click="saveEnvironmentConfig">
{{ t('Save Environment Configuration') }}
</n-button>
<n-button @click="loadEnvironmentConfig">
{{ t('Refresh') }}
</n-button>
</n-space>
</n-form-item>
</n-form>
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { NGrid, NGridItem, NCard, NForm, NFormItem, NInput, NButton, NInputNumber, NSelect, useMessage } from 'naive-ui'
import { reactive, onMounted, computed, ref } from 'vue'
import {
NGrid,
NGridItem,
NCard,
NForm,
NFormItem,
NInput,
NButton,
NInputNumber,
NSelect,
NSwitch,
NAlert,
NCollapse,
NCollapseItem,
NSpace,
useMessage
} from 'naive-ui'
import { getCurrentLocale, setLocale, locales, initLocale, t } from '../shared/i18n'
import { useAuthStore } from '../shared/stores/auth'
import { getEnvironmentConfig, updateEnvironmentConfig, type EnvironmentConfig } from '../shared/api/system'
const message = useMessage()
const authStore = useAuthStore()
//
const isAdmin = computed(() => {
const user = authStore.user
return user?.username === 'Administrator' || user?.id === 'Administrator'
})
//
const envConfig = reactive<Partial<EnvironmentConfig>>({})
const envConfigLoading = ref(false)
const envConfigSaving = ref(false)
const systemSettings = reactive({
appName: localStorage.getItem('appName') || 'Jingrow',
@ -111,6 +259,36 @@ const timezoneOptions = [
{ label: 'Pacific/Auckland (新西兰时间)', value: 'Pacific/Auckland' }
]
//
const dbTypeOptions = [
{ label: 'MariaDB', value: 'mariadb' },
{ label: 'MySQL', value: 'mysql' },
{ label: 'PostgreSQL', value: 'postgresql' }
]
//
const runModeOptions = [
{ label: 'API', value: 'api' },
{ label: 'Worker', value: 'worker' },
{ label: 'Scheduler', value: 'scheduler' }
]
//
const environmentOptions = [
{ label: 'Development', value: 'development' },
{ label: 'Production', value: 'production' },
{ label: 'Testing', value: 'testing' }
]
//
const logLevelOptions = [
{ label: 'DEBUG', value: 'DEBUG' },
{ label: 'INFO', value: 'INFO' },
{ label: 'WARNING', value: 'WARNING' },
{ label: 'ERROR', value: 'ERROR' },
{ label: 'CRITICAL', value: 'CRITICAL' }
]
const changeLanguage = (locale: string) => {
setLocale(locale)
message.success(t('Language updated'))
@ -131,9 +309,60 @@ const saveSystemSettings = () => {
}, 1000)
}
onMounted(() => {
//
const loadEnvironmentConfig = async () => {
if (!isAdmin.value) {
return
}
envConfigLoading.value = true
try {
const result = await getEnvironmentConfig()
if (result.success && result.data) {
Object.assign(envConfig, result.data)
message.success(t('Environment configuration loaded'))
} else {
message.error(result.message || t('Failed to load environment configuration'))
}
} catch (error: any) {
message.error(error.message || t('Failed to load environment configuration'))
} finally {
envConfigLoading.value = false
}
}
//
const saveEnvironmentConfig = async () => {
if (!isAdmin.value) {
message.error(t('Only system administrators can edit environment configuration'))
return
}
envConfigSaving.value = true
try {
const result = await updateEnvironmentConfig(envConfig)
if (result.success) {
message.success(result.message || t('Environment configuration saved'))
//
await loadEnvironmentConfig()
} else {
message.error(result.message || t('Failed to save environment configuration'))
}
} catch (error: any) {
message.error(error.message || t('Failed to save environment configuration'))
} finally {
envConfigSaving.value = false
}
}
onMounted(async () => {
initLocale()
systemSettings.language = getCurrentLocale()
//
if (isAdmin.value) {
await loadEnvironmentConfig()
}
})
</script>

View File

@ -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)}")

View File

@ -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