feat: resource authorization

This commit is contained in:
wangdan-fit2cloud 2025-05-30 14:02:54 +08:00
parent b1a6c463d0
commit 1dfff30daf
9 changed files with 845 additions and 6 deletions

View File

@ -0,0 +1,43 @@
import { Result } from '@/request/Result'
import { get, put, post, del } from '@/request/index'
import type { pageRequest } from '@/api/type/common'
import type { Ref } from 'vue'
const prefix = '/workspace'
/**
*
* @query
*/
const getResourceAuthorization: (workspace_id: String) => Promise<Result<any>> = (workspace_id) => {
return get(`${prefix}/${workspace_id}/user_resource_permission`)
}
/**
*
* @param member_id
* @param {
"team_resource_permission_list": [
{
"auth_target_type": "KNOWLEDGE",
"target_id": "string",
"auth_type": "ROLE",
"permission": {
"VIEW": true,
"MANAGE": true,
"ROLE": true
}
}
]
}
*/
const putResourceAuthorization: (workspace_id: String, body: any) => Promise<Result<any>> = (
workspace_id,
body,
) => {
return put(`${prefix}/${workspace_id}/user_resource_permission`, body)
}
export default {
getResourceAuthorization,
putResourceAuthorization,
}

6
ui/src/enums/system.ts Normal file
View File

@ -0,0 +1,6 @@
export enum AuthorizationEnum {
MANAGE = 'MANAGE',
USE = 'USE',
DATASET = 'DATASET',
APPLICATION = 'APPLICATION'
}

View File

