add wxwork kf account management functionality
This commit is contained in:
@@ -45,6 +45,17 @@ func (c *ChannelController) GetBy(id int64) *web.JsonResult {
|
|||||||
return web.JsonData(buildChannelResponse(item))
|
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 {
|
func (c *ChannelController) PostCreate() *web.JsonResult {
|
||||||
operator, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionChannelCreate)
|
operator, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionChannelCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ type ChannelResponse struct {
|
|||||||
Remark string `json:"remark"`
|
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 {
|
func BuildChannelResponse(item *models.Channel) ChannelResponse {
|
||||||
if item == nil {
|
if item == nil {
|
||||||
return ChannelResponse{}
|
return ChannelResponse{}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/pkg/dto"
|
"cs-agent/internal/pkg/dto"
|
||||||
"cs-agent/internal/pkg/dto/request"
|
"cs-agent/internal/pkg/dto/request"
|
||||||
|
"cs-agent/internal/pkg/dto/response"
|
||||||
"cs-agent/internal/pkg/enums"
|
"cs-agent/internal/pkg/enums"
|
||||||
"cs-agent/internal/pkg/errorsx"
|
"cs-agent/internal/pkg/errorsx"
|
||||||
"cs-agent/internal/pkg/irisx"
|
"cs-agent/internal/pkg/irisx"
|
||||||
"cs-agent/internal/pkg/utils"
|
"cs-agent/internal/pkg/utils"
|
||||||
"cs-agent/internal/repositories"
|
"cs-agent/internal/repositories"
|
||||||
|
"cs-agent/internal/wxwork"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +19,7 @@ import (
|
|||||||
"github.com/mlogclub/simple/common/strs"
|
"github.com/mlogclub/simple/common/strs"
|
||||||
"github.com/mlogclub/simple/sqls"
|
"github.com/mlogclub/simple/sqls"
|
||||||
"github.com/mlogclub/simple/web/params"
|
"github.com/mlogclub/simple/web/params"
|
||||||
|
"github.com/silenceper/wechat/v2/work/kf"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChannelService = newChannelService()
|
var ChannelService = newChannelService()
|
||||||
@@ -161,6 +164,44 @@ func (s *channelService) ParseWxWorkKFChannelConfig(raw string) (*dto.WxWorkKFCh
|
|||||||
return cfg, nil
|
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) {
|
func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfig, error) {
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
cfg := &dto.WebChannelConfig{
|
cfg := &dto.WebChannelConfig{
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import {
|
|||||||
type AIAgent,
|
type AIAgent,
|
||||||
type AdminChannel,
|
type AdminChannel,
|
||||||
type CreateAdminChannelPayload,
|
type CreateAdminChannelPayload,
|
||||||
|
type WxWorkKFAccount,
|
||||||
fetchAIAgentsAll,
|
fetchAIAgentsAll,
|
||||||
fetchChannel,
|
fetchChannel,
|
||||||
|
fetchWxWorkKFAccounts,
|
||||||
} from "@/lib/api/admin"
|
} from "@/lib/api/admin"
|
||||||
|
|
||||||
type ChannelFormDialogProps = {
|
type ChannelFormDialogProps = {
|
||||||
@@ -58,18 +60,28 @@ const defaultWebChannelConfig: Required<WebChannelConfig> = {
|
|||||||
width: "380px",
|
width: "380px",
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z
|
||||||
channelType: z.enum(["web", "wxwork_kf"], "请选择渠道类型"),
|
.object({
|
||||||
aiAgentId: z.string().trim().regex(/^\d+$/, "请选择 AI Agent"),
|
channelType: z.enum(["web", "wxwork_kf"], "请选择渠道类型"),
|
||||||
name: z.string().trim().min(1, "渠道名称不能为空"),
|
aiAgentId: z.string().trim().regex(/^\d+$/, "请选择 AI Agent"),
|
||||||
openKfId: z.string().trim(),
|
name: z.string().trim().min(1, "渠道名称不能为空"),
|
||||||
widgetTitle: z.string().trim(),
|
openKfId: z.string().trim(),
|
||||||
widgetSubtitle: z.string().trim(),
|
widgetTitle: z.string().trim(),
|
||||||
widgetThemeColor: z.string().trim(),
|
widgetSubtitle: z.string().trim(),
|
||||||
widgetPosition: z.enum(["left", "right"]),
|
widgetThemeColor: z.string().trim(),
|
||||||
widgetWidth: z.string().trim(),
|
widgetPosition: z.enum(["left", "right"]),
|
||||||
remark: z.string().trim(),
|
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<typeof schema>
|
type EditForm = z.infer<typeof schema>
|
||||||
|
|
||||||
@@ -199,6 +211,9 @@ function ChannelFormBody({
|
|||||||
const formId = "channel-edit-form"
|
const formId = "channel-edit-form"
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [aiAgents, setAIAgents] = useState<AIAgent[]>([])
|
const [aiAgents, setAIAgents] = useState<AIAgent[]>([])
|
||||||
|
const [wxWorkKFAccounts, setWxWorkKFAccounts] = useState<WxWorkKFAccount[]>([])
|
||||||
|
const [wxWorkKFAccountsLoading, setWxWorkKFAccountsLoading] = useState(false)
|
||||||
|
const [wxWorkKFAccountsError, setWxWorkKFAccountsError] = useState("")
|
||||||
const [currentStatus, setCurrentStatus] = useState(0)
|
const [currentStatus, setCurrentStatus] = useState(0)
|
||||||
const form = useForm<
|
const form = useForm<
|
||||||
z.input<typeof schema>,
|
z.input<typeof schema>,
|
||||||
@@ -216,6 +231,7 @@ function ChannelFormBody({
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = form
|
} = form
|
||||||
const channelType = useWatch({ control, name: "channelType" })
|
const channelType = useWatch({ control, name: "channelType" })
|
||||||
|
const openKfId = useWatch({ control, name: "openKfId" })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadAIAgents() {
|
async function loadAIAgents() {
|
||||||
@@ -250,10 +266,54 @@ function ChannelFormBody({
|
|||||||
void loadDetail()
|
void loadDetail()
|
||||||
}, [itemId, reset])
|
}, [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) => ({
|
const aiAgentOptions = aiAgents.map((item) => ({
|
||||||
value: String(item.id),
|
value: String(item.id),
|
||||||
label: item.name,
|
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) {
|
async function onFormSubmit(values: EditForm) {
|
||||||
await onSubmit(buildPayload(values, currentStatus))
|
await onSubmit(buildPayload(values, currentStatus))
|
||||||
@@ -336,9 +396,27 @@ function ChannelFormBody({
|
|||||||
|
|
||||||
{channelType === "wxwork_kf" ? (
|
{channelType === "wxwork_kf" ? (
|
||||||
<Field data-invalid={!!errors.openKfId}>
|
<Field data-invalid={!!errors.openKfId}>
|
||||||
<FieldLabel htmlFor="channel-open-kf-id">OpenKfID</FieldLabel>
|
<FieldLabel>企业微信客服账号</FieldLabel>
|
||||||
<FieldContent>
|
<FieldContent>
|
||||||
<Input id="channel-open-kf-id" {...register("openKfId")} />
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="openKfId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<OptionCombobox
|
||||||
|
value={field.value}
|
||||||
|
options={wxWorkKFAccountOptions}
|
||||||
|
placeholder={
|
||||||
|
wxWorkKFAccountsLoading ? "正在加载客服账号" : "请选择客服账号"
|
||||||
|
}
|
||||||
|
searchPlaceholder="搜索客服账号"
|
||||||
|
emptyText={
|
||||||
|
wxWorkKFAccountsError || "未找到企业微信客服账号"
|
||||||
|
}
|
||||||
|
disabled={wxWorkKFAccountsLoading}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FieldError errors={[errors.openKfId]} />
|
<FieldError errors={[errors.openKfId]} />
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -185,6 +185,13 @@ export type AdminChannel = {
|
|||||||
remark: string
|
remark: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WxWorkKFAccount = {
|
||||||
|
openKfId: string
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
managePrivilege: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type CreateAdminChannelPayload = {
|
export type CreateAdminChannelPayload = {
|
||||||
channelType: string
|
channelType: string
|
||||||
aiAgentId: number
|
aiAgentId: number
|
||||||
@@ -557,6 +564,10 @@ export function fetchChannel(id: number) {
|
|||||||
return request<AdminChannel>(`/api/dashboard/channel/${id}`)
|
return request<AdminChannel>(`/api/dashboard/channel/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchWxWorkKFAccounts() {
|
||||||
|
return request<WxWorkKFAccount[]>("/api/dashboard/channel/wxwork/kf/accounts")
|
||||||
|
}
|
||||||
|
|
||||||
export function createChannel(payload: CreateAdminChannelPayload) {
|
export function createChannel(payload: CreateAdminChannelPayload) {
|
||||||
return request<AdminChannel>("/api/dashboard/channel/create", {
|
return request<AdminChannel>("/api/dashboard/channel/create", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user