feat: change hermes channel logic (#12551)

This commit is contained in:
CityFun
2026-04-21 16:35:24 +08:00
committed by GitHub
parent d50a8983bc
commit 810c6fd232
39 changed files with 730 additions and 91 deletions
+41
View File
@@ -864,6 +864,27 @@ func (b *BaseApi) UpdateAgentDingTalkConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent Weixin channel config
// @Accept json
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentWeixinConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/weixin/get [post]
func (b *BaseApi) GetAgentWeixinConfig(c *gin.Context) {
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.GetWeixinConfig(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Get Agent QQ Bot channel config
// @Accept json
@@ -905,6 +926,26 @@ func (b *BaseApi) UpdateAgentQQBotConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Delete Agent channel config
// @Accept json
// @Param request body dto.AgentChannelDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/delete [post]
func (b *BaseApi) DeleteAgentChannelConfig(c *gin.Context) {
var req dto.AgentChannelDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.DeleteChannelConfig(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Install Agent plugin
// @Accept json
+11 -2
View File
@@ -376,11 +376,16 @@ type AgentTelegramConfig struct {
type AgentChannelPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom qqbot"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom qqbot dingtalk"`
PairingCode string `json:"pairingCode" validate:"required"`
AccountID string `json:"accountId"`
}
type AgentChannelDeleteReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom qqbot dingtalk weixin"`
}
type AgentWecomConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
@@ -406,7 +411,7 @@ type AgentWecomConfig struct {
type AgentDingTalkConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
DmPolicy string `json:"dmPolicy" validate:"required,oneof=allowlist open disabled"`
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing allowlist open disabled"`
AllowFrom []string `json:"allowFrom"`
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
GroupAllowFrom []string `json:"groupAllowFrom"`
@@ -438,6 +443,10 @@ type AgentWeixinLoginReq struct {
TaskID string `json:"taskID" validate:"required"`
}
type AgentWeixinConfig struct {
Enabled bool `json:"enabled"`
}
type AgentQQBotConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
+2
View File
@@ -86,9 +86,11 @@ type IAgentService interface {
UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error
GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error)
UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error
GetWeixinConfig(req dto.AgentIDReq) (*dto.AgentWeixinConfig, error)
LoginWeixinChannel(req dto.AgentWeixinLoginReq) error
GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error)
UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error
DeleteChannelConfig(req dto.AgentChannelDeleteReq) error
InstallPlugin(req dto.AgentPluginInstallReq) error
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
UninstallPlugin(req dto.AgentPluginUninstallReq) error
+44 -5
View File
@@ -306,6 +306,17 @@ func (a AgentService) GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkC
return &result, nil
}
func (a AgentService) GetWeixinConfig(req dto.AgentIDReq) (*dto.AgentWeixinConfig, error) {
agent, _, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppHermesAgent {
return readHermesWeixinChannelConfig(path.Dir(agent.ConfigPath))
}
return nil, fmt.Errorf("%s does not support", agent.AgentType)
}
func (a AgentService) UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
@@ -495,6 +506,36 @@ func (a AgentService) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error {
return nil
}
func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType != constant.AppHermesAgent {
return fmt.Errorf("%s does not support", agent.AgentType)
}
return updateHermesChannelConfig(agent, install, func(confDir string) error {
switch req.Type {
case "telegram":
return deleteHermesTelegramChannelConfig(confDir)
case "discord":
return deleteHermesDiscordChannelConfig(confDir)
case "qqbot":
return deleteHermesQQBotChannelConfig(confDir)
case "wecom":
return deleteHermesWecomChannelConfig(confDir)
case "dingtalk":
return deleteHermesDingTalkChannelConfig(confDir)
case "feishu":
return deleteHermesFeishuChannelConfig(confDir)
case "weixin":
return deleteHermesWeixinChannelConfig(confDir)
default:
return fmt.Errorf("unsupported channel type: %s", req.Type)
}
})
}
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
@@ -535,14 +576,12 @@ func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveRe
return err
}
if agent.AgentType == constant.AppHermesAgent {
output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
output, err := mgr.RunWithStdout(
"docker",
buildHermesDockerExecArgs(install.ContainerName, "pairing", "approve", req.Type, req.PairingCode)...,
)
if err != nil {
return err
}
return validateHermesPairingApproveOutput(output)
return validateHermesPairingApproveResult(output, err)
}
if req.AccountID != "" {
return cmd.RunDefaultBashCf(
+72 -6
View File
@@ -326,6 +326,64 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
return writeHermesConfigMap(configPath, cfg)
}
func deleteHermesEnvKeys(confDir string, keys ...string) error {
envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
for _, key := range keys {
delete(envMap, key)
}
return writeHermesEnvMap(envPath, envMap, keys)
}
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
configPath := path.Join(confDir, "config.yaml")
cfg, err := readHermesConfigMap(configPath)
if err != nil {
return err
}
for _, key := range topLevelKeys {
delete(cfg, key)
}
if len(platformKeys) > 0 {
if platforms, ok := cfg["platforms"].(map[string]interface{}); ok {
for _, key := range platformKeys {
delete(platforms, key)
}
if len(platforms) == 0 {
delete(cfg, "platforms")
}
}
}
return writeHermesConfigMap(configPath, cfg)
}
func deleteHermesTelegramChannelConfig(confDir string) error {
if err := deleteHermesEnvKeys(confDir,
"TELEGRAM_BOT_TOKEN",
"TELEGRAM_ALLOWED_USERS",
"TELEGRAM_ALLOW_ALL_USERS",
"TELEGRAM_HOME_CHANNEL",
); err != nil {
return err
}
return deleteHermesConfigSections(confDir, []string{"telegram"}, []string{"telegram"})
}
func deleteHermesDiscordChannelConfig(confDir string) error {
if err := deleteHermesEnvKeys(confDir,
"DISCORD_BOT_TOKEN",
"DISCORD_ALLOWED_USERS",
"DISCORD_ALLOW_ALL_USERS",
"DISCORD_HOME_CHANNEL",
); err != nil {
return err
}
return deleteHermesConfigSections(confDir, []string{"discord"}, []string{"discord"})
}
func normalizeHermesTimezone(timezone string) string {
timezone = strings.TrimSpace(timezone)
if timezone == "" {
@@ -566,13 +624,21 @@ func extractHermesEnvBool(envMap map[string]string, key string, defaultValue boo
return strings.EqualFold(value, "true")
}
func validateHermesPairingApproveOutput(output string) error {
text := strings.TrimSpace(output)
if text == "" {
func validateHermesPairingApproveResult(output string, err error) error {
if strings.Contains(output, "Approved!") {
return nil
}
if strings.Contains(text, "not found or expired for platform") {
return errors.New(text)
if strings.Contains(output, "not found or expired for platform") {
return buserr.New("ErrHermesPairingCodeUnavailable")
}
return nil
if err == nil {
if strings.TrimSpace(output) == "" {
return fmt.Errorf("unexpected hermes pairing approve result")
}
return errors.New(strings.TrimSpace(output))
}
if strings.Contains(err.Error(), "not found or expired for platform") {
return buserr.New("ErrHermesPairingCodeUnavailable")
}
return err
}
+111 -4
View File
@@ -137,6 +137,23 @@ func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig)
return writeHermesConfigMap(configPath, cfg)
}
func deleteHermesQQBotChannelConfig(confDir string) error {
if err := deleteHermesEnvKeys(confDir,
"QQ_APP_ID",
"QQ_CLIENT_SECRET",
"QQ_ALLOW_ALL_USERS",
"QQ_ALLOWED_USERS",
"QQ_HOME_CHANNEL",
"QQ_HOME_CHANNEL_NAME",
"QQ_STT_API_KEY",
"QQ_STT_BASE_URL",
"QQ_STT_MODEL",
); err != nil {
return err
}
return deleteHermesConfigSections(confDir, nil, []string{"qq"})
}
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
@@ -243,6 +260,19 @@ func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig)
return writeHermesConfigMap(configPath, cfg)
}
func deleteHermesWecomChannelConfig(confDir string) error {
if err := deleteHermesEnvKeys(confDir,
"WECOM_BOT_ID",
"WECOM_SECRET",
"WECOM_ALLOW_ALL_USERS",
"WECOM_ALLOWED_USERS",
"WECOM_HOME_CHANNEL",
); err != nil {
return err
}
return deleteHermesConfigSections(confDir, nil, []string{"wecom"})
}
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
@@ -254,10 +284,15 @@ func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig,
}
platform := childMap(childMap(cfg, "platforms"), "dingtalk")
extra := childMap(platform, "extra")
allowFrom := splitHermesEnvList(envMap["DINGTALK_ALLOWED_USERS"])
dmPolicy := "open"
if len(allowFrom) > 0 {
dmPolicy := "pairing"
if extractHermesEnvBool(envMap, "DINGTALK_ALLOW_ALL_USERS", false) {
dmPolicy = "open"
} else if len(allowFrom) > 0 {
dmPolicy = "allowlist"
} else if extractStringValue(extra["unauthorized_dm_behavior"]) == "ignore" {
dmPolicy = "disabled"
}
clientID := envMap["DINGTALK_CLIENT_ID"]
clientSecret := envMap["DINGTALK_CLIENT_SECRET"]
@@ -292,25 +327,29 @@ func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkCo
return err
}
clientID, clientSecret := firstHermesDingTalkBotCredentials(config.Bots)
if clientID != "" {
if config.Enabled && clientID != "" {
envMap["DINGTALK_CLIENT_ID"] = clientID
} else {
delete(envMap, "DINGTALK_CLIENT_ID")
}
if clientSecret != "" {
if config.Enabled && clientSecret != "" {
envMap["DINGTALK_CLIENT_SECRET"] = clientSecret
} else {
delete(envMap, "DINGTALK_CLIENT_SECRET")
}
delete(envMap, "DINGTALK_ALLOWED_USERS")
delete(envMap, "DINGTALK_ALLOW_ALL_USERS")
if config.DmPolicy == "allowlist" {
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
envMap["DINGTALK_ALLOWED_USERS"] = allow
}
} else if config.DmPolicy == "open" {
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
}
if err := writeHermesEnvMap(envPath, envMap, []string{
"DINGTALK_CLIENT_ID",
"DINGTALK_CLIENT_SECRET",
"DINGTALK_ALLOW_ALL_USERS",
"DINGTALK_ALLOWED_USERS",
}); err != nil {
return err
@@ -323,9 +362,31 @@ func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkCo
}
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "dingtalk")
platform["enabled"] = config.Enabled && clientID != "" && clientSecret != ""
extra := ensureChildMap(platform, "extra")
switch config.DmPolicy {
case "pairing":
extra["unauthorized_dm_behavior"] = "pair"
case "disabled":
extra["unauthorized_dm_behavior"] = "ignore"
default:
delete(extra, "unauthorized_dm_behavior")
}
return writeHermesConfigMap(configPath, cfg)
}
func deleteHermesDingTalkChannelConfig(confDir string) error {
if err := deleteHermesEnvKeys(confDir,
"DINGTALK_CLIENT_ID",
"DINGTALK_CLIENT_SECRET",
"DINGTALK_ALLOW_ALL_USERS",
"DINGTALK_ALLOWED_USERS",
"DINGTALK_HOME_CHANNEL",
); err != nil {
return err
}
return deleteHermesConfigSections(confDir, nil, []string{"dingtalk"})
}
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
@@ -432,6 +493,52 @@ func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig
return writeHermesConfigMap(configPath, cfg)
}
func deleteHermesFeishuChannelConfig(confDir string) error {
if err := deleteHermesEnvKeys(confDir,
"FEISHU_APP_ID",
"FEISHU_APP_SECRET",
"FEISHU_DOMAIN",
"FEISHU_CONNECTION_MODE",
"FEISHU_ALLOW_ALL_USERS",
"FEISHU_ALLOWED_USERS",
"FEISHU_GROUP_POLICY",
"FEISHU_HOME_CHANNEL",
"FEISHU_VERIFICATION_TOKEN",
"FEISHU_ENCRYPT_KEY",
); err != nil {
return err
}
return deleteHermesConfigSections(confDir, nil, []string{"feishu"})
}
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
return nil, err
}
return &dto.AgentWeixinConfig{
Enabled: envMap["WEIXIN_ACCOUNT_ID"] != "" || envMap["WEIXIN_TOKEN"] != "",
}, nil
}
func deleteHermesWeixinChannelConfig(confDir string) error {
if err := deleteHermesEnvKeys(confDir,
"WEIXIN_ACCOUNT_ID",
"WEIXIN_TOKEN",
"WEIXIN_BASE_URL",
"WEIXIN_CDN_BASE_URL",
"WEIXIN_DM_POLICY",
"WEIXIN_ALLOW_ALL_USERS",
"WEIXIN_ALLOWED_USERS",
"WEIXIN_GROUP_POLICY",
"WEIXIN_GROUP_ALLOWED_USERS",
"WEIXIN_HOME_CHANNEL",
); err != nil {
return err
}
return deleteHermesConfigSections(confDir, nil, []string{"weixin"})
}
func firstHermesDingTalkBotCredentials(bots []dto.AgentDingTalkBot) (string, string) {
for _, bot := range bots {
if bot.IsDefault || bot.AccountID == "default" {
+1
View File
@@ -65,6 +65,7 @@ ErrAgentWebsiteBound: 'This agent is already bound to a website'
ErrAgentWebsiteTypeUnsupported: 'Only proxy or static websites can be bound'
ErrAgentWebsiteInUse: 'This website is already bound to another agent'
ErrAgentWebsiteUnbindUnsupported: 'Deployment websites cannot be unbound manually'
ErrHermesPairingCodeUnavailable: 'The pairing code is temporarily unavailable in Hermes, possibly due to network issues. Please try again later.'
#backup
Localhost: 'Local'
+1
View File
@@ -60,6 +60,7 @@ ErrAgentWebsiteBound: 'Este agente ya está vinculado a un sitio web'
ErrAgentWebsiteTypeUnsupported: 'Solo se pueden vincular sitios proxy o estáticos'
ErrAgentWebsiteInUse: 'Este sitio web ya está vinculado a otro agente'
ErrAgentWebsiteUnbindUnsupported: 'Los sitios web de despliegue no se pueden desvincular manualmente'
ErrHermesPairingCodeUnavailable: 'El código de emparejamiento no está disponible temporalmente en Hermes, posiblemente por un problema de red. Inténtalo de nuevo más tarde.'
Localhost: 'Máquina local'
ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada'
ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}'
+1
View File
@@ -60,6 +60,7 @@ ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付
ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイトまたは静的サイトのみです'
ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています'
ErrAgentWebsiteUnbindUnsupported: 'ワンクリックデプロイのサイトは手動で関連解除できません'
ErrHermesPairingCodeUnavailable: 'Hermes でペアリングコードが一時的に見つかりません。ネットワーク要因の可能性があるため、しばらくしてから再試行してください。'
Localhost: 'ローカルマシン'
ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中'
ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'
+1
View File
@@ -60,6 +60,7 @@ ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어
ErrAgentWebsiteTypeUnsupported: '프록시 또는 정적 웹사이트만 연결할 수 있습니다'
ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다'
ErrAgentWebsiteUnbindUnsupported: '원클릭 배포 웹사이트는 수동으로 연결 해제할 수 없습니다'
ErrHermesPairingCodeUnavailable: 'Hermes에서 페어링 코드가 일시적으로 존재하지 않습니다. 네트워크 문제일 수 있으니 잠시 후 다시 시도해 주세요.'
Localhost: '로컬 머신'
ErrBackupInUsed: '백업 계정이 예약에 사용 중'
ErrBackupCheck: '연결 테스트 실패: {{ .err }}'
+1
View File
@@ -60,6 +60,7 @@ ErrAgentWebsiteBound: 'Ejen ini sudah dipautkan ke laman web'
ErrAgentWebsiteTypeUnsupported: 'Hanya laman web proxy atau statik boleh dipautkan'
ErrAgentWebsiteInUse: 'Laman web ini sudah dipautkan ke ejen lain'
ErrAgentWebsiteUnbindUnsupported: 'Laman web one-click deployment tidak menyokong nyahikat manual'
ErrHermesPairingCodeUnavailable: 'Kod pasangan buat sementara waktu tidak wujud dalam Hermes, mungkin disebabkan masalah rangkaian. Sila cuba lagi sebentar nanti.'
Localhost: 'Mesin Tempatan'
ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas'
ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}'
+1
View File
@@ -60,6 +60,7 @@ ErrAgentWebsiteBound: 'Este agente já está vinculado a um site'
ErrAgentWebsiteTypeUnsupported: 'Somente sites proxy ou estáticos podem ser vinculados'
ErrAgentWebsiteInUse: 'Este site já está vinculado a outro agente'
ErrAgentWebsiteUnbindUnsupported: 'Sites implantados em um clique não podem ser desvinculados manualmente'
ErrHermesPairingCodeUnavailable: 'O código de pareamento está temporariamente indisponível no Hermes, possivelmente por causa de rede. Tente novamente mais tarde.'
Localhost: 'Máquina Local'
ErrBackupInUsed: 'Conta de backup em uso por tarefa'
ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}'
+1
View File
@@ -60,6 +60,7 @@ ErrAgentWebsiteBound: 'Этот агент уже связан с сайтом'
ErrAgentWebsiteTypeUnsupported: 'Можно связывать только proxy- или static-сайты'
ErrAgentWebsiteInUse: 'Этот сайт уже связан с другим агентом'
ErrAgentWebsiteUnbindUnsupported: 'Сайты one-click deployment нельзя отвязать вручную'
ErrHermesPairingCodeUnavailable: 'Код сопряжения временно недоступен в Hermes, возможно из-за проблем с сетью. Повторите попытку позже.'
Localhost: 'Локальная машина'
ErrBackupInUsed: 'Аккаунт бэкапа занят задачей'
ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}'
+1
View File
@@ -60,6 +60,7 @@ ErrAgentWebsiteBound: 'Bu ajan zaten bir web sitesine bağlı'
ErrAgentWebsiteTypeUnsupported: 'Yalnızca proxy veya statik web siteleri bağlanabilir'
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'
ErrHermesPairingCodeUnavailable: 'Eşleştirme kodu Hermes içinde geçici olarak bulunamıyor; bu durum ağ kaynaklı olabilir. Lütfen daha sonra tekrar deneyin.'
Localhost: 'Yerel Makine'
ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor'
ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}'
+1
View File
@@ -60,6 +60,7 @@ ErrAgentWebsiteBound: '該智能體已關聯網站'
ErrAgentWebsiteTypeUnsupported: '只能關聯反向代理或靜態網站'
ErrAgentWebsiteInUse: '該網站已被其他智能體關聯'
ErrAgentWebsiteUnbindUnsupported: '一鍵部署網站不支援手動解綁'
ErrHermesPairingCodeUnavailable: '配對碼在 Hermes 中暫時不存在,可能是由於網路原因,請稍後再試'
Localhost: '本機'
ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除'
ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}'
+1
View File
@@ -65,6 +65,7 @@ ErrAgentWebsiteBound: "该智能体已关联网站"
ErrAgentWebsiteTypeUnsupported: "只能关联反向代理或静态网站"
ErrAgentWebsiteInUse: "该网站已被其他智能体关联"
ErrAgentWebsiteUnbindUnsupported: "一键部署网站不支持手动解绑"
ErrHermesPairingCodeUnavailable: "配对码在 hermes 中暂时不存在,可能是由于网络原因,请稍后尝试"
#backup
Localhost: '本机'
+2
View File
@@ -82,9 +82,11 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/channel/wecom/update", baseApi.UpdateAgentWecomConfig)
aiToolsRouter.POST("/agents/channel/dingtalk/get", baseApi.GetAgentDingTalkConfig)
aiToolsRouter.POST("/agents/channel/dingtalk/update", baseApi.UpdateAgentDingTalkConfig)
aiToolsRouter.POST("/agents/channel/weixin/get", baseApi.GetAgentWeixinConfig)
aiToolsRouter.POST("/agents/channel/weixin/login", baseApi.LoginAgentWeixinChannel)
aiToolsRouter.POST("/agents/channel/qqbot/get", baseApi.GetAgentQQBotConfig)
aiToolsRouter.POST("/agents/channel/qqbot/update", baseApi.UpdateAgentQQBotConfig)
aiToolsRouter.POST("/agents/channel/delete", baseApi.DeleteAgentChannelConfig)
aiToolsRouter.POST("/agents/plugin/install", baseApi.InstallAgentPlugin)
aiToolsRouter.POST("/agents/plugin/upgrade", baseApi.UpgradeAgentPlugin)
aiToolsRouter.POST("/agents/plugin/uninstall", baseApi.UninstallAgentPlugin)
+12 -3
View File
@@ -626,7 +626,7 @@ export namespace AI {
export interface AgentChannelPairingApproveReq {
agentId: number;
type: 'feishu' | 'telegram' | 'discord' | 'wecom' | 'qqbot';
type: 'feishu' | 'telegram' | 'discord' | 'wecom' | 'qqbot' | 'dingtalk';
pairingCode: string;
accountId?: string;
}
@@ -670,7 +670,7 @@ export namespace AI {
export interface AgentDingTalkConfig {
enabled: boolean;
dmPolicy: 'allowlist' | 'open' | 'disabled';
dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';
allowFrom: string[];
groupPolicy: 'open' | 'allowlist' | 'disabled';
groupAllowFrom: string[];
@@ -686,7 +686,7 @@ export namespace AI {
export interface AgentDingTalkConfigUpdateReq {
agentId: number;
enabled: boolean;
dmPolicy: 'allowlist' | 'open' | 'disabled';
dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';
allowFrom: string[];
groupPolicy: 'open' | 'allowlist' | 'disabled';
groupAllowFrom: string[];
@@ -703,6 +703,15 @@ export namespace AI {
taskID: string;
}
export interface AgentWeixinConfig {
enabled: boolean;
}
export interface AgentChannelDeleteReq {
agentId: number;
type: 'feishu' | 'telegram' | 'discord' | 'wecom' | 'qqbot' | 'dingtalk' | 'weixin';
}
export interface AgentQQBotConfigReq {
agentId: number;
}
+8
View File
@@ -262,6 +262,10 @@ export const updateAgentDingTalkConfig = (req: AI.AgentDingTalkConfigUpdateReq)
return http.post(`/ai/agents/channel/dingtalk/update`, req);
};
export const getAgentWeixinConfig = (req: AI.AgentIDReq) => {
return http.post<AI.AgentWeixinConfig>(`/ai/agents/channel/weixin/get`, req);
};
export const loginAgentWeixinChannel = (req: AI.AgentWeixinLoginReq) => {
return http.post(`/ai/agents/channel/weixin/login`, req);
};
@@ -274,6 +278,10 @@ export const updateAgentQQBotConfig = (req: AI.AgentQQBotConfigUpdateReq) => {
return http.post(`/ai/agents/channel/qqbot/update`, req);
};
export const deleteAgentChannelConfig = (req: AI.AgentChannelDeleteReq) => {
return http.post(`/ai/agents/channel/delete`, req);
};
export const installAgentPlugin = (req: AI.AgentPluginInstallReq) => {
return http.post(`/ai/agents/plugin/install`, req);
};
+1 -2
View File
@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="open" :show-close="showClose" @close="handleClose" :width="width">
<el-dialog v-model="open" :show-close="showClose" @closed="handleClose" :width="width">
<div v-if="open">
<LogFile :config="config" :showTail="showTail"></LogFile>
</div>
@@ -67,7 +67,6 @@ const openWithResourceID = (taskType: string, taskOperate: string, resourceID: n
const em = defineEmits(['close']);
const handleClose = () => {
em('close', true);
open.value = false;
bus.emit('refreshTask', true);
bus.emit('refreshApp', true);
};
+3
View File
@@ -786,6 +786,7 @@ const message = {
hermesChatDeleteConfirm: 'Delete session {0}?',
hermesChatDeleteSuccess: 'Session deleted',
weixin: 'Weixin',
qq: 'QQ',
wecom: 'WeCom',
dingtalk: 'DingTalk',
feishu: 'Feishu',
@@ -842,6 +843,8 @@ const message = {
scanConnectHelper:
'Click to start the QR login task. The QR code will appear in the task log, and the container will restart automatically after the scan succeeds.',
channelAutoRestartHelper: 'Saving will automatically restart the container so the changes take effect.',
channelDeleteConfirm: 'Delete the {0} channel configuration?',
deleteAndRestartSuccess: 'Deleted successfully. The container is restarting automatically.',
customProviderHelper: 'Custom model providers do not validate whether the account is available.',
},
model: {
+3
View File
@@ -796,6 +796,7 @@ const message = {
hermesChatDeleteConfirm: '¿Eliminar la sesión {0}?',
hermesChatDeleteSuccess: 'Sesión eliminada',
weixin: 'Weixin',
qq: 'QQ',
wecom: 'WeCom',
dingtalk: 'DingTalk',
feishu: 'Feishu',
@@ -855,6 +856,8 @@ const message = {
'Haga clic para iniciar la tarea de inicio de sesión por QR. El código QR aparecerá en el registro de tareas y el contenedor se reiniciará automáticamente después de completar el escaneo.',
channelAutoRestartHelper:
'Al guardar, el contenedor se reiniciará automáticamente para que la configuración surta efecto.',
channelDeleteConfirm: '¿Eliminar la configuración del canal {0}?',
deleteAndRestartSuccess: 'Se eliminó correctamente. El contenedor se está reiniciando automáticamente.',
customProviderHelper: 'En el proveedor de modelo personalizado no se valida si la cuenta está disponible',
},
model: {
+3
View File
@@ -789,6 +789,7 @@ const message = {
hermesChatDeleteConfirm: 'セッション {0} を削除しますか',
hermesChatDeleteSuccess: 'セッションを削除しました',
weixin: 'Weixin',
qq: 'QQ',
wecom: 'WeCom',
dingtalk: 'DingTalk',
feishu: 'Feishu',
@@ -846,6 +847,8 @@ const message = {
scanConnectHelper:
'クリックして QR ログインタスクを開始しますQR コードはタスクログに表示されスキャン成功後にコンテナが自動で再起動されます',
channelAutoRestartHelper: '保存後設定を反映するためにコンテナが自動で再起動されます',
channelDeleteConfirm: '{0} チャンネルの設定を削除しますか',
deleteAndRestartSuccess: '削除に成功しましたコンテナは自動的に再起動しています',
customProviderHelper: 'カスタムモデルプロバイダーではアカウントの有効性を検証しません',
},
model: {
+3
View File
@@ -775,6 +775,7 @@ const message = {
hermesChatDeleteConfirm: '세션 {0}() 삭제하시겠습니까?',
hermesChatDeleteSuccess: '세션이 삭제되었습니다',
weixin: 'Weixin',
qq: 'QQ',
wecom: 'WeCom',
dingtalk: 'DingTalk',
feishu: 'Feishu',
@@ -831,6 +832,8 @@ const message = {
scanConnectHelper:
'클릭하여 QR 로그인 작업을 시작하세요. QR 코드는 작업 로그에 표시되며 스캔이 완료되면 컨테이너가 자동으로 재시작됩니다.',
channelAutoRestartHelper: '저장하면 설정 적용을 위해 컨테이너가 자동으로 재시작됩니다.',
channelDeleteConfirm: '{0} 채널 구성을 삭제하시겠습니까?',
deleteAndRestartSuccess: '삭제되었습니다. 컨테이너가 자동으로 다시 시작되고 있습니다.',
customProviderHelper: '사용자 정의 모델 공급자는 계정 사용 가능 여부를 검증하지 않습니다',
},
model: {
+3
View File
@@ -794,6 +794,7 @@ const message = {
hermesChatDeleteConfirm: 'Padam sesi {0}?',
hermesChatDeleteSuccess: 'Sesi berjaya dipadam',
weixin: 'Weixin',
qq: 'QQ',
wecom: 'WeCom',
dingtalk: 'DingTalk',
feishu: 'Feishu',
@@ -854,6 +855,8 @@ const message = {
'Klik untuk memulakan tugas log masuk QR. Kod QR akan dipaparkan dalam log tugas dan bekas akan dimulakan semula secara automatik selepas imbasan berjaya.',
channelAutoRestartHelper:
'Menyimpan akan memulakan semula bekas secara automatik supaya konfigurasi berkuat kuasa.',
channelDeleteConfirm: 'Padam konfigurasi saluran {0}?',
deleteAndRestartSuccess: 'Berjaya dipadam. Bekas sedang dimulakan semula secara automatik.',
customProviderHelper: 'Penyedia model tersuai tidak mengesahkan sama ada akaun boleh digunakan',
},
model: {
+3
View File
@@ -791,6 +791,7 @@ const message = {
hermesChatDeleteConfirm: 'Excluir a sessão {0}?',
hermesChatDeleteSuccess: 'Sessão excluída',
weixin: 'Weixin',
qq: 'QQ',
wecom: 'WeCom',
dingtalk: 'DingTalk',
feishu: 'Feishu',
@@ -850,6 +851,8 @@ const message = {
'Clique para iniciar a tarefa de login por QR. O código QR aparecerá no log da tarefa e o contêiner será reiniciado automaticamente após a leitura ser concluída.',
channelAutoRestartHelper:
'Ao salvar, o contêiner será reiniciado automaticamente para que a configuração entre em vigor.',
channelDeleteConfirm: 'Excluir a configuração do canal {0}?',
deleteAndRestartSuccess: 'Excluído com sucesso. O contêiner está sendo reiniciado automaticamente.',
customProviderHelper: 'Provedores de modelo personalizados não validam se a conta está disponível',
},
model: {
+3
View File
@@ -786,6 +786,7 @@ const message = {
hermesChatDeleteConfirm: 'Удалить сессию {0}?',
hermesChatDeleteSuccess: 'Сессия удалена',
weixin: 'Weixin',
qq: 'QQ',
wecom: 'WeCom',
dingtalk: 'DingTalk',
feishu: 'Feishu',
@@ -845,6 +846,8 @@ const message = {
'Нажмите, чтобы запустить задачу входа по QR-коду. QR-код появится в журнале задач, а после успешного сканирования контейнер будет автоматически перезапущен.',
channelAutoRestartHelper:
'После сохранения контейнер будет автоматически перезапущен, чтобы настройки вступили в силу.',
channelDeleteConfirm: 'Удалить конфигурацию канала {0}?',
deleteAndRestartSuccess: 'Удаление выполнено. Контейнер автоматически перезапускается.',
customProviderHelper: 'Для пользовательского провайдера модели доступность учетной записи не проверяется',
},
model: {
+3
View File
@@ -793,6 +793,7 @@ const message = {
hermesChatDeleteConfirm: '{0} oturumu silinsin mi?',
hermesChatDeleteSuccess: 'Oturum silindi',
weixin: 'Weixin',
qq: 'QQ',
wecom: 'WeCom',
dingtalk: 'DingTalk',
feishu: 'Feishu',
@@ -852,6 +853,8 @@ const message = {
'QR giriş görevini başlatmak için tıklayın. QR kodu görev günlüğünde görünecek ve tarama başarılı olduktan sonra konteyner otomatik olarak yeniden başlatılacaktır.',
channelAutoRestartHelper:
'Kaydettiğinizde ayarların etkili olması için konteyner otomatik olarak yeniden başlatılır.',
channelDeleteConfirm: '{0} kanal yapılandırması silinsin mi?',
deleteAndRestartSuccess: 'Başarıyla silindi. Konteyner otomatik olarak yeniden başlatılıyor.',
customProviderHelper: 'Özel model sağlayıcısında hesabın kullanılabilirliği doğrulanmaz',
},
model: {
+3
View File
@@ -740,6 +740,7 @@ const message = {
hermesChatDeleteConfirm: '確認刪除會話 {0}',
hermesChatDeleteSuccess: '會話已刪除',
weixin: '微信',
qq: 'QQ',
wecom: '企業微信',
dingtalk: '釘釘',
feishu: '飛書',
@@ -795,6 +796,8 @@ const message = {
scanConnect: '掃碼對接',
scanConnectHelper: '點擊後將在任務日誌中顯示 QR Code,掃碼確認成功後將自動重新啟動容器。',
channelAutoRestartHelper: '保存後將自動重新啟動容器以使設定生效。',
channelDeleteConfirm: '確認刪除 {0} 頻道設定?',
deleteAndRestartSuccess: '刪除成功,容器正在自動重新啟動。',
customProviderHelper: '自訂模型供應商不驗證帳號是否可用',
},
model: {
+3
View File
@@ -735,6 +735,7 @@ const message = {
hermesChatDeleteConfirm: '确认删除会话 {0}',
hermesChatDeleteSuccess: '会话已删除',
weixin: '微信',
qq: 'QQ',
wecom: '企业微信',
dingtalk: '钉钉',
feishu: '飞书',
@@ -789,6 +790,8 @@ const message = {
scanConnect: '扫码对接',
scanConnectHelper: '点击后将在任务日志中显示二维码扫码确认成功后将自动重启容器',
channelAutoRestartHelper: '保存后将自动重启容器以使配置生效',
channelDeleteConfirm: '确认删除 {0} 频道配置',
deleteAndRestartSuccess: '删除成功容器正在自动重启',
customProviderHelper: '自定义模型供应商不验证账号是否可用',
},
model: {
@@ -2,12 +2,6 @@
<DrawerPro v-model="open" :header="$t('commons.button.create')" size="large" @close="handleClose">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-card class="form-card">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item :label="$t('website.remark')" prop="remark">
<el-input v-model="form.remark" />
</el-form-item>
<el-form-item :label="`${$t('aiTools.agents.agent')}${$t('commons.table.type')}`" prop="agentType">
<el-select v-model="form.agentType" @change="handleAgentTypeChange">
<el-option :label="$t('aiTools.agents.openclawType')" value="openclaw" />
@@ -15,6 +9,12 @@
<el-option :label="$t('aiTools.agents.copawType')" value="copaw" />
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item :label="$t('website.remark')" prop="remark">
<el-input v-model="form.remark" />
</el-form-item>
<el-form-item :label="$t('aiTools.agents.appVersion')" prop="appVersion">
<el-select v-model="form.appVersion" filterable>
<el-option v-for="item in versions" :key="item" :label="item" :value="item" />
@@ -1,7 +1,9 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item :label="t('commons.table.status')">
<el-switch v-model="form.enabled" />
<el-form ref="formRef" v-loading="deleting" :model="form" :rules="rules" label-position="top">
<el-form-item v-if="configured">
<el-button type="danger" plain :loading="deleting" @click="deleteChannel">
{{ t('commons.button.delete') }}
</el-button>
</el-form-item>
<el-form-item label="Client ID" prop="clientId">
<el-input v-model="form.clientId" />
@@ -9,7 +11,15 @@
<el-form-item label="Client Secret" prop="clientSecret">
<el-input v-model="form.clientSecret" type="password" show-password />
</el-form-item>
<el-form-item :label="t('aiTools.agents.allowFrom')">
<el-form-item :label="t('aiTools.agents.dmPolicy')" prop="dmPolicy">
<el-select v-model="form.dmPolicy">
<el-option :label="t('aiTools.agents.pairingCode')" value="pairing" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
</el-form-item>
<el-form-item v-if="form.dmPolicy === 'allowlist'" :label="t('aiTools.agents.allowFrom')" prop="allowFromText">
<el-input
v-model="form.allowFromText"
type="textarea"
@@ -24,32 +34,51 @@
</el-button>
</el-form-item>
<el-alert type="info" :closable="false" :title="t('aiTools.agents.channelAutoRestartHelper')" />
<template v-if="form.dmPolicy === 'pairing'">
<el-form-item :label="t('aiTools.agents.pairingCode')">
<el-input v-model="pairingCode" :placeholder="t('aiTools.agents.pairingCodePlaceholder')" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain :loading="approving" @click="approvePairing">
{{ t('aiTools.agents.approvePairing') }}
</el-button>
</el-form-item>
</template>
</el-form>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessageBox, type FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { getAgentDingTalkConfig, updateAgentDingTalkConfig } from '@/api/modules/ai';
import {
approveAgentChannelPairing,
deleteAgentChannelConfig,
getAgentDingTalkConfig,
updateAgentDingTalkConfig,
} from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import { MsgSuccess, MsgWarning } from '@/utils/message';
interface DingTalkForm {
enabled: boolean;
clientId: string;
clientSecret: string;
dmPolicy: 'pairing' | 'open' | 'allowlist' | 'disabled';
allowFromText: string;
}
const { t } = useI18n();
const formRef = ref<FormInstance>();
const saving = ref(false);
const approving = ref(false);
const deleting = ref(false);
const agentId = ref(0);
const pairingCode = ref('');
const configured = ref(false);
const form = reactive<DingTalkForm>({
enabled: true,
clientId: '',
clientSecret: '',
dmPolicy: 'pairing',
allowFromText: '',
});
@@ -67,14 +96,29 @@ const parseTextList = (value: string): string[] => {
const rules = reactive({
clientId: [Rules.requiredInput],
clientSecret: [Rules.requiredInput],
dmPolicy: [Rules.requiredSelect],
allowFromText: [
{
validator: (_rule, value, callback) => {
if (form.dmPolicy === 'allowlist' && parseTextList(String(value || '')).length === 0) {
callback(new Error(t('aiTools.agents.allowFromRequired')));
return;
}
callback();
},
trigger: 'blur',
},
],
});
const load = async (id: number) => {
agentId.value = id;
pairingCode.value = '';
const res = await getAgentDingTalkConfig({ agentId: id });
form.enabled = res.data?.enabled ?? true;
configured.value = !!res.data?.enabled;
form.clientId = res.data?.bots?.[0]?.clientId || '';
form.clientSecret = res.data?.bots?.[0]?.clientSecret || '';
form.dmPolicy = (res.data?.dmPolicy as DingTalkForm['dmPolicy']) || 'pairing';
form.allowFromText = (res.data?.allowFrom || []).join('\n');
};
@@ -85,12 +129,11 @@ const save = async () => {
await formRef.value.validate();
saving.value = true;
try {
const allowFrom = parseTextList(form.allowFromText);
await updateAgentDingTalkConfig({
agentId: agentId.value,
enabled: form.enabled,
dmPolicy: allowFrom.length > 0 ? 'allowlist' : 'open',
allowFrom,
enabled: true,
dmPolicy: form.dmPolicy,
allowFrom: form.dmPolicy === 'allowlist' ? parseTextList(form.allowFromText) : [],
groupPolicy: 'open',
groupAllowFrom: [],
separateSessionByConversation: true,
@@ -110,11 +153,60 @@ const save = async () => {
],
});
MsgSuccess(t('aiTools.agents.saveAndRestartSuccess'));
configured.value = true;
} finally {
saving.value = false;
}
};
const deleteChannel = async () => {
if (!agentId.value) {
return;
}
await ElMessageBox.confirm(
t('aiTools.agents.channelDeleteConfirm', [t('aiTools.agents.dingtalk')]),
t('commons.msg.infoTitle'),
{
confirmButtonText: t('commons.button.confirm'),
cancelButtonText: t('commons.button.cancel'),
type: 'warning',
},
);
deleting.value = true;
try {
await deleteAgentChannelConfig({
agentId: agentId.value,
type: 'dingtalk',
});
await load(agentId.value);
MsgSuccess(t('aiTools.agents.deleteAndRestartSuccess'));
} finally {
deleting.value = false;
}
};
const approvePairing = async () => {
if (!agentId.value) {
return;
}
if (!pairingCode.value) {
MsgWarning(t('aiTools.agents.pairingCodePlaceholder'));
return;
}
approving.value = true;
try {
await approveAgentChannelPairing({
agentId: agentId.value,
type: 'dingtalk',
pairingCode: pairingCode.value,
});
pairingCode.value = '';
MsgSuccess(t('aiTools.agents.pairingApproveSuccess'));
} finally {
approving.value = false;
}
};
defineExpose({
load,
});
@@ -1,5 +1,10 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form ref="formRef" v-loading="deleting" :model="form" :rules="rules" label-position="top">
<el-form-item v-if="configured">
<el-button type="danger" plain :loading="deleting" @click="deleteChannel">
{{ t('commons.button.delete') }}
</el-button>
</el-form-item>
<el-form-item label="Token" prop="token">
<el-input v-model="form.token" show-password />
</el-form-item>
@@ -43,9 +48,14 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessageBox, type FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { approveAgentChannelPairing, getAgentDiscordConfig, updateAgentDiscordConfig } from '@/api/modules/ai';
import {
approveAgentChannelPairing,
deleteAgentChannelConfig,
getAgentDiscordConfig,
updateAgentDiscordConfig,
} from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess, MsgWarning } from '@/utils/message';
@@ -60,8 +70,10 @@ const { t } = useI18n();
const formRef = ref<FormInstance>();
const saving = ref(false);
const approving = ref(false);
const deleting = ref(false);
const agentId = ref(0);
const pairingCode = ref('');
const configured = ref(false);
const form = reactive<DiscordForm>({
token: '',
dmPolicy: 'pairing',
@@ -96,7 +108,9 @@ const rules = reactive({
const load = async (id: number) => {
agentId.value = id;
pairingCode.value = '';
const res = await getAgentDiscordConfig({ agentId: id });
configured.value = !!res.data?.enabled;
form.token = res.data?.bots?.[0]?.token || '';
form.dmPolicy = (res.data?.dmPolicy as DiscordForm['dmPolicy']) || 'pairing';
form.allowFromText = (res.data?.allowFrom || []).join('\n');
@@ -132,11 +146,34 @@ const save = async () => {
],
});
MsgSuccess(t('aiTools.agents.saveAndRestartSuccess'));
configured.value = true;
} finally {
saving.value = false;
}
};
const deleteChannel = async () => {
if (!agentId.value) {
return;
}
await ElMessageBox.confirm(t('aiTools.agents.channelDeleteConfirm', ['Discord']), t('commons.msg.infoTitle'), {
confirmButtonText: t('commons.button.confirm'),
cancelButtonText: t('commons.button.cancel'),
type: 'warning',
});
deleting.value = true;
try {
await deleteAgentChannelConfig({
agentId: agentId.value,
type: 'discord',
});
await load(agentId.value);
MsgSuccess(t('aiTools.agents.deleteAndRestartSuccess'));
} finally {
deleting.value = false;
}
};
const approvePairing = async () => {
if (!agentId.value) {
return;
@@ -1,7 +1,9 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item :label="t('commons.table.status')">
<el-switch v-model="form.enabled" />
<el-form ref="formRef" v-loading="deleting" :model="form" :rules="rules" label-position="top">
<el-form-item v-if="configured">
<el-button type="danger" plain :loading="deleting" @click="deleteChannel">
{{ t('commons.button.delete') }}
</el-button>
</el-form-item>
<el-form-item label="App ID" prop="appId">
<el-input v-model="form.appId" />
@@ -53,14 +55,18 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessageBox, type FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { approveAgentChannelPairing, getAgentFeishuConfig, updateAgentFeishuConfig } from '@/api/modules/ai';
import {
approveAgentChannelPairing,
deleteAgentChannelConfig,
getAgentFeishuConfig,
updateAgentFeishuConfig,
} from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess, MsgWarning } from '@/utils/message';
interface FeishuForm {
enabled: boolean;
appId: string;
appSecret: string;
dmPolicy: 'pairing' | 'open' | 'allowlist';
@@ -72,10 +78,11 @@ const { t } = useI18n();
const formRef = ref<FormInstance>();
const saving = ref(false);
const approving = ref(false);
const deleting = ref(false);
const agentId = ref(0);
const pairingCode = ref('');
const configured = ref(false);
const form = reactive<FeishuForm>({
enabled: true,
appId: '',
appSecret: '',
dmPolicy: 'pairing',
@@ -117,7 +124,7 @@ const load = async (id: number) => {
agentId.value = id;
pairingCode.value = '';
const res = await getAgentFeishuConfig({ agentId: id });
form.enabled = res.data?.enabled ?? true;
configured.value = !!res.data?.enabled;
form.appId = res.data?.bots?.[0]?.appId || '';
form.appSecret = res.data?.bots?.[0]?.appSecret || '';
form.dmPolicy = (res.data?.bots?.[0]?.dmPolicy as FeishuForm['dmPolicy']) || 'pairing';
@@ -135,7 +142,7 @@ const save = async () => {
const allowFrom = parseTextList(form.allowFromText);
await updateAgentFeishuConfig({
agentId: agentId.value,
enabled: form.enabled,
enabled: true,
threadSession: true,
replyMode: 'auto',
streaming: false,
@@ -156,11 +163,38 @@ const save = async () => {
],
});
MsgSuccess(t('aiTools.agents.saveAndRestartSuccess'));
configured.value = true;
} finally {
saving.value = false;
}
};
const deleteChannel = async () => {
if (!agentId.value) {
return;
}
await ElMessageBox.confirm(
t('aiTools.agents.channelDeleteConfirm', [t('aiTools.agents.feishu')]),
t('commons.msg.infoTitle'),
{
confirmButtonText: t('commons.button.confirm'),
cancelButtonText: t('commons.button.cancel'),
type: 'warning',
},
);
deleting.value = true;
try {
await deleteAgentChannelConfig({
agentId: agentId.value,
type: 'feishu',
});
await load(agentId.value);
MsgSuccess(t('aiTools.agents.deleteAndRestartSuccess'));
} finally {
deleting.value = false;
}
};
const approvePairing = async () => {
if (!agentId.value) {
return;
@@ -1,7 +1,9 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item :label="t('commons.table.status')">
<el-switch v-model="form.enabled" />
<el-form ref="formRef" v-loading="deleting" :model="form" :rules="rules" label-position="top">
<el-form-item v-if="configured">
<el-button type="danger" plain :loading="deleting" @click="deleteChannel">
{{ t('commons.button.delete') }}
</el-button>
</el-form-item>
<el-form-item label="App ID" prop="appId">
<el-input v-model="form.appId" />
@@ -67,14 +69,18 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessageBox, type FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { approveAgentChannelPairing, getAgentQQBotConfig, updateAgentQQBotConfig } from '@/api/modules/ai';
import {
approveAgentChannelPairing,
deleteAgentChannelConfig,
getAgentQQBotConfig,
updateAgentQQBotConfig,
} from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess, MsgWarning } from '@/utils/message';
interface QQBotForm {
enabled: boolean;
appId: string;
clientSecret: string;
dmPolicy: 'pairing' | 'open' | 'allowlist' | 'disabled';
@@ -87,10 +93,11 @@ const { t } = useI18n();
const formRef = ref<FormInstance>();
const saving = ref(false);
const approving = ref(false);
const deleting = ref(false);
const agentId = ref(0);
const pairingCode = ref('');
const configured = ref(false);
const form = reactive<QQBotForm>({
enabled: true,
appId: '',
clientSecret: '',
dmPolicy: 'open',
@@ -145,7 +152,7 @@ const load = async (id: number) => {
agentId.value = id;
pairingCode.value = '';
const res = await getAgentQQBotConfig({ agentId: id });
form.enabled = res.data?.enabled ?? true;
configured.value = !!res.data?.enabled;
form.appId = res.data?.bots?.[0]?.appId || '';
form.clientSecret = res.data?.bots?.[0]?.clientSecret || '';
form.dmPolicy = (res.data?.dmPolicy as QQBotForm['dmPolicy']) || 'open';
@@ -163,7 +170,7 @@ const save = async () => {
try {
await updateAgentQQBotConfig({
agentId: agentId.value,
enabled: form.enabled,
enabled: true,
dmPolicy: form.dmPolicy,
allowFrom: parseTextList(form.allowFromText),
groupPolicy: form.groupPolicy,
@@ -182,11 +189,38 @@ const save = async () => {
],
});
MsgSuccess(t('aiTools.agents.saveAndRestartSuccess'));
configured.value = true;
} finally {
saving.value = false;
}
};
const deleteChannel = async () => {
if (!agentId.value) {
return;
}
await ElMessageBox.confirm(
t('aiTools.agents.channelDeleteConfirm', [t('aiTools.agents.qq')]),
t('commons.msg.infoTitle'),
{
confirmButtonText: t('commons.button.confirm'),
cancelButtonText: t('commons.button.cancel'),
type: 'warning',
},
);
deleting.value = true;
try {
await deleteAgentChannelConfig({
agentId: agentId.value,
type: 'qqbot',
});
await load(agentId.value);
MsgSuccess(t('aiTools.agents.deleteAndRestartSuccess'));
} finally {
deleting.value = false;
}
};
const approvePairing = async () => {
if (!agentId.value) {
return;
@@ -1,5 +1,10 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form ref="formRef" v-loading="deleting" :model="form" :rules="rules" label-position="top">
<el-form-item v-if="configured">
<el-button type="danger" plain :loading="deleting" @click="deleteChannel">
{{ t('commons.button.delete') }}
</el-button>
</el-form-item>
<el-form-item label="Bot Token" prop="botToken">
<el-input v-model="form.botToken" show-password />
</el-form-item>
@@ -43,9 +48,14 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessageBox, type FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { approveAgentChannelPairing, getAgentTelegramConfig, updateAgentTelegramConfig } from '@/api/modules/ai';
import {
approveAgentChannelPairing,
deleteAgentChannelConfig,
getAgentTelegramConfig,
updateAgentTelegramConfig,
} from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess, MsgWarning } from '@/utils/message';
@@ -60,8 +70,10 @@ const { t } = useI18n();
const formRef = ref<FormInstance>();
const saving = ref(false);
const approving = ref(false);
const deleting = ref(false);
const agentId = ref(0);
const pairingCode = ref('');
const configured = ref(false);
const form = reactive<TelegramForm>({
botToken: '',
dmPolicy: 'pairing',
@@ -96,7 +108,9 @@ const rules = reactive({
const load = async (id: number) => {
agentId.value = id;
pairingCode.value = '';
const res = await getAgentTelegramConfig({ agentId: id });
configured.value = !!res.data?.enabled;
form.botToken = res.data?.bots?.[0]?.botToken || '';
form.dmPolicy = (res.data?.dmPolicy as TelegramForm['dmPolicy']) || 'pairing';
form.allowFromText = (res.data?.allowFrom || []).join('\n');
@@ -137,11 +151,34 @@ const save = async () => {
],
});
MsgSuccess(t('aiTools.agents.saveAndRestartSuccess'));
configured.value = true;
} finally {
saving.value = false;
}
};
const deleteChannel = async () => {
if (!agentId.value) {
return;
}
await ElMessageBox.confirm(t('aiTools.agents.channelDeleteConfirm', ['Telegram']), t('commons.msg.infoTitle'), {
confirmButtonText: t('commons.button.confirm'),
cancelButtonText: t('commons.button.cancel'),
type: 'warning',
});
deleting.value = true;
try {
await deleteAgentChannelConfig({
agentId: agentId.value,
type: 'telegram',
});
await load(agentId.value);
MsgSuccess(t('aiTools.agents.deleteAndRestartSuccess'));
} finally {
deleting.value = false;
}
};
const approvePairing = async () => {
if (!agentId.value) {
return;
@@ -1,7 +1,15 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item :label="t('commons.table.status')">
<el-switch v-model="form.enabled" />
<el-form ref="formRef" v-loading="deleting" :model="form" :rules="rules" label-position="top">
<el-form-item v-if="configured">
<el-button type="danger" plain :loading="deleting" @click="deleteChannel">
{{ t('commons.button.delete') }}
</el-button>
</el-form-item>
<el-form-item :label="t('aiTools.agents.botId')" prop="botId">
<el-input v-model="form.botId" />
</el-form-item>
<el-form-item :label="t('setting.secret')" prop="secret">
<el-input v-model="form.secret" type="password" show-password />
</el-form-item>
<el-form-item :label="t('aiTools.agents.dmPolicy')" prop="dmPolicy">
<el-select v-model="form.dmPolicy">
@@ -40,12 +48,6 @@
/>
<span class="input-help">{{ t('aiTools.agents.groupAllowFromHelper') }}</span>
</el-form-item>
<el-form-item :label="t('aiTools.agents.botId')" prop="botId">
<el-input v-model="form.botId" />
</el-form-item>
<el-form-item :label="t('setting.secret')" prop="secret">
<el-input v-model="form.secret" type="password" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="save">
{{ t('commons.button.save') }}
@@ -67,14 +69,18 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessageBox, type FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { approveAgentChannelPairing, getAgentWecomConfig, updateAgentWecomConfig } from '@/api/modules/ai';
import {
approveAgentChannelPairing,
deleteAgentChannelConfig,
getAgentWecomConfig,
updateAgentWecomConfig,
} from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess, MsgWarning } from '@/utils/message';
interface WecomForm {
enabled: boolean;
dmPolicy: 'pairing' | 'open' | 'allowlist' | 'disabled';
allowFromText: string;
groupPolicy: 'open' | 'allowlist' | 'disabled';
@@ -87,10 +93,11 @@ const { t } = useI18n();
const formRef = ref<FormInstance>();
const saving = ref(false);
const approving = ref(false);
const deleting = ref(false);
const agentId = ref(0);
const pairingCode = ref('');
const configured = ref(false);
const form = reactive<WecomForm>({
enabled: true,
dmPolicy: 'pairing',
allowFromText: '',
groupPolicy: 'open',
@@ -139,7 +146,7 @@ const load = async (id: number) => {
agentId.value = id;
pairingCode.value = '';
const res = await getAgentWecomConfig({ agentId: id });
form.enabled = res.data?.enabled ?? true;
configured.value = !!res.data?.enabled;
form.dmPolicy = (res.data?.dmPolicy as WecomForm['dmPolicy']) || 'pairing';
form.allowFromText = (res.data?.allowFrom || []).join('\n');
form.groupPolicy = (res.data?.groupPolicy as WecomForm['groupPolicy']) || 'open';
@@ -157,7 +164,7 @@ const save = async () => {
try {
await updateAgentWecomConfig({
agentId: agentId.value,
enabled: form.enabled,
enabled: true,
dmPolicy: form.dmPolicy,
allowFrom: parseTextList(form.allowFromText),
groupPolicy: form.groupPolicy,
@@ -166,11 +173,38 @@ const save = async () => {
secret: form.secret,
});
MsgSuccess(t('aiTools.agents.saveAndRestartSuccess'));
configured.value = true;
} finally {
saving.value = false;
}
};
const deleteChannel = async () => {
if (!agentId.value) {
return;
}
await ElMessageBox.confirm(
t('aiTools.agents.channelDeleteConfirm', [t('aiTools.agents.wecom')]),
t('commons.msg.infoTitle'),
{
confirmButtonText: t('commons.button.confirm'),
cancelButtonText: t('commons.button.cancel'),
type: 'warning',
},
);
deleting.value = true;
try {
await deleteAgentChannelConfig({
agentId: agentId.value,
type: 'wecom',
});
await load(agentId.value);
MsgSuccess(t('aiTools.agents.deleteAndRestartSuccess'));
} finally {
deleting.value = false;
}
};
const approvePairing = async () => {
if (!agentId.value) {
return;
@@ -1,5 +1,10 @@
<template>
<el-form label-position="top">
<el-form v-loading="deleting" label-position="top">
<el-form-item v-if="configured">
<el-button type="danger" plain :loading="deleting" @click="deleteChannel">
{{ t('commons.button.delete') }}
</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loggingIn" @click="loginChannel">
{{ t('aiTools.agents.scanConnect') }}
@@ -12,19 +17,26 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { loginAgentWeixinChannel } from '@/api/modules/ai';
import { deleteAgentChannelConfig, getAgentWeixinConfig, loginAgentWeixinChannel } from '@/api/modules/ai';
import TaskLog from '@/components/log/task/index.vue';
import { newUUID } from '@/utils/id';
import { MsgSuccess } from '@/utils/message';
const props = defineProps<{
agentId: number;
}>();
const { t } = useI18n();
const loggingIn = ref(false);
const deleting = ref(false);
const loginTaskLogRef = ref();
const configured = ref(false);
const load = async () => {};
const load = async () => {
const res = await getAgentWeixinConfig({ agentId: props.agentId });
configured.value = !!res.data?.enabled;
};
const loginChannel = async () => {
const taskID = newUUID();
@@ -34,12 +46,36 @@ const loginChannel = async () => {
agentId: props.agentId,
taskID,
});
configured.value = true;
loginTaskLogRef.value?.openWithTaskID(taskID);
} finally {
loggingIn.value = false;
}
};
const deleteChannel = async () => {
await ElMessageBox.confirm(
t('aiTools.agents.channelDeleteConfirm', [t('aiTools.agents.weixin')]),
t('commons.msg.infoTitle'),
{
confirmButtonText: t('commons.button.confirm'),
cancelButtonText: t('commons.button.cancel'),
type: 'warning',
},
);
deleting.value = true;
try {
await deleteAgentChannelConfig({
agentId: props.agentId,
type: 'weixin',
});
await load();
MsgSuccess(t('aiTools.agents.deleteAndRestartSuccess'));
} finally {
deleting.value = false;
}
};
defineExpose({
load,
});
+20 -7
View File
@@ -95,12 +95,9 @@
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('menu.website')" min-width="180" show-overflow-tooltip>
<el-table-column :label="$t('menu.website')" min-width="180">
<template #default="{ row }">
<div v-if="row.websiteId > 0" class="website-link-cell">
<el-text type="primary" class="cursor-pointer" @click="openWebsite(row)">
{{ getWebsiteDisplayName(row) }}
</el-text>
<el-popover
placement="right"
trigger="hover"
@@ -108,7 +105,13 @@
@before-enter="loadWebsiteDomains(row.websiteId)"
>
<template #reference>
<el-button link icon="Promotion" class="ml-2.5"></el-button>
<el-text
type="primary"
class="cursor-pointer website-link-cell__name"
@click="openWebsite(row)"
>
{{ getWebsiteDisplayName(row) }}
</el-text>
</template>
<table v-if="getWebsiteBaseUrls(row).length > 0">
<tbody>
@@ -130,6 +133,7 @@
v-if="canUnbindWebsite(row)"
link
type="primary"
class="website-link-cell__unbind"
@click="onUnbindWebsite(row)"
>
{{ $t('commons.button.unbind') }}
@@ -714,7 +718,16 @@ onMounted(async () => {
.website-link-cell {
display: inline-flex;
align-items: center;
gap: 10px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.website-link-cell__name {
white-space: nowrap;
}
.website-link-cell__unbind {
margin-left: 0;
}
</style>