feat: Add parameter configuration functionality

This commit is contained in:
fit2cloud-chenyw
2025-12-09 15:02:42 +08:00
committed by fit2cloud-chenyw
parent 9deca7e6b6
commit e47e0e8ab8
10 changed files with 363 additions and 89 deletions

View File

@@ -5,7 +5,7 @@ from apps.dashboard.api import dashboard_api
from apps.data_training.api import data_training
from apps.datasource.api import datasource, table_relation, recommended_problem
from apps.mcp import mcp
from apps.system.api import login, user, aimodel, workspace, assistant
from apps.system.api import login, user, aimodel, workspace, assistant, parameter
from apps.terminology.api import terminology
from apps.settings.api import base
@@ -23,5 +23,6 @@ api_router.include_router(chat.router)
api_router.include_router(dashboard_api.router)
api_router.include_router(mcp.router)
api_router.include_router(table_relation.router)
api_router.include_router(parameter.router)
api_router.include_router(recommended_problem.router)

View File

@@ -0,0 +1,17 @@
from fastapi import APIRouter, Request
from sqlbot_xpack.config.model import SysArgModel
from apps.system.crud.parameter_manage import get_parameter_args, save_parameter_args
from common.core.deps import SessionDep
router = APIRouter(tags=["system/parameter"], prefix="/system/parameter")
@router.get("")
async def get_args(session: SessionDep) -> list[SysArgModel]:
return await get_parameter_args(session)
@router.post("", )
async def save_args(session: SessionDep, request: Request):
return await save_parameter_args(session = session, request = request)

View File

@@ -0,0 +1,41 @@
from fastapi import Request
from sqlbot_xpack.config.arg_manage import get_group_args, save_group_args
from sqlbot_xpack.config.model import SysArgModel
import json
from common.core.deps import SessionDep
from sqlbot_xpack.file_utils import SQLBotFileUtils
async def get_parameter_args(session: SessionDep) -> list[SysArgModel]:
group_args = await get_group_args(session=session)
return [x for x in group_args if not x.pkey.startswith('appearance.')]
async def save_parameter_args(session: SessionDep, request: Request):
allow_file_mapping = {
""" "test_logo": { "types": [".jpg", ".jpeg", ".png", ".svg"], "size": 5 * 1024 * 1024 } """
}
form_data = await request.form()
files = form_data.getlist("files")
json_text = form_data.get("data")
sys_args = [
SysArgModel(**{**item, "pkey": f"{item['pkey']}"})
for item in json.loads(json_text)
if "pkey" in item
]
if not sys_args:
return
file_mapping = None
if files:
file_mapping = {}
for file in files:
origin_file_name = file.filename
file_name, flag_name = SQLBotFileUtils.split_filename_and_flag(origin_file_name)
file.filename = file_name
allow_limit_obj = allow_file_mapping.get(flag_name)
if allow_limit_obj:
SQLBotFileUtils.check_file(file=file, file_types=allow_limit_obj.get("types"), limit_file_size=allow_limit_obj.get("size"))
else:
raise Exception(f'The file [{file_name}] is not allowed to be uploaded!')
file_id = await SQLBotFileUtils.upload(file)
file_mapping[f"{flag_name}"] = file_id
await save_group_args(session=session, sys_args=sys_args, file_mapping=file_mapping)

View File

@@ -14,13 +14,15 @@
"rows_of_data": "Limit 1000 Rows of Data",
"third_party_platform_settings": "Third-Party Platform Settings",
"by_third_party_platform": "Automatic User Creation by Third-Party Platform",
"platform_user_organization": "Third-Party Platform User Organization",
"platform_user_organization": "Third-Party Platform Workspace",
"platform_user_roles": "Third-Party Platform User Roles",
"excessive_data_volume": "Disabling the 1000-row data limit may cause system lag due to excessive data volume.",
"prompt": "Prompt",
"disabling_successfully": "Disabling Successfully",
"closed_by_default": "In the Question Count window, control whether the model thinking process is expanded or closed by default.",
"and_platform_integration": "Scope includes authentication settings and platform integration."
"and_platform_integration": "Scope includes authentication settings and platform integration.",
"login_settings": "Login Settings",
"default_login": "Default Login Method"
},
"prompt": {
"default_password": "Default password:{msg}",
@@ -816,4 +818,4 @@
"modelType": {
"llm": "Large Language Model"
}
}
}

