Merge branch 'yongpxu-dev' into yongpxu-dev2

This commit is contained in:
超级老白兔
2025-11-12 14:02:01 +08:00
9 changed files with 703 additions and 107 deletions

View File

@@ -23,12 +23,16 @@ import {
SendOutlined,
} from "@ant-design/icons";
import styles from "./PushTaskModal.module.scss";
import {
useCustomerStore,
} from "@/store/module/weChat/customer";
import { useCustomerStore } from "@/store/module/weChat/customer";
import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
export type PushType = "friend-message" | "group-message" | "group-announcement";
const DEFAULT_FRIEND_INTERVAL: [number, number] = [3, 10];
const DEFAULT_MESSAGE_INTERVAL: [number, number] = [1, 3];
export type PushType =
| "friend-message"
| "group-message"
| "group-announcement";
interface PushTaskModalProps {
visible: boolean;
@@ -67,8 +71,12 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
const [selectedAccounts, setSelectedAccounts] = useState<any[]>([]);
const [selectedContacts, setSelectedContacts] = useState<ContactItem[]>([]);
const [messageContent, setMessageContent] = useState("");
const [friendInterval, setFriendInterval] = useState(10);
const [messageInterval, setMessageInterval] = useState(1);
const [friendInterval, setFriendInterval] = useState<[number, number]>([
...DEFAULT_FRIEND_INTERVAL,
]);
const [messageInterval, setMessageInterval] = useState<[number, number]>([
...DEFAULT_MESSAGE_INTERVAL,
]);
const [selectedTag, setSelectedTag] = useState<string>("");
const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false);
const [aiPrompt, setAiPrompt] = useState("");
@@ -120,8 +128,8 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
setSelectedAccounts([]);
setSelectedContacts([]);
setMessageContent("");
setFriendInterval(10);
setMessageInterval(1);
setFriendInterval([...DEFAULT_FRIEND_INTERVAL]);
setMessageInterval([...DEFAULT_MESSAGE_INTERVAL]);
setSelectedTag("");
setAiRewriteEnabled(false);
setAiPrompt("");
@@ -270,7 +278,9 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
setCurrentStep(2);
} else if (currentStep === 2) {
if (selectedContacts.length === 0) {
message.warning(`请至少选择一个${pushType === "friend-message" ? "好友" : "群"}`);
message.warning(
`请至少选择一个${pushType === "friend-message" ? "好友" : "群"}`,
);
return;
}
setCurrentStep(3);
@@ -343,7 +353,9 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
<div className={styles.accountCards}>
{filteredAccounts.length > 0 ? (
filteredAccounts.map(account => {
const isSelected = selectedAccounts.some(a => a.id === account.id);
const isSelected = selectedAccounts.some(
a => a.id === account.id,
);
return (
<div
key={account.id}
@@ -355,7 +367,8 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
size={48}
style={{ backgroundColor: "#1890ff" }}
>
{!account.avatar && (account.nickname || account.name || "").charAt(0)}
{!account.avatar &&
(account.nickname || account.name || "").charAt(0)}
</Avatar>
<div className={styles.cardName}>
{account.nickname || account.name || "未知"}
@@ -570,10 +583,7 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
<Button type="text" icon="⭐" />
</div>
<div className={styles.aiRewriteSection}>
<Switch
checked={aiRewriteEnabled}
onChange={setAiRewriteEnabled}
/>
<Switch checked={aiRewriteEnabled} onChange={setAiRewriteEnabled} />
<span style={{ marginLeft: 8 }}>AI智能话术改写</span>
{aiRewriteEnabled && (
<Input
@@ -598,13 +608,16 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
<div className={styles.settingControl}>
<span>()</span>
<Slider
min={10}
max={20}
range
min={1}
max={60}
value={friendInterval}
onChange={setFriendInterval}
onChange={value => setFriendInterval(value as [number, number])}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>{friendInterval} - 20</span>
<span>
{friendInterval[0]} - {friendInterval[1]}
</span>
</div>
</div>
<div className={styles.settingItem}>
@@ -612,13 +625,18 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
<div className={styles.settingControl}>
<span>()</span>
<Slider
range
min={1}
max={12}
max={60}
value={messageInterval}
onChange={setMessageInterval}
onChange={value =>
setMessageInterval(value as [number, number])
}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>{messageInterval} - 12</span>
<span>
{messageInterval[0]} - {messageInterval[1]}
</span>
</div>
</div>
<div className={styles.settingItem}>

View File

@@ -0,0 +1,6 @@
import request from "@/api/request";
// 获取客服列表
export function queryWorkbenchCreate(params) {
return request("/v1/workbench/create", params, "POST");
}

View File

@@ -24,6 +24,8 @@ import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
import styles from "../../index.module.scss";
import { ContactItem, PushType } from "../../types";
import PoolSelection from "@/components/PoolSelection";
import type { PoolSelectionItem } from "@/components/PoolSelection/data";
interface ContactFilterValues {
includeTags: string[];
@@ -61,6 +63,8 @@ interface StepSelectContactsProps {
selectedAccounts: any[];
selectedContacts: ContactItem[];
onChange: (contacts: ContactItem[]) => void;
selectedTrafficPools: PoolSelectionItem[];
onTrafficPoolsChange: (pools: PoolSelectionItem[]) => void;
}
const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
@@ -68,6 +72,8 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
selectedAccounts,
selectedContacts,
onChange,
selectedTrafficPools,
onTrafficPoolsChange,
}) => {
const [contactsData, setContactsData] = useState<ContactItem[]>([]);
const [loadingContacts, setLoadingContacts] = useState(false);
@@ -415,6 +421,13 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
allowClear
/>
</div>
<PoolSelection
selectedOptions={selectedTrafficPools}
onSelect={onTrafficPoolsChange}
placeholder="选择流量池包"
showSelectedList
selectedListMaxHeight={200}
/>
<div className={styles.contentBody}>
<div className={styles.contactList}>
<div className={styles.listHeader}>

View File

@@ -0,0 +1,31 @@
import request from "@/api/request";
// 创建内容库参数
export interface CreateContentLibraryParams {
name: string;
sourceType: number;
sourceFriends?: string[];
sourceGroups?: string[];
keywordInclude?: string[];
keywordExclude?: string[];
aiPrompt?: string;
timeEnabled?: number;
timeStart?: string;
timeEnd?: string;
}
// 创建内容库
export function createContentLibrary(
params: CreateContentLibraryParams,
): Promise<any> {
return request("/v1/content/library/create", params, "POST");
}
// 删除内容库
export function deleteContentLibrary(params: { id: number }) {
return request(`/v1/content/library/update`, params, "DELETE");
}
// 智能话术改写
export function aiEditContent(params: { aiPrompt: string; content: string }) {
return request(`/v1/content/library/aiEditContent`, params, "GET");
}

View File

@@ -254,6 +254,7 @@
.aiRewriteSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
gap: 12px;
@@ -272,6 +273,16 @@
.aiRewriteInput {
max-width: 240px;
}
.aiRewriteActions {
display: flex;
align-items: center;
gap: 8px;
}
.aiRewriteButton {
min-width: 96px;
}
}
}

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import {
Button,
Checkbox,
@@ -10,7 +10,12 @@ import {
Switch,
message as antdMessage,
} from "antd";
import { CopyOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
import {
CopyOutlined,
DeleteOutlined,
PlusOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import type { CheckboxChangeEvent } from "antd/es/checkbox";
import styles from "./index.module.scss";
@@ -18,6 +23,12 @@ import { ContactItem, ScriptGroup } from "../../types";
import InputMessage from "./InputMessage/InputMessage";
import ContentLibrarySelector from "./ContentLibrarySelector";
import type { ContentItem } from "@/components/ContentSelection/data";
import {
createContentLibrary,
deleteContentLibrary,
aiEditContent,
type CreateContentLibraryParams,
} from "./api";
interface StepSendMessageProps {
selectedAccounts: any[];
@@ -25,10 +36,10 @@ interface StepSendMessageProps {
targetLabel: string;
messageContent: string;
onMessageContentChange: (value: string) => void;
friendInterval: number;
onFriendIntervalChange: (value: number) => void;
messageInterval: number;
onMessageIntervalChange: (value: number) => void;
friendInterval: [number, number];
onFriendIntervalChange: (value: [number, number]) => void;
messageInterval: [number, number];
onMessageIntervalChange: (value: [number, number]) => void;
selectedTag: string;
onSelectedTagChange: (value: string) => void;
aiRewriteEnabled: boolean;
@@ -74,6 +85,10 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
selectedContentLibraries,
onSelectedContentLibrariesChange,
}) => {
const [savingScriptGroup, setSavingScriptGroup] = useState(false);
const [aiRewriting, setAiRewriting] = useState(false);
const [deletingGroupIds, setDeletingGroupIds] = useState<string[]>([]);
const handleAddMessage = useCallback(
(content?: string, showSuccess?: boolean) => {
const finalContent = (content ?? messageContent).trim();
@@ -103,24 +118,57 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
[currentScriptMessages, onCurrentScriptMessagesChange],
);
const handleSaveScriptGroup = useCallback(() => {
const handleSaveScriptGroup = useCallback(async () => {
if (savingScriptGroup) {
return;
}
if (currentScriptMessages.length === 0) {
antdMessage.warning("请先添加消息内容");
return;
}
const groupName =
currentScriptName.trim() || `话术组${savedScriptGroups.length + 1}`;
const newGroup: ScriptGroup = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
const messages = [...currentScriptMessages];
const params: CreateContentLibraryParams = {
name: groupName,
messages: currentScriptMessages,
sourceType: 1,
keywordInclude: messages,
};
onSavedScriptGroupsChange([...savedScriptGroups, newGroup]);
onCurrentScriptMessagesChange([]);
onCurrentScriptNameChange("");
onMessageContentChange("");
antdMessage.success("已保存为话术组");
const trimmedPrompt = aiPrompt.trim();
if (aiRewriteEnabled && trimmedPrompt) {
params.aiPrompt = trimmedPrompt;
}
let hideLoading: ReturnType<typeof antdMessage.loading> | undefined;
try {
setSavingScriptGroup(true);
hideLoading = antdMessage.loading("正在保存话术组...", 0);
const response = await createContentLibrary(params);
hideLoading?.();
const responseId =
response?.id ?? response?.data?.id ?? response?.libraryId;
const newGroup: ScriptGroup = {
id:
responseId !== undefined
? String(responseId)
: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: groupName,
messages,
};
onSavedScriptGroupsChange([...savedScriptGroups, newGroup]);
onCurrentScriptMessagesChange([]);
onCurrentScriptNameChange("");
onMessageContentChange("");
antdMessage.success("已保存为话术组");
} catch (error) {
hideLoading?.();
console.error("保存话术组失败:", error);
antdMessage.error("保存失败,请稍后重试");
} finally {
setSavingScriptGroup(false);
}
}, [
aiPrompt,
aiRewriteEnabled,
currentScriptMessages,
currentScriptName,
onCurrentScriptMessagesChange,
@@ -128,6 +176,100 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
onMessageContentChange,
onSavedScriptGroupsChange,
savedScriptGroups,
savingScriptGroup,
]);
const handleAiRewrite = useCallback(async () => {
if (!aiRewriteEnabled) {
antdMessage.warning("请先开启AI智能话术改写");
return;
}
const trimmedPrompt = aiPrompt.trim();
const originalContent = messageContent;
const trimmedContent = originalContent.trim();
if (!trimmedPrompt) {
antdMessage.warning("请输入改写提示词");
return;
}
if (!trimmedContent) {
antdMessage.warning("请输入需要改写的内容");
return;
}
if (aiRewriting) {
return;
}
let hideLoading: ReturnType<typeof antdMessage.loading> | undefined;
try {
setAiRewriting(true);
hideLoading = antdMessage.loading("AI正在改写话术...", 0);
const response = await aiEditContent({
aiPrompt: trimmedPrompt,
content: originalContent,
});
hideLoading?.();
const normalizedResponse = response as {
content?: string;
contentAfter?: string;
contentFront?: string;
data?:
| string
| {
content?: string;
contentAfter?: string;
contentFront?: string;
};
result?: string;
};
const dataField = normalizedResponse?.data;
const dataContent =
typeof dataField === "string"
? dataField
: (dataField?.content ?? undefined);
const dataContentAfter =
typeof dataField === "string" ? undefined : dataField?.contentAfter;
const dataContentFront =
typeof dataField === "string" ? undefined : dataField?.contentFront;
const primaryAfter =
normalizedResponse?.contentAfter ?? dataContentAfter ?? undefined;
const primaryFront =
normalizedResponse?.contentFront ?? dataContentFront ?? undefined;
let rewrittenContent = "";
if (typeof response === "string") {
rewrittenContent = response;
} else if (primaryAfter) {
rewrittenContent = primaryFront
? `${primaryFront}\n${primaryAfter}`
: primaryAfter;
} else if (typeof normalizedResponse?.content === "string") {
rewrittenContent = normalizedResponse.content;
} else if (typeof dataContent === "string") {
rewrittenContent = dataContent;
} else if (typeof normalizedResponse?.result === "string") {
rewrittenContent = normalizedResponse.result;
} else if (primaryFront) {
rewrittenContent = primaryFront;
}
if (!rewrittenContent || typeof rewrittenContent !== "string") {
antdMessage.error("AI改写失败请稍后重试");
return;
}
onMessageContentChange(rewrittenContent.trim());
antdMessage.success("AI改写完成请确认内容");
} catch (error) {
hideLoading?.();
console.error("AI改写失败:", error);
antdMessage.error("AI改写失败请稍后重试");
} finally {
setAiRewriting(false);
}
}, [
aiPrompt,
aiRewriting,
aiRewriteEnabled,
messageContent,
onMessageContentChange,
]);
const handleApplyGroup = useCallback(
@@ -145,23 +287,47 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
);
const handleDeleteGroup = useCallback(
(groupId: string) => {
const nextGroups = savedScriptGroups.filter(
group => group.id !== groupId,
);
onSavedScriptGroupsChange(nextGroups);
if (selectedScriptGroupIds.includes(groupId)) {
const nextSelected = selectedScriptGroupIds.filter(
id => id !== groupId,
);
onSelectedScriptGroupIdsChange(nextSelected);
async (groupId: string) => {
if (deletingGroupIds.includes(groupId)) {
return;
}
const numericGroupId = Number(groupId);
if (Number.isNaN(numericGroupId)) {
antdMessage.error("无法删除缺少有效的内容库ID");
return;
}
let hideLoading: ReturnType<typeof antdMessage.loading> | undefined;
try {
setDeletingGroupIds(prev => [...prev, groupId]);
hideLoading = antdMessage.loading("正在删除话术组...", 0);
await deleteContentLibrary({ id: numericGroupId });
hideLoading?.();
const nextGroups = savedScriptGroups.filter(
group => group.id !== groupId,
);
onSavedScriptGroupsChange(nextGroups);
if (selectedScriptGroupIds.includes(groupId)) {
const nextSelected = selectedScriptGroupIds.filter(
id => id !== groupId,
);
onSelectedScriptGroupIdsChange(nextSelected);
}
antdMessage.success("已删除话术组");
} catch (error) {
hideLoading?.();
console.error("删除话术组失败:", error);
antdMessage.error("删除失败,请稍后重试");
} finally {
setDeletingGroupIds(prev =>
prev.filter(deletingId => deletingId !== groupId),
);
}
antdMessage.success("已删除话术组");
},
[
deletingGroupIds,
onSavedScriptGroupsChange,
savedScriptGroups,
onSelectedScriptGroupIdsChange,
savedScriptGroups,
selectedScriptGroupIds,
],
);
@@ -192,7 +358,8 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
type="primary"
icon={<PlusOutlined />}
onClick={handleSaveScriptGroup}
disabled={currentScriptMessages.length === 0}
disabled={currentScriptMessages.length === 0 || savingScriptGroup}
loading={savingScriptGroup}
>
</Button>
@@ -279,6 +446,8 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
icon={<DeleteOutlined />}
className={styles.actionButton}
onClick={() => handleDeleteGroup(group.id)}
loading={deletingGroupIds.includes(group.id)}
disabled={deletingGroupIds.includes(group.id)}
/>
</div>
</div>
@@ -307,23 +476,37 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
checked={aiRewriteEnabled}
onChange={onAiRewriteToggle}
/>
<span className={styles.aiRewriteLabel}>AI智能话术改写</span>
<div className={styles.aiRewriteLabel}>AI智能话术改写</div>
<div>
{aiRewriteEnabled && (
<Input
placeholder="输入改写提示词"
value={aiPrompt}
onChange={event => onAiPromptChange(event.target.value)}
className={styles.aiRewriteInput}
/>
)}
</div>
</div>
<div className={styles.aiRewriteActions}>
<Button
icon={<ReloadOutlined />}
onClick={handleAiRewrite}
disabled={!aiRewriteEnabled}
loading={aiRewriting}
className={styles.aiRewriteButton}
>
AI改写
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleAddMessage(undefined, true)}
>
</Button>
</div>
{aiRewriteEnabled && (
<Input
placeholder="输入改写提示词"
value={aiPrompt}
onChange={event => onAiPromptChange(event.target.value)}
className={styles.aiRewriteInput}
/>
)}
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleAddMessage(undefined, true)}
>
</Button>
</div>
</div>
</div>
@@ -336,13 +519,18 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
<div className={styles.settingControl}>
<span>()</span>
<Slider
min={10}
max={20}
range
min={1}
max={60}
value={friendInterval}
onChange={value => onFriendIntervalChange(value as number)}
onChange={value =>
onFriendIntervalChange(value as [number, number])
}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>{friendInterval} - 20</span>
<span>
{friendInterval[0]} - {friendInterval[1]}
</span>
</div>
</div>
<div className={styles.settingItem}>
@@ -350,13 +538,18 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
<div className={styles.settingControl}>
<span>()</span>
<Slider
range
min={1}
max={12}
max={60}
value={messageInterval}
onChange={value => onMessageIntervalChange(value as number)}
onChange={value =>
onMessageIntervalChange(value as [number, number])
}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>{messageInterval} - 12</span>
<span>
{messageInterval[0]} - {messageInterval[1]}
</span>
</div>
</div>
</div>

View File

@@ -17,6 +17,47 @@ import StepSendMessage from "./components/StepSendMessage";
import { ContactItem, PushType, ScriptGroup } from "./types";
import StepIndicator from "@/components/StepIndicator";
import type { ContentItem } from "@/components/ContentSelection/data";
import type { PoolSelectionItem } from "@/components/PoolSelection/data";
import { queryWorkbenchCreate } from "./api";
const DEFAULT_FRIEND_INTERVAL: [number, number] = [3, 10];
const DEFAULT_MESSAGE_INTERVAL: [number, number] = [1, 3];
const DEFAULT_TIME_RANGE: Record<
PushType,
{ startTime: string; endTime: string }
> = {
"friend-message": { startTime: "10:00", endTime: "22:00" },
"group-message": { startTime: "09:00", endTime: "20:00" },
"group-announcement": { startTime: "08:30", endTime: "18:30" },
};
const DEFAULT_PUSH_ORDER: Record<PushType, 1 | 2> = {
"friend-message": 1,
"group-message": 1,
"group-announcement": 2,
};
const DEFAULT_MAX_PER_DAY: Record<PushType, number> = {
"friend-message": 150,
"group-message": 200,
"group-announcement": 80,
};
const DEFAULT_AUTO_START: Record<PushType, 0 | 1> = {
"friend-message": 1,
"group-message": 1,
"group-announcement": 0,
};
const DEFAULT_PUSH_TYPE: Record<PushType, 0 | 1> = {
"friend-message": 0,
"group-message": 0,
"group-announcement": 1,
};
const isValidNumber = (value: unknown): value is number =>
typeof value === "number" && Number.isFinite(value);
const CreatePushTask: React.FC = () => {
const navigate = useNavigate();
@@ -44,11 +85,19 @@ const CreatePushTask: React.FC = () => {
const [selectedContentLibraries, setSelectedContentLibraries] = useState<
ContentItem[]
>([]);
const [friendInterval, setFriendInterval] = useState(10);
const [messageInterval, setMessageInterval] = useState(1);
const [friendInterval, setFriendInterval] = useState<[number, number]>([
...DEFAULT_FRIEND_INTERVAL,
]);
const [messageInterval, setMessageInterval] = useState<[number, number]>([
...DEFAULT_MESSAGE_INTERVAL,
]);
const [selectedTag, setSelectedTag] = useState<string>("");
const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false);
const [aiPrompt, setAiPrompt] = useState("");
const [creatingTask, setCreatingTask] = useState(false);
const [selectedTrafficPools, setSelectedTrafficPools] = useState<
PoolSelectionItem[]
>([]);
const customerList = useCustomerStore(state => state.customerList);
@@ -131,33 +180,206 @@ const CreatePushTask: React.FC = () => {
setSelectedAccounts([]);
};
const handleSend = () => {
const handleSend = async () => {
if (creatingTask) {
return;
}
const selectedGroups = savedScriptGroups.filter(group =>
selectedScriptGroupIds.includes(group.id),
);
if (currentScriptMessages.length === 0 && selectedGroups.length === 0) {
message.warning("请先添加话术内容或选择话术组");
if (
currentScriptMessages.length === 0 &&
selectedGroups.length === 0 &&
selectedContentLibraries.length === 0
) {
message.warning("请添加话术内容、选择话术组或内容库");
return;
}
// TODO: 实现发送逻辑
console.log("发送推送", {
pushType: validPushType,
accounts: selectedAccounts,
contacts: selectedContacts,
currentScript: {
name: currentScriptName,
messages: currentScriptMessages,
},
selectedScriptGroups: selectedGroups,
friendInterval,
messageInterval,
selectedTag,
aiRewriteEnabled,
aiPrompt,
selectedContentLibraries,
});
message.success("推送任务已创建");
navigate("/pc/powerCenter/message-push-assistant");
const manualMessages = currentScriptMessages
.map(item => item.trim())
.filter(Boolean);
if (validPushType === "group-announcement" && manualMessages.length === 0) {
message.warning("请先填写公告内容");
return;
}
const toNumberId = (value: unknown) => {
const numeric = Number(value);
return Number.isFinite(numeric) && !Number.isNaN(numeric)
? numeric
: null;
};
const contentGroupIds = Array.from(
new Set(
[
...selectedContentLibraries
.map(item => toNumberId(item?.id))
.filter((id): id is number => id !== null),
...selectedScriptGroupIds
.map(id => toNumberId(id))
.filter((id): id is number => id !== null),
].filter((id): id is number => id !== null),
),
);
if (
manualMessages.length === 0 &&
selectedGroups.length === 0 &&
contentGroupIds.length === 0
) {
message.warning("缺少有效的话术内容,请重新检查");
return;
}
const ownerWechatIds = Array.from(
new Set(
selectedAccounts
.map(account => toNumberId(account?.id))
.filter((id): id is number => id !== null),
),
);
if (ownerWechatIds.length === 0) {
message.error("缺少有效的推送账号信息");
return;
}
const selectedContactIds = Array.from(
new Set(
selectedContacts.map(contact => contact?.id).filter(isValidNumber),
),
);
if (selectedContactIds.length === 0) {
message.error("缺少有效的推送对象");
return;
}
const friendIntervalMin = friendInterval[0];
const friendIntervalMax = friendInterval[1];
const messageIntervalMin = messageInterval[0];
const messageIntervalMax = messageInterval[1];
const trafficPoolIds = selectedTrafficPools
.map(pool => pool.id)
.filter(
id => id !== undefined && id !== null && String(id).trim() !== "",
);
const { startTime, endTime } = DEFAULT_TIME_RANGE[validPushType];
const maxPerDay =
selectedContacts.length > 0
? selectedContacts.length
: DEFAULT_MAX_PER_DAY[validPushType];
const pushOrder = DEFAULT_PUSH_ORDER[validPushType];
const normalizedPostPushTags =
selectedTag.trim().length > 0
? [
toNumberId(selectedTag) !== null
? (toNumberId(selectedTag) as number)
: selectedTag,
]
: [];
const taskName =
currentScriptName.trim() ||
selectedGroups[0]?.name ||
(manualMessages[0] ? manualMessages[0].slice(0, 20) : "") ||
`推送任务-${Date.now()}`;
const deviceGroupIds = Array.from(
new Set(
selectedAccounts
.map(account => toNumberId(account?.currentDeviceId))
.filter((id): id is number => id !== null),
),
);
if (validPushType === "friend-message" && deviceGroupIds.length === 0) {
message.error("缺少有效的推送设备分组");
return;
}
const basePayload: Record<string, any> = {
name: taskName,
type: 3,
autoStart: DEFAULT_AUTO_START[validPushType],
status: 1,
pushType: DEFAULT_PUSH_TYPE[validPushType],
targetType: validPushType === "friend-message" ? 2 : 1,
groupPushSubType: validPushType === "group-announcement" ? 2 : 1,
startTime,
endTime,
maxPerDay,
pushOrder,
friendIntervalMin,
friendIntervalMax,
messageIntervalMin,
messageIntervalMax,
isRandomTemplate: selectedScriptGroupIds.length > 1 ? 1 : 0,
contentGroups: contentGroupIds,
postPushTags: normalizedPostPushTags,
ownerWechatIds,
enableAiRewrite: aiRewriteEnabled ? 1 : 0,
};
if (trafficPoolIds.length > 0) {
basePayload.trafficPools = trafficPoolIds;
}
if (validPushType === "friend-message") {
basePayload.isLoop = 0;
basePayload.deviceGroups = deviceGroupIds;
}
if (manualMessages.length > 0) {
basePayload.manualMessages = manualMessages;
if (currentScriptName.trim()) {
basePayload.manualScriptName = currentScriptName.trim();
}
}
if (selectedScriptGroupIds.length > 0) {
basePayload.selectedScriptGroupIds = selectedScriptGroupIds;
}
if (aiRewriteEnabled && aiPrompt.trim()) {
basePayload.aiRewritePrompt = aiPrompt.trim();
}
if (selectedGroups.length > 0) {
basePayload.scriptGroups = selectedGroups.map(group => ({
id: group.id,
name: group.name,
messages: group.messages,
}));
}
if (validPushType === "friend-message") {
basePayload.wechatFriends = Array.from(
new Set(
selectedContacts
.map(contact => toNumberId(contact?.id))
.filter((id): id is number => id !== null),
),
);
basePayload.targetType = 2;
} else {
const groupIds = Array.from(
new Set(
selectedContacts
.map(contact => toNumberId(contact.groupId ?? contact.id))
.filter((id): id is number => id !== null),
),
);
basePayload.wechatGroups = groupIds;
basePayload.groupPushSubType =
validPushType === "group-announcement" ? 2 : 1;
basePayload.targetType = 1;
if (validPushType === "group-announcement") {
basePayload.announcementContent = manualMessages.join("\n");
}
}
let hideLoading: ReturnType<typeof message.loading> | undefined;
try {
setCreatingTask(true);
hideLoading = message.loading("正在创建推送任务...", 0);
await queryWorkbenchCreate(basePayload);
hideLoading?.();
message.success("推送任务已创建");
navigate("/pc/powerCenter/message-push-assistant");
} catch (error) {
hideLoading?.();
console.error("创建推送任务失败:", error);
const errorMessage =
(error as any)?.message ||
(error as any)?.response?.data?.message ||
"创建推送任务失败,请稍后重试";
message.error(errorMessage);
} finally {
setCreatingTask(false);
}
};
return (
@@ -242,6 +464,8 @@ const CreatePushTask: React.FC = () => {
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
loading={creatingTask}
disabled={creatingTask}
>
</Button>
@@ -266,6 +490,8 @@ const CreatePushTask: React.FC = () => {
selectedAccounts={selectedAccounts}
selectedContacts={selectedContacts}
onChange={setSelectedContacts}
selectedTrafficPools={selectedTrafficPools}
onTrafficPoolsChange={setSelectedTrafficPools}
/>
)}
{currentStep === 3 && (

View File

@@ -0,0 +1,79 @@
帮我对接数据,以下是传参实例,三种模式都是同一界面的。
群发助手传参实例
{
"name": "群群发-新品宣传", // 任务名称
"type": 3, // 工作台类型3=群消息推送
"autoStart": 1, // 保存后自动启动
"status": 1, // 是否启用
"pushType": 0, // 推送方式0=定时1=立即
"targetType": 1, // 目标类型1=群推送
"groupPushSubType": 1, // 群推送子类型1=群群发2=群公告
"startTime": "09:00", // 推送起始时间
"endTime": "20:00", // 推送结束时间
"maxPerDay": 200, // 每日最大推送群数
"pushOrder": 1, // 推送顺序1=最早优先2=最新优先
"wechatGroups": [102, 205, 318], // 选择的微信群 ID 列表
"contentGroups": [11, 12], // 关联内容库 ID 列表
"friendIntervalMin": 10, // 群间最小间隔(秒)
"friendIntervalMax": 25, // 群间最大间隔(秒)
"messageIntervalMin": 2, // 同一群消息间最小间隔(秒)
"messageIntervalMax": 6, // 同一群消息间最大间隔(秒)
"isRandomTemplate": 1, // 是否随机选择话术模板
"postPushTags": [301, 302], // 推送完成后打的标签
ownerWechatIds[123123,1231231] //客服id
}
//群公告传参实例
{
"name": "群公告-双11活动", // 任务名称
"type": 3, // 群消息推送
"autoStart": 0, // 不自动启动
"status": 1, // 启用
"pushType": 1, // 立即推送
"targetType": 1, // 群推送
"groupPushSubType": 2, // 群公告
"startTime": "08:30", // 开始时间
"endTime": "18:30", // 结束时间
"maxPerDay": 80, // 每日最大公告数
"pushOrder": 2, // 最新优先
"wechatGroups": [5021, 5026], // 公告目标群
"announcementContent": "…", // 公告正文
"enableAiRewrite": 1, // 启用 AI 改写
"aiRewritePrompt": "保持活泼口吻…", // AI 改写提示词
"contentGroups": [21], // 关联内容库
"friendIntervalMin": 15, // 群间最小间隔
"friendIntervalMax": 30, // 群间最大间隔
"messageIntervalMin": 3, // 消息间最小间隔
"messageIntervalMax": 9, // 消息间最大间隔
"isRandomTemplate": 0, // 不随机模板
"postPushTags": [], // 推送后标签
ownerWechatIds[123123,1231231] //客服id
}
//好友传参实例
{
"name": "好友私聊-新客转化", // 任务名称
"type": 3, // 群消息推送
"autoStart": 1, // 自动启动
"status": 1, // 启用
"pushType": 0, // 定时推送
"targetType": 2, // 目标类型2=好友推送
"groupPushSubType": 1, // 固定为群群发(好友推送不支持公告)
"startTime": "10:00", // 开始时间
"endTime": "22:00", // 结束时间
"maxPerDay": 150, // 每日最大推送好友数
"pushOrder": 1, // 最早优先
"wechatFriends": ["12312"], // 指定好友列表(可为空数组)
"deviceGroups": [9001, 9002], // 必选:推送设备分组 ID
"contentGroups": [41, 42], // 话术内容库
"friendIntervalMin": 12, // 好友间最小间隔
"friendIntervalMax": 28, // 好友间最大间隔
"messageIntervalMin": 4, // 消息间最小间隔
"messageIntervalMax": 10, // 消息间最大间隔
"isRandomTemplate": 1, // 随机话术
"postPushTags": [501], // 推送后标签
ownerWechatIds[123123,1231231] //客服id
}
请求接口是 queryWorkbenchCreate

View File

@@ -7,6 +7,7 @@
* 4. 提供回调机制通知组件更新
*/
import Dexie from "dexie";
import { db, chatSessionService, ChatSession } from "../db";
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
@@ -243,7 +244,9 @@ export class MessageManager {
"userId",
userId,
)) as ChatSession[];
const localSessionMap = new Map(localSessions.map(s => [s.id, s]));
const localSessionMap = new Map(
localSessions.map(session => [session.serverId, session]),
);
// 2. 转换服务器数据为统一格式
const serverSessions: ChatSession[] = [];
@@ -264,16 +267,18 @@ export class MessageManager {
serverSessions.push(...groups);
}
const serverSessionMap = new Map(serverSessions.map(s => [s.id, s]));
const serverSessionMap = new Map(
serverSessions.map(session => [session.serverId, session]),
);
// 3. 计算差异
const toAdd: ChatSession[] = [];
const toUpdate: ChatSession[] = [];
const toDelete: number[] = [];
const toDelete: string[] = [];
// 检查新增和更新
for (const serverSession of serverSessions) {
const localSession = localSessionMap.get(serverSession.id);
const localSession = localSessionMap.get(serverSession.serverId);
if (!localSession) {
toAdd.push(serverSession);
@@ -286,8 +291,8 @@ export class MessageManager {
// 检查删除
for (const localSession of localSessions) {
if (!serverSessionMap.has(localSession.id)) {
toDelete.push(localSession.id);
if (!serverSessionMap.has(localSession.serverId)) {
toDelete.push(localSession.serverId);
}
}
@@ -334,7 +339,19 @@ export class MessageManager {
serverId: `${session.type}_${session.id}`,
}));
await db.chatSessions.bulkAdd(dataToInsert);
try {
await db.chatSessions.bulkAdd(dataToInsert);
} catch (error) {
if (error instanceof Dexie.BulkError) {
console.warn(
`批量新增会话时检测到重复主键,切换为 bulkPut 以覆盖更新。错误详情:`,
error,
);
await db.chatSessions.bulkPut(dataToInsert);
} else {
throw error;
}
}
}
/**
@@ -357,14 +374,16 @@ export class MessageManager {
*/
private static async batchDeleteSessions(
userId: number,
sessionIds: number[],
serverIds: string[],
) {
if (sessionIds.length === 0) return;
if (serverIds.length === 0) return;
const serverIdSet = new Set(serverIds);
await db.chatSessions
.where("userId")
.equals(userId)
.and(session => sessionIds.includes(session.id))
.and(session => serverIdSet.has(session.serverId))
.delete();
}