@ -0,0 +1,216 @@
export default {
title: '应用',
createApplication: '创建应用',
importApplication: '导入应用',
copyApplication: '复制应用',
workflow: '高级编排',
simple: '简单配置',
searchBar: {
placeholder: '按名称搜索'
},
setting: {
demo: '演示'
},
delete: {
confirmTitle: '是否删除应用:',
confirmMessage: '删除后该应用将不再提供服务,请谨慎操作。'
},
tip: {
ExportError: '导出失败',
professionalMessage: '社区版最多支持 5 个应用,如需拥有更多应用,请升级为专业版。',
saveErrorMessage: '保存失败,请检查输入或稍后再试',
loadingErrorMessage: '加载配置失败,请检查输入或稍后再试'
},
applicationForm: {
title: {
appTest: '调试预览',
copy: '副本'
},
form: {
appName: {
label: '名称',
placeholder: '请输入应用名称',
requiredMessage: '请输入应用名称'
},
appDescription: {
label: '描述',
placeholder: '描述该应用的应用场景及用途XXX 小助手回答用户提出的 XXX 产品使用问题'
},
appType: {
label: '类型',
simplePlaceholder: '适合新手创建小助手',
workflowPlaceholder: '适合高级用户自定义小助手的工作流'
},
appTemplate: {
blankApp: '空白应用',
assistantApp: '知识库问答助手'
},
aiModel: {
label: 'AI 模型',
placeholder: '请选择 AI 模型'
},
roleSettings: {
label: '系统角色',
placeholder: '你是 xxx 小助手'
},
prompt: {
label: '提示词',
noReferences: ' (无引用知识库)',
references: ' (引用知识库)',
placeholder: '请输入提示词',
requiredMessage: '请输入提示词',
tooltip:
'通过调整提示词内容,可以引导大模型聊天方向,该提示词会被固定在上下文的开头,可以使用变量。',
noReferencesTooltip:
'通过调整提示词内容,可以引导大模型聊天方向,该提示词会被固定在上下文的开头。可以使用变量:{question} 是用户提出问题的占位符。',
referencesTooltip:
'通过调整提示词内容,可以引导大模型聊天方向,该提示词会被固定在上下文的开头。可以使用变量:{data} 是引用知识库中分段的占位符;{question} 是用户提出问题的占位符。',
defaultPrompt: `已知信息:{data}
{question}
- 使`
},
historyRecord: {
label: '历史聊天记录'
},
relatedKnowledge: {
label: '关联知识库',
placeholder: '关联的知识库展示在这里'
},
multipleRoundsDialogue: '多轮对话',
prologue: '开场白',
defaultPrologue:
'您好,我是 XXX 小助手,您可以向我提出 XXX 使用问题。\n- XXX 主要功能有什么?\n- XXX 如何收费?\n- 需要转人工服务',
problemOptimization: {
label: '问题优化',
tooltip: '根据历史聊天优化完善当前问题,更利于匹配知识点。'
},
voiceInput: {
label: '语音输入',
placeholder: '请选择语音识别模型',
requiredMessage: '请选择语音输入模型',
autoSend: '自动发送'
},
voicePlay: {
label: '语音播放',
placeholder: '请选择语音合成模型',
requiredMessage: '请选择语音播放模型',
autoPlay: '自动播放',
browser: '浏览器播放(免费)',
tts: 'TTS模型',
listeningTest: '试听'
},
reasoningContent: {
label: '输出思考',
tooltip: '请根据模型返回的思考标签设置,标签中间的内容将会认定为思考过程',
start: '开始',
end: '结束'
}
},
buttons: {
publish: '保存并发布',
addModel: '添加模型'
},
dialog: {
addDataset: '添加关联知识库',
addDatasetPlaceholder: '所选知识库必须使用相同的 Embedding 模型',
selected: '已选',
countDataset: '个知识库',
selectSearchMode: '检索模式',
vectorSearch: '向量检索',
vectorSearchTooltip: '向量检索是一种基于向量相似度的检索方式,适用于知识库中的大数据量场景。',
fullTextSearch: '全文检索',
fullTextSearchTooltip:
'全文检索是一种基于文本相似度的检索方式,适用于知识库中的小数据量场景。',
hybridSearch: '混合检索',
hybridSearchTooltip:
'混合检索是一种基于向量和文本相似度的检索方式,适用于知识库中的中等数据量场景。',
similarityThreshold: '相似度高于',
similarityTooltip: '相似度越高相关性越强。',
topReferences: '引用分段数 TOP',
maxCharacters: '最多引用字符数',
noReferencesAction: '无引用知识库分段时',
continueQuestioning: '继续向 AI 模型提问',
provideAnswer: '指定回答内容',
designated_answer:
'你好,我是 XXX 小助手,我的知识库只包含了 XXX 产品相关知识,请重新描述您的问题。',
defaultPrompt1:
'()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在',
defaultPrompt2: '标签中'
}
},
applicationAccess: {
title: '应用接入',
wecom: '企业微信应用',
wecomTip: '打造企业微信智能应用',
dingtalk: '钉钉应用',
dingtalkTip: '打造钉钉智能应用',
wechat: '公众号',
wechatTip: '打造公众号智能应用',
lark: '飞书应用',
larkTip: '打造飞书智能应用',
slack: 'Slack',
slackTip: '打造 Slack 智能应用',
setting: '配置',
callback: '回调地址',
callbackTip: '请输入回调地址',
wecomPlatform: '企业微信后台',
wechatPlatform: '微信公众平台',
dingtalkPlatform: '钉钉开放平台',
larkPlatform: '飞书开放平台',
wecomSetting: {
title: '企业微信应用配置',
cropId: '企业 ID',
cropIdPlaceholder: '请输入企业 ID',
agentIdPlaceholder: '请输入Agent ID',
secretPlaceholder: '请输入Secret',
tokenPlaceholder: '请输入Token',
encodingAesKeyPlaceholder: '请输入EncodingAESKey',
authenticationSuccessful: '认证成功',
urlInfo: '-应用管理-自建-创建的应用-接收消息-设置 API 接收的 "URL" 中'
},
dingtalkSetting: {
title: '钉钉应用配置',
clientIdPlaceholder: '请输入Client ID',
clientSecretPlaceholder: '请输入Client Secret',
urlInfo: '-机器人页面,设置 "消息接收模式" 为 HTTP模式 并把上面URL填写到"消息接收地址"中'
},
wechatSetting: {
title: '公众号应用配置',
appId: '开发者ID (APP ID)',
appIdPlaceholder: '请输入开发者ID (APP ID)',
appSecret: '开发者密钥 (APP SECRET)',
appSecretPlaceholder: '请输入开发者密钥 (APP SECRET)',
token: '令牌 (TOKEN)',
tokenPlaceholder: '请输入令牌 (TOKEN)',
aesKey: '消息加解密密钥',
aesKeyPlaceholder: '请输入消息加解密密钥',
urlInfo: '-设置与开发-基本配置-服务器配置的 "服务器地址URL" 中'
},
larkSetting: {
title: '飞书应用配置',
appIdPlaceholder: '请输入App ID',
appSecretPlaceholder: '请输入App Secret',
verificationTokenPlaceholder: '请输入Verification Token',
urlInfo: '-事件与回调-事件配置-配置订阅方式的 "请求地址" 中',
folderTokenPlaceholder: '请输入Folder Token'
},
slackSetting: {
title: 'Slack 应用配置',
signingSecretPlaceholder: '请输入 Signing Secret',
botUserTokenPlaceholder: '请输入 Bot User Token'
},
copyUrl: '复制链接填入到'
},
hitTest: {
title: '命中测试',
text: '针对用户提问调试段落匹配情况,保障回答效果。',
emptyMessage1: '命中段落显示在这里',
emptyMessage2: '没有命中的分段'
}
}

