Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,10 @@ Model 配置器的默认模型也使用 `SelectModel`,仅展示已选的可选
启停成功后刷新,失败保留原状态。查询与列表操作共用 loading,编辑抽屉单独管理保存状态。
密钥默认隐藏,翻页或刷新恢复隐藏,复制始终使用完整值,隐藏状态不通过 Tooltip 暴露明文。

`EditApiKeyDrawer` 是本功能内部抽屉,按永不过期、预设有效期或自定义日期编辑,预设有效期
创建入口与结果 Dialog 封装在内部 `create-api-key/index.vue`(`ButtonCreateApiKey`),接收完整 API、
可选 `applicationId`、共享 `v-model:loading` 和异步 `refresh` 回调;创建成功展示密钥,并等待列表回到第一页刷新后释放 loading。
编辑入口与 Drawer 封装在内部 `edit-api-key/index.vue`(`ButtonEditApiKey`),接收完整 API、可选 `applicationId`
和当前 `record`,保存成功通过 `refresh` 事件通知列表。按永不过期、预设有效期或自定义日期编辑,预设有效期
从保存时起算;跨域地址逐行去空白后提交数组。保存失败保留草稿,成功刷新列表。
备注按 `remark` 预留,当前后端尚未持久化。Base URL 或 API 文档地址由调用方在列表外展示。

Expand Down
61 changes: 61 additions & 0 deletions ui/src/components/business/api-key-list/create-api-key/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { ref } from 'vue'
import type ApplicationApiKeyApi from '@/api/admin/workspace/application/api-key'
import type SystemResourceApiKeyApi from '@/api/admin/system/resource-management/application/api-key'
import type SystemApiKeyApi from '@/api/admin/system/api-key'
import { copyText } from '@/utils/clipboard'

defineOptions({ name: 'ButtonCreateApiKey' })

const props = defineProps<{
api: typeof ApplicationApiKeyApi | typeof SystemResourceApiKeyApi | typeof SystemApiKeyApi
applicationId?: string
refresh: () => Promise<void>
}>()
const loading = defineModel<boolean>('loading', { required: true })

/* 创建 API Key 与结果弹窗 */
const createdApiKey = ref('')
const createdDialogVisible = ref(false)

function handleCreate() {
if (loading.value) return
loading.value = true
const request =
props.applicationId !== undefined
? (props.api as typeof ApplicationApiKeyApi | typeof SystemResourceApiKeyApi).postApiKey(props.applicationId)
: (props.api as typeof SystemApiKeyApi).postApiKey()
return request
.then((record) => {
createdApiKey.value = record.secret_key
createdDialogVisible.value = true
return props.refresh()
})
.finally(() => {
loading.value = false
})
}
</script>

<template>
<!-- 创建 API Key -->
<el-button type="primary" @click="handleCreate">
<MkIcon name="icon_add_outlined" />
<span>创建</span>
</el-button>
<MkDialog v-model="createdDialogVisible" title="API Key" @closed="createdApiKey = ''">
<template #subtitle>请安全保存此密钥,不要泄露。</template>
<el-input :model-value="createdApiKey" readonly>
<template #suffix>
<!-- 复制新建的完整 API Key -->
<el-button text @click="copyText(createdApiKey)" class="-mr-1">
<MkIcon name="icon_copy_outlined" />
</el-button>
</template>
</el-input>
<template #footer>
<!-- 关闭创建结果弹窗 -->
<el-button plain @click="createdDialogVisible = false">知道了</el-button>
</template>
</MkDialog>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ import type { ApiKeyPayload, ApiKeyRecord } from '@/api/types'
import type ApplicationApiKeyApi from '@/api/admin/workspace/application/api-key'
import type SystemResourceApiKeyApi from '@/api/admin/system/resource-management/application/api-key'
import type SystemApiKeyApi from '@/api/admin/system/api-key'
import { expirationOptions, getApiKeyExpireTime, type ExpirationType } from './expiration'
import { expirationOptions, getApiKeyExpireTime, type ExpirationType } from '../expiration'
import { copyText } from '@/utils/clipboard'
import { MsgSuccess } from '@/utils/message'

defineOptions({ name: 'EditApiKeyDrawer' })
defineOptions({ name: 'ButtonEditApiKey' })

