3d47227fbd
Remove embedded frontend and workflow editor assets, add standalone API deployment configuration, and retain the current backend service updates.
535 lines
16 KiB
Go
535 lines
16 KiB
Go
package services
|
|
|
|
import (
|
|
"agent-desk/internal/models"
|
|
"agent-desk/internal/pkg/dto"
|
|
"agent-desk/internal/pkg/dto/request"
|
|
"agent-desk/internal/pkg/dto/response"
|
|
"agent-desk/internal/pkg/enums"
|
|
"agent-desk/internal/pkg/errorsx"
|
|
"agent-desk/internal/pkg/httpx"
|
|
"agent-desk/internal/pkg/utils"
|
|
"agent-desk/internal/repositories"
|
|
"agent-desk/internal/wxwork"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"agent-desk/internal/pkg/httpx/params"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/mlogclub/simple/common/strs"
|
|
"github.com/mlogclub/simple/sqls"
|
|
"github.com/silenceper/wechat/v2/work/kf"
|
|
)
|
|
|
|
var ChannelService = newChannelService()
|
|
|
|
func newChannelService() *channelService {
|
|
return &channelService{}
|
|
}
|
|
|
|
type channelService struct {
|
|
}
|
|
|
|
func (s *channelService) Get(id int64) *models.Channel {
|
|
return repositories.ChannelRepository.Get(sqls.DB(), id)
|
|
}
|
|
|
|
func (s *channelService) Take(where ...interface{}) *models.Channel {
|
|
return repositories.ChannelRepository.Take(sqls.DB(), where...)
|
|
}
|
|
|
|
func (s *channelService) Find(cnd *sqls.Cnd) []models.Channel {
|
|
return repositories.ChannelRepository.Find(sqls.DB(), cnd)
|
|
}
|
|
|
|
func (s *channelService) FindOne(cnd *sqls.Cnd) *models.Channel {
|
|
return repositories.ChannelRepository.FindOne(sqls.DB(), cnd)
|
|
}
|
|
|
|
func (s *channelService) FindPageByParams(params *params.QueryParams) (list []models.Channel, paging *sqls.Paging) {
|
|
return repositories.ChannelRepository.FindPageByParams(sqls.DB(), params)
|
|
}
|
|
|
|
func (s *channelService) FindPageByCnd(cnd *sqls.Cnd) (list []models.Channel, paging *sqls.Paging) {
|
|
return repositories.ChannelRepository.FindPageByCnd(sqls.DB(), cnd)
|
|
}
|
|
|
|
func (s *channelService) Count(cnd *sqls.Cnd) int64 {
|
|
return repositories.ChannelRepository.Count(sqls.DB(), cnd)
|
|
}
|
|
|
|
func (s *channelService) Create(t *models.Channel) error {
|
|
return repositories.ChannelRepository.Create(sqls.DB(), t)
|
|
}
|
|
|
|
func (s *channelService) Update(t *models.Channel) error {
|
|
return repositories.ChannelRepository.Update(sqls.DB(), t)
|
|
}
|
|
|
|
func (s *channelService) Updates(id int64, columns map[string]interface{}) error {
|
|
return repositories.ChannelRepository.Updates(sqls.DB(), id, columns)
|
|
}
|
|
|
|
func (s *channelService) UpdateColumn(id int64, name string, value interface{}) error {
|
|
return repositories.ChannelRepository.UpdateColumn(sqls.DB(), id, name, value)
|
|
}
|
|
|
|
func (s *channelService) CreateChannel(req request.CreateChannelRequest, operator *dto.AuthPrincipal) (*models.Channel, error) {
|
|
if operator == nil {
|
|
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
|
}
|
|
item, err := s.buildChannelModel(0, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.AuditFields = utils.BuildAuditFields(operator)
|
|
if err := repositories.ChannelRepository.Create(sqls.DB(), item); err != nil {
|
|
return nil, err
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func (s *channelService) UpdateChannel(req request.UpdateChannelRequest, operator *dto.AuthPrincipal) error {
|
|
if operator == nil {
|
|
return errorsx.UnauthorizedI18n("error.auth.expired")
|
|
}
|
|
current := s.Get(req.ID)
|
|
if current == nil || current.Status == enums.StatusDeleted {
|
|
return errorsx.InvalidParamI18n("error.e0208")
|
|
}
|
|
item, err := s.buildChannelModel(req.ID, req.CreateChannelRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
columns := map[string]any{
|
|
"channel_type": item.ChannelType,
|
|
"channel_id": item.ChannelID,
|
|
"ai_agent_id": item.AIAgentID,
|
|
"ai_agent_rollout_percent": item.AIAgentRolloutPercent,
|
|
"name": item.Name,
|
|
"config_json": item.ConfigJSON,
|
|
"status": item.Status,
|
|
"remark": item.Remark,
|
|
"update_user_id": operator.UserID,
|
|
"update_user_name": operator.Username,
|
|
"updated_at": time.Now(),
|
|
}
|
|
if item.AIAgentRolloutPercent != current.AIAgentRolloutPercent {
|
|
columns["previous_ai_agent_rollout_percent"] = current.AIAgentRolloutPercent
|
|
}
|
|
return repositories.ChannelRepository.Updates(sqls.DB(), req.ID, columns)
|
|
}
|
|
|
|
// RollbackChannelAIAgentRollout restores the last channel-level rollout value
|
|
// and swaps it into history so the action itself is reversible.
|
|
func (s *channelService) RollbackChannelAIAgentRollout(id int64, operator *dto.AuthPrincipal) error {
|
|
if operator == nil {
|
|
return errorsx.UnauthorizedI18n("error.auth.expired")
|
|
}
|
|
if id <= 0 {
|
|
return errorsx.InvalidParam("channel id is required")
|
|
}
|
|
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
|
channel := repositories.ChannelRepository.Get(ctx.Tx, id)
|
|
if channel == nil || channel.Status == enums.StatusDeleted {
|
|
return errorsx.InvalidParamI18n("error.e0208")
|
|
}
|
|
if channel.PreviousAIAgentRolloutPercent < 1 || channel.PreviousAIAgentRolloutPercent > 100 {
|
|
return errorsx.InvalidParam("channel rollout has no previous value to restore")
|
|
}
|
|
return repositories.ChannelRepository.Updates(ctx.Tx, channel.ID, map[string]any{
|
|
"ai_agent_rollout_percent": channel.PreviousAIAgentRolloutPercent,
|
|
"previous_ai_agent_rollout_percent": channel.AIAgentRolloutPercent,
|
|
"update_user_id": operator.UserID,
|
|
"update_user_name": operator.Username,
|
|
"updated_at": time.Now(),
|
|
})
|
|
})
|
|
}
|
|
|
|
func (s *channelService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error {
|
|
if operator == nil {
|
|
return errorsx.UnauthorizedI18n("error.auth.expired")
|
|
}
|
|
item := s.Get(id)
|
|
if item == nil || item.Status == enums.StatusDeleted {
|
|
return errorsx.InvalidParamI18n("error.e0208")
|
|
}
|
|
if status != int(enums.StatusOk) && status != int(enums.StatusDisabled) {
|
|
return errorsx.InvalidParamI18n("error.e0254")
|
|
}
|
|
return s.Updates(id, map[string]any{
|
|
"status": status,
|
|
"update_user_id": operator.UserID,
|
|
"update_user_name": operator.Username,
|
|
"updated_at": time.Now(),
|
|
})
|
|
}
|
|
|
|
func (s *channelService) DeleteChannel(id int64, operator *dto.AuthPrincipal) error {
|
|
if operator == nil {
|
|
return errorsx.UnauthorizedI18n("error.auth.expired")
|
|
}
|
|
item := s.Get(id)
|
|
if item == nil || item.Status == enums.StatusDeleted {
|
|
return errorsx.InvalidParamI18n("error.e0208")
|
|
}
|
|
return s.Updates(id, map[string]any{
|
|
"status": enums.StatusDeleted,
|
|
"update_user_id": operator.UserID,
|
|
"update_user_name": operator.Username,
|
|
"updated_at": time.Now(),
|
|
})
|
|
}
|
|
|
|
func (s *channelService) ParseWxWorkKFChannelConfig(raw string) (*dto.WxWorkKFChannelConfig, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return &dto.WxWorkKFChannelConfig{}, nil
|
|
}
|
|
cfg := &dto.WxWorkKFChannelConfig{}
|
|
if err := json.Unmarshal([]byte(raw), cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.OpenKfID = strings.TrimSpace(cfg.OpenKfID)
|
|
return cfg, nil
|
|
}
|
|
|
|
func (s *channelService) ListWxWorkKFAccounts() ([]response.WxWorkKFAccountResponse, error) {
|
|
if !wxwork.Enabled() || wxwork.GetWorkCli() == nil {
|
|
return nil, errorsx.BusinessErrorI18n(1, "error.wxwork.configIncomplete")
|
|
}
|
|
cli, err := wxwork.GetWorkCli().GetKF()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
const limit = 100
|
|
accounts := make([]response.WxWorkKFAccountResponse, 0)
|
|
for offset := 0; ; offset += limit {
|
|
result, err := cli.AccountPaging(&kf.AccountPagingRequest{
|
|
Offset: offset,
|
|
Limit: limit,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range result.AccountList {
|
|
openKfID := strings.TrimSpace(item.OpenKFID)
|
|
if openKfID == "" {
|
|
continue
|
|
}
|
|
accounts = append(accounts, response.WxWorkKFAccountResponse{
|
|
OpenKfID: openKfID,
|
|
Name: strings.TrimSpace(item.Name),
|
|
Avatar: strings.TrimSpace(item.Avatar),
|
|
ManagePrivilege: item.ManagePrivilege,
|
|
})
|
|
}
|
|
if len(result.AccountList) < limit {
|
|
break
|
|
}
|
|
}
|
|
return accounts, nil
|
|
}
|
|
|
|
func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfig, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
cfg := &dto.WebChannelConfig{
|
|
Title: "Support",
|
|
Subtitle: "How can we help?",
|
|
ThemeColor: "#2563eb",
|
|
Position: "right",
|
|
Width: "380px",
|
|
}
|
|
if raw != "" {
|
|
if err := json.Unmarshal([]byte(raw), cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
cfg.Title = strings.TrimSpace(cfg.Title)
|
|
if cfg.Title == "" {
|
|
cfg.Title = "Support"
|
|
}
|
|
cfg.Subtitle = strings.TrimSpace(cfg.Subtitle)
|
|
cfg.ThemeColor = strings.TrimSpace(cfg.ThemeColor)
|
|
if cfg.ThemeColor == "" {
|
|
cfg.ThemeColor = "#2563eb"
|
|
}
|
|
cfg.Position = strings.TrimSpace(cfg.Position)
|
|
if cfg.Position == "" {
|
|
cfg.Position = "right"
|
|
}
|
|
if cfg.Position != "left" && cfg.Position != "right" {
|
|
return nil, errorsx.InvalidParamI18n("error.e0059")
|
|
}
|
|
cfg.Width = strings.TrimSpace(cfg.Width)
|
|
if cfg.Width == "" {
|
|
cfg.Width = "380px"
|
|
}
|
|
cfg.UserTokenSecret = strings.TrimSpace(cfg.UserTokenSecret)
|
|
return cfg, nil
|
|
}
|
|
|
|
func (s *channelService) ParseWechatMPChannelConfig(raw string) (*dto.WechatMPChannelConfig, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
cfg := &dto.WechatMPChannelConfig{
|
|
Title: "Official Account Support",
|
|
Subtitle: "How can we help?",
|
|
ThemeColor: "#2563eb",
|
|
}
|
|
if raw != "" {
|
|
if err := json.Unmarshal([]byte(raw), cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
cfg.Title = strings.TrimSpace(cfg.Title)
|
|
if cfg.Title == "" {
|
|
cfg.Title = "Official Account Support"
|
|
}
|
|
cfg.Subtitle = strings.TrimSpace(cfg.Subtitle)
|
|
cfg.ThemeColor = strings.TrimSpace(cfg.ThemeColor)
|
|
if cfg.ThemeColor == "" {
|
|
cfg.ThemeColor = "#2563eb"
|
|
}
|
|
cfg.UserTokenSecret = strings.TrimSpace(cfg.UserTokenSecret)
|
|
return cfg, nil
|
|
}
|
|
|
|
func (s *channelService) GetUserTokenSecret(channel *models.Channel) string {
|
|
if channel == nil {
|
|
return ""
|
|
}
|
|
switch channel.ChannelType {
|
|
case enums.ChannelTypeWeb:
|
|
cfg, err := s.ParseWebChannelConfig(channel.ConfigJSON)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(cfg.UserTokenSecret)
|
|
case enums.ChannelTypeWechatMP:
|
|
cfg, err := s.ParseWechatMPChannelConfig(channel.ConfigJSON)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(cfg.UserTokenSecret)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (s *channelService) ResetUserTokenSecret(channelID int64, operator *dto.AuthPrincipal) (string, error) {
|
|
if operator == nil {
|
|
return "", errorsx.UnauthorizedI18n("error.auth.expired")
|
|
}
|
|
channel := s.Get(channelID)
|
|
if channel == nil || channel.Status == enums.StatusDeleted {
|
|
return "", errorsx.InvalidParamI18n("error.e0208")
|
|
}
|
|
if channel.ChannelType != enums.ChannelTypeWeb && channel.ChannelType != enums.ChannelTypeWechatMP {
|
|
return "", errorsx.InvalidParamI18n("error.e0196")
|
|
}
|
|
secret, err := generateUserTokenSecret()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var configJSON string
|
|
switch channel.ChannelType {
|
|
case enums.ChannelTypeWeb:
|
|
cfg, err := s.ParseWebChannelConfig(channel.ConfigJSON)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cfg.UserTokenSecret = secret
|
|
raw, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
configJSON = string(raw)
|
|
case enums.ChannelTypeWechatMP:
|
|
cfg, err := s.ParseWechatMPChannelConfig(channel.ConfigJSON)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cfg.UserTokenSecret = secret
|
|
raw, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
configJSON = string(raw)
|
|
}
|
|
if err := repositories.ChannelRepository.Updates(sqls.DB(), channelID, map[string]any{
|
|
"config_json": configJSON,
|
|
"update_user_id": operator.UserID,
|
|
"update_user_name": operator.Username,
|
|
"updated_at": time.Now(),
|
|
}); err != nil {
|
|
return "", err
|
|
}
|
|
return secret, nil
|
|
}
|
|
|
|
func generateUserTokenSecret() (string, error) {
|
|
buf := make([]byte, 32)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(buf), nil
|
|
}
|
|
|
|
func (s *channelService) GetEnabledWxWorkKFChannelByOpenKfID(openKfID string) *models.Channel {
|
|
openKfID = strings.TrimSpace(openKfID)
|
|
if openKfID == "" {
|
|
return nil
|
|
}
|
|
channels := s.Find(sqls.NewCnd().
|
|
Eq("channel_type", enums.ChannelTypeWxWorkKF).
|
|
Eq("status", enums.StatusOk).
|
|
Asc("id"))
|
|
for i := range channels {
|
|
cfg, err := s.ParseWxWorkKFChannelConfig(channels[i].ConfigJSON)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if cfg != nil && cfg.OpenKfID == openKfID {
|
|
return &channels[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *channelService) GetEnabledChannel(ctx *gin.Context) *models.Channel {
|
|
channelID := httpx.GetChannelID(ctx)
|
|
channel := repositories.ChannelRepository.GetByChannelID(sqls.DB(), channelID)
|
|
if channel == nil {
|
|
return nil
|
|
}
|
|
if channel.Status != enums.StatusOk {
|
|
return nil
|
|
}
|
|
return channel
|
|
}
|
|
|
|
func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRequest) (*models.Channel, error) {
|
|
channelType := strings.TrimSpace(req.ChannelType)
|
|
if channelType != enums.ChannelTypeWeb && channelType != enums.ChannelTypeWechatMP && channelType != enums.ChannelTypeWxWorkKF {
|
|
return nil, errorsx.InvalidParamI18n("error.e0250")
|
|
}
|
|
name := strings.TrimSpace(req.Name)
|
|
if name == "" {
|
|
return nil, errorsx.InvalidParamI18n("error.e0247")
|
|
}
|
|
if req.AIAgentRolloutPercent == 0 {
|
|
req.AIAgentRolloutPercent = 100
|
|
}
|
|
if req.AIAgentRolloutPercent < 1 || req.AIAgentRolloutPercent > 100 {
|
|
return nil, errorsx.InvalidParam("channel ai agent rollout percent must be between 1 and 100")
|
|
}
|
|
if req.AIAgentID > 0 {
|
|
aiAgent := AIAgentService.Get(req.AIAgentID)
|
|
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
|
|
return nil, errorsx.InvalidParamI18n("error.e0004")
|
|
}
|
|
if aiAgent.PublishedRevisionID <= 0 {
|
|
return nil, errorsx.InvalidParam("ai agent must be published before binding channel")
|
|
}
|
|
}
|
|
status := enums.Status(req.Status)
|
|
if req.Status == 0 {
|
|
status = enums.StatusOk
|
|
}
|
|
if status != enums.StatusOk && status != enums.StatusDisabled {
|
|
return nil, errorsx.InvalidParamI18n("error.e0249")
|
|
}
|
|
|
|
channelID := ""
|
|
if id > 0 {
|
|
current := s.Get(id)
|
|
if current == nil || current.Status == enums.StatusDeleted {
|
|
return nil, errorsx.InvalidParamI18n("error.e0208")
|
|
}
|
|
channelID = strings.TrimSpace(current.ChannelID)
|
|
}
|
|
configJSON := strings.TrimSpace(req.ConfigJSON)
|
|
switch channelType {
|
|
case enums.ChannelTypeWeb:
|
|
if channelID == "" {
|
|
channelID = strs.UUID()
|
|
}
|
|
if exists := s.Take("channel_id = ? AND status <> ? AND id <> ?", channelID, enums.StatusDeleted, id); exists != nil {
|
|
return nil, errorsx.InvalidParamI18n("error.e0248")
|
|
}
|
|
cfg, err := s.ParseWebChannelConfig(configJSON)
|
|
if err != nil {
|
|
return nil, errorsx.InvalidParamI18n("error.e0060")
|
|
}
|
|
if strings.TrimSpace(cfg.UserTokenSecret) == "" {
|
|
secret, err := generateUserTokenSecret()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.UserTokenSecret = secret
|
|
}
|
|
configBytes, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configJSON = string(configBytes)
|
|
case enums.ChannelTypeWechatMP:
|
|
if channelID == "" {
|
|
channelID = strs.UUID()
|
|
}
|
|
if exists := s.Take("channel_id = ? AND status <> ? AND id <> ?", channelID, enums.StatusDeleted, id); exists != nil {
|
|
return nil, errorsx.InvalidParamI18n("error.e0248")
|
|
}
|
|
cfg, err := s.ParseWechatMPChannelConfig(configJSON)
|
|
if err != nil {
|
|
return nil, errorsx.InvalidParamI18n("error.e0201")
|
|
}
|
|
if strings.TrimSpace(cfg.UserTokenSecret) == "" {
|
|
secret, err := generateUserTokenSecret()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.UserTokenSecret = secret
|
|
}
|
|
configBytes, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configJSON = string(configBytes)
|
|
case enums.ChannelTypeWxWorkKF:
|
|
if channelID == "" {
|
|
channelID = strs.UUID()
|
|
}
|
|
if exists := s.Take("channel_id = ? AND status <> ? AND id <> ?", channelID, enums.StatusDeleted, id); exists != nil {
|
|
return nil, errorsx.InvalidParamI18n("error.e0248")
|
|
}
|
|
cfg, err := s.ParseWxWorkKFChannelConfig(configJSON)
|
|
if err != nil {
|
|
return nil, errorsx.InvalidParamI18n("error.e0102")
|
|
}
|
|
if cfg == nil || cfg.OpenKfID == "" {
|
|
return nil, errorsx.InvalidParamI18n("error.e0103")
|
|
}
|
|
if channel := s.GetEnabledWxWorkKFChannelByOpenKfID(cfg.OpenKfID); channel != nil && channel.ID != id {
|
|
return nil, errorsx.InvalidParamI18n("error.e0069")
|
|
}
|
|
}
|
|
|
|
return &models.Channel{
|
|
ChannelType: channelType,
|
|
ChannelID: channelID,
|
|
AIAgentID: req.AIAgentID,
|
|
AIAgentRolloutPercent: req.AIAgentRolloutPercent,
|
|
Name: name,
|
|
ConfigJSON: configJSON,
|
|
Status: status,
|
|
Remark: strings.TrimSpace(req.Remark),
|
|
}, nil
|
|
}
|