feat: add agent role binding management (#12382)

This commit is contained in:
ssongliu
2026-04-01 09:12:07 +00:00
committed by GitHub
parent d83b49cc70
commit 5ce155eee6
13 changed files with 688 additions and 77 deletions
+40
View File
@@ -393,6 +393,46 @@ func (b *BaseApi) DeleteAgentRole(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Bind Agent role channel
// @Accept json
// @Param request body dto.AgentRoleBindReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/agent/bind [post]
func (b *BaseApi) BindAgentRole(c *gin.Context) {
var req dto.AgentRoleBindReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.BindRole(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Unbind Agent role channel
// @Accept json
// @Param request body dto.AgentRoleBindReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/agent/unbind [post]
func (b *BaseApi) UnbindAgentRole(c *gin.Context) {
var req dto.AgentRoleBindReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UnbindRole(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Get configured Agent roles from config file
// @Accept json
+7
View File
@@ -109,6 +109,13 @@ type AgentRoleDeleteReq struct {
ID string `json:"id" validate:"required"`
}
type AgentRoleBindReq struct {
AgentID uint `json:"agentId" validate:"required"`
ID string `json:"id" validate:"required"`
Channel string `json:"channel" validate:"required"`
AccountID string `json:"accountId"`
}
type AgentConfiguredAgentsReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
+2
View File
@@ -46,6 +46,8 @@ type IAgentService interface {
CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error)
DeleteRole(req dto.AgentRoleDeleteReq) error
BindRole(req dto.AgentRoleBindReq) error
UnbindRole(req dto.AgentRoleBindReq) error
GetConfiguredAgents(req dto.AgentConfiguredAgentsReq) ([]dto.AgentConfiguredAgentItem, error)
GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.AgentRoleChannelItem, error)
GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error)
+163 -8
View File
@@ -91,18 +91,19 @@ func (a AgentService) GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.Agent
if !ok || len(channels) == 0 {
return []dto.AgentRoleChannelItem{}, nil
}
boundChannels := loadBoundChannelSet(conf["bindings"])
boundBindings := loadBoundChannelBindings(conf["bindings"])
result := make([]dto.AgentRoleChannelItem, 0, len(channels))
for key := range channels {
key = strings.TrimSpace(key)
if key == "" {
continue
}
channelConf, _ := channels[key].(map[string]interface{})
accountIDs := extractRoleChannelAccountIDs(conf, key)
availableAccountIDs := filterAvailableChannelAccountIDs(boundBindings, key, accountIDs)
result = append(result, dto.AgentRoleChannelItem{
Name: key,
Bound: boundChannels[key],
AccountIDs: extractChannelAccountIDs(channelConf),
Bound: isRoleChannelFullyBound(boundBindings, key, accountIDs, availableAccountIDs),
AccountIDs: availableAccountIDs,
})
}
sort.Slice(result, func(i, j int) bool {
@@ -146,6 +147,50 @@ func (a AgentService) DeleteRole(req dto.AgentRoleDeleteReq) error {
return nil
}
func (a AgentService) BindRole(req dto.AgentRoleBindReq) error {
return a.operateRoleBinding(req, "bind")
}
func (a AgentService) UnbindRole(req dto.AgentRoleBindReq) error {
return a.operateRoleBinding(req, "unbind")
}
func (a AgentService) operateRoleBinding(req dto.AgentRoleBindReq, action string) error {
agent, install, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return err
}
baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data")
roleID := req.ID
if roleID == "" {
return buserr.New("ErrRecordNotFound")
}
if _, ok := findConfiguredAgentByID(baseDir, conf, roleID); !ok {
return buserr.New("ErrRecordNotFound")
}
binding := formatRoleBinding(req.Channel, req.AccountID)
if binding == "" {
return buserr.New("ErrInvalidParams")
}
args := []string{
"exec",
install.ContainerName,
"openclaw",
"agents",
action,
"--agent",
roleID,
"--bind",
binding,
}
args = append(args, "--json")
mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
_, err = mgr.RunWithStdout("docker", args...)
return err
}
func (a AgentService) GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error) {
agent, err := loadOpenclawAgentByID(req.AgentID)
if err != nil {
@@ -250,6 +295,16 @@ func findConfiguredAgentByID(baseDir string, conf map[string]interface{}, id str
return dto.AgentConfiguredAgentItem{}, false
}
func formatRoleBinding(channel, accountID string) string {
if channel == "" {
return ""
}
if accountID == "" {
return channel
}
return channel + ":" + accountID
}
func applyConfiguredAgentBindings(agents []dto.AgentConfiguredAgentItem, value interface{}) {
bindings, ok := value.([]interface{})
if !ok || len(agents) == 0 {
@@ -305,8 +360,8 @@ func applyConfiguredAgentBindings(agents []dto.AgentConfiguredAgentItem, value i
}
}
func loadBoundChannelSet(value interface{}) map[string]bool {
result := make(map[string]bool)
func loadBoundChannelBindings(value interface{}) map[string]map[string]struct{} {
result := make(map[string]map[string]struct{})
bindings, ok := value.([]interface{})
if !ok {
return result
@@ -328,12 +383,112 @@ func loadBoundChannelSet(value interface{}) map[string]bool {
if channel == "" {
continue
}
result[channel] = true
accountID, _ := match["accountId"].(string)
accountID = strings.TrimSpace(accountID)
if accountID == "" {
accountID, _ = record["accountId"].(string)
accountID = strings.TrimSpace(accountID)
}
if _, ok := result[channel]; !ok {
result[channel] = make(map[string]struct{})
}
result[channel][accountID] = struct{}{}
}
return result
}
func extractChannelAccountIDs(channel map[string]interface{}) []string {
func extractRoleChannelAccountIDs(conf map[string]interface{}, channel string) []string {
switch channel {
case "feishu":
config := extractFeishuConfig(conf)
accountIDs := make([]string, 0, len(config.Bots))
for _, item := range config.Bots {
if accountID := item.AccountID; accountID != "" {
accountIDs = append(accountIDs, accountID)
}
}
sort.Strings(accountIDs)
return accountIDs
case "telegram":
config := extractTelegramConfig(conf)
accountIDs := make([]string, 0, len(config.Bots))
for _, item := range config.Bots {
if accountID := item.AccountID; accountID != "" {
accountIDs = append(accountIDs, accountID)
}
}
sort.Strings(accountIDs)
return accountIDs
case "discord":
config := extractDiscordConfig(conf)
accountIDs := make([]string, 0, len(config.Bots))
for _, item := range config.Bots {
if accountID := item.AccountID; accountID != "" {
accountIDs = append(accountIDs, accountID)
}
}
sort.Strings(accountIDs)
return accountIDs
case "qqbot":
config := extractQQBotConfig(conf)
accountIDs := make([]string, 0, len(config.Bots))
for _, item := range config.Bots {
if accountID := item.AccountID; accountID != "" {
accountIDs = append(accountIDs, accountID)
}
}
sort.Strings(accountIDs)
return accountIDs
case "dingtalk-connector":
config := extractDingTalkConfig(conf)
accountIDs := make([]string, 0, len(config.Bots))
for _, item := range config.Bots {
if accountID := item.AccountID; accountID != "" {
accountIDs = append(accountIDs, accountID)
}
}
sort.Strings(accountIDs)
return accountIDs
case "wecom":
return []string{}
default:
return extractRawChannelAccountIDs(getChannelConfig(conf, channel))
}
}
func filterAvailableChannelAccountIDs(bindings map[string]map[string]struct{}, channel string, accountIDs []string) []string {
channelBindings, ok := bindings[strings.TrimSpace(channel)]
if !ok || len(channelBindings) == 0 {
return append([]string(nil), accountIDs...)
}
if _, ok := channelBindings[""]; ok {
return []string{}
}
result := make([]string, 0, len(accountIDs))
for _, accountID := range accountIDs {
if _, ok := channelBindings[accountID]; ok {
continue
}
result = append(result, accountID)
}
return result
}
func isRoleChannelFullyBound(bindings map[string]map[string]struct{}, channel string, allAccountIDs, availableAccountIDs []string) bool {
channelBindings, ok := bindings[strings.TrimSpace(channel)]
if !ok || len(channelBindings) == 0 {
return false
}
if _, ok := channelBindings[""]; ok {
return true
}
if len(allAccountIDs) == 0 {
return true
}
return len(availableAccountIDs) == 0
}
func extractRawChannelAccountIDs(channel map[string]interface{}) []string {
if len(channel) == 0 {
return []string{}
}
+2
View File
@@ -59,6 +59,8 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount)
aiToolsRouter.POST("/agents/agent/create", baseApi.CreateAgentRole)
aiToolsRouter.POST("/agents/agent/delete", baseApi.DeleteAgentRole)
aiToolsRouter.POST("/agents/agent/bind", baseApi.BindAgentRole)
aiToolsRouter.POST("/agents/agent/unbind", baseApi.UnbindAgentRole)
aiToolsRouter.POST("/agents/agent/list", baseApi.GetConfiguredAgentRoles)
aiToolsRouter.POST("/agents/agent/channels", baseApi.GetAgentRoleChannels)
aiToolsRouter.POST("/agents/agent/md/list", baseApi.GetAgentRoleMarkdownFiles)
+7
View File
@@ -335,6 +335,13 @@ export namespace AI {
id: string;
}
export interface AgentRoleBindReq {
agentId: number;
id: string;
channel: string;
accountId: string;
}
export interface AgentConfiguredAgentsReq {
agentId: number;
}
+8
View File
@@ -129,6 +129,14 @@ export const deleteAgentRole = (req: AI.AgentRoleDeleteReq) => {
return http.post(`/ai/agents/agent/delete`, req);
};
export const bindAgentRole = (req: AI.AgentRoleBindReq) => {
return http.post(`/ai/agents/agent/bind`, req);
};
export const unbindAgentRole = (req: AI.AgentRoleBindReq) => {
return http.post(`/ai/agents/agent/unbind`, req);
};
export const getConfiguredAgentRoles = (req: AI.AgentConfiguredAgentsReq) => {
return http.post<AI.AgentConfiguredAgentItem[]>(`/ai/agents/agent/list`, req);
};
+1
View File
@@ -752,6 +752,7 @@ const message = {
],
},
bindings: 'Bindings',
duplicateBinding: 'The channel and account ID combination must be unique',
accountIdOptional: 'Account ID (Optional)',
saveAllMd: 'Save All',
roleMarkdownRestartHelper:
+1
View File
@@ -703,6 +703,7 @@ const message = {
'BOOTSTRAP.md': ['首次运行引导流程', '只会在全新的工作区中创建。'],
},
bindings: '绑定',
duplicateBinding: '频道和账号 ID 不能重复',
accountIdOptional: '账号 ID(可选)',
saveAllMd: '保存全部',
roleMarkdownRestartHelper: '保存当前全部 MD 文件后,需要重启容器才能生效。请选择立即重启或稍后重启。',
-4
View File
@@ -234,10 +234,6 @@ html {
padding: 5px;
}
.el-card {
border: none !important;
}
.el-input-group__append {
button.el-button {
span {
@@ -0,0 +1,247 @@
<template>
<DialogPro v-model="open" :title="$t('commons.button.bind')" size="large" @close="handleClose">
<div v-loading="loading" class="binding-dialog">
<el-table v-if="form.bindings.length" :data="form.bindings" class="bindings-table" size="small">
<el-table-column :label="$t('aiTools.agents.channelsTab')" min-width="180">
<template #default="{ row, $index }">
<el-select
v-model="row.channel"
clearable
filterable
class="w-full"
@change="handleBindingChannelChange($index)"
>
<el-option
v-for="item in channelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="isChannelDisabled(item, $index)"
/>
</el-select>
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.agents.accountIdOptional')" min-width="220">
<template #default="{ row }">
<el-select
v-model="row.accountId"
clearable
filterable
allow-create
default-first-option
class="w-full"
>
<el-option
v-for="item in getAccountIdOptions(row.channel)"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.operate')" width="100" align="right">
<template #default="{ $index }">
<el-button link type="danger" @click="removeBinding($index)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<div v-else class="binding-dialog__empty">
<el-empty :description="$t('commons.msg.noneData')" :image-size="60" />
</div>
<div class="binding-dialog__actions">
<el-button type="primary" link :disabled="loading || submitting" @click="addBinding">
{{ $t('commons.button.add') }}
</el-button>
</div>
</div>
<template #footer>
<el-button
type="primary"
:loading="submitting"
:disabled="loading || !form.bindings.length"
@click="submitBind"
>
{{ $t('commons.button.bind') }}
</el-button>
<el-button :disabled="loading || submitting" @click="handleClose">
{{ $t('commons.button.cancel') }}
</el-button>
</template>
</DialogPro>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { bindAgentRole, getAgentRoleChannels } from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
interface SelectOption {
label: string;
value: string;
accountIds: string[];
}
interface DialogParams {
agentId: number;
role: AI.AgentConfiguredAgentItem;
}
const emit = defineEmits(['success']);
const open = ref(false);
const loading = ref(false);
const submitting = ref(false);
const agentId = ref(0);
const roleId = ref('');
const channelOptions = ref<SelectOption[]>([]);
const form = ref<{ bindings: AI.AgentRoleBinding[] }>({
bindings: [],
});
const resetForm = () => {
form.value = {
bindings: [],
};
};
const addBinding = () => {
form.value.bindings.push({
channel: '',
accountId: '',
});
};
const removeBinding = (index: number) => {
form.value.bindings.splice(index, 1);
};
const handleBindingChannelChange = (index: number) => {
const binding = form.value.bindings[index];
if (!binding) {
return;
}
binding.accountId = '';
const options = getAccountIdOptions(binding.channel);
if (options.length === 1) {
binding.accountId = options[0];
}
};
const getAccountIdOptions = (channel: string) => {
return channelOptions.value.find((item) => item.value === channel)?.accountIds || [];
};
const isChannelDisabled = (option: SelectOption, index: number) => {
if (option.accountIds.length > 0) {
return false;
}
return form.value.bindings.some((item, bindingIndex) => bindingIndex !== index && item.channel === option.value);
};
const loadBindOptions = async () => {
const res = await getAgentRoleChannels({ agentId: agentId.value });
channelOptions.value = (res.data || [])
.filter((item) => !item.bound)
.map((item) => ({
label: item.name,
value: item.name,
accountIds: item.accountIds || [],
}));
};
const refreshData = async () => {
await loadBindOptions();
};
const acceptParams = async (params: DialogParams) => {
agentId.value = params.agentId;
roleId.value = params.role.id;
resetForm();
loading.value = true;
try {
await refreshData();
addBinding();
} finally {
loading.value = false;
}
open.value = true;
};
const submitBind = async () => {
const bindings = form.value.bindings.filter((item) => item.channel);
if (!bindings.length) {
MsgError(i18n.global.t('commons.msg.selectOne', [i18n.global.t('aiTools.agents.channelsTab')]));
return;
}
const hasDuplicate = bindings.some((item, index) =>
bindings.some(
(current, currentIndex) =>
currentIndex !== index && current.channel === item.channel && current.accountId === item.accountId,
),
);
if (hasDuplicate) {
MsgError(i18n.global.t('aiTools.agents.duplicateBinding'));
return;
}
submitting.value = true;
loading.value = true;
try {
for (const item of bindings) {
await bindAgentRole({
agentId: agentId.value,
id: roleId.value,
channel: item.channel,
accountId: item.accountId,
});
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
resetForm();
emit('success');
handleClose();
} finally {
submitting.value = false;
loading.value = false;
}
};
const handleClose = () => {
open.value = false;
channelOptions.value = [];
resetForm();
};
defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.binding-dialog {
display: flex;
flex-direction: column;
gap: 20px;
}
.bindings-table {
width: 100%;
}
.binding-dialog__actions {
display: flex;
justify-content: flex-start;
}
.bindings-table :deep(.el-select) {
width: 100%;
}
.binding-dialog__empty {
padding: 12px 0;
}
</style>
@@ -3,7 +3,6 @@
<div v-loading="loading" class="create-role-dialog">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<div class="create-role-section">
<div class="create-role-section__title">{{ $t('commons.table.name') }}</div>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="form.name" />
</el-form-item>
@@ -90,7 +89,7 @@ import { createAgentRole, getAgentRoleChannels, pageAgentAccounts } from '@/api/
import { AI } from '@/api/interface/ai';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
interface SelectOption {
@@ -151,6 +150,10 @@ const handleBindingChannelChange = (index: number) => {
return;
}
binding.accountId = '';
const options = getAccountIdOptions(binding.channel);
if (options.length === 1) {
binding.accountId = options[0];
}
};
const getAccountIdOptions = (channel: string) => {
@@ -161,6 +164,9 @@ const isChannelDisabled = (option: SelectOption, index: number) => {
if (option.bound) {
return true;
}
if ((option.accountIds || []).length > 0) {
return false;
}
return form.bindings.some((item, bindingIndex) => bindingIndex !== index && item.channel === option.value);
};
@@ -209,18 +215,27 @@ const submit = async () => {
return;
}
await formRef.value.validate();
const bindings = form.bindings.filter((item) => item.channel);
const hasDuplicate = bindings.some((item, index) =>
bindings.some(
(current, currentIndex) =>
currentIndex !== index && current.channel === item.channel && current.accountId === item.accountId,
),
);
if (hasDuplicate) {
MsgError(i18n.global.t('aiTools.agents.duplicateBinding'));
return;
}
loading.value = true;
try {
await createAgentRole({
agentId: agentId.value,
name: form.name.trim(),
model: form.model.trim(),
bindings: form.bindings
.filter((item) => item.channel)
.map((item) => ({
channel: item.channel,
accountId: item.accountId.trim(),
})),
bindings: bindings.map((item) => ({
channel: item.channel,
accountId: item.accountId.trim(),
})),
} as AI.AgentRoleCreateReq);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('success');
@@ -12,49 +12,79 @@
{{ $t('commons.button.add') }}
</el-button>
</div>
<ComplexTable :data="configuredAgents">
<el-table-column :label="$t('commons.table.name')" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<el-button v-if="row.workspace" type="primary" link @click="openDetailDrawer(row)">
{{ row.name || row.id || '-' }}
</el-button>
<span v-else>{{ row.name || row.id || '-' }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.model.model')" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.model || '-' }}
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.agents.workspace')" width="100" align="center">
<template #default="{ row }">
<el-tooltip v-if="row.workspace" :content="row.workspace" placement="top">
<el-button type="primary" link @click="routerToFileWithPath(row.workspace)">
<el-icon><FolderOpened /></el-icon>
<el-empty
v-if="!configuredAgents.length"
:description="$t('commons.msg.noneData')"
:image-size="80"
class="role-empty"
/>
<div v-else>
<el-card v-for="row in configuredAgents" :key="row.id || row.name" class="role-card">
<div class="role-card__header">
<div class="role-card__name">
<div class="role-card__field">
<el-button
v-if="row.workspace"
type="primary"
link
class="role-card__title"
@click="openDetailDrawer(row)"
>
{{ row.name || row.id || '-' }}
</el-button>
<span v-else class="role-card__title role-card__title--static">
{{ row.name || row.id || '-' }}
</span>
<div class="role-card__model">{{ row.model || '-' }}</div>
</div>
</div>
<div class="role-card__path-actions">
<el-tooltip v-if="row.workspace" :content="row.workspace" placement="top">
<el-button plain size="small" round @click="routerToFileWithPath(row.workspace)">
{{ $t('aiTools.agents.workspace') }}
</el-button>
</el-tooltip>
<el-tooltip v-if="row.agentDir" :content="row.agentDir" placement="top">
<el-button plain size="small" round @click="routerToFileWithPath(row.agentDir)">
{{ $t('aiTools.agents.agentDir') }}
</el-button>
</el-tooltip>
<el-button plain size="small" round @click="handleDelete(row)">
{{ $t('commons.button.delete') }}
</el-button>
</el-tooltip>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.agents.agentDir')" width="120" align="center">
<template #default="{ row }">
<el-tooltip v-if="row.agentDir" :content="row.agentDir" placement="top">
<el-button type="primary" link @click="routerToFileWithPath(row.agentDir)">
<el-icon><FolderOpened /></el-icon>
</el-button>
</el-tooltip>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.agents.bindings')" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
{{ formatBindings(row.bindings) }}
</template>
</el-table-column>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" width="90" fixed="right" />
</ComplexTable>
</div>
</div>
<div class="role-card__content">
<div class="role-card__field role-card__field--bindings">
<el-divider class="role-card__divider" border-style="dashed" />
<div class="role-card__section-title">
{{ $t('aiTools.agents.channelsTab') }}
</div>
<div v-if="row.bindings?.length" class="role-card__tags">
<el-tag
v-for="item in row.bindings"
:key="`${item.channel}-${item.accountId || 'default'}`"
closable
@close="handleUnbind(row, item)"
>
{{ item.accountId ? `${item.channel}:${item.accountId}` : item.channel }}
</el-tag>
<el-button size="small" @click="openBindingDialog(row)">
+ {{ $t('commons.button.add') }}
</el-button>
</div>
<div v-else class="role-card__tags">
<el-button size="small" @click="openBindingDialog(row)">
+ {{ $t('commons.button.add') }}
</el-button>
</div>
</div>
</div>
</el-card>
</div>
<CreateDialog ref="createRef" @success="handleCreated" />
<BindingDialog ref="bindingRef" @success="handleCreated" />
<DetailDrawer ref="detailRef" />
<OpDialog ref="opRef" @search="handleCreated" />
</template>
@@ -63,11 +93,13 @@
<script setup lang="ts">
import { ref } from 'vue';
import { deleteAgentRole, getConfiguredAgentRoles } from '@/api/modules/ai';
import { deleteAgentRole, getConfiguredAgentRoles, unbindAgentRole } from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import i18n from '@/lang';
import { routerToFileWithPath } from '@/utils/router';
import { MsgSuccess } from '@/utils/message';
import CreateDialog from './create/index.vue';
import BindingDialog from './binding/index.vue';
import DetailDrawer from './detail/index.vue';
interface AgentRoleLoadParams {
@@ -80,6 +112,7 @@ interface AgentRoleLoadParams {
const loading = ref(false);
const createRef = ref<InstanceType<typeof CreateDialog> | null>(null);
const bindingRef = ref<InstanceType<typeof BindingDialog> | null>(null);
const detailRef = ref<InstanceType<typeof DetailDrawer> | null>(null);
const opRef = ref();
const agentId = ref(0);
@@ -101,6 +134,13 @@ const openCreateDialog = () => {
});
};
const openBindingDialog = (row: AI.AgentConfiguredAgentItem) => {
bindingRef.value?.acceptParams({
agentId: agentId.value,
role: row,
});
};
const openDetailDrawer = (row: AI.AgentConfiguredAgentItem) => {
if (!row.workspace) {
return;
@@ -112,16 +152,6 @@ const openDetailDrawer = (row: AI.AgentConfiguredAgentItem) => {
});
};
const formatBindings = (bindings: AI.AgentRoleBinding[] = []) => {
if (!bindings.length) {
return '-';
}
return bindings
.map((item) => (item.accountId ? `${item.channel}:${item.accountId}` : item.channel))
.filter(Boolean)
.join(', ');
};
const handleDelete = async (row: AI.AgentConfiguredAgentItem) => {
opRef.value?.acceptParams({
title: i18n.global.t('commons.button.delete'),
@@ -140,12 +170,21 @@ const handleDelete = async (row: AI.AgentConfiguredAgentItem) => {
});
};
const buttons = [
{
label: i18n.global.t('commons.button.delete'),
click: (row: AI.AgentConfiguredAgentItem) => handleDelete(row),
},
];
const handleUnbind = async (row: AI.AgentConfiguredAgentItem, item: AI.AgentRoleBinding) => {
loading.value = true;
try {
await unbindAgentRole({
agentId: agentId.value,
id: row.id,
channel: item.channel,
accountId: item.accountId || '',
});
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
await handleCreated();
} finally {
loading.value = false;
}
};
const handleCreated = async () => {
if (!agentId.value) {
@@ -180,4 +219,95 @@ defineExpose({
display: flex;
margin-bottom: 16px;
}
.role-empty {
padding: 24px 0;
}
.role-card {
--el-card-border-color: var(--el-border-color);
border-radius: 18px;
margin-top: 7px;
}
.role-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.role-card__name {
min-width: 0;
flex: 1;
}
.role-card__title {
justify-content: flex-start;
padding: 0;
font-size: 16px;
font-weight: 600;
}
.role-card__title--static {
color: var(--el-text-color-primary);
}
.role-card__model {
margin-top: 6px;
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 1.5;
word-break: break-word;
}
.role-card__field {
min-width: 0;
}
.role-card__field--bindings {
word-break: break-word;
}
.role-card__path-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
justify-content: flex-end;
}
.role-card__content {
display: block;
}
.role-card__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.role-card__divider {
margin: 0 0 8px;
}
.role-card__section-title {
margin-bottom: 10px;
font-size: 13px;
line-height: 1.4;
color: var(--el-text-color-secondary);
}
@media (max-width: 1200px) {
.role-card__header {
flex-direction: column;
align-items: stretch;
}
.role-card__path-actions {
justify-content: flex-start;
}
}
</style>