Merge pull request #4659 from jumpserver/dev

v4.7.0
This commit is contained in:
Bryan 2025-02-20 10:20:32 +08:00 committed by GitHub
commit a861f77609
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 376 additions and 159 deletions

BIN
src/assets/img/deepSeek.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,16 +1,25 @@
<template> <template>
<div :class="{'user-role': isUserRole}" class="chat-item"> <div :class="{ 'user-role': isUserRole }" class="chat-item">
<div class="chart-item-container">
<div class="avatar"> <div class="avatar">
<el-avatar :src="isUserRole ? userUrl : chatUrl" class="header-avatar" /> <el-avatar
:src="isUserRole ? userUrl : chatUrl"
class="header-avatar"
/>
</div> </div>
<div class="content"> <div class="content">
<div class="operational"> <div class="operational">
<span class="date"> <div v-if="!item.message.is_reasoning" class="date">
{{ $moment(item.message.create_time).format('YYYY-MM-DD HH:mm:ss') }} {{
</span> $moment(item.message.create_time).format("YYYY-MM-DD HH:mm:ss")
}}
</div> </div>
<div class="message">
<div v-else class="thinking-time">{{ $i18n.t('DeeplyThoughtAbout') }}</div>
</div>
<div :class="item.reasoning ? 'reasoning' : 'message'">
<div class="message-content"> <div class="message-content">
<div v-if="!item.reasoning">
<span v-if="isSystemError" class="error"> <span v-if="isSystemError" class="error">
{{ item.message.content }} {{ item.message.content }}
</span> </span>
@ -18,6 +27,24 @@
<MessageText :message="item.message" /> <MessageText :message="item.message" />
</span> </span>
</div> </div>
<div v-else class="thinking-wrapper">
<div class="thinking-content">
<!-- eslint-disable-next-line -->
<div class="divider"></div>
<p>
<MessageText :message="item.reasoning" />
</p>
</div>
<div class="thinking-result">
<span v-if="isServerError" class="error">
{{ isServerError }}
</span>
<MessageText :message="item.result" />
</div>
</div>
</div>
<div class="action"> <div class="action">
<el-tooltip <el-tooltip
v-if="isSystemError && isLoading" v-if="isSystemError && isLoading"
@ -32,7 +59,11 @@
<i class="fa fa-ellipsis-v" /> <i class="fa fa-ellipsis-v" />
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="i in dropdownOptions" :key="i.action" :command="i.action"> <el-dropdown-item
v-for="i in dropdownOptions"
:key="i.action"
:command="i.action"
>
{{ i.label }} {{ i.label }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
@ -41,11 +72,12 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
import MessageText from './MessageText.vue' import MessageText from './MessageText.vue'
import { mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import { copy } from '@/utils/common' import { copy } from '@/utils/common'
import { useChat } from '../../useChat.js' import { useChat } from '../../useChat.js'
import { reconnect } from '@/utils/socket' import { reconnect } from '@/utils/socket'
@ -65,7 +97,6 @@ export default {
}, },
data() { data() {
return { return {
chatUrl: require('@/assets/img/chat.png'),
userUrl: '/api/v1/settings/logo/', userUrl: '/api/v1/settings/logo/',
dropdownOptions: [ dropdownOptions: [
{ {
@ -79,11 +110,26 @@ export default {
...mapState({ ...mapState({
isLoading: state => state.chat.loading isLoading: state => state.chat.loading
}), }),
...mapGetters([
'publicSettings'
]),
isUserRole() { isUserRole() {
return this.item.message?.role === 'user' return this.item.message?.role === 'user'
}, },
isSystemError() { isSystemError() {
return this.item.type === 'error' && this.item.message?.role === 'assistant' return (
this.item.type === 'error' && this.item?.role === 'assistant'
)
},
isServerError() {
return (this.item.type === 'finish' && this.item.result.content === '')
? this.$i18n.t('ServerBusyRetry')
: ''
},
chatUrl() {
return this.publicSettings.CHAT_AI_TYPE === 'gpt'
? require('@/assets/img/chat.png')
: require('@/assets/img/deepSeek.png')
} }
}, },
methods: { methods: {
@ -94,7 +140,7 @@ export default {
}, },
handleCommand(value) { handleCommand(value) {
if (value === 'copy') { if (value === 'copy') {
copy(this.item.message.content) copy(this.item.result.content)
} }
} }
} }
@ -104,29 +150,32 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-item { .chat-item {
display: flex; display: flex;
padding: 16px 14px 0; padding: 0.5rem;
&:last-child { .chart-item-container {
padding-bottom: 16px; display: flex;
} gap: 0.5rem;
.avatar { .avatar {
width: 22px; width: 24px;
height: 22px; height: 24px;
margin-top: 2px; margin-top: 2px;
.header-avatar { .header-avatar {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 50%;
&::v-deep img { &::v-deep img {
background-color: #e5e5e7; background-color: #fff;
} }
} }
} }
.content { .content {
margin-left: 6px; display: flex;
flex-direction: column;
// gap: 0.5rem;
overflow: hidden; overflow: hidden;
.operational { .operational {
@ -134,12 +183,59 @@ export default {
justify-content: space-between; justify-content: space-between;
overflow: hidden; overflow: hidden;
.date {
padding-top: 5px;
}
.thinking-time {
width: 6rem;
display: flex;
justify-content: center;
padding: 5px 10px;
border-radius: 0.5rem;
background-color: #f5f5f5;
}
.copy { .copy {
float: right; float: right;
cursor: pointer; cursor: pointer;
} }
} }
.reasoning {
display: flex;
gap: 0.5rem;
align-items: flex-end;
.message-content .thinking-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
.thinking-content {
position: relative;
color: #8b8b8b;
.divider {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-left: 2px solid #e5e5e5;
}
p {
margin: unset;
padding-left: 0.5rem;
::v-deep p {
color: #8b8b8b;
}
}
}
}
}
.message { .message {
display: -webkit-box; display: -webkit-box;
@ -169,7 +265,7 @@ export default {
color: #8d9091; color: #8d9091;
&:hover { &:hover {
color: #7b8085 color: #7b8085;
} }
} }
} }
@ -181,11 +277,19 @@ export default {
} }
} }
} }
} }
.user-role { &:last-child {
padding-bottom: 16px;
}
&.user-role {
flex-direction: row-reverse; flex-direction: row-reverse;
.chart-item-container {
flex-direction: row-reverse;
}
.content { .content {
margin-right: 10px; margin-right: 10px;
@ -202,5 +306,6 @@ export default {
} }
} }
} }
}
} }
</style> </style>

