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,42 +1,74 @@
<template>
<div :class="{'user-role': isUserRole}" class="chat-item">
<div class="avatar">
<el-avatar :src="isUserRole ? userUrl : chatUrl" class="header-avatar" />
</div>
<div class="content">
<div class="operational">
<span class="date">
{{ $moment(item.message.create_time).format('YYYY-MM-DD HH:mm:ss') }}
</span>
<div :class="{ 'user-role': isUserRole }" class="chat-item">
<div class="chart-item-container">
<div class="avatar">
<el-avatar
:src="isUserRole ? userUrl : chatUrl"
class="header-avatar"
/>
</div>
<div class="message">
<div class="message-content">
<span v-if="isSystemError" class="error">
{{ item.message.content }}
</span>
<span v-else class="chat-text">
<MessageText :message="item.message" />
</span>
<div class="content">
<div class="operational">
<div v-if="!item.message.is_reasoning" class="date">
{{
$moment(item.message.create_time).format("YYYY-MM-DD HH:mm:ss")
}}
</div>
<div v-else class="thinking-time">{{ $i18n.t('DeeplyThoughtAbout') }}</div>
</div>
<div class="action">
<el-tooltip
v-if="isSystemError && isLoading"
:content="$tc('Reconnect')"
:open-delay="500"
placement="top"
>
<svg-icon icon-class="refresh" @click="onRefresh" />
</el-tooltip>
<el-dropdown v-else size="small" @command="handleCommand">
<span class="el-dropdown-link">
<i class="fa fa-ellipsis-v" />
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="i in dropdownOptions" :key="i.action" :command="i.action">
{{ i.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div :class="item.reasoning ? 'reasoning' : 'message'">
<div class="message-content">
<div v-if="!item.reasoning">
<span v-if="isSystemError" class="error">
{{ item.message.content }}
</span>
<span v-else class="chat-text">
<MessageText :message="item.message" />
</span>
</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">
<el-tooltip
v-if="isSystemError && isLoading"
:content="$tc('Reconnect')"
:open-delay="500"
placement="top"
>
<svg-icon icon-class="refresh" @click="onRefresh" />
</el-tooltip>
<el-dropdown v-else size="small" @command="handleCommand">
<span class="el-dropdown-link">
<i class="fa fa-ellipsis-v" />
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="i in dropdownOptions"
:key="i.action"
:command="i.action"
>
{{ i.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
</div>
@ -45,7 +77,7 @@
<script>
import MessageText from './MessageText.vue'
import { mapState } from 'vuex'
import { mapGetters, mapState } from 'vuex'
import { copy } from '@/utils/common'
import { useChat } from '../../useChat.js'
import { reconnect } from '@/utils/socket'
@ -65,7 +97,6 @@ export default {
},
data() {
return {
chatUrl: require('@/assets/img/chat.png'),
userUrl: '/api/v1/settings/logo/',
dropdownOptions: [
{
@ -79,11 +110,26 @@ export default {
...mapState({
isLoading: state => state.chat.loading
}),
...mapGetters([
'publicSettings'
]),
isUserRole() {
return this.item.message?.role === 'user'
},
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: {
@ -94,7 +140,7 @@ export default {
},
handleCommand(value) {
if (value === 'copy') {
copy(this.item.message.content)
copy(this.item.result.content)
}
}
}
@ -104,101 +150,160 @@ export default {
<style lang="scss" scoped>
.chat-item {
display: flex;
padding: 16px 14px 0;
padding: 0.5rem;
&:last-child {
padding-bottom: 16px;
}
.chart-item-container {
display: flex;
gap: 0.5rem;
.avatar {
width: 22px;
height: 22px;
margin-top: 2px;
.avatar {
width: 24px;
height: 24px;
margin-top: 2px;
.header-avatar {
width: 100%;
height: 100%;
.header-avatar {
width: 100%;
height: 100%;
border-radius: 50%;
&::v-deep img {
background-color: #e5e5e7;
&::v-deep img {
background-color: #fff;
}
}
}
}
.content {
margin-left: 6px;
overflow: hidden;
.operational {
.content {
display: flex;
justify-content: space-between;
flex-direction: column;
// gap: 0.5rem;
overflow: hidden;
.copy {
float: right;
cursor: pointer;
}
}
.operational {
display: flex;
justify-content: space-between;
overflow: hidden;
.message {
display: -webkit-box;
.message-content {
flex: 1;
padding: 6px 10px;
border-radius: 2px 12px 12px;
background-color: #f0f1f5;
}
.action {
.svg-icon {
transform: translateY(50%);
margin-left: 3px;
cursor: pointer;
.date {
padding-top: 5px;
}
.el-dropdown {
height: 32px;
line-height: 37px;
font-size: 13px;
.thinking-time {
width: 6rem;
display: flex;
justify-content: center;
padding: 5px 10px;
border-radius: 0.5rem;
background-color: #f5f5f5;
}
.el-dropdown-link {
i {
padding: 4px 5px;
font-size: 15px;
color: #8d9091;
.copy {
float: right;
cursor: pointer;
}
}
&:hover {
color: #7b8085
.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;
}
}
}
}
}
.error {
color: red;
.message {
display: -webkit-box;
.message-content {
flex: 1;
padding: 6px 10px;
border-radius: 2px 12px 12px;
background-color: #f0f1f5;
}
.action {
.svg-icon {
transform: translateY(50%);
margin-left: 3px;
cursor: pointer;
}
.el-dropdown {
height: 32px;
line-height: 37px;
font-size: 13px;
.el-dropdown-link {
i {
padding: 4px 5px;
font-size: 15px;
color: #8d9091;
&:hover {
color: #7b8085;
}
}
}
}
}
.error {
color: red;
}
}
}
}
}
.user-role {
flex-direction: row-reverse;
&:last-child {
padding-bottom: 16px;
}
.content {
margin-right: 10px;
&.user-role {
flex-direction: row-reverse;
.operational {
.chart-item-container {
flex-direction: row-reverse;
}
.message {
flex-direction: row-reverse;
.content {
margin-right: 10px;
.message-content {
background-color: var(--menu-hover);
border-radius: 12px 2px 12px 12px;
.operational {
flex-direction: row-reverse;
}
.message {
flex-direction: row-reverse;
.message-content {
background-color: var(--menu-hover);
border-radius: 12px 2px 12px 12px;
}
}
}
}

View File

@ -123,7 +123,17 @@ export default {
setLoading(true)
removeLoadingMessageInChat()
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') {
setLoading(false)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,8 +40,8 @@ export default {
handleExportClick: {
type: Function,
default: function({ selectedRows }) {
const { exportOptions, tableUrl } = this
const url = exportOptions?.url ? exportOptions.url : tableUrl
// const { exportOptions, tableUrl } = this
const url = this.iExportOptions.url
this.dialogExportVisible = true
this.$nextTick(() => {
this.$eventBus.$emit('showExportDialog', { selectedRows, url, name: this.name })
@ -158,10 +158,15 @@ export default {
/**
* 原本是使用 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: {

View File

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

View File

@ -40,11 +40,26 @@ const mutations = {
updateChaMessageContentById(state, { id, data }) {
const chats = state.activeChat.chats || []
const filterChat = chats.filter((chat) => chat.message.id === id)?.[0] || {}
if (Object.keys(filterChat).length > 0) {
filterChat.message.content = data.message.content
const index = chats.findIndex((chat) => chat.message.id === data.message.id)
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 {
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'],
columnsMeta: {
name: {
formatterArgs: {
route: 'AccountTemplateDetail'
}
formatter: (row) => <span>{row.name}</span>
//
// formatterArgs: {
// route: 'AccountTemplateDetail'
// }
},
privileged: {
width: '120px'

View File

@ -71,10 +71,9 @@ export default {
form.validate((valid) => {
if (valid) {
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) {
this.$emit('update:visible', false)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<template>
<template>
<div>
<div class="variables el-data-table">
<el-table :data="variables" class="el-table--fit el-table--border">
@ -81,7 +81,7 @@ export default {
if (oldVal === undefined) return
if (newVal.length > 0 || !this.initial) {
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)
}

View File

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

View File

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

View File

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

View File

@ -19,7 +19,6 @@ import AssetPermissionAsset from '@/views/perms/AssetPermission/AssetPermissionD
import AssetPermissionDetail from '@/views/perms/AssetPermission/AssetPermissionDetail/index.vue'
import AssetPermissionAccount from '@/views/perms/AssetPermission/AssetPermissionDetail/AssetPermissionAccount.vue'
import UserAssetPermissionRules from './UserAssetPermissionRules'
import store from '@/store'
export default {
components: {
@ -64,7 +63,7 @@ export default {
{
title: this.$t('UserAclLists'),
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'),