View File

@@ -14,13 +14,15 @@
"rows_of_data": "데이터 1,000행 제한",
"third_party_platform_settings": "타사 플랫폼 설정",
"by_third_party_platform": "타사 플랫폼에 의한 자동 사용자 생성",
"platform_user_organization": "타사 플랫폼 사용자 구성",
"platform_user_organization": "제3자 플랫폼 작업 공간",
"platform_user_roles": "타사 플랫폼 사용자 역할",
"excessive_data_volume": "1,000행 데이터 제한을 비활성화하면 과도한 데이터 양으로 인해 시스템 지연이 발생할 수 있습니다.",
"prompt": "프롬프트",
"disabling_successfully": "비활성화 완료",
"closed_by_default": "질문 수 창에서 모델 사고 프로세스를 기본적으로 확장할지 또는 닫을지 여부를 제어합니다.",
"and_platform_integration": "범위에는 인증 설정 및 플랫폼 통합이 포함됩니다."
"and_platform_integration": "범위에는 인증 설정 및 플랫폼 통합이 포함됩니다.",
"login_settings": "로그인 설정",
"default_login": "기본 로그인 방식"
},
"prompt": {
"default_password:": "기본 비밀번호:{msg}",
@@ -816,4 +818,4 @@
"modelType": {
"llm": "대형 언어 모델"
}
}
}

View File

@@ -14,13 +14,15 @@
"rows_of_data": "限制 1000 行数据",
"third_party_platform_settings": "第三方平台设置",
"by_third_party_platform": "第三方自动创建用户",
"platform_user_organization": "第三方平台用户组织",
"platform_user_organization": "第三方平台工作空间",
"platform_user_roles": "第三方平台用户角色",
"excessive_data_volume": "关闭1000行的数据限制后数据量过大可能会造成系统卡顿",
"prompt": "提示",
"disabling_successfully": "关闭成功",
"closed_by_default": "在问数窗口中,控制模型思考过程默认展开或者关闭",
"and_platform_integration": "作用域包括认证设置和平台对接"
"and_platform_integration": "作用域包括认证设置和平台对接",
"login_settings": "登录设置",
"default_login": "默认登录方式"
},
"prompt": {
"default_password": "默认密码:{msg}",
@@ -817,4 +819,4 @@
"modelType": {
"llm": "大语言模型"
}
}
}

View File