View File

@ -6,8 +6,9 @@ import document from './document'
import system from './system'
import userManage from './user-manage'
import resourceAuthorization from './resource-authorization'
import application from './application'
// import notFound from './404'
// import application from './application'
// import applicationOverview from './application-overview'
// import user from './user'
@ -27,9 +28,10 @@ export default {
document,
system,
userManage,
resourceAuthorization
resourceAuthorization,
application,
// notFound,
// application,
// applicationOverview,
// user,

View File

@ -1,3 +1,31 @@
export default {
title: '资源授权',
member: '成员',
manage: '所有者',
permissionSetting: '资源权限配置',
addMember: '添加成员',
addSubTitle: '成员登录后可以访问到您授权的数据。',
searchBar: {
placeholder: '请输入用户名搜索'
},
delete: {
button: '移除',
confirmTitle: '是否移除成员:',
confirmMessage: '移除后将会取消成员拥有的知识库和应用权限。'
},
setting: {
management: '管理',
check: '查看'
},
teamForm: {
form: {
userName: {
label: '用户名/邮箱',
placeholder: '请输入成员的用户名或邮箱',
requiredMessage: '请输入用户名/邮箱'
},
},
}
}

View File

@ -39,4 +39,7 @@ $primary-color: #3370ff;
/** ai-chat */
--dialog-bg-gradient-color:
linear-gradient(188deg, rgba(235, 241, 255, 0.2) 39.6%, rgba(231, 249, 255, 0.2) 94.3%), #eff0f1;
/** 资源授权 */
--setting-left-width: 280px;
}

View File