const props = defineProps<{
api: typeof ApplicationApiKeyApi | typeof SystemResourceApiKeyApi | typeof SystemApiKeyApi
applicationId?: string
record: ApiKeyRecord
}>()
const emit = defineEmits<{ refresh: [] }>()
/* 编辑 API Key 与配置抽屉 */
const visible = ref(false)
const saving = ref(false)
const apiKeyId = ref('')
Expand Down Expand Up @@ -54,10 +56,10 @@ const rules: FormRules<ApiKeyForm> = {
trigger: 'change',
},
],
remark: [{ max: 256, message: '备注最多 256 个字符', trigger: 'blur' }],
}

function open(record: ApiKeyRecord) {
function handleOpenEdit() {
const record = props.record
apiKeyId.value = record.id
Object.assign(form, {
secret_key: record.secret_key,
Expand Down Expand Up @@ -119,11 +121,15 @@ function handleClosed() {
form.is_active = true
formRef.value?.clearValidate()
}

defineExpose({ open })
</script>

<template>
<!-- 编辑 API Key -->
<MkTooltip content="编辑" placement="top">
<el-button type="primary" text @click.stop="handleOpenEdit">
<MkIcon name="icon_edit_outlined" />
</el-button>
</MkTooltip>
<MkDrawer v-model="visible" title="编辑 API Key" @closed="handleClosed">
<el-form ref="formRef" :model="form" :rules="rules" :inert="saving" label-position="top" require-asterisk-position="right" @submit.prevent>
<el-form-item label="API KEY">
Expand Down Expand Up @@ -158,7 +164,7 @@ defineExpose({ open })
v-model="form.cross_domain_list"
type="textarea"
:rows="8"
placeholder="请输入允许的跨域地址,开启后不输入跨域地址则不限制。&#10;跨域地址一行一个,如:&#10;http://127.0.0.1:5678&#10;https://dataease.io"
:placeholder="'请输入允许的跨域地址,开启后不输入跨域地址则不限制。\n跨域地址一行一个,如:\nhttp://127.0.0.1:5678\nhttps://dataease.io'"
/>
</el-form-item>
<el-form-item label="启用状态">
Expand Down
74 changes: 17 additions & 57 deletions ui/src/components/business/api-key-list/index.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed, onMounted, ref, useTemplateRef } from 'vue'
import { computed, onMounted, ref } from 'vue'
import type { ApiKeyRecord } from '@/api/types'
import type ApplicationApiKeyApi from '@/api/admin/workspace/application/api-key'
import type SystemResourceApiKeyApi from '@/api/admin/system/resource-management/application/api-key'
import type SystemApiKeyApi from '@/api/admin/system/api-key'
import EditApiKeyDrawer from './EditApiKeyDrawer.vue'
import ButtonCreateApiKey from './create-api-key/index.vue'
import ButtonEditApiKey from './edit-api-key/index.vue'
import { getApiKeyExpiration } from './expiration'
import { copyText } from '@/utils/clipboard'
import { datetimeFormat } from '@/utils/time'
Expand All @@ -18,10 +19,7 @@ const props = defineProps<{
}>()
const loading = ref(false)
const apiKeys = ref<ApiKeyRecord[]>([])
const createdApiKey = ref('')
const createdDialogVisible = ref(false)
const pagination = ref({ currentPage: 1, pageSize: 20, total: 0 })
const editDrawerRef = useTemplateRef<InstanceType<typeof EditApiKeyDrawer>>('editDrawerRef')
const apiKeyRows = computed(() => apiKeys.value.map((record) => ({ ...record, expiration: getApiKeyExpiration(record) })))

function fetchApiKeys() {
Expand All @@ -43,23 +41,9 @@ function loadApiKeys() {
})
}

function handleCreate() {
if (loading.value) return
loading.value = true
const request =
props.applicationId !== undefined
? (props.api as typeof ApplicationApiKeyApi | typeof SystemResourceApiKeyApi).postApiKey(props.applicationId)
: (props.api as typeof SystemApiKeyApi).postApiKey()
return request
.then((record) => {
createdApiKey.value = record.secret_key
createdDialogVisible.value = true
pagination.value.currentPage = 1
return fetchApiKeys()
})
.finally(() => {
loading.value = false
})
function handleCreated() {
pagination.value.currentPage = 1
return fetchApiKeys()
}

function handleChangeActive(record: ApiKeyRecord) {
Expand Down Expand Up @@ -105,28 +89,24 @@ function handleDelete(record: ApiKeyRecord) {
)
}

function handleOpenEdit(record: ApiKeyRecord) {
editDrawerRef.value?.open(record)
}

onMounted(loadApiKeys)
</script>

<template>
<div v-loading="loading" :inert="loading">
<!-- 创建 API Key -->
<el-button type="primary" class="mb-4" @click="handleCreate">
<MkIcon name="icon_add_outlined" />
<span>创建</span>
</el-button>
<!-- 创建 API Key -->
<div class="mb-4">
<ButtonCreateApiKey :api="props.api" :application-id="props.applicationId" v-model:loading="loading" :refresh="handleCreated" />
</div>

<MkTable
v-model:pagination-config="pagination"
:data="apiKeyRows"
:max-table-height="425"
@current-change="loadApiKeys"
@size-change="loadApiKeys"
>
<el-table-column label="API Key" min-width="160">
<el-table-column label="API Key" width="140">
<template #default="{ row }">
<div class="flex-align-center gap-2">
<span>
Expand All @@ -151,7 +131,7 @@ onMounted(loadApiKeys)
</el-tag>
</template>
</el-table-column>
<el-table-column label="到期时间" min-width="265">
<el-table-column label="到期时间" width="265">
<template #default="{ row }">
<span :class="{ 'text-danger': row.expiration.status === 'expired', 'text-warning': row.expiration.status === 'soon' }">
{{ row.expiration.label }}
Expand All @@ -161,10 +141,10 @@ onMounted(loadApiKeys)
</span>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" show-overflow-tooltip>
<el-table-column label="备注" show-overflow-tooltip>
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" min-width="180">
<el-table-column prop="create_time" label="创建时间" width="180">
<template #default="{ row }">{{ datetimeFormat(row.create_time) || '—' }}</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right">
Expand All @@ -173,12 +153,8 @@ onMounted(loadApiKeys)
<!-- 启用或禁用 API Key -->
<el-switch :model-value="row.is_active" size="small" :before-change="() => handleChangeActive(row)" />
<el-divider direction="vertical" />
<!-- 编辑 API Key -->
<MkTooltip content="编辑" placement="top">
<el-button type="primary" text @click.stop="handleOpenEdit(row)">
<MkIcon name="icon_edit_outlined" />
</el-button>
</MkTooltip>
<!-- 编辑API Key -->
<ButtonEditApiKey :api="props.api" :application-id="props.applicationId" :record="row" @refresh="loadApiKeys" />
<!-- 删除 API Key -->
<MkTooltip content="删除" placement="top">
<el-button class="ml-0!" type="primary" text @click.stop="handleDelete(row)">
Expand All @@ -190,20 +166,4 @@ onMounted(loadApiKeys)
</el-table-column>
</MkTable>
</div>
<MkDialog v-model="createdDialogVisible" title="API Key" @closed="createdApiKey = ''">
<template #subtitle>请安全保存此密钥,不要泄露。</template>
<el-input :model-value="createdApiKey" readonly>
<template #suffix>
<!-- 复制新建的完整 API Key -->
<el-button text @click="copyText(createdApiKey)" class="-mr-1">
<MkIcon name="icon_copy_outlined" />
</el-button>
</template>
</el-input>
<template #footer>
<!-- 关闭创建结果弹窗 -->
<el-button plain @click="createdDialogVisible = false">知道了</el-button>
</template>
</MkDialog>
<EditApiKeyDrawer v-bind="props" ref="editDrawerRef" @refresh="loadApiKeys" />
</template>
2 changes: 1 addition & 1 deletion ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排
Xpack 版可用登录方式查询 `/chat_user/auth/types`,开启认证时至少选一种;账号登录的验证码阈值沿用 -1~10。
默认登录方式从已勾选的登录方式中选择,取消对应登录方式时同步调整默认值;账号锁定次数与分钟数正常编辑。
前端开放认证配置,不因字段缺失或旧版密码模式禁用;保留已有认证类型,字段生效能力由后台联调确认。
未取得完整配置时不提交保存,不提供失败空态或重试按钮;提交失败保留草稿。
查询与保存共用 loading,请求期间阻止重复提交;不提供失败空态或重试按钮,提交失败保留草稿。
白名单逐行去空白,保存时保留原语言和其他认证字段。
三类场景统一调用 `getMonitoring(workspaceId, range, applicationId)`;全部智能体使用空 ID,
两类首页不提交智能体筛选,概览传当前智能体 ID。页面直接传入完整 API,不包装接口。
Expand Down
23 changes: 10 additions & 13 deletions ui/src/views/application-detail/access/AccessSettingDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { computed, onMounted, reactive, ref, useTemplateRef } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'

import { APPLICATION_ACCESS_PLATFORM } from '@/api/enums'
import type { ApplicationAccessConfig, Dict } from '@/api/types'
import type { ApplicationAccessConfig } from '@/api/types'
import { ADMIN_API_BASE_PATH } from '@/api/constants'
import { copyText } from '@/utils/clipboard'
import { MsgSuccess } from '@/utils/message'
Expand All @@ -18,15 +18,13 @@ const emit = defineEmits<{ refresh: []; closed: [] }>()
const visible = ref(true)
const loading = ref(false)
const formRef = useTemplateRef<FormInstance>('formRef')
const form = reactive<Dict<string>>({})
const isCertification = ref(false)
let savedConfig: ApplicationAccessConfig = {}
const form = reactive<ApplicationAccessConfig>({})

const isWechat = computed(() => props.platform.key === APPLICATION_ACCESS_PLATFORM.WECHAT)
const modeField = computed(() => props.platform.modeField)
// 表单字段随连接方式切换:长连接(ws)需要 bot_id/secret,URL 回调(callback)需要 token/encoding_aes_key。
const activeFields = computed(() => {
const mode = form[modeField.value?.key ?? ''] ?? modeField.value?.default
const mode = String(form[modeField.value?.key ?? ''] ?? modeField.value?.default ?? '')
return (modeField.value && mode ? modeField.value.fields?.[mode] : undefined) ?? props.platform.fields
})
// 智能机器人默认长连接(ws),无需回调地址;切到 URL 回调时才展示回调地址。
Expand All @@ -47,12 +45,12 @@ function loadPlatformConfig() {
return resourceApi.access
.getPlatformConfig(props.applicationId, props.platform.key)
.then((config) => {
savedConfig = config ?? {}
if (modeField.value) form[modeField.value.key] = String(savedConfig[modeField.value.key] ?? modeField.value.default)
Object.assign(form, config ?? {})
if (modeField.value) form[modeField.value.key] = String(form[modeField.value.key] ?? modeField.value.default)
activeFields.value.forEach((field) => {
form[field.key] = String(savedConfig[field.key] ?? '')
form[field.key] = String(form[field.key] ?? '')
})
isCertification.value = savedConfig.is_certification ?? false
if (isWechat.value) form.is_certification ??= false
})
.finally(() => {
loading.value = false
Expand All @@ -65,8 +63,7 @@ function handleSave() {
formRef.value?.validate((valid) => {
if (!valid || loading.value) return
loading.value = true
const payload: ApplicationAccessConfig = { ...savedConfig, ...form, callback_url: callbackUrl.value }
if (isWechat.value) payload.is_certification = isCertification.value
const payload: ApplicationAccessConfig = { ...form, callback_url: callbackUrl.value }
resourceApi.access
.postPlatformConfig(props.applicationId, props.platform.key, payload)
.then(() => {
Expand Down Expand Up @@ -103,7 +100,7 @@ onMounted(loadPlatformConfig)
<template v-for="field in activeFields" :key="field.key">
<el-form-item :label="field.label" :prop="field.key">
<el-input
v-model="form[field.key]"
v-model="form[field.key] as string"
:type="field.secret ? 'password' : 'text'"
:show-password="field.secret"
:autocomplete="field.secret ? 'new-password' : 'off'"
Expand All @@ -113,7 +110,7 @@ onMounted(loadPlatformConfig)
</template>
<el-form-item v-if="isWechat" label="认证成功">
<!-- 设置公众号认证状态 -->
<el-switch v-model="isCertification" />
<el-switch v-model="form.is_certification" />
</el-form-item>

<el-form-item v-if="showCallback" label="回调地址">
Expand Down
Loading
Loading