@@ -95,6 +95,7 @@ export const routes = [
},
{
path: '/set',
name: 'set',
component: LayoutDsl,
redirect: '/set/member',
meta: { title: t('workspace.set'), iconActive: 'set', iconDeActive: 'noSet' },
@@ -145,6 +146,7 @@ export const routes = [
},
{
path: '/system',
name: 'system',
component: LayoutDsl,
redirect: '/system/user',
meta: { hidden: true },

View File

@@ -263,3 +263,22 @@ export const getSQLBotAddr = (portEnd?: boolean) => {
}
return addr.substring(0, addr.length - 1)
}
export const formatArg = (text: string) => {
if (!text) {
return false
}
const mappingArray = ['true', 'false', '1', '0']
const match = mappingArray.some((item: string) => {
return item === text.toLocaleLowerCase()
})
if (!match) {
return text
}
try {
return JSON.parse(text)
} catch (e: any) {
console.warn(e)
return text
}
}

View File

@@ -1,23 +1,63 @@
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { onMounted, provide, reactive, unref } from 'vue'
import icon_info_outlined_1 from '@/assets/svg/icon_info_outlined_1.svg'
import { useI18n } from 'vue-i18n'
import PlatformParam from './xpack/PlatformParam.vue'
import { request } from '@/utils/request'
import { formatArg } from '@/utils/utils'
const { t } = useI18n()
const chatSetting = ref({
modelThinkingProcess: false,
rows_of_data: false,
const state = reactive({
parameterForm: reactive<any>({
'chat.enable_model_thinking': false,
'chat.rows_of_data': false,
}),
})
const platform = ref({
organization: false,
modelThinkingProcess: false,
roles: [],
provide('parameterForm', state.parameterForm)
const loadData = () => {
request.get('/system/parameter').then((res: any) => {
if (res) {
res.forEach((item: any) => {
if (
item.pkey?.startsWith('chat') ||
item.pkey?.startsWith('login') ||
item.pkey?.startsWith('platform')
) {
state.parameterForm[item.pkey] = formatArg(item.pval)
}
})
console.log(state.parameterForm)
}
})
}
const buildParam = () => {
const changedItemArray = Object.keys(state.parameterForm).map((key: string) => {
return {
pkey: key,
pval: Object.prototype.hasOwnProperty.call(state.parameterForm, 'key')
? state.parameterForm[key].toString()
: state.parameterForm[key],
}
})
const formData = new FormData()
formData.append('data', JSON.stringify(unref(changedItemArray)))
return formData
}
const saveHandler = () => {
const param = buildParam()
request
.post('/system/parameter', param, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then(() => {
ElMessage.success(t('common.save_success'))
})
}
onMounted(() => {
loadData()
})
const organizations = shallowRef<any[]>([])
const roles = shallowRef<any[]>([])
</script>
<template>
@@ -41,7 +81,7 @@ const roles = shallowRef<any[]>([])
</el-tooltip>
</div>
<div class="value">
<el-switch v-model="chatSetting.modelThinkingProcess" />
<el-switch v-model="state.parameterForm['chat.enable_model_thinking']" />
</div>
</div>
@@ -59,77 +99,15 @@ const roles = shallowRef<any[]>([])
</el-tooltip>
</div>
<div class="value">
<el-switch v-model="chatSetting.rows_of_data" />
<el-switch v-model="state.parameterForm['chat.rows_of_data']" />
</div>
</div>
</div>
<div class="card">
<div class="card-title">
{{ t('parameter.third_party_platform_settings') }}
</div>
<div class="card-item" style="width: 100%">
<div class="label">
{{ t('parameter.by_third_party_platform') }}
</div>
<div class="value">
<el-switch v-model="platform.modelThinkingProcess" />
</div>
</div>
<div class="card-item">
<div class="label">
{{ t('parameter.platform_user_organization') }}
<span class="require"></span>
<el-tooltip
effect="dark"
:content="t('parameter.and_platform_integration')"
placement="top"
>
<el-icon size="16">
<icon_info_outlined_1></icon_info_outlined_1>
</el-icon>
</el-tooltip>
</div>
<div class="value">
<el-select filterable v-model="platform.organization">
<el-option
v-for="item in organizations"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
<div class="card-item" style="margin-left: 16px">
<div class="label">
{{ t('parameter.platform_user_roles') }}
<span class="require"></span>
<el-tooltip
effect="dark"
:content="t('parameter.and_platform_integration')"
placement="top"
>
<el-icon size="16">
<icon_info_outlined_1></icon_info_outlined_1>
</el-icon>
</el-tooltip>
</div>
<div class="value">
<el-select multiple filterable v-model="platform.roles">
<el-option
v-for="item in roles"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
</div>
<platform-param />
</div>
<div class="save" style="margin-top: 16px">
<el-button type="primary">{{ t('common.save') }}</el-button>
<el-button type="primary" @click="saveHandler">{{ t('common.save') }}</el-button>
</div>
</div>
</template>

View File

@@ -0,0 +1,210 @@
<template>
<div v-if="xpackValid" class="card">
<div class="card-title">
{{ t('parameter.third_party_platform_settings') }}
</div>
<div class="card-item" style="width: 100%">
<div class="label">
{{ t('parameter.by_third_party_platform') }}
</div>
<div class="value">
<el-switch v-model="formData['platform.auto_create']" />
</div>
</div>
<div class="card-item">
<div class="label">
{{ t('parameter.platform_user_organization') }}
<span class="require"></span>
<el-tooltip
effect="dark"
:content="t('parameter.and_platform_integration')"
placement="top"
>
<el-icon size="16">
<icon_info_outlined_1></icon_info_outlined_1>
</el-icon>
</el-tooltip>
</div>
<div class="value">
<el-select v-model="formData['platform.oid']" filterable>
<el-option
v-for="item in organizations"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</div>
</div>
<div class="card-item" style="margin-left: 16px">
<div class="label">
{{ t('workspace.member_type') }}
<span class="require"></span>
<el-tooltip
effect="dark"
:content="t('parameter.and_platform_integration')"
placement="top"
>
<el-icon size="16">
<icon_info_outlined_1></icon_info_outlined_1>
</el-icon>
</el-tooltip>
</div>
<div class="value">
<el-select v-model="formData['platform.rid']" filterable>
<el-option
v-for="item in roles"
:key="item.value"
:label="item.name"
:value="item.value"
/>
</el-select>
</div>
</div>
</div>
<div v-if="anyPlatformEnable" class="card">
<div class="card-title">
{{ t('parameter.login_settings') }}
</div>
<div class="card-item" style="width: 100%">
<div class="label">
{{ t('parameter.default_login') }}
</div>
<div class="value">
<el-radio-group v-model="formData['login.default_login']">
<el-radio v-for="item in loginTypeOptions" :key="item.value" :label="item.value">{{
item.label
}}</el-radio>
</el-radio-group>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { inject, onMounted, reactive, ref, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { workspaceList } from '@/api/workspace'
import { request } from '@/utils/request'
import icon_info_outlined_1 from '@/assets/svg/icon_info_outlined_1.svg'
const { t } = useI18n()
const anyPlatformEnable = ref(false)
const defaultForm = reactive<Record<string, any>>({
'platform.auto_create': false,
'platform.oid': 1,
'platform.rid': 1,
'login.default_login': 0,
})
const loginTypeOptions = shallowRef<any[]>([{ value: 0, label: t('login.account_login') }])
const formData = inject<Record<string, any>>('parameterForm', {})
const xpackValid = ref(false)
const organizations = shallowRef<any[]>([])
const roles = [
{
name: t('workspace.administrator'),
value: 1,
},
{
name: t('workspace.ordinary_member'),
value: 0,
},
]
const platformMapping = {
cas: { value: 1, label: 'CAS' },
oidc: { value: 2, label: 'OIDC' },
ldap: { value: 3, label: 'LDAP' },
oauth2: { value: 4, label: 'Oauth2' },
saml2: { value: 5, label: 'Saml2' },
} as any
const setDefaultForm = () => {
for (const key in defaultForm) {
if (formData[key] === undefined) {
formData[key] = defaultForm[key]
}
}
}
const queryCategoryStatus = () => {
const url = `/system/authentication/platform/status`
return request.get(url)
}
onMounted(async () => {
// eslint-disable-next-line no-undef
const obj = LicenseGenerator.getLicense()
if (obj?.status !== 'valid') {
xpackValid.value = false
return
}
const wsRes: any = await workspaceList()
organizations.value = wsRes
const platformStatusRes: any = await queryCategoryStatus()
platformStatusRes.forEach((item: any) => {
if (item.enable) {
loginTypeOptions.value.push(platformMapping[item.name])
anyPlatformEnable.value = true
}
})
if (
!formData['login.default_login'] ||
!loginTypeOptions.value.some(
(option: any) => parseInt(formData['login.default_login']) === option.value
)
) {
formData['login.default_login'] = 0
}
formData['login.default_login'] = parseInt(formData['login.default_login'])
setDefaultForm()
xpackValid.value = true
})
</script>
<style lang="less" scoped>
.card {
width: 100%;
border-radius: 12px;
padding: 16px;
border: 1px solid #dee0e3;
display: flex;
flex-wrap: wrap;
margin-top: 16px;
.card-title {
font-weight: 500;
font-style: Medium;
font-size: 16px;
line-height: 24px;
width: 100%;
}
.card-item {
margin-top: 16px;
width: calc(50% - 8px);
.label {
font-weight: 400;
font-size: 14px;
line-height: 22px;
display: flex;
align-items: center;
.ed-icon {
margin-left: 4px;
}
.require::after {
content: '*';
color: var(--ed-color-danger);
margin-left: 4px;
}
}
.value {
margin-top: 8px;
line-height: 20px;
}
}
}
</style>