@ -0,0 +1,122 @@
<template>
<el-dialog
v-model="dialogVisible"
:close-on-press-escape="false"
:close-on-click-modal="false"
:destroy-on-close="true"
width="600"
class="member-dialog"
>
<template #header="{ titleId, titleClass }">
<h4 :id="titleId" :class="titleClass">{{ $t('views.team.addMember') }}</h4>
<div class="dialog-sub-title">{{ $t('views.team.addSubTitle') }}</div>
</template>
<el-form
ref="addMemberFormRef"
:model="memberForm"
label-position="top"
:rules="rules"
require-asterisk-position="right"
@submit.prevent
>
<el-form-item :label="$t('views.team.teamForm.form.userName.label')" prop="users">
<tags-input v-model:tags="memberForm.users" :placeholder="$t('views.team.teamForm.form.userName.placeholder')" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submitMember(addMemberFormRef)" :loading="loading">
{{ $t('common.add') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { MsgSuccess } from '@/utils/message'
import AuthorizationApi from '@/api/user/resource-authorization'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const dialogVisible = ref<boolean>(false)
const memberForm = ref({
users: []
})
const addMemberFormRef = ref<FormInstance>()
const loading = ref<boolean>(false)
const rules = ref<FormRules>({
users: [
{
type: 'array',
required: true,
message: t('views.team.teamForm.form.userName.requiredMessage'),
trigger: 'change'
}
]
})
watch(dialogVisible, (bool) => {
if (!bool) {
memberForm.value = {
users: []
}
loading.value = false
}
})
const open = () => {
dialogVisible.value = true
}
const submitMember = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
loading.value = true
let idsArray = memberForm.value.users.map((obj: any) => obj.id)
AuthorizationApi.postCreatTeamMember(idsArray)
.then((res) => {
MsgSuccess(t('common.submitSuccess'))
emit('refresh', idsArray)
dialogVisible.value = false
loading.value = false
})
.catch(() => {
loading.value = false
})
}
})
}
onMounted(() => {})
defineExpose({ open, close })
</script>
<style lang="scss" scoped>
.member-dialog {
.el-dialog__header {
padding-bottom: 19px;
}
}
.custom-select-multiple {
width: 200%;
.el-input {
min-height: 100px;
}
.el-select__tags {
top: 0;
transform: none;
padding-top: 8px;
}
.el-input__wrapper {
align-items: start;
}
}
</style>

View File

