feat: add unbind website for agent (#12500)

This commit is contained in:
CityFun
2026-04-15 18:32:46 +08:00
committed by GitHub
parent c28a4f6d69
commit 0b146f7ff3
19 changed files with 139 additions and 14 deletions

View File

@@ -152,6 +152,26 @@ func (b *BaseApi) BindAgentWebsite(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags AI
// @Summary Unbind Agent website
// @Accept json
// @Param request body dto.AgentIDReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/website/unbind [post]
func (b *BaseApi) UnbindAgentWebsite(c *gin.Context) {
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UnbindWebsite(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI // @Tags AI
// @Summary Get Agent model config // @Summary Get Agent model config
// @Accept json // @Accept json

View File

@@ -46,6 +46,7 @@ type AgentItem struct {
AppInstallID uint `json:"appInstallId"` AppInstallID uint `json:"appInstallId"`
WebsiteID uint `json:"websiteId"` WebsiteID uint `json:"websiteId"`
WebsitePrimaryDomain string `json:"websitePrimaryDomain"` WebsitePrimaryDomain string `json:"websitePrimaryDomain"`
WebsiteType string `json:"websiteType"`
WebsiteProtocol string `json:"websiteProtocol"` WebsiteProtocol string `json:"websiteProtocol"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
AppVersion string `json:"appVersion"` AppVersion string `json:"appVersion"`

View File

@@ -36,6 +36,7 @@ type IAgentService interface {
ResetToken(req dto.AgentTokenResetReq) error ResetToken(req dto.AgentTokenResetReq) error
UpdateRemark(req dto.AgentRemarkUpdateReq) error UpdateRemark(req dto.AgentRemarkUpdateReq) error
BindWebsite(req dto.AgentWebsiteBindReq) error BindWebsite(req dto.AgentWebsiteBindReq) error
UnbindWebsite(req dto.AgentIDReq) error
GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error)
UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error) GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error)

View File

@@ -48,6 +48,27 @@ func (a AgentService) BindWebsite(req dto.AgentWebsiteBindReq) error {
return ensureOpenclawWebsiteAllowedOrigin(agent, &website) return ensureOpenclawWebsiteAllowedOrigin(agent, &website)
} }
func (a AgentService) UnbindWebsite(req dto.AgentIDReq) error {
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
if err != nil {
return err
}
if agent.WebsiteID == 0 {
return nil
}
website, err := websiteRepo.GetFirst(repo.WithByID(agent.WebsiteID))
if err != nil {
return err
}
if website.Type == constant.Deployment {
return buserr.New("ErrAgentWebsiteUnbindUnsupported")
}
agent.WebsiteID = 0
return agentRepo.Save(agent)
}
func hydrateAgentWebsiteItems(items []dto.AgentItem) error { func hydrateAgentWebsiteItems(items []dto.AgentItem) error {
explicitWebsiteMap, err := loadAgentWebsiteMapByID(items) explicitWebsiteMap, err := loadAgentWebsiteMapByID(items)
if err != nil { if err != nil {
@@ -126,9 +147,11 @@ func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]mo
if !ok { if !ok {
items[index].WebsiteID = 0 items[index].WebsiteID = 0
items[index].WebsitePrimaryDomain = "" items[index].WebsitePrimaryDomain = ""
items[index].WebsiteType = ""
items[index].WebsiteProtocol = "" items[index].WebsiteProtocol = ""
continue continue
} }
items[index].WebsiteType = website.Type
items[index].WebsiteProtocol = website.Protocol items[index].WebsiteProtocol = website.Protocol
websiteDomains := websiteDomainMap[items[index].WebsiteID] websiteDomains := websiteDomainMap[items[index].WebsiteID]
if len(websiteDomains) == 0 { if len(websiteDomains) == 0 {

View File

@@ -64,6 +64,7 @@ ErrAgentLimitReached: 'Community Edition supports up to {{ .max }} AI agents. Up
ErrAgentWebsiteBound: 'This agent is already bound to a website' ErrAgentWebsiteBound: 'This agent is already bound to a website'
ErrAgentWebsiteTypeUnsupported: 'Only proxy or static websites can be bound' ErrAgentWebsiteTypeUnsupported: 'Only proxy or static websites can be bound'
ErrAgentWebsiteInUse: 'This website is already bound to another agent' ErrAgentWebsiteInUse: 'This website is already bound to another agent'
ErrAgentWebsiteUnbindUnsupported: 'Deployment websites cannot be unbound manually'
#backup #backup
Localhost: 'Local' Localhost: 'Local'

View File

@@ -59,6 +59,7 @@ ErrAgentLimitReached: 'La edición Community admite hasta {{ .max }} agentes de
ErrAgentWebsiteBound: 'Este agente ya está vinculado a un sitio web' ErrAgentWebsiteBound: 'Este agente ya está vinculado a un sitio web'
ErrAgentWebsiteTypeUnsupported: 'Solo se pueden vincular sitios proxy o estáticos' ErrAgentWebsiteTypeUnsupported: 'Solo se pueden vincular sitios proxy o estáticos'
ErrAgentWebsiteInUse: 'Este sitio web ya está vinculado a otro agente' ErrAgentWebsiteInUse: 'Este sitio web ya está vinculado a otro agente'
ErrAgentWebsiteUnbindUnsupported: 'Los sitios web de despliegue no se pueden desvincular manualmente'
Localhost: 'Máquina local' Localhost: 'Máquina local'
ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada' ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada'
ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}' ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}'

View File

@@ -59,6 +59,7 @@ ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {
ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付けられています' ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付けられています'
ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイトまたは静的サイトのみです' ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイトまたは静的サイトのみです'
ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています' ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています'
ErrAgentWebsiteUnbindUnsupported: 'ワンクリックデプロイのサイトは手動で関連解除できません'
Localhost: 'ローカルマシン' Localhost: 'ローカルマシン'
ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中' ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中'
ErrBackupCheck: '接続テストに失敗しました: {{ .err }}' ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'

View File

@@ -59,6 +59,7 @@ ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대
ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어 있습니다' ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어 있습니다'
ErrAgentWebsiteTypeUnsupported: '프록시 또는 정적 웹사이트만 연결할 수 있습니다' ErrAgentWebsiteTypeUnsupported: '프록시 또는 정적 웹사이트만 연결할 수 있습니다'
ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다' ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다'
ErrAgentWebsiteUnbindUnsupported: '원클릭 배포 웹사이트는 수동으로 연결 해제할 수 없습니다'
Localhost: '로컬 머신' Localhost: '로컬 머신'
ErrBackupInUsed: '백업 계정이 예약에 사용 중' ErrBackupInUsed: '백업 계정이 예약에 사용 중'
ErrBackupCheck: '연결 테스트 실패: {{ .err }}' ErrBackupCheck: '연결 테스트 실패: {{ .err }}'

View File

@@ -59,6 +59,7 @@ ErrAgentLimitReached: 'Edisi Community menyokong sehingga {{ .max }} ejen AI. Na
ErrAgentWebsiteBound: 'Ejen ini sudah dipautkan ke laman web' ErrAgentWebsiteBound: 'Ejen ini sudah dipautkan ke laman web'
ErrAgentWebsiteTypeUnsupported: 'Hanya laman web proxy atau statik boleh dipautkan' ErrAgentWebsiteTypeUnsupported: 'Hanya laman web proxy atau statik boleh dipautkan'
ErrAgentWebsiteInUse: 'Laman web ini sudah dipautkan ke ejen lain' ErrAgentWebsiteInUse: 'Laman web ini sudah dipautkan ke ejen lain'
ErrAgentWebsiteUnbindUnsupported: 'Laman web one-click deployment tidak menyokong nyahikat manual'
Localhost: 'Mesin Tempatan' Localhost: 'Mesin Tempatan'
ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas' ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas'
ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}' ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}'

View File

@@ -59,6 +59,7 @@ ErrAgentLimitReached: 'A edição Community suporta até {{ .max }} agentes de I
ErrAgentWebsiteBound: 'Este agente já está vinculado a um site' ErrAgentWebsiteBound: 'Este agente já está vinculado a um site'
ErrAgentWebsiteTypeUnsupported: 'Somente sites proxy ou estáticos podem ser vinculados' ErrAgentWebsiteTypeUnsupported: 'Somente sites proxy ou estáticos podem ser vinculados'
ErrAgentWebsiteInUse: 'Este site já está vinculado a outro agente' ErrAgentWebsiteInUse: 'Este site já está vinculado a outro agente'
ErrAgentWebsiteUnbindUnsupported: 'Sites implantados em um clique não podem ser desvinculados manualmente'
Localhost: 'Máquina Local' Localhost: 'Máquina Local'
ErrBackupInUsed: 'Conta de backup em uso por tarefa' ErrBackupInUsed: 'Conta de backup em uso por tarefa'
ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}' ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}'

View File

@@ -59,6 +59,7 @@ ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }
ErrAgentWebsiteBound: 'Этот агент уже связан с сайтом' ErrAgentWebsiteBound: 'Этот агент уже связан с сайтом'
ErrAgentWebsiteTypeUnsupported: 'Можно связывать только proxy- или static-сайты' ErrAgentWebsiteTypeUnsupported: 'Можно связывать только proxy- или static-сайты'
ErrAgentWebsiteInUse: 'Этот сайт уже связан с другим агентом' ErrAgentWebsiteInUse: 'Этот сайт уже связан с другим агентом'
ErrAgentWebsiteUnbindUnsupported: 'Сайты one-click deployment нельзя отвязать вручную'
Localhost: 'Локальная машина' Localhost: 'Локальная машина'
ErrBackupInUsed: 'Аккаунт бэкапа занят задачей' ErrBackupInUsed: 'Аккаунт бэкапа занят задачей'
ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}' ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}'

View File

@@ -59,6 +59,7 @@ ErrAgentLimitReached: 'Community sürümü en fazla {{ .max }} AI ajanını dest
ErrAgentWebsiteBound: 'Bu ajan zaten bir web sitesine bağlı' ErrAgentWebsiteBound: 'Bu ajan zaten bir web sitesine bağlı'
ErrAgentWebsiteTypeUnsupported: 'Yalnızca proxy veya statik web siteleri bağlanabilir' ErrAgentWebsiteTypeUnsupported: 'Yalnızca proxy veya statik web siteleri bağlanabilir'
ErrAgentWebsiteInUse: 'Bu web sitesi zaten başka bir ajana bağlı' ErrAgentWebsiteInUse: 'Bu web sitesi zaten başka bir ajana bağlı'
ErrAgentWebsiteUnbindUnsupported: 'Tek tıkla dağıtılan web sitelerinin bağlantısı manuel olarak kaldırılamaz'
Localhost: 'Yerel Makine' Localhost: 'Yerel Makine'
ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor' ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor'
ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}' ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}'

View File

@@ -59,6 +59,7 @@ ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個 AI 智能體
ErrAgentWebsiteBound: '該智能體已關聯網站' ErrAgentWebsiteBound: '該智能體已關聯網站'
ErrAgentWebsiteTypeUnsupported: '只能關聯反向代理或靜態網站' ErrAgentWebsiteTypeUnsupported: '只能關聯反向代理或靜態網站'
ErrAgentWebsiteInUse: '該網站已被其他智能體關聯' ErrAgentWebsiteInUse: '該網站已被其他智能體關聯'
ErrAgentWebsiteUnbindUnsupported: '一鍵部署網站不支援手動解綁'
Localhost: '本機' Localhost: '本機'
ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除' ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除'
ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}' ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}'

View File

@@ -64,6 +64,7 @@ ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升
ErrAgentWebsiteBound: "该智能体已关联网站" ErrAgentWebsiteBound: "该智能体已关联网站"
ErrAgentWebsiteTypeUnsupported: "只能关联反向代理或静态网站" ErrAgentWebsiteTypeUnsupported: "只能关联反向代理或静态网站"
ErrAgentWebsiteInUse: "该网站已被其他智能体关联" ErrAgentWebsiteInUse: "该网站已被其他智能体关联"
ErrAgentWebsiteUnbindUnsupported: "一键部署网站不支持手动解绑"
#backup #backup
Localhost: '本机' Localhost: '本机'

View File

@@ -47,6 +47,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/token/reset", baseApi.ResetAgentToken) aiToolsRouter.POST("/agents/token/reset", baseApi.ResetAgentToken)
aiToolsRouter.POST("/agents/remark", baseApi.UpdateAgentRemark) aiToolsRouter.POST("/agents/remark", baseApi.UpdateAgentRemark)
aiToolsRouter.POST("/agents/website/bind", baseApi.BindAgentWebsite) aiToolsRouter.POST("/agents/website/bind", baseApi.BindAgentWebsite)
aiToolsRouter.POST("/agents/website/unbind", baseApi.UnbindAgentWebsite)
aiToolsRouter.POST("/agents/model/get", baseApi.GetAgentModelConfig) aiToolsRouter.POST("/agents/model/get", baseApi.GetAgentModelConfig)
aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig) aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig)
aiToolsRouter.POST("/agents/hermes/chat/sessions", baseApi.GetHermesChatSessions) aiToolsRouter.POST("/agents/hermes/chat/sessions", baseApi.GetHermesChatSessions)

View File

@@ -282,6 +282,7 @@ export namespace AI {
appInstallId: number; appInstallId: number;
websiteId: number; websiteId: number;
websitePrimaryDomain: string; websitePrimaryDomain: string;
websiteType: string;
websiteProtocol: string; websiteProtocol: string;
accountId: number; accountId: number;
appVersion: string; appVersion: string;

View File

@@ -122,6 +122,10 @@ export const bindAgentWebsite = (req: AI.AgentWebsiteBindReq) => {
return http.post(`/ai/agents/website/bind`, req); return http.post(`/ai/agents/website/bind`, req);
}; };
export const unbindAgentWebsite = (req: AI.AgentIDReq) => {
return http.post(`/ai/agents/website/unbind`, req);
};
export const getAgentModelConfig = (req: AI.AgentIDReq) => { export const getAgentModelConfig = (req: AI.AgentIDReq) => {
return http.post<AI.AgentModelConfig>(`/ai/agents/model/get`, req); return http.post<AI.AgentModelConfig>(`/ai/agents/model/get`, req);
}; };

View File

@@ -126,6 +126,14 @@
</table> </table>
<el-empty v-else :image-size="48" :description="$t('commons.msg.noneData')" /> <el-empty v-else :image-size="48" :description="$t('commons.msg.noneData')" />
</el-popover> </el-popover>
<el-button
v-if="canUnbindWebsite(row)"
link
type="primary"
@click="onUnbindWebsite(row)"
>
{{ $t('commons.button.unbind') }}
</el-button>
</div> </div>
<el-button v-else link type="primary" @click="openBindWebsite(row)"> <el-button v-else link type="primary" @click="openBindWebsite(row)">
{{ $t('commons.button.bind') }} {{ $t('commons.button.bind') }}
@@ -204,7 +212,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'; import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { deleteAgentCheck, pageAgents, resetAgentToken, updateAgentRemark } from '@/api/modules/ai'; import { deleteAgentCheck, pageAgents, resetAgentToken, unbindAgentWebsite, updateAgentRemark } from '@/api/modules/ai';
import { checkAppInstalled, installedOp, searchApp, searchAppInstalled } from '@/api/modules/app'; import { checkAppInstalled, installedOp, searchApp, searchAppInstalled } from '@/api/modules/app';
import { AI } from '@/api/interface/ai'; import { AI } from '@/api/interface/ai';
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
@@ -573,6 +581,28 @@ const openBindWebsite = (row: AI.AgentItem) => {
bindWebsiteRef.value?.acceptParams(row); bindWebsiteRef.value?.acceptParams(row);
}; };
const canUnbindWebsite = (row: AI.AgentItem) => {
return row.websiteType === 'proxy' || row.websiteType === 'static';
};
const onUnbindWebsite = async (row: AI.AgentItem) => {
await ElMessageBox.confirm(
i18n.global.t('aiTools.mcp.operatorHelper', [
i18n.global.t('menu.website'),
i18n.global.t('commons.button.unbind'),
]),
i18n.global.t('commons.button.unbind'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
);
await unbindAgentWebsite({ agentId: row.id });
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
await search();
};
const loadOpenrestyHttpsPort = async () => { const loadOpenrestyHttpsPort = async () => {
if (openrestyPortLoaded.value) { if (openrestyPortLoaded.value) {
return; return;

View File

@@ -1,18 +1,35 @@
<template> <template>
<div> <div>
<el-card class="router_card p-2 sm:p-3 mt-2"> <div class="content-container__search">
<div class="flex w-full justify-start items-center"> <el-card>
<el-button type="primary" :plain="index !== '0'" @click="changeTab('0')"> <div>
{{ $t('xpack.alert.list') }} <el-button
</el-button> class="tag-button"
<el-button type="primary" :plain="index !== '1'" @click="changeTab('1')"> :class="index === '0' ? '' : 'no-active'"
{{ $t('xpack.alert.logs') }} :type="index === '0' ? 'primary' : ''"
</el-button> @click="changeTab('0')"
<el-button type="primary" :plain="index !== '2'" @click="changeTab('2')"> >
{{ $t('commons.button.set') }} {{ $t('xpack.alert.list') }}
</el-button> </el-button>
</div> <el-button
</el-card> class="tag-button"
:class="index === '1' ? '' : 'no-active'"
:type="index === '1' ? 'primary' : ''"
@click="changeTab('1')"
>
{{ $t('xpack.alert.logs') }}
</el-button>
<el-button
class="tag-button"
:class="index === '2' ? '' : 'no-active'"
:type="index === '2' ? 'primary' : ''"
@click="changeTab('2')"
>
{{ $t('commons.button.set') }}
</el-button>
</div>
</el-card>
</div>
<AlertDash v-if="index == '0'" /> <AlertDash v-if="index == '0'" />
<AlertLogs v-if="index == '1'" /> <AlertLogs v-if="index == '1'" />
<AlertSetting v-if="index == '2'" /> <AlertSetting v-if="index == '2'" />
@@ -37,3 +54,20 @@ onMounted(async () => {
} }
}); });
</script> </script>
<style scoped lang="scss">
.content-container__search {
margin-top: 7px;
:deep(.el-card) {
--el-card-padding: 12px;
}
}
.tag-button {
&.no-active {
background: none;
border: none;
}
}
</style>