feat: add WeCom outbox management features including retry and ignore actions, permissions, and UI integration
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { BanIcon, RotateCcwIcon } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import {
|
||||
DashboardListPage,
|
||||
type DashboardListRenderContext,
|
||||
} from "@/components/dashboard/list"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
fetchWxWorkOutboxFailures,
|
||||
ignoreWxWorkOutbox,
|
||||
retryWxWorkOutbox,
|
||||
type ChannelMessageOutbox,
|
||||
} from "@/lib/api/admin"
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: "failed", label: "失败" },
|
||||
{ value: "ignored", label: "已忽略" },
|
||||
{ value: "all", label: "全部" },
|
||||
] as const
|
||||
|
||||
function statusLabel(status: string) {
|
||||
if (status === "failed") return "失败"
|
||||
if (status === "ignored") return "已忽略"
|
||||
return status || "-"
|
||||
}
|
||||
|
||||
function statusVariant(status: string) {
|
||||
if (status === "failed") return "destructive" as const
|
||||
if (status === "ignored") return "outline" as const
|
||||
return "secondary" as const
|
||||
}
|
||||
|
||||
function formatOptionalTime(value: string) {
|
||||
return value ? formatDateTime(value) : "-"
|
||||
}
|
||||
|
||||
function OutboxActions({
|
||||
item,
|
||||
reload,
|
||||
}: {
|
||||
item: ChannelMessageOutbox
|
||||
reload: DashboardListRenderContext<ChannelMessageOutbox>["reload"]
|
||||
}) {
|
||||
const [runningAction, setRunningAction] = useState<"retry" | "ignore" | null>(null)
|
||||
|
||||
async function runAction(action: "retry" | "ignore") {
|
||||
setRunningAction(action)
|
||||
try {
|
||||
if (action === "retry") {
|
||||
await retryWxWorkOutbox(item.id)
|
||||
toast.success("已重新加入发送队列")
|
||||
} else {
|
||||
await ignoreWxWorkOutbox(item.id)
|
||||
toast.success("已忽略该失败记录")
|
||||
}
|
||||
await reload()
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "操作失败")
|
||||
} finally {
|
||||
setRunningAction(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={runningAction !== null}
|
||||
onClick={() => void runAction("retry")}
|
||||
>
|
||||
<RotateCcwIcon />
|
||||
重试
|
||||
</Button>
|
||||
{item.sendStatus === "failed" ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={runningAction !== null}
|
||||
onClick={() => void runAction("ignore")}
|
||||
>
|
||||
<BanIcon />
|
||||
忽略
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DashboardWxWorkOutboxPage() {
|
||||
return (
|
||||
<DashboardListPage<ChannelMessageOutbox>
|
||||
filters={[
|
||||
{
|
||||
name: "sendStatus",
|
||||
label: "状态",
|
||||
defaultValue: "failed",
|
||||
type: "segment",
|
||||
options: STATUS_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "conversationId",
|
||||
label: "会话 ID",
|
||||
placeholder: "会话 ID",
|
||||
defaultValue: "",
|
||||
valueType: "number",
|
||||
className: "w-full sm:w-40",
|
||||
},
|
||||
{
|
||||
name: "messageId",
|
||||
label: "消息 ID",
|
||||
placeholder: "消息 ID",
|
||||
defaultValue: "",
|
||||
valueType: "number",
|
||||
className: "w-full sm:w-40",
|
||||
},
|
||||
]}
|
||||
fetchList={fetchWxWorkOutboxFailures}
|
||||
getItemId={(item) => item.id}
|
||||
columns={[
|
||||
{
|
||||
key: "id",
|
||||
label: "Outbox",
|
||||
className: "w-28 text-xs text-muted-foreground",
|
||||
render: (item) => `#${item.id}`,
|
||||
},
|
||||
{
|
||||
key: "message",
|
||||
label: "消息",
|
||||
className: "w-48",
|
||||
render: (item) => (
|
||||
<div className="space-y-1 text-xs">
|
||||
<div>会话 #{item.conversationId || "-"}</div>
|
||||
<div className="text-muted-foreground">消息 #{item.messageId || "-"}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
className: "w-28",
|
||||
render: (item) => (
|
||||
<Badge variant={statusVariant(item.sendStatus)}>
|
||||
{statusLabel(item.sendStatus)}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "retry",
|
||||
label: "重试",
|
||||
className: "w-44 text-xs",
|
||||
render: (item) => (
|
||||
<div className="space-y-1">
|
||||
<div>{item.retryCount} 次</div>
|
||||
<div className="text-muted-foreground">
|
||||
下次 {formatOptionalTime(item.nextRetryAt)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "error",
|
||||
label: "失败原因",
|
||||
className: "min-w-72 max-w-[32rem]",
|
||||
render: (item) =>
|
||||
item.lastError ? (
|
||||
<span className="block truncate text-xs text-destructive" title={item.lastError}>
|
||||
{item.lastError}
|
||||
</span>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
label: "更新时间",
|
||||
className: "w-44 text-xs text-muted-foreground",
|
||||
render: (item) => formatOptionalTime(item.updatedAt),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: <span className="block text-right">操作</span>,
|
||||
className: "w-44",
|
||||
render: (item, context) => (
|
||||
<OutboxActions item={item} reload={context.reload} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
labels={{
|
||||
refresh: "刷新",
|
||||
query: "查询",
|
||||
loading: "正在加载企业微信 outbox...",
|
||||
empty: "暂无失败 outbox",
|
||||
loadFailed: "加载企业微信 outbox 失败",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -193,6 +193,23 @@ export type WxWorkKFAccount = {
|
||||
managePrivilege: boolean
|
||||
}
|
||||
|
||||
export type ChannelMessageOutbox = {
|
||||
id: number
|
||||
channelType: string
|
||||
conversationId: number
|
||||
messageId: number
|
||||
payload: string
|
||||
sendStatus: string
|
||||
retryCount: number
|
||||
nextRetryAt: string
|
||||
lastError: string
|
||||
sentAt: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
createUserName: string
|
||||
updateUserName: string
|
||||
}
|
||||
|
||||
export type CreateAdminChannelPayload = {
|
||||
channelType: string
|
||||
aiAgentId: number
|
||||
@@ -907,6 +924,28 @@ export function fetchWxWorkKFAccounts() {
|
||||
return request<WxWorkKFAccount[]>("/api/dashboard/channel/wxwork/kf/accounts")
|
||||
}
|
||||
|
||||
export function fetchWxWorkOutboxFailures(
|
||||
query?: Record<string, string | number | undefined>
|
||||
) {
|
||||
return request<PageResult<ChannelMessageOutbox>>(
|
||||
`/api/dashboard/channel/wxwork/outbox/failed/list${toQueryString(query)}`
|
||||
)
|
||||
}
|
||||
|
||||
export function retryWxWorkOutbox(id: number) {
|
||||
return request<void>("/api/dashboard/channel/wxwork/outbox/retry", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
}
|
||||
|
||||
export function ignoreWxWorkOutbox(id: number) {
|
||||
return request<void>("/api/dashboard/channel/wxwork/outbox/ignore", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
}
|
||||
|
||||
export function createChannel(payload: CreateAdminChannelPayload) {
|
||||
return request<AdminChannel>("/api/dashboard/channel/create", {
|
||||
method: "POST",
|
||||
|
||||
@@ -38,6 +38,13 @@ describe("isDashboardNavItemActive", () => {
|
||||
isDashboardNavItemActive("/dashboard/tickets-extra", "/dashboard/tickets"),
|
||||
false
|
||||
)
|
||||
assert.equal(
|
||||
isDashboardNavItemActive(
|
||||
"/dashboard/wxwork-outbox",
|
||||
"/dashboard/channels"
|
||||
),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it("only marks the dashboard home item active on the exact dashboard path", async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
GlobeIcon,
|
||||
KeyRoundIcon,
|
||||
LayoutDashboardIcon,
|
||||
MessageSquareWarningIcon,
|
||||
MessageSquareCodeIcon,
|
||||
MessageSquareMoreIcon,
|
||||
ShieldCheckIcon,
|
||||
@@ -169,6 +170,12 @@ export const dashboardNavSections: DashboardNavSectionConfig[] = [
|
||||
icon: <GlobeIcon />,
|
||||
requiredPermission: "channel.view",
|
||||
},
|
||||
{
|
||||
titleKey: "nav.wxworkOutbox",
|
||||
url: "/dashboard/wxwork-outbox",
|
||||
icon: <MessageSquareWarningIcon />,
|
||||
requiredPermission: "wxworkOutbox.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,6 +36,7 @@ const PERMISSION_RESOURCE_LABELS: Record<string, { singular: string; plural: str
|
||||
tag: { singular: "tag", plural: "tags" },
|
||||
company: { singular: "company", plural: "companies" },
|
||||
channel: { singular: "channel", plural: "channels" },
|
||||
wxworkOutbox: { singular: "WeCom outbox record", plural: "WeCom outbox records" },
|
||||
customer: { singular: "customer", plural: "customers" },
|
||||
agent: { singular: "agent", plural: "agents" },
|
||||
agentTeam: { singular: "agent team", plural: "agent teams" },
|
||||
@@ -60,6 +61,7 @@ const PERMISSION_NAME_OVERRIDES: Record<string, string> = {
|
||||
"channel.resetUserTokenSecret": "Reset channel user token secret",
|
||||
"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",
|
||||
}
|
||||
|
||||
@@ -2341,6 +2341,7 @@
|
||||
"agents": "Human Agents",
|
||||
"agentTeamSchedules": "Team Schedules",
|
||||
"channels": "Channels",
|
||||
"wxworkOutbox": "WeCom Outbox",
|
||||
"aiCapabilities": "AI Capabilities",
|
||||
"knowledge": "Knowledge Base",
|
||||
"aiConfigs": "Model Settings",
|
||||
|
||||
@@ -2341,6 +2341,7 @@
|
||||
"agents": "人工客服",
|
||||
"agentTeamSchedules": "客服组排班",
|
||||
"channels": "接入渠道",
|
||||
"wxworkOutbox": "企微 Outbox",
|
||||
"aiCapabilities": "AI能力",
|
||||
"knowledge": "知识库",
|
||||
"aiConfigs": "AI模型",
|
||||
|
||||
Reference in New Issue
Block a user