View File

@ -123,7 +123,17 @@ export default {
setLoading(true) setLoading(true)
removeLoadingMessageInChat() removeLoadingMessageInChat()
this.conversationId = data.id this.conversationId = data.id
updateChaMessageContentById(data.message.id, data)
const newFragment = {
message: { id: data.message.id, is_reasoning: data.message.is_reasoning },
reasoning: { content: data.message.is_reasoning ? data.message.content : '' },
result: { content: data.message.is_reasoning ? '' : data.message.content },
role: data.message.role,
type: data.message.type,
create_time: data.message.create_time
}
updateChaMessageContentById(data.message.id, newFragment)
} }
if (data.message?.type === 'finish') { if (data.message?.type === 'finish') {
setLoading(false) setLoading(false)

View File

@ -98,7 +98,7 @@
type="primary" type="primary"
@click="handleFaceCapture" @click="handleFaceCapture"
> >
开始人脸识别 {{ this.$tc('VerifyFace') }}
</el-button> </el-button>
</el-col> </el-col>
</el-row> </el-row>

View File

@ -84,6 +84,12 @@ export default {
</script> </script>
<style lang='scss' scoped> <style lang='scss' scoped>
html:lang(pt-br) {
.datepicker ::v-deep .el-range-separator {
padding: 0 10px;
}
}
.datepicker { .datepicker {
margin-left: 10px; margin-left: 10px;
width: 233px; width: 233px;

View File

@ -76,8 +76,7 @@ export default {
font-size: 13px; font-size: 13px;
span { span {
overflow: hidden; white-space: nowrap;
white-space: nowrap; /* 控制文本不换行 */
text-overflow: ellipsis; text-overflow: ellipsis;
display: block; display: block;
} }

View File

@ -36,6 +36,7 @@
<el-checkbox <el-checkbox
:disabled="item.prop==='actions' || minColumns.indexOf(item.prop)!==-1" :disabled="item.prop==='actions' || minColumns.indexOf(item.prop)!==-1"
:label="item.prop" :label="item.prop"
:title="item.label"
> >
{{ item.label }} {{ item.label }}
</el-checkbox> </el-checkbox>

View File

@ -65,7 +65,17 @@ export default {
isDeactivated: false isDeactivated: false
} }
}, },
computed: {}, computed: {
dynamicActionWidth() {
if (this.$i18n.locale === 'en') {
return '120px'
}
if (this.$i18n.locale === 'pt-br') {
return '160px'
}
return '100px'
}
},
watch: { watch: {
config: { config: {
handler: _.debounce(function(iNew, iOld) { handler: _.debounce(function(iNew, iOld) {
@ -131,7 +141,7 @@ export default {
prop: 'actions', prop: 'actions',
label: i18n.t('Actions'), label: i18n.t('Actions'),
align: 'center', align: 'center',
width: '100px', width: this.dynamicActionWidth,
formatter: ActionsFormatter, formatter: ActionsFormatter,
fixed: 'right', fixed: 'right',
formatterArgs: {} formatterArgs: {}

View File

@ -40,8 +40,8 @@ export default {
handleExportClick: { handleExportClick: {
type: Function, type: Function,
default: function({ selectedRows }) { default: function({ selectedRows }) {
const { exportOptions, tableUrl } = this // const { exportOptions, tableUrl } = this
const url = exportOptions?.url ? exportOptions.url : tableUrl const url = this.iExportOptions.url
this.dialogExportVisible = true this.dialogExportVisible = true
this.$nextTick(() => { this.$nextTick(() => {
this.$eventBus.$emit('showExportDialog', { selectedRows, url, name: this.name }) this.$eventBus.$emit('showExportDialog', { selectedRows, url, name: this.name })
@ -158,10 +158,15 @@ export default {
/** /**
* 原本是使用 assignIfNot 此函数内部使用 partialRight, 该函数 * 原本是使用 assignIfNot 此函数内部使用 partialRight, 该函数
* 只在目标对象的属性未定义时才从源对象复制属性如果目标对象已经有值则保留原值 * 只在目标对象的属性未定义时才从源对象复制属性如果目标对象已经有值则保留原值
* 那如果首次点击的树节点那么此时 url 就会被确定后续点击的树节点那么 url 不会 * 那如果首次点击的树节点那么此时 url 就会被确定后续点击的树节点那么 url 不会携带节点信息
* 改变了 *
*/ */
return Object.assign({}, this.exportOptions, { url: this.tableUrl }) // return assignIfNot(this.exportOptions, { url: this.tableUrl })
return {
...this.exportOptions,
url: this.tableUrl
}
} }
}, },
methods: { methods: {

View File

@ -24,7 +24,6 @@ export default [
meta: { meta: {
title: i18n.t('BaseUserLoginAclList'), title: i18n.t('BaseUserLoginAclList'),
app: 'acls', app: 'acls',
licenseRequired: true,
resource: 'loginacl', resource: 'loginacl',
disableOrgsChange: true disableOrgsChange: true
}, },

View File

@ -40,11 +40,26 @@ const mutations = {
updateChaMessageContentById(state, { id, data }) { updateChaMessageContentById(state, { id, data }) {
const chats = state.activeChat.chats || [] const chats = state.activeChat.chats || []
const filterChat = chats.filter((chat) => chat.message.id === id)?.[0] || {} const index = chats.findIndex((chat) => chat.message.id === data.message.id)
if (Object.keys(filterChat).length > 0) {
filterChat.message.content = data.message.content if (index === -1) {
// 如果没有记录,直接添加新消息
chats.push({
message: { id: data.message.id },
reasoning: { content: data.reasoning.content },
result: { content: data.result.content },
role: data.role,
type: data.type,
create_time: data.create_time
})
} else { } else {
chats?.push(data) if (data.reasoning.content !== '') {
chats[index].reasoning.content = data.reasoning.content
}
if (data.result.content !== '') {
chats[index].result.content = data.result.content
}
} }
} }
} }

View File

@ -65,9 +65,11 @@ export default {
columns: ['name', 'username', 'secret_type', 'privileged'], columns: ['name', 'username', 'secret_type', 'privileged'],
columnsMeta: { columnsMeta: {
name: { name: {
formatterArgs: { formatter: (row) => <span>{row.name}</span>
route: 'AccountTemplateDetail' //
} // formatterArgs: {
// route: 'AccountTemplateDetail'
// }
}, },
privileged: { privileged: {
width: '120px' width: '120px'

View File

@ -71,10 +71,9 @@ export default {
form.validate((valid) => { form.validate((valid) => {
if (valid) { if (valid) {
btn.loading = true btn.loading = true
this.$refs.form.$refs.form.dataForm.submitForm('form', false)
} }
}) })
form.value.interval = parseInt(form.value.interval, 10)
this.$refs.form.$refs.form.dataForm.submitForm('form', false)
}, },
handleSubmitSuccess(res) { handleSubmitSuccess(res) {
this.$emit('update:visible', false) this.$emit('update:visible', false)

View File

@ -39,8 +39,7 @@ export default {
submitBtnSize: 'mini', submitBtnSize: 'mini',
submitBtnText: this.$t('Add'), submitBtnText: this.$t('Add'),
hasReset: false, hasReset: false,
onSubmit: () => { onSubmit: () => {},
},
submitMethod: () => 'post', submitMethod: () => 'post',
getUrl: () => '', getUrl: () => '',
cleanFormValue(data) { cleanFormValue(data) {
@ -110,26 +109,30 @@ export default {
url: '', url: '',
clearable: false, clearable: false,
transformOption: (item) => { transformOption: (item) => {
let label let display
switch (this.resourceType) { switch (this.resourceType) {
case 'platform': case 'platform':
label = item?.name display = item?.name
this.globalProtocols[item.id] = item.protocols this.globalProtocols[item.id] = item.protocols
this.globalResource[item.id] = label this.globalResource[item.id] = display
break break
case 'account_template': case 'account_template':
label = `${item.name}(${item.username})` display = `${item.name}(${item.username})`
this.globalResource[item.id] = label this.globalResource[item.id] = display
break break
case 'node': case 'node':
label = item?.full_value display = item?.full_value
this.globalResource[item.id] = label this.globalResource[item.id] = display
break
case 'label':
display = `${item.name}:${item.value}`
this.globalResource[item.id] = display
break break
default: default:
label = item?.name display = item?.name
this.globalResource[item.id] = label this.globalResource[item.id] = display
} }
return { label: label, value: item.id } return { label: display, value: item.id }
} }
}, },
multiple: false multiple: false

View File

@ -32,8 +32,7 @@ export default {
}, },
{ {
prop: 'total', prop: 'total',
label: this.$t('LoginCount'), label: this.$t('LoginCount')
width: '120px'
} }
] ]
}, },
@ -49,8 +48,7 @@ export default {
}, },
{ {
prop: 'total', prop: 'total',
label: this.$t('NumberOfVisits'), label: this.$t('NumberOfVisits')
width: '140px'
} }
] ]
} }
@ -58,7 +56,3 @@ export default {
} }
} }
</script> </script>
<style scoped>
</style>

View File

@ -9,18 +9,22 @@
class="table" class="table"
style="width: 100%" style="width: 100%"
> >
<el-table-column :label="$tc('Ranking')" width="80px"> <el-table-column :label="$tc('Ranking')">
<template v-slot="scope" #header> <template #header>
<el-tooltip :content="$t('Ranking')" placement="top" :open-delay="500"> <el-tooltip :content="$t('Ranking')" placement="top" :open-delay="500">
<span style="cursor: pointer;">{{ $t('Ranking') }}</span> <span style="cursor: pointer;">{{ $t('Ranking') }}</span>
</el-tooltip> </el-tooltip>
</template> </template>
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
v-for="i in config.columns" v-for="i in config.columns"
:key="i.prop" :key="i.prop"
:prop="i.prop" :prop="i.prop"
:width="i.width" :width="getColumnWidth(i)"
> >
<template #header> <template #header>
<el-tooltip :content="i.label" placement="top" :open-delay="500"> <el-tooltip :content="i.label" placement="top" :open-delay="500">
@ -63,6 +67,19 @@ export default {
this.getList() this.getList()
}, },
methods: { methods: {
getColumnWidth(column) {
if (column.prop === 'total') {
const locale = this.$i18n.locale
switch (locale) {
case 'en':
return '120px'
case 'pt-br':
return '220px'
default:
return '100px'
}
}
},
getList() { getList() {
this.$axios.get(this.tableUrl).then(res => { this.$axios.get(this.tableUrl).then(res => {
this.tableData = this.config.data ? res?.[this.config.data] : res this.tableData = this.config.data ? res?.[this.config.data] : res

View File

@ -76,7 +76,7 @@ export default {
} }
}, },
actions: { actions: {
width: '120px', width: '130px',
formatterArgs: { formatterArgs: {
hasUpdate: false, hasUpdate: false,
hasDelete: false, hasDelete: false,

View File

@ -1,4 +1,4 @@
<template> <template>
<div> <div>
<div class="variables el-data-table"> <div class="variables el-data-table">
<el-table :data="variables" class="el-table--fit el-table--border"> <el-table :data="variables" class="el-table--fit el-table--border">
@ -81,7 +81,7 @@ export default {
if (oldVal === undefined) return if (oldVal === undefined) return
if (newVal.length > 0 || !this.initial) { if (newVal.length > 0 || !this.initial) {
newVal.map((item) => { newVal.map((item) => {
item.default_value = item.text_default_value || item.select_default_value item.default_value = item.text_default_value || item.select_default_value || undefined
}) })
this.$emit('input', newVal) this.$emit('input', newVal)
} }

View File

@ -9,21 +9,21 @@ export const UserAssetPermissionListPageSearchConfigOptions = [
{ label: i18n.t('AssetAddress'), value: 'address' }, { label: i18n.t('AssetAddress'), value: 'address' },
{ label: i18n.t('Account'), value: 'accounts' }, { label: i18n.t('Account'), value: 'accounts' },
{ {
label: i18n.t('isValid'), value: 'is_valid', label: i18n.t('Valid'), value: 'is_valid',
children: [ children: [
{ value: '1', label: i18n.t('Yes') }, { value: '1', label: i18n.t('Yes') },
{ value: '0', label: i18n.t('No') } { value: '0', label: i18n.t('No') }
] ]
}, },
{ {
label: i18n.t('isEffective'), value: 'is_effective', label: i18n.t('Effective'), value: 'is_effective',
children: [ children: [
{ value: '1', label: i18n.t('Yes') }, { value: '1', label: i18n.t('Yes') },
{ value: '0', label: i18n.t('No') } { value: '0', label: i18n.t('No') }
] ]
}, },
{ {
label: i18n.t('fromTicket'), value: 'from_ticket', label: i18n.t('FromTicket'), value: 'from_ticket',
children: [ children: [
{ value: '1', label: i18n.t('Yes') }, { value: '1', label: i18n.t('Yes') },
{ value: '0', label: i18n.t('No') } { value: '0', label: i18n.t('No') }

View File

@ -136,7 +136,7 @@ export default {
actions: { actions: {
prop: 'actions', prop: 'actions',
label: this.$t('Actions'), label: this.$t('Actions'),
width: '130px', width: this.dynamicActionWidth,
formatter: ActionsFormatter, formatter: ActionsFormatter,
formatterArgs: { formatterArgs: {
hasEdit: false, hasEdit: false,
@ -159,6 +159,14 @@ export default {
} }
} }
} }
},
computed: {
dynamicActionWidth() {
if (this.$i18n.locale === 'pt-br') {
return '160px'
}
return '130px'
}
} }
} }
</script> </script>

View File

@ -41,23 +41,68 @@ export default {
encryptedFields: ['VAULT_HCP_TOKEN'], encryptedFields: ['VAULT_HCP_TOKEN'],
fields: [ fields: [
'CHAT_AI_ENABLED', 'CHAT_AI_ENABLED',
'GPT_MODEL', 'CHAT_AI_TYPE',
'DEEPSEEK_BASE_URL',
'DEEPSEEK_API_KEY',
'DEEPSEEK_PROXY',
'DEEPSEEK_MODEL',
'GPT_BASE_URL', 'GPT_BASE_URL',
'GPT_API_KEY', 'GPT_API_KEY',
'GPT_PROXY' 'GPT_PROXY',
'GPT_MODEL'
], ],
fieldsMeta: { fieldsMeta: {
GPT_BASE_URL: { GPT_BASE_URL: {
el: { el: {
autocomplete: 'new-password' autocomplete: 'new-password'
},
hidden: (formValue) => {
return formValue.CHAT_AI_TYPE !== 'gpt'
} }
}, },
GPT_API_KEY: { GPT_API_KEY: {
el: { el: {
autocomplete: 'new-password' autocomplete: 'new-password'
},
hidden: (formValue) => {
return formValue.CHAT_AI_TYPE !== 'gpt'
} }
}, },
GPT_PROXY: { GPT_PROXY: {
hidden: (formValue) => {
return formValue.CHAT_AI_TYPE !== 'gpt'
}
},
GPT_MODEL: {
hidden: (formValue) => {
return formValue.CHAT_AI_TYPE !== 'gpt'
}
},
DEEPSEEK_BASE_URL: {
el: {
autocomplete: 'new-password'
},
hidden: (formValue) => {
return formValue.CHAT_AI_TYPE !== 'deep-seek'
}
},
DEEPSEEK_API_KEY: {
el: {
autocomplete: 'new-password'
},
hidden: (formValue) => {
return formValue.CHAT_AI_TYPE !== 'deep-seek'
}
},
DEEPSEEK_PROXY: {
hidden: (formValue) => {
return formValue.CHAT_AI_TYPE !== 'deep-seek'
}
},
DEEPSEEK_MODEL: {
hidden: (formValue) => {
return formValue.CHAT_AI_TYPE !== 'deep-seek'
}
} }
}, },
submitMethod() { submitMethod() {

View File

@ -19,7 +19,6 @@ import AssetPermissionAsset from '@/views/perms/AssetPermission/AssetPermissionD
import AssetPermissionDetail from '@/views/perms/AssetPermission/AssetPermissionDetail/index.vue' import AssetPermissionDetail from '@/views/perms/AssetPermission/AssetPermissionDetail/index.vue'
import AssetPermissionAccount from '@/views/perms/AssetPermission/AssetPermissionDetail/AssetPermissionAccount.vue' import AssetPermissionAccount from '@/views/perms/AssetPermission/AssetPermissionDetail/AssetPermissionAccount.vue'
import UserAssetPermissionRules from './UserAssetPermissionRules' import UserAssetPermissionRules from './UserAssetPermissionRules'
import store from '@/store'
export default { export default {
components: { components: {
@ -64,7 +63,7 @@ export default {
{ {
title: this.$t('UserAclLists'), title: this.$t('UserAclLists'),
name: 'UserLoginAcl', name: 'UserLoginAcl',
hidden: () => !vm.$hasPerm('acls.view_loginacl') || !store.getters.publicSettings.XPACK_LICENSE_IS_VALID hidden: () => !vm.$hasPerm('acls.view_loginacl')
}, },
{ {
title: this.$t('UserSession'), title: this.$t('UserSession'),