From 12f66bcf2766f8d8b131feb117eaa969d1dd494a Mon Sep 17 00:00:00 2001 From: mlogclub Date: Mon, 27 Apr 2026 11:43:34 +0800 Subject: [PATCH] add wxwork kf account management functionality --- .../dashboard/channel_controller.go | 11 ++ internal/pkg/dto/response/channel_response.go | 7 ++ internal/services/channel_service.go | 41 +++++++ .../dashboard/channels/_components/edit.tsx | 106 +++++++++++++++--- web/lib/api/admin.ts | 11 ++ 5 files changed, 162 insertions(+), 14 deletions(-) diff --git a/internal/controllers/dashboard/channel_controller.go b/internal/controllers/dashboard/channel_controller.go index 4fe3b32..d0f31f8 100644 --- a/internal/controllers/dashboard/channel_controller.go +++ b/internal/controllers/dashboard/channel_controller.go @@ -45,6 +45,17 @@ func (c *ChannelController) GetBy(id int64) *web.JsonResult { return web.JsonData(buildChannelResponse(item)) } +func (c *ChannelController) AnyWxworkKfAccounts() *web.JsonResult { + if _, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionChannelView); err != nil { + return web.JsonError(err) + } + list, err := services.ChannelService.ListWxWorkKFAccounts() + if err != nil { + return web.JsonError(err) + } + return web.JsonData(list) +} + func (c *ChannelController) PostCreate() *web.JsonResult { operator, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionChannelCreate) if err != nil { diff --git a/internal/pkg/dto/response/channel_response.go b/internal/pkg/dto/response/channel_response.go index 5011257..03e8ae3 100644 --- a/internal/pkg/dto/response/channel_response.go +++ b/internal/pkg/dto/response/channel_response.go @@ -17,6 +17,13 @@ type ChannelResponse struct { Remark string `json:"remark"` } +type WxWorkKFAccountResponse struct { + OpenKfID string `json:"openKfId"` + Name string `json:"name"` + Avatar string `json:"avatar"` + ManagePrivilege bool `json:"managePrivilege"` +} + func BuildChannelResponse(item *models.Channel) ChannelResponse { if item == nil { return ChannelResponse{} diff --git a/internal/services/channel_service.go b/internal/services/channel_service.go index 655bdf5..b2494bf 100644 --- a/internal/services/channel_service.go +++ b/internal/services/channel_service.go @@ -4,11 +4,13 @@ import ( "cs-agent/internal/models" "cs-agent/internal/pkg/dto" "cs-agent/internal/pkg/dto/request" + "cs-agent/internal/pkg/dto/response" "cs-agent/internal/pkg/enums" "cs-agent/internal/pkg/errorsx" "cs-agent/internal/pkg/irisx" "cs-agent/internal/pkg/utils" "cs-agent/internal/repositories" + "cs-agent/internal/wxwork" "encoding/json" "strings" "time" @@ -17,6 +19,7 @@ import ( "github.com/mlogclub/simple/common/strs" "github.com/mlogclub/simple/sqls" "github.com/mlogclub/simple/web/params" + "github.com/silenceper/wechat/v2/work/kf" ) var ChannelService = newChannelService() @@ -161,6 +164,44 @@ func (s *channelService) ParseWxWorkKFChannelConfig(raw string) (*dto.WxWorkKFCh return cfg, nil } +func (s *channelService) ListWxWorkKFAccounts() ([]response.WxWorkKFAccountResponse, error) { + if !wxwork.Enabled() || wxwork.GetWorkCli() == nil { + return nil, errorsx.BusinessError(1, "企业微信未启用或配置不完整") + } + 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{ diff --git a/web/app/dashboard/channels/_components/edit.tsx b/web/app/dashboard/channels/_components/edit.tsx index e8eab2b..719cdc1 100644 --- a/web/app/dashboard/channels/_components/edit.tsx +++ b/web/app/dashboard/channels/_components/edit.tsx @@ -20,8 +20,10 @@ import { type AIAgent, type AdminChannel, type CreateAdminChannelPayload, + type WxWorkKFAccount, fetchAIAgentsAll, fetchChannel, + fetchWxWorkKFAccounts, } from "@/lib/api/admin" type ChannelFormDialogProps = { @@ -58,18 +60,28 @@ const defaultWebChannelConfig: Required = { width: "380px", } -const schema = z.object({ - channelType: z.enum(["web", "wxwork_kf"], "请选择渠道类型"), - aiAgentId: z.string().trim().regex(/^\d+$/, "请选择 AI Agent"), - name: z.string().trim().min(1, "渠道名称不能为空"), - openKfId: z.string().trim(), - widgetTitle: z.string().trim(), - widgetSubtitle: z.string().trim(), - widgetThemeColor: z.string().trim(), - widgetPosition: z.enum(["left", "right"]), - widgetWidth: z.string().trim(), - remark: z.string().trim(), -}) +const schema = z + .object({ + channelType: z.enum(["web", "wxwork_kf"], "请选择渠道类型"), + aiAgentId: z.string().trim().regex(/^\d+$/, "请选择 AI Agent"), + name: z.string().trim().min(1, "渠道名称不能为空"), + openKfId: z.string().trim(), + widgetTitle: z.string().trim(), + widgetSubtitle: z.string().trim(), + widgetThemeColor: z.string().trim(), + widgetPosition: z.enum(["left", "right"]), + widgetWidth: z.string().trim(), + remark: z.string().trim(), + }) + .superRefine((values, ctx) => { + if (values.channelType === "wxwork_kf" && !values.openKfId.trim()) { + ctx.addIssue({ + code: "custom", + path: ["openKfId"], + message: "请选择企业微信客服账号", + }) + } + }) type EditForm = z.infer @@ -199,6 +211,9 @@ function ChannelFormBody({ const formId = "channel-edit-form" const [loading, setLoading] = useState(false) const [aiAgents, setAIAgents] = useState([]) + const [wxWorkKFAccounts, setWxWorkKFAccounts] = useState([]) + const [wxWorkKFAccountsLoading, setWxWorkKFAccountsLoading] = useState(false) + const [wxWorkKFAccountsError, setWxWorkKFAccountsError] = useState("") const [currentStatus, setCurrentStatus] = useState(0) const form = useForm< z.input, @@ -216,6 +231,7 @@ function ChannelFormBody({ formState: { errors }, } = form const channelType = useWatch({ control, name: "channelType" }) + const openKfId = useWatch({ control, name: "openKfId" }) useEffect(() => { async function loadAIAgents() { @@ -250,10 +266,54 @@ function ChannelFormBody({ void loadDetail() }, [itemId, reset]) + useEffect(() => { + if ( + channelType !== "wxwork_kf" || + wxWorkKFAccounts.length > 0 || + wxWorkKFAccountsLoading || + wxWorkKFAccountsError + ) { + return + } + async function loadWxWorkKFAccounts() { + setWxWorkKFAccountsLoading(true) + setWxWorkKFAccountsError("") + try { + const data = await fetchWxWorkKFAccounts() + setWxWorkKFAccounts(data) + } catch (error) { + console.error("Failed to load WeCom KF accounts:", error) + setWxWorkKFAccountsError("企业微信客服账号加载失败") + } finally { + setWxWorkKFAccountsLoading(false) + } + } + void loadWxWorkKFAccounts() + }, [ + channelType, + wxWorkKFAccounts.length, + wxWorkKFAccountsError, + wxWorkKFAccountsLoading, + ]) + const aiAgentOptions = aiAgents.map((item) => ({ value: String(item.id), label: item.name, })) + const wxWorkKFAccountOptions = wxWorkKFAccounts.map((item) => ({ + value: item.openKfId, + label: item.name ? `${item.name} (${item.openKfId})` : item.openKfId, + })) + if ( + channelType === "wxwork_kf" && + openKfId && + !wxWorkKFAccountOptions.some((item) => item.value === openKfId) + ) { + wxWorkKFAccountOptions.unshift({ + value: openKfId, + label: openKfId, + }) + } async function onFormSubmit(values: EditForm) { await onSubmit(buildPayload(values, currentStatus)) @@ -336,9 +396,27 @@ function ChannelFormBody({ {channelType === "wxwork_kf" ? ( - OpenKfID + 企业微信客服账号 - + ( + + )} + /> diff --git a/web/lib/api/admin.ts b/web/lib/api/admin.ts index ad0b0a9..fc1dbe7 100644 --- a/web/lib/api/admin.ts +++ b/web/lib/api/admin.ts @@ -185,6 +185,13 @@ export type AdminChannel = { remark: string } +export type WxWorkKFAccount = { + openKfId: string + name: string + avatar: string + managePrivilege: boolean +} + export type CreateAdminChannelPayload = { channelType: string aiAgentId: number @@ -557,6 +564,10 @@ export function fetchChannel(id: number) { return request(`/api/dashboard/channel/${id}`) } +export function fetchWxWorkKFAccounts() { + return request("/api/dashboard/channel/wxwork/kf/accounts") +} + export function createChannel(payload: CreateAdminChannelPayload) { return request("/api/dashboard/channel/create", { method: "POST",