@ -0,0 +1,205 @@
<template>
<el-input
v-model="filterText"
:placeholder="$t('common.search')"
prefix-icon="Search"
class="p-24 pt-0 pb-0 mb-16 mt-4"
clearable
/>
<div class="p-24 pt-0">
<el-table :data="filterData" :max-height="tableHeight">
<el-table-column
prop="name"
:label="
isApplication
? $t('views.application.applicationForm.form.appName.label')
: $t('views.dataset.datasetForm.form.datasetName.label')
"
>
<template #default="{ row }">
<div class="flex align-center">
<AppAvatar
v-if="isApplication && isAppIcon(row?.icon)"
style="background: none"
class="mr-12"
shape="square"
:size="24"
>
<img :src="row?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="row?.name && isApplication"
:name="row?.name"
pinyinColor
shape="square"
:size="24"
class="mr-12"
/>
<AppAvatar
v-if="row.icon === '1' && isDataset"
class="mr-8 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="row.icon === '2' && isDataset"
class="mr-8 avatar-purple"
shape="square"
:size="24"
style="background: none"
>
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar v-else-if="isDataset" class="mr-8 avatar-blue" shape="square" :size="24">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<auto-tooltip :content="row?.name">
{{ row?.name }}
</auto-tooltip>
</div>
</template>
</el-table-column>
<el-table-column
:label="$t('views.team.setting.management')"
align="center"
width="100"
fixed="right"
>
<template #header>
<el-checkbox
:disabled="props.manage"
v-model="allChecked[TeamEnum.MANAGE]"
:indeterminate="allIndeterminate[TeamEnum.MANAGE]"
:label="$t('views.team.setting.management')"
/>
</template>
<template #default="{ row }">
<el-checkbox
:disabled="props.manage"
v-model="row.operate[TeamEnum.MANAGE]"
@change="(e: boolean) => checkedOperateChange(TeamEnum.MANAGE, row, e)"
/>
</template>
</el-table-column>
<el-table-column
:label="$t('views.team.setting.check')"
align="center"
width="100"
fixed="right"
>
<template #header>
<el-checkbox
:disabled="props.manage"
v-model="allChecked[TeamEnum.USE]"
:indeterminate="allIndeterminate[TeamEnum.USE]"
:label="$t('views.team.setting.check')"
/>
</template>
<template #default="{ row }">
<el-checkbox
:disabled="props.manage"
v-model="row.operate[TeamEnum.USE]"
@change="(e: boolean) => checkedOperateChange(TeamEnum.USE, row, e)"
/>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import { TeamEnum } from '@/enums/team'
import { isAppIcon } from '@/utils/application'
const props = defineProps({
data: {
type: Array,
default: () => []
},
id: String,
type: String,
tableHeight: Number,
manage: Boolean
})
const isDataset = computed(() => props.type === TeamEnum.DATASET)
const isApplication = computed(() => props.type === TeamEnum.APPLICATION)
const emit = defineEmits(['update:data'])
const allChecked: any = ref({
[TeamEnum.MANAGE]: computed({
get: () => {
return filterData.value.some((item: any) => item.operate[TeamEnum.MANAGE])
},
set: (val: boolean) => {
if (val) {
filterData.value.map((item: any) => {
item.operate[TeamEnum.MANAGE] = true
item.operate[TeamEnum.USE] = true
})
} else {
filterData.value.map((item: any) => {
item.operate[TeamEnum.MANAGE] = false
})
}
}
}),
[TeamEnum.USE]: computed({
get: () => {
return filterData.value.some((item: any) => item.operate[TeamEnum.USE])
},
set: (val: boolean) => {
if (val) {
filterData.value.map((item: any) => {
item.operate[TeamEnum.USE] = true
})
} else {
filterData.value.map((item: any) => {
item.operate[TeamEnum.USE] = false
item.operate[TeamEnum.MANAGE] = false
})
}
}
})
})
const filterText = ref('')
const filterData = computed(() =>
props.data.filter((v: any) => v.name.toLowerCase().includes(filterText.value.toLowerCase()))
)
const allIndeterminate: any = ref({
[TeamEnum.MANAGE]: computed(() => {
const all_not_checked = filterData.value.every((item: any) => !item.operate[TeamEnum.MANAGE])
if (all_not_checked) {
return false
}
return !filterData.value.every((item: any) => item.operate[TeamEnum.MANAGE])
}),
[TeamEnum.USE]: computed(() => {
const all_not_checked = filterData.value.every((item: any) => !item.operate[TeamEnum.USE])
if (all_not_checked) {
return false
}
return !filterData.value.every((item: any) => item.operate[TeamEnum.USE])
})
})
function checkedOperateChange(Name: string | number, row: any, e: boolean) {
props.data.map((item: any) => {
if (item.id === row.id) {
item.operate[Name] = e
if (Name === TeamEnum.MANAGE && e) {
item.operate[TeamEnum.USE] = true
} else if (Name === TeamEnum.USE && !e) {
item.operate[TeamEnum.MANAGE] = false
}
}
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -1,9 +1,223 @@
<template><span>22222</span></template>
<template>
<div class="p-16-24">
<h4 class="mb-16">{{ $t('views.userManage.title') }}</h4>
<el-card>
<div class="resource-authorization flex main-calc-height">
<div class="team-member p-8 border-r">
<div class="flex-between p-16">
<h4>{{ $t('views.resourceAuthorization.member') }}</h4>
</div>
<div class="team-member-input">
<el-input
v-model="filterText"
:placeholder="$t('common.search')"
prefix-icon="Search"
clearable
/>
</div>
<div class="list-height-left">
<el-scrollbar>
<common-list
:data="filterMember"
class="mt-8"
v-loading="loading"
@click="clickMemberHandle"
:default-active="currentUser"
>
<template #default="{ row }">
<div class="flex-between">
<div>
<span class="mr-8">{{ row.username }}</span>
<el-tag v-if="isManage(row.type)" class="default-tag">{{
$t('views.resourceAuthorization.manage')
}}</el-tag>
</div>
<div @click.stop style="margin-top: 5px">
<el-dropdown trigger="click" v-if="!isManage(row.type)">
<span class="cursor">
<el-icon class="rotate-90"><MoreFilled /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.prevent="deleteMember(row)">{{
$t('views.resourceAuthorization.delete.button')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
</common-list>
</el-scrollbar>
</div>
</div>
<div class="permission-setting flex" v-loading="rLoading">
<div class="team-manage__table">
<h4 class="p-24 pb-0 mb-4">{{ $t('views.resourceAuthorization.permissionSetting') }}</h4>
<el-tabs v-model="activeName" class="team-manage__tabs">
<el-tab-pane
v-for="(item, index) in settingTags"
:key="item.value"
:label="item.label"
:name="item.value"
>
<!-- <PermissionSetting
:key="index"
:data="item.data"
:type="item.value"
:tableHeight="tableHeight"
:manage="isManage(currentType)"
></PermissionSetting> -->
</el-tab-pane>
</el-tabs>
</div>
<div class="submit-button">
<el-button type="primary" @click="submitPermissions">{{ $t('common.save') }}</el-button>
</div>
</div>
</div>
</el-card>
<!-- <CreateMemberDialog ref="CreateMemberRef" @refresh="refresh" /> -->
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, reactive, watch } from 'vue'
import AuthorizationApi from '@/api/user/resource-authorization'
import type { TeamMember } from '@/api/type/team'
// import CreateMemberDialog from './component/CreateMemberDialog.vue'
// import PermissionSetting from './component/PermissionSetting.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { AuthorizationEnum } from '@/enums/system'
import { t } from '@/locales'
// const CreateMemberRef = ref<InstanceType<typeof CreateMemberDialog>>()
const loading = ref(false)
const rLoading = ref(false)
const memberList = ref<TeamMember[]>([]) //
const filterMember = ref<TeamMember[]>([]) //
const currentUser = ref<String>('')
const currentType = ref<String>('')
onMounted(() => {})
const filterText = ref('')
const activeName = ref(AuthorizationEnum.DATASET)
const tableHeight = ref(0)
const settingTags = reactive([
{
label: t('views.knowledge.title'),
value: AuthorizationEnum.DATASET,
data: [] as any,
},
{
label: t('views.application.title'),
value: AuthorizationEnum.APPLICATION,
data: [] as any,
},
])
watch(filterText, (val) => {
if (val) {
filterMember.value = memberList.value.filter((v) =>
v.username.toLowerCase().includes(val.toLowerCase()),
)
} else {
filterMember.value = memberList.value
}
})
function isManage(type: String) {
return type === 'manage'
}
function submitPermissions() {
rLoading.value = true
const obj: any = {
team_member_permission_list: [],
}
settingTags.map((item) => {
item.data.map((v: any) => {
obj['team_member_permission_list'].push({
target_id: v.id,
type: v.type,
operate: v.operate,
})
})
})
AuthorizationApi.putResourceAuthorization(currentUser.value, obj)
.then(() => {
MsgSuccess(t('common.submitSuccess'))
ResourcePermissions(currentUser.value)
})
.catch(() => {
rLoading.value = false
})
}
function ResourcePermissions() {
rLoading.value = true
AuthorizationApi.getResourceAuthorization('default')
.then((res) => {
rLoading.value = false
})
.catch(() => {
rLoading.value = false
})
}
function refresh(data?: string[]) {}
onMounted(() => {
tableHeight.value = window.innerHeight - 330
window.onresize = () => {
return (() => {
tableHeight.value = window.innerHeight - 330
})()
}
ResourcePermissions()
})
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.resource-authorization {
.add-user-icon {
font-size: 17px;
}
.team-member-input {
padding: 0 calc(var(--app-base-px) * 2);
}
.team-member {
box-sizing: border-box;
width: var(--setting-left-width);
min-width: var(--setting-left-width);
}
.permission-setting {
box-sizing: border-box;
width: calc(100% - var(--setting-left-width));
flex-direction: column;
position: relative;
.submit-button {
position: absolute;
top: 54px;
right: 24px;
}
}
.list-height-left {
height: calc(var(--create-dataset-height) - 60px);
}
&__tabs {
margin-top: 10px;
:deep(.el-tabs__nav-scroll) {
padding: 0 24px;
}
}
&__table {
flex: 1;
}
}
</style>