mirror of
https://github.com/jumpserver/lina.git
synced 2025-11-24 22:37:20 +00:00
Compare commits
10 Commits
dependabot
...
pr@dev@ope
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c025b494e | ||
|
|
3dd199ef01 | ||
|
|
20e1c833a6 | ||
|
|
f2c7a6bc71 | ||
|
|
6063e03d89 | ||
|
|
e2d9eb5bae | ||
|
|
84caf35f67 | ||
|
|
1d7a1bfff1 | ||
|
|
9039f572ac | ||
|
|
7798324a4b |
@@ -107,6 +107,10 @@ export default {
|
||||
this.$emit('add', true)
|
||||
}
|
||||
}).catch(error => {
|
||||
if (error?.response?.data?.code === 'no_valid_assets') {
|
||||
this.$message.error(error?.response?.data?.detail)
|
||||
return
|
||||
}
|
||||
this.iVisible = true
|
||||
this.handleResult(null, error)
|
||||
})
|
||||
|
||||
@@ -223,9 +223,21 @@ export default {
|
||||
const mapped = {}
|
||||
Object.entries(errors || {}).forEach(([k, v]) => {
|
||||
let msg = v
|
||||
console.log(k, v)
|
||||
// v是数组并且数组都是字符串,则拼接为字符串
|
||||
if (Array.isArray(v) && v.every(item => typeof item === 'string')) msg = v.join('; ')
|
||||
else if (typeof v === 'object' && v !== null) msg = JSON.stringify(v)
|
||||
// 处理 [{"port":["请确保该值小于或者等于 65535。"]},{},{}] 这种情况
|
||||
else if (Array.isArray(v) && v.every(item => _.isPlainObject(item))) {
|
||||
const subMsg = []
|
||||
v.forEach((subItem) => {
|
||||
Object.values(subItem).forEach((subMsgArr) => {
|
||||
if (Array.isArray(subMsgArr)) {
|
||||
subMsg.push(...subMsgArr)
|
||||
}
|
||||
})
|
||||
})
|
||||
msg = subMsg.join(' ')
|
||||
} else if (typeof v === 'object' && v !== null) msg = JSON.stringify(v)
|
||||
mapped[k] = String(msg || '')
|
||||
})
|
||||
this.serverErrors = mapped
|
||||
|
||||
@@ -65,6 +65,17 @@ export default {
|
||||
'usingOrgs',
|
||||
'currentViewRoute'
|
||||
]),
|
||||
currentOrgDisplayName() {
|
||||
const currentOrgId = this.currentOrg?.id
|
||||
if (!currentOrgId) {
|
||||
return this.$tc('Select')
|
||||
}
|
||||
const matchedOrg = this.usingOrgs.find(item => item.id === currentOrgId)
|
||||
if (matchedOrg?.name) {
|
||||
return matchedOrg.name
|
||||
}
|
||||
return this.currentOrg.name || this.$tc('Select')
|
||||
},
|
||||
orgActionsGroup() {
|
||||
const orgActions = {
|
||||
label: this.$t('OrganizationList'),
|
||||
@@ -110,11 +121,8 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentOrg: {
|
||||
handler() {
|
||||
this.updateWidth()
|
||||
},
|
||||
deep: true
|
||||
currentOrgDisplayName() {
|
||||
this.updateWidth()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -133,8 +141,9 @@ export default {
|
||||
tempSpan.style.fontWeight = 'normal'
|
||||
tempSpan.style.letterSpacing = 'normal'
|
||||
|
||||
// 获取当前组织名称
|
||||
const orgName = this.currentOrg.name || this.$tc('Select')
|
||||
// 获取当前组织显示名称
|
||||
const orgName = this.currentOrgDisplayName
|
||||
|
||||
tempSpan.textContent = orgName
|
||||
document.body.appendChild(tempSpan)
|
||||
|
||||
|
||||
@@ -167,8 +167,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page.no-title {
|
||||
::v-deep {
|
||||
.page-submenu .el-tabs__header {
|
||||
@@ -199,6 +198,14 @@ export default {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__nav-next {
|
||||
|
||||
@@ -149,6 +149,11 @@ export function getErrorResponseMsg(error) {
|
||||
.join('; ')
|
||||
} else if (typeof data === 'string') {
|
||||
return data
|
||||
} else if (_.isPlainObject(data)) {
|
||||
return Object.values(data)
|
||||
.map(item => getErrorResponseMsg(item))
|
||||
.filter(i => i)
|
||||
.join('; ')
|
||||
} else {
|
||||
msg = error.toString()
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ export default {
|
||||
asset: ''
|
||||
},
|
||||
treeSetting: {
|
||||
showMenu: true,
|
||||
showMenu: (node) => {
|
||||
return node?.meta?.type === 'asset'
|
||||
},
|
||||
showRefresh: true,
|
||||
showSearch: true,
|
||||
showAssets: true,
|
||||
@@ -55,9 +57,8 @@ export default {
|
||||
menu: [
|
||||
{
|
||||
id: 'check',
|
||||
name: this.$t('Check'),
|
||||
name: this.$t('RiskDetection'),
|
||||
icon: 'scan',
|
||||
has: (node) => node?.meta?.type === 'asset',
|
||||
callback: (node) => {
|
||||
vm.detectDialog.asset = node.id
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
class="risk-review-drawer"
|
||||
destroy-on-close
|
||||
direction="rtl"
|
||||
style="z-index: 999"
|
||||
>
|
||||
<div class="drawer-container">
|
||||
<div class="drawer-body">
|
||||
|
||||
@@ -408,7 +408,6 @@ export default {
|
||||
createJob(data).then(res => {
|
||||
this.progressLength = 0
|
||||
this.executionInfo.timeCost = 0
|
||||
this.showProgress = true
|
||||
this.speedText = ''
|
||||
const form = new FormData()
|
||||
const start = Date.now()
|
||||
@@ -436,6 +435,7 @@ export default {
|
||||
}
|
||||
}
|
||||
}).then(res => {
|
||||
this.showProgress = true
|
||||
this.executionInfo.status = 'running'
|
||||
this.currentTaskId = res.task_id
|
||||
this.xtermConfig = { taskId: this.currentTaskId, type: 'shortcut_cmd' }
|
||||
|
||||
@@ -32,6 +32,22 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
passwordMenuDisabled() {
|
||||
return this.$store.state.users.profile.source.value !== 'local'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
passwordMenuDisabled: {
|
||||
immediate: true,
|
||||
handler(disabled) {
|
||||
this.config.submenu = this.getSubmenu()
|
||||
if (disabled && this.config.activeMenu === 'Password') {
|
||||
this.config.activeMenu = 'SSHKeyList'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSubmenu() {
|
||||
return [
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { GenericCreateUpdateForm } from '@/layout/components'
|
||||
import IBox from '@/components/Common/IBox/index.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import ChatProvidersField from './components/ChatProvidersField.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -17,106 +18,54 @@ export default {
|
||||
GenericCreateUpdateForm
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
const hasProviders = (formValue) => {
|
||||
const providers = formValue?.CHAT_AI_PROVIDERS?.providers || []
|
||||
return Array.isArray(providers) && providers.length > 0
|
||||
}
|
||||
return {
|
||||
url: '/api/v1/settings/setting/?category=chat',
|
||||
hasReset: false,
|
||||
moreButtons: [
|
||||
{
|
||||
title: this.$t('Test'),
|
||||
loading: false,
|
||||
callback: function(value, form, btn) {
|
||||
btn.loading = true
|
||||
vm.$axios.post(
|
||||
'/api/v1/settings/chatai/testing/',
|
||||
value
|
||||
).then(res => {
|
||||
vm.$message.success(res['msg'])
|
||||
}).catch(() => {
|
||||
vm.$log.error('err occur')
|
||||
}).finally(() => {
|
||||
btn.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
encryptedFields: ['VAULT_HCP_TOKEN'],
|
||||
fields: [
|
||||
'CHAT_AI_ENABLED',
|
||||
'CHAT_AI_METHOD',
|
||||
'CHAT_AI_EMBED_URL',
|
||||
'CHAT_AI_TYPE',
|
||||
'DEEPSEEK_BASE_URL',
|
||||
'DEEPSEEK_API_KEY',
|
||||
'DEEPSEEK_PROXY',
|
||||
'DEEPSEEK_MODEL',
|
||||
'GPT_BASE_URL',
|
||||
'GPT_API_KEY',
|
||||
'GPT_PROXY',
|
||||
'GPT_MODEL'
|
||||
'CHAT_AI_PROVIDERS',
|
||||
'CHAT_AI_EMBED_URL'
|
||||
],
|
||||
fieldsMeta: {
|
||||
CHAT_AI_TYPE: {
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api'
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || hasProviders(formValue)
|
||||
}
|
||||
},
|
||||
GPT_BASE_URL: {
|
||||
el: {
|
||||
autocomplete: 'new-password'
|
||||
},
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt'
|
||||
}
|
||||
},
|
||||
GPT_API_KEY: {
|
||||
el: {
|
||||
autocomplete: 'new-password'
|
||||
},
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt'
|
||||
}
|
||||
},
|
||||
GPT_PROXY: {
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt'
|
||||
}
|
||||
},
|
||||
GPT_MODEL: {
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt'
|
||||
}
|
||||
},
|
||||
DEEPSEEK_BASE_URL: {
|
||||
el: {
|
||||
autocomplete: 'new-password'
|
||||
},
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek'
|
||||
}
|
||||
},
|
||||
DEEPSEEK_API_KEY: {
|
||||
el: {
|
||||
autocomplete: 'new-password'
|
||||
},
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek'
|
||||
}
|
||||
},
|
||||
DEEPSEEK_PROXY: {
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek'
|
||||
}
|
||||
},
|
||||
DEEPSEEK_MODEL: {
|
||||
hidden: (formValue) => {
|
||||
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek'
|
||||
}
|
||||
CHAT_AI_PROVIDERS: {
|
||||
component: ChatProvidersField,
|
||||
hidden: (formValue) => formValue.CHAT_AI_METHOD !== 'api'
|
||||
},
|
||||
CHAT_AI_EMBED_URL: {
|
||||
hidden: (formValue) => formValue.CHAT_AI_METHOD !== 'embed'
|
||||
}
|
||||
},
|
||||
afterGetFormValue(formValue) {
|
||||
const providers = Array.isArray(formValue.CHAT_AI_PROVIDERS) ? formValue.CHAT_AI_PROVIDERS : []
|
||||
return {
|
||||
...formValue,
|
||||
CHAT_AI_PROVIDERS: {
|
||||
providers: providers
|
||||
}
|
||||
}
|
||||
},
|
||||
cleanFormValue(values) {
|
||||
const config = values.CHAT_AI_PROVIDERS || {}
|
||||
const providers = Array.isArray(config.providers) ? config.providers : []
|
||||
return {
|
||||
...values,
|
||||
CHAT_AI_PROVIDERS: providers,
|
||||
CHAT_AI_TYPE: values.CHAT_AI_TYPE || providers[0]?.type || values.CHAT_AI_TYPE
|
||||
}
|
||||
},
|
||||
submitMethod() {
|
||||
return 'patch'
|
||||
}
|
||||
|
||||
203
src/views/settings/Feature/components/ChatProvidersField.vue
Normal file
203
src/views/settings/Feature/components/ChatProvidersField.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div class="providers-card">
|
||||
<div class="providers-header">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="addProvider">
|
||||
{{ $t('Add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="localProviders" size="mini" border>
|
||||
<el-table-column :label="$t('Type')" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.type" size="mini" @change="emitChange">
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('Base URL')" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.base_url"
|
||||
size="mini"
|
||||
placeholder="https://api.example.com/v1"
|
||||
@input="emitChange"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('API Key')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.api_key"
|
||||
size="mini"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
@input="emitChange"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('Proxy')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.proxy"
|
||||
size="mini"
|
||||
placeholder="http://ip:port"
|
||||
@input="emitChange"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('Assistant')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.is_assistant"
|
||||
size="mini"
|
||||
@change="handleAssistantChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('Actions')" width="100" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
class="danger-text"
|
||||
icon="el-icon-delete"
|
||||
@click="removeProvider($index)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
|
||||
export default {
|
||||
name: 'ChatProvidersField',
|
||||
props: {
|
||||
value: {
|
||||
type: [Object, Array],
|
||||
default: () => ({ providers: [], defaultProvider: '' })
|
||||
},
|
||||
typeOptions: {
|
||||
type: Array,
|
||||
default: () => ([
|
||||
{ label: 'Ollama', value: 'ollama' },
|
||||
{ label: 'OpenAI', value: 'openai' }
|
||||
])
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const { providers, defaultProvider } = this.normalizeValue(this.value)
|
||||
return {
|
||||
localProviders: providers.length ? providers : [this.emptyProvider()],
|
||||
defaultProvider: defaultProvider || providers[0]?.name || ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(v) {
|
||||
const { providers, defaultProvider } = this.normalizeValue(v)
|
||||
this.localProviders = providers.length ? providers : [this.emptyProvider()]
|
||||
this.defaultProvider = this.pickDefault(defaultProvider, providers)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emptyProvider() {
|
||||
return {
|
||||
type: 'openai',
|
||||
base_url: 'https://api.openai.com/v1',
|
||||
api_key: 'sk-JumpserveraAndWebOpenUI',
|
||||
proxy: '',
|
||||
is_assistant: false
|
||||
}
|
||||
},
|
||||
normalizeValue(v) {
|
||||
if (!v) {
|
||||
return { providers: [], defaultProvider: 'openai' }
|
||||
}
|
||||
// allow legacy array value
|
||||
if (Array.isArray(v)) {
|
||||
return { providers: cloneDeep(v), defaultProvider: '' }
|
||||
}
|
||||
const providers = Array.isArray(v.providers) ? cloneDeep(v.providers) : []
|
||||
return { providers, defaultProvider: v.defaultProvider || '' }
|
||||
},
|
||||
pickDefault(current, providers) {
|
||||
const enabledProviders = providers.filter(item => item?.enabled !== false)
|
||||
if (current && providers.some(item => item.name === current)) {
|
||||
return current
|
||||
}
|
||||
return enabledProviders[0]?.name || providers[0]?.name || ''
|
||||
},
|
||||
emitChange() {
|
||||
const providers = cloneDeep(this.localProviders)
|
||||
const defaultProvider = this.pickDefault(this.defaultProvider, providers)
|
||||
this.defaultProvider = defaultProvider
|
||||
this.$emit('input', { providers, defaultProvider })
|
||||
this.$emit('change', { providers, defaultProvider })
|
||||
},
|
||||
handleAssistantChange(current) {
|
||||
if (current.IsAssistant) {
|
||||
this.localProviders.forEach(item => {
|
||||
if (item !== current) item.IsAssistant = false
|
||||
})
|
||||
}
|
||||
this.emitChange()
|
||||
},
|
||||
addProvider() {
|
||||
this.localProviders.push(this.emptyProvider())
|
||||
this.emitChange()
|
||||
},
|
||||
removeProvider(index) {
|
||||
this.localProviders.splice(index, 1)
|
||||
if (this.localProviders.length === 0) {
|
||||
this.localProviders.push(this.emptyProvider())
|
||||
}
|
||||
this.emitChange()
|
||||
},
|
||||
setDefault(name) {
|
||||
this.defaultProvider = name
|
||||
this.emitChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.providers-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.providers-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: #909399;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
@@ -147,7 +147,7 @@ export default {
|
||||
this.loading = true
|
||||
const url = `/api/v1/tickets/comments/?ticket_id=${this.object.id}`
|
||||
this.$axios.get(url).then(res => {
|
||||
this.comments = res.results
|
||||
this.comments = res
|
||||
}).catch(err => {
|
||||
this.$message.error(err)
|
||||
}).finally(() => {
|
||||
|
||||
@@ -161,8 +161,7 @@ module.exports = {
|
||||
target: process.env.VUE_APP_CORE_HOST || 'http://127.0.0.1:8080',
|
||||
changeOrigin: true
|
||||
}
|
||||
},
|
||||
after: require('./mock/mock-server.js')
|
||||
}
|
||||
},
|
||||
css: {},
|
||||
configureWebpack: {
|
||||
|
||||
Reference in New Issue
Block a user