feat: add WeCom outbox management features including retry and ignore actions, permissions, and UI integration

This commit is contained in:
mlogclub
2026-08-02 14:03:23 +08:00
parent c69b30a659
commit 1d6c0e37db
16 changed files with 457 additions and 1 deletions
+3
View File
@@ -182,6 +182,9 @@ func registerDashboardChannelRoutes(group *gin.RouterGroup) {
group.POST("/update", dashboard.ChannelPostUpdate)
group.POST("/update_status", dashboard.ChannelPostUpdate_status)
group.Any("/wxwork/kf/accounts", dashboard.ChannelAnyWxworkKfAccounts)
group.Any("/wxwork/outbox/failed/list", dashboard.ChannelAnyWxworkOutboxFailedList)
group.POST("/wxwork/outbox/retry", dashboard.ChannelPostWxworkOutboxRetry)
group.POST("/wxwork/outbox/ignore", dashboard.ChannelPostWxworkOutboxIgnore)
}
func registerDashboardAgentRoutes(group *gin.RouterGroup) {
@@ -6,8 +6,10 @@ import (
"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/services"
"strings"
"agent-desk/internal/pkg/httpx/params"
@@ -63,6 +65,74 @@ func ChannelAnyWxworkKfAccounts(ctx *gin.Context) {
httpx.WriteJSON(ctx, list)
}
func ChannelAnyWxworkOutboxFailedList(ctx *gin.Context) {
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionWxWorkOutboxView); err != nil {
httpx.WriteJSON(ctx, err)
return
}
cnd := params.NewPagedSqlCnd(ctx,
params.QueryFilter{ParamName: "conversationId"},
params.QueryFilter{ParamName: "messageId"},
).Eq("channel_type", enums.ChannelTypeWxWorkKF)
status := strings.TrimSpace(params.FormValue(ctx, "sendStatus"))
switch status {
case "":
cnd.Eq("send_status", string(enums.ChannelMessageOutboxStatusFailed))
case "all":
cnd.In("send_status", []string{
string(enums.ChannelMessageOutboxStatusFailed),
string(enums.ChannelMessageOutboxStatusIgnored),
})
case string(enums.ChannelMessageOutboxStatusFailed), string(enums.ChannelMessageOutboxStatusIgnored):
cnd.Eq("send_status", status)
default:
httpx.WriteJSON(ctx, errorsx.InvalidParam("invalid sendStatus"))
return
}
list, paging := services.ChannelMessageOutboxService.FindPageByCnd(cnd.Desc("id"))
results := make([]response.ChannelMessageOutboxResponse, 0, len(list))
for _, item := range list {
results = append(results, response.BuildChannelMessageOutboxResponse(&item))
}
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
}
func ChannelPostWxworkOutboxRetry(ctx *gin.Context) {
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionWxWorkOutboxUpdate)
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
req := request.ChannelMessageOutboxActionRequest{}
if err := params.ReadJSON(ctx, &req); err != nil {
httpx.WriteJSON(ctx, err)
return
}
if err := services.ChannelMessageOutboxService.RetryWxWorkFailure(req.ID, operator); err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, nil)
}
func ChannelPostWxworkOutboxIgnore(ctx *gin.Context) {
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionWxWorkOutboxUpdate)
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
req := request.ChannelMessageOutboxActionRequest{}
if err := params.ReadJSON(ctx, &req); err != nil {
httpx.WriteJSON(ctx, err)
return
}
if err := services.ChannelMessageOutboxService.IgnoreWxWorkFailure(req.ID, operator); err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, nil)
}
func ChannelPostCreate(ctx *gin.Context) {
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelCreate)
if err != nil {
@@ -0,0 +1,21 @@
package migration
import "github.com/mlogclub/simple/sqls"
func init() {
register(9, "sync wxwork outbox permissions", func() error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
permissions, err := ensurePermissions(ctx.Tx)
if err != nil {
return err
}
roles, err := ensureRoles(ctx.Tx)
if err != nil {
return err
}
return ensureRolePermissions(ctx.Tx, roles, permissions)
})
})
}
+9 -1
View File
@@ -103,6 +103,10 @@ var (
PermissionChannelUpdate = Permission{Name: "更新接入渠道", Code: "channel.update", Type: "api", GroupName: "channel", Method: "POST", APIPath: "/api/dashboard/channel/update", SortNo: 627}
PermissionChannelDelete = Permission{Name: "删除接入渠道", Code: "channel.delete", Type: "api", GroupName: "channel", Method: "POST", APIPath: "/api/dashboard/channel/delete", SortNo: 628}
// 企微 Outbox 相关权限
PermissionWxWorkOutboxView = Permission{Name: "查看企微 Outbox", Code: "wxworkOutbox.view", Type: "api", GroupName: "wxworkOutbox", Method: "ANY", APIPath: "/api/dashboard/channel/wxwork/outbox/failed/list", SortNo: 629}
PermissionWxWorkOutboxUpdate = Permission{Name: "处置企微 Outbox", Code: "wxworkOutbox.update", Type: "api", GroupName: "wxworkOutbox", Method: "POST", APIPath: "/api/dashboard/channel/wxwork/outbox/retry", SortNo: 630}
// 客户相关权限
PermissionCustomerView = Permission{Name: "查看客户", Code: "customer.view", Type: "api", GroupName: "customer", Method: "POST", APIPath: "/api/dashboard/customer/list", SortNo: 630}
PermissionCustomerCreate = Permission{Name: "创建客户", Code: "customer.create", Type: "api", GroupName: "customer", Method: "POST", APIPath: "/api/dashboard/customer/create", SortNo: 640}
@@ -223,6 +227,8 @@ var Permissions = []Permission{
PermissionChannelCreate,
PermissionChannelUpdate,
PermissionChannelDelete,
PermissionWxWorkOutboxView,
PermissionWxWorkOutboxUpdate,
PermissionCustomerView,
PermissionCustomerCreate,
PermissionCustomerUpdate,
@@ -355,6 +361,7 @@ var builtinPermissionResourceLabels = map[string]string{
"tag": "tags",
"company": "companies",
"channel": "channels",
"wxworkOutbox": "WeCom outbox records",
"customer": "customers",
"agent": "agents",
"agentTeam": "agent teams",
@@ -379,6 +386,7 @@ var builtinPermissionNameOverrides = map[string]string{
"ticket.progress": "Update ticket progress",
"agent.config": "Configure agent service rules",
"agentTeamSchedule.batchGenerate": "Batch generate agent team schedules",
"wxworkOutbox.update": "Handle WeCom outbox records",
"mcp.view": "View MCP debug information",
"mcp.call": "Call MCP tools",
}
@@ -409,7 +417,7 @@ var RolePermissions = map[string][]Permission{
PermissionQuickReplyView, PermissionQuickReplyCreate, PermissionQuickReplyUpdate, PermissionQuickReplyDelete,
PermissionTagView, PermissionTagCreate, PermissionTagUpdate, PermissionTagDelete,
PermissionCompanyView, PermissionCompanyCreate, PermissionCompanyUpdate, PermissionCompanyDelete,
PermissionChannelView, PermissionChannelCreate, PermissionChannelUpdate, PermissionChannelDelete,
PermissionChannelView, PermissionChannelCreate, PermissionChannelUpdate, PermissionChannelDelete, PermissionWxWorkOutboxView, PermissionWxWorkOutboxUpdate,
PermissionCustomerView, PermissionCustomerCreate, PermissionCustomerUpdate, PermissionCustomerDelete,
PermissionAgentView, PermissionAgentCreate, PermissionAgentUpdate, PermissionAgentDelete, PermissionAgentUpdateStatus, PermissionAgentConfig,
PermissionAgentTeamView, PermissionAgentTeamCreate, PermissionAgentTeamUpdate, PermissionAgentTeamDelete,
+1
View File
@@ -36,6 +36,7 @@ func TestBuiltinAuthSeedNamesDefaultToEnglish(t *testing.T) {
"ticket.create": "Create tickets",
"conversation.send": "Send conversation messages",
"channel.view": "View channels",
"wxworkOutbox.view": "View WeCom outbox records",
"agent.view": "View agents",
}
for code, want := range permissionTests {
@@ -31,3 +31,7 @@ type DeleteChannelRequest struct {
type ResetChannelUserTokenSecretRequest struct {
ID int64 `json:"id"`
}
type ChannelMessageOutboxActionRequest struct {
ID int64 `json:"id"`
}
@@ -3,6 +3,7 @@ package response
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/utils"
)
type ChannelResponse struct {
@@ -26,6 +27,23 @@ type WxWorkKFAccountResponse struct {
ManagePrivilege bool `json:"managePrivilege"`
}
type ChannelMessageOutboxResponse struct {
ID int64 `json:"id"`
ChannelType string `json:"channelType"`
ConversationID int64 `json:"conversationId"`
MessageID int64 `json:"messageId"`
Payload string `json:"payload"`
SendStatus string `json:"sendStatus"`
RetryCount int `json:"retryCount"`
NextRetryAt string `json:"nextRetryAt"`
LastError string `json:"lastError"`
SentAt string `json:"sentAt"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
}
func BuildChannelResponse(item *models.Channel) ChannelResponse {
if item == nil {
return ChannelResponse{}
@@ -43,3 +61,25 @@ func BuildChannelResponse(item *models.Channel) ChannelResponse {
Remark: item.Remark,
}
}
func BuildChannelMessageOutboxResponse(item *models.ChannelMessageOutbox) ChannelMessageOutboxResponse {
if item == nil {
return ChannelMessageOutboxResponse{}
}
return ChannelMessageOutboxResponse{
ID: item.ID,
ChannelType: item.ChannelType,
ConversationID: item.ConversationID,
MessageID: item.MessageID,
Payload: item.Payload,
SendStatus: item.SendStatus,
RetryCount: item.RetryCount,
NextRetryAt: utils.FormatTimePtr(item.NextRetryAt),
LastError: item.LastError,
SentAt: utils.FormatTimePtr(item.SentAt),
CreatedAt: utils.FormatTime(item.CreatedAt),
UpdatedAt: utils.FormatTime(item.UpdatedAt),
CreateUserName: item.CreateUserName,
UpdateUserName: item.UpdateUserName,
}
}
+1
View File
@@ -14,6 +14,7 @@ const (
ChannelMessageOutboxStatusSending ChannelMessageOutboxStatus = "sending"
ChannelMessageOutboxStatusSent ChannelMessageOutboxStatus = "sent"
ChannelMessageOutboxStatusFailed ChannelMessageOutboxStatus = "failed"
ChannelMessageOutboxStatusIgnored ChannelMessageOutboxStatus = "ignored"
)
const (
@@ -2,7 +2,9 @@ package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/repositories"
"encoding/json"
"strings"
@@ -137,3 +139,46 @@ func (s *channelMessageOutboxService) ListPending(channelType string, limit int)
Limit(limit)
return s.Find(cnd)
}
func (s *channelMessageOutboxService) RetryWxWorkFailure(id int64, operator *dto.AuthPrincipal) error {
item := s.Get(id)
if item == nil || item.ChannelType != enums.ChannelTypeWxWorkKF {
return errorsx.InvalidParam("outbox record does not exist")
}
if item.SendStatus != string(enums.ChannelMessageOutboxStatusFailed) &&
item.SendStatus != string(enums.ChannelMessageOutboxStatusIgnored) {
return errorsx.InvalidParam("only failed or ignored outbox records can be retried")
}
now := time.Now()
columns := map[string]interface{}{
"send_status": string(enums.ChannelMessageOutboxStatusPending),
"next_retry_at": nil,
"updated_at": now,
}
if operator != nil {
columns["update_user_id"] = operator.UserID
columns["update_user_name"] = operator.Username
}
return s.Updates(id, columns)
}
func (s *channelMessageOutboxService) IgnoreWxWorkFailure(id int64, operator *dto.AuthPrincipal) error {
item := s.Get(id)
if item == nil || item.ChannelType != enums.ChannelTypeWxWorkKF {
return errorsx.InvalidParam("outbox record does not exist")
}
if item.SendStatus != string(enums.ChannelMessageOutboxStatusFailed) {
return errorsx.InvalidParam("only failed outbox records can be ignored")
}
now := time.Now()
columns := map[string]interface{}{
"send_status": string(enums.ChannelMessageOutboxStatusIgnored),
"next_retry_at": nil,
"updated_at": now,
}
if operator != nil {
columns["update_user_id"] = operator.UserID
columns["update_user_name"] = operator.Username
}
return s.Updates(id, columns)
}