- 规范7国语言全局词典中AI咨询为在线咨询,去除内部黑话与营销说教 - 在线咨询去除机械人套话与写死的大号AI文本标签 - 交换名片剔除留资行话与写死AI,规范为名片交换与纸质名片识别 - 工作台去除战报、闪电出示、高光足迹、破冰等中二词汇,回归严谨商务数据 - 产品案例与专栏文章去除夸大修饰词与冗余操作说明 - 潜客跟进去除写死销售套话,规范为得体自然的跟进建议 - 硬件安全与场景模式去除秒级一键、芯片防盗等噱头词 - 导航栏标题统一规范为在线咨询、名片编辑、展会数据 - 新增专项测试并通过全量62项回归测试
591 行
14 KiB
Plaintext
591 行
14 KiB
Plaintext
/**
|
||
* OmniCard AI (CRMmp) - 门户 AI 顾问对话业务驱动层
|
||
* 调用已有 askAiChat,空文本不发,cardId 必填
|
||
*/
|
||
|
||
import {
|
||
AiChatMessage,
|
||
AiChatReply,
|
||
AiChatRequest,
|
||
ChatMessage,
|
||
ChatMessageType,
|
||
ChatLeadPayload,
|
||
SubmitLeadParams,
|
||
} from '../../common/types.uts';
|
||
import { askAiChat, submitLead } from '../../common/api.uts';
|
||
import {
|
||
OmniLocale,
|
||
getCurrentLocale,
|
||
getOmniText,
|
||
resolveDeviceLocale,
|
||
setCurrentLocale,
|
||
} from '../../common/locale.uts';
|
||
|
||
export {
|
||
ChatMessage,
|
||
ChatMessageType,
|
||
ChatLeadPayload,
|
||
AiChatMessage,
|
||
AiChatReply,
|
||
AiChatRequest,
|
||
};
|
||
|
||
export type AiChatCopyKey =
|
||
| 'title'
|
||
| 'send'
|
||
| 'sending'
|
||
| 'placeholder'
|
||
| 'empty'
|
||
| 'leadHint'
|
||
| 'missingCard'
|
||
| 'inputLabel'
|
||
| 'nameLabel'
|
||
| 'namePlaceholder'
|
||
| 'contactLabel'
|
||
| 'contactPlaceholder'
|
||
| 'submitLead'
|
||
| 'submitting'
|
||
| 'submittedSuccess'
|
||
| 'leadCardTitle'
|
||
| 'leadCardDesc'
|
||
| 'submitFailed'
|
||
| 'error'
|
||
| 'statusOnline'
|
||
| 'advisorSubtitle'
|
||
| 'quickPricing'
|
||
| 'quickCases'
|
||
| 'quickServices';
|
||
|
||
export type SendChatInput = {
|
||
cardId: string | number;
|
||
locale?: string;
|
||
messages: ChatMessage[];
|
||
text: string;
|
||
};
|
||
|
||
export type AskAiChatFn = (req: AiChatRequest) => Promise<AiChatReply>;
|
||
|
||
export type SendChatApi = {
|
||
askAiChat: AskAiChatFn;
|
||
};
|
||
|
||
export type SendChatResult = {
|
||
sent: boolean;
|
||
reply: AiChatReply | null;
|
||
messages: ChatMessage[];
|
||
shouldCollectLead: boolean;
|
||
leadHint: string;
|
||
};
|
||
|
||
export type AiChatState = {
|
||
cardId: string;
|
||
locale: OmniLocale;
|
||
draft: string;
|
||
sending: boolean;
|
||
messages: ChatMessage[];
|
||
showLeadHint: boolean;
|
||
leadHint: string;
|
||
errorMessage: string;
|
||
};
|
||
|
||
export type AiChatPageOptions = {
|
||
cardId?: string;
|
||
id?: string;
|
||
lang?: string;
|
||
locale?: string;
|
||
};
|
||
|
||
function isZhLocale(): boolean {
|
||
return getCurrentLocale() === 'zh-CN';
|
||
}
|
||
|
||
function cloneMessages(messages: ChatMessage[]): ChatMessage[] {
|
||
const next: ChatMessage[] = [];
|
||
if (messages == null) {
|
||
return next;
|
||
}
|
||
for (let i = 0; i < messages.length; i++) {
|
||
const item = messages[i];
|
||
const cloned: ChatMessage = {
|
||
role: item.role,
|
||
content: item.content,
|
||
type: item.type != null ? item.type : 'text',
|
||
};
|
||
if (item.cardPayload != null) {
|
||
cloned.cardPayload = {
|
||
title: item.cardPayload.title,
|
||
description: item.cardPayload.description,
|
||
name: item.cardPayload.name,
|
||
contact: item.cardPayload.contact,
|
||
submitted: item.cardPayload.submitted,
|
||
submitting: item.cardPayload.submitting,
|
||
successMessage: item.cardPayload.successMessage,
|
||
errorMessage: item.cardPayload.errorMessage,
|
||
};
|
||
}
|
||
next.push(cloned);
|
||
}
|
||
return next;
|
||
}
|
||
|
||
function resolveCardId(cardId: string | number): string {
|
||
if (cardId == null) {
|
||
return '';
|
||
}
|
||
return String(cardId).trim();
|
||
}
|
||
|
||
function requestCardId(cardId: string | number): string | number {
|
||
if (typeof cardId === 'number') {
|
||
return cardId;
|
||
}
|
||
return resolveCardId(cardId);
|
||
}
|
||
|
||
function emptySendResult(messages: ChatMessage[]): SendChatResult {
|
||
return {
|
||
sent: false,
|
||
reply: null,
|
||
messages,
|
||
shouldCollectLead: false,
|
||
leadHint: '',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 页面文案:标题走 card.ai_chat,其余本地词典
|
||
*/
|
||
export function resolveAiChatText(key: AiChatCopyKey): string {
|
||
const locale = getCurrentLocale();
|
||
if (key === 'title') {
|
||
return getOmniText(locale, 'card.ai_chat');
|
||
}
|
||
if (key === 'error') {
|
||
return getOmniText(locale, 'common.error');
|
||
}
|
||
|
||
const zh = isZhLocale();
|
||
if (key === 'send') {
|
||
return zh ? '发送' : 'Send';
|
||
}
|
||
if (key === 'sending') {
|
||
return zh ? '发送中' : 'Sending';
|
||
}
|
||
if (key === 'placeholder') {
|
||
return zh ? '输入你想了解的内容' : 'Ask about this card';
|
||
}
|
||
if (key === 'empty') {
|
||
return zh ? '您好,欢迎咨询。您可以了解业务介绍、服务案例或提交合作需求。' : 'Welcome! Feel free to inquire about our services, cases, or partnership.';
|
||
}
|
||
if (key === 'leadHint') {
|
||
return zh
|
||
? '留下您的联系方式,方便进一步沟通。'
|
||
: 'Leave your contact details so we can follow up with you.';
|
||
}
|
||
if (key === 'missingCard') {
|
||
return zh ? '缺少名片标识,暂时无法对话。' : 'A card ID is required to start this chat.';
|
||
}
|
||
if (key === 'inputLabel') {
|
||
return zh ? '消息' : 'Message';
|
||
}
|
||
if (key === 'nameLabel') {
|
||
return zh ? '您的称呼' : 'Your Name';
|
||
}
|
||
if (key === 'namePlaceholder') {
|
||
return zh ? '例如:张经理 / Alex' : 'e.g. Alex Smith';
|
||
}
|
||
if (key === 'contactLabel') {
|
||
return zh ? '联系方式' : 'Contact (WhatsApp / Email)';
|
||
}
|
||
if (key === 'contactPlaceholder') {
|
||
return zh ? 'WhatsApp 或 电子邮箱' : 'WhatsApp or Email';
|
||
}
|
||
if (key === 'submitLead') {
|
||
return zh ? '提交需求' : 'Submit Request';
|
||
}
|
||
if (key === 'submitting') {
|
||
return zh ? '提交中' : 'Submitting';
|
||
}
|
||
if (key === 'submittedSuccess') {
|
||
return zh
|
||
? '信息已提交,我们将尽快与您联系。'
|
||
: 'Information received. We will get in touch shortly.';
|
||
}
|
||
if (key === 'leadCardTitle') {
|
||
return zh ? '获取方案与报价' : 'Proposal & Pricing';
|
||
}
|
||
if (key === 'leadCardDesc') {
|
||
return zh
|
||
? '请留下您的联系方式,商务人员将尽快与您对接。'
|
||
: 'Leave your contact info and our team will get in touch.';
|
||
}
|
||
if (key === 'submitFailed') {
|
||
return zh ? '提交失败,请重试' : 'Submission failed, please retry';
|
||
}
|
||
if (key === 'statusOnline') {
|
||
return zh ? '在线' : 'Online';
|
||
}
|
||
if (key === 'advisorSubtitle') {
|
||
return zh ? '商务咨询与业务对接' : 'Business inquiry & consultation';
|
||
}
|
||
if (key === 'quickPricing') {
|
||
return zh ? '产品方案与报价' : 'Pricing & Plans';
|
||
}
|
||
if (key === 'quickCases') {
|
||
return zh ? '客户案例' : 'Client Cases';
|
||
}
|
||
if (key === 'quickServices') {
|
||
return zh ? '核心服务能力' : 'Core Capabilities';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
export function createAiChatState(): AiChatState {
|
||
return {
|
||
cardId: '',
|
||
locale: getCurrentLocale(),
|
||
draft: '',
|
||
sending: false,
|
||
messages: [],
|
||
showLeadHint: false,
|
||
leadHint: '',
|
||
errorMessage: '',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 从页面 query 写入 cardId / lang
|
||
*/
|
||
export function applyAiChatOptions(
|
||
state: AiChatState,
|
||
options: AiChatPageOptions | null
|
||
): boolean {
|
||
let cardIdRaw = options != null ? options.cardId || options.id || '' : '';
|
||
if (String(cardIdRaw).trim() === '') {
|
||
try {
|
||
const stored = uni.getStorageSync('omnicard_active_card_id');
|
||
if (stored != null && String(stored).trim() !== '') {
|
||
cardIdRaw = String(stored).trim();
|
||
} else {
|
||
cardIdRaw = '1001';
|
||
}
|
||
} catch (_e) {
|
||
cardIdRaw = '1001';
|
||
}
|
||
}
|
||
const langRaw = options != null ? options.lang || options.locale || '' : '';
|
||
state.cardId = String(cardIdRaw).trim();
|
||
|
||
if (langRaw.trim() !== '') {
|
||
const locale = resolveDeviceLocale(langRaw);
|
||
setCurrentLocale(locale);
|
||
state.locale = locale;
|
||
} else {
|
||
state.locale = getCurrentLocale();
|
||
}
|
||
|
||
return state.cardId !== '';
|
||
}
|
||
|
||
export function buildUserMessage(text: string): ChatMessage {
|
||
const content = text != null ? text.trim() : '';
|
||
return {
|
||
role: 'user',
|
||
content,
|
||
type: 'text',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 追加一轮 user + assistant,返回新数组
|
||
*/
|
||
export function appendTurn(
|
||
messages: ChatMessage[],
|
||
userText: string,
|
||
reply: AiChatReply
|
||
): ChatMessage[] {
|
||
const next = cloneMessages(messages);
|
||
next.push(buildUserMessage(userText));
|
||
const replyText = reply != null && reply.reply != null ? reply.reply : '';
|
||
next.push({
|
||
role: 'assistant',
|
||
content: replyText,
|
||
type: 'text',
|
||
});
|
||
return next;
|
||
}
|
||
|
||
/**
|
||
* 发送顾问对话。空文本不发,cardId 必填。可注入 askAiChat
|
||
*/
|
||
export async function sendChat(
|
||
input: SendChatInput,
|
||
api?: SendChatApi
|
||
): Promise<SendChatResult> {
|
||
const text = input != null && input.text != null ? input.text.trim() : '';
|
||
const cardId = input != null ? resolveCardId(input.cardId) : '';
|
||
const existing = input != null && input.messages != null ? input.messages : [];
|
||
|
||
if (text === '' || cardId === '') {
|
||
return emptySendResult(existing);
|
||
}
|
||
|
||
const outgoing = cloneMessages(existing);
|
||
outgoing.push(buildUserMessage(text));
|
||
|
||
const req: AiChatRequest = {
|
||
cardId: requestCardId(input.cardId),
|
||
messages: outgoing,
|
||
};
|
||
const locale = input.locale != null ? input.locale.trim() : '';
|
||
if (locale !== '') {
|
||
req.locale = locale;
|
||
}
|
||
|
||
const ask = api != null ? api.askAiChat : askAiChat;
|
||
const reply = await ask(req);
|
||
const shouldCollectLead = reply != null && reply.shouldCollectLead === true;
|
||
|
||
return {
|
||
sent: true,
|
||
reply,
|
||
messages: appendTurn(existing, text, reply),
|
||
shouldCollectLead,
|
||
leadHint: shouldCollectLead ? resolveAiChatText('leadHint') : '',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 页面发送:写入会话并给出留资引导标记
|
||
*/
|
||
export async function sendDraft(
|
||
state: AiChatState,
|
||
api?: SendChatApi
|
||
): Promise<SendChatResult> {
|
||
if (state.sending) {
|
||
return emptySendResult(state.messages);
|
||
}
|
||
|
||
state.sending = true;
|
||
state.errorMessage = '';
|
||
|
||
try {
|
||
const result = await sendChat(
|
||
{
|
||
cardId: state.cardId,
|
||
locale: state.locale,
|
||
messages: state.messages,
|
||
text: state.draft,
|
||
},
|
||
api
|
||
);
|
||
if (result.sent) {
|
||
state.draft = '';
|
||
state.messages = result.messages;
|
||
state.showLeadHint = result.shouldCollectLead;
|
||
state.leadHint = result.leadHint;
|
||
}
|
||
state.sending = false;
|
||
return result;
|
||
} catch (_e) {
|
||
state.sending = false;
|
||
state.errorMessage = resolveAiChatText('error');
|
||
return emptySendResult(state.messages);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 意向词检测算法(如“价格”, "报价", "合作", "buy", "quote", "price")
|
||
*/
|
||
export function hasLeadIntent(text: string): boolean {
|
||
if (text == null || text.trim() === '') {
|
||
return false;
|
||
}
|
||
const pattern = /价格|报价|多少钱|合作|联系|购买|buy|quote|price|pricing|cost|partner|cooperat|contact|order|purchase/i;
|
||
return pattern.test(text);
|
||
}
|
||
|
||
/**
|
||
* 模拟流式推送
|
||
*/
|
||
export async function sendStreamMessage(
|
||
prompt: string,
|
||
onChunk: (chunk: string) => void,
|
||
onComplete: () => void,
|
||
intervalMs: number = 20
|
||
): Promise<void> {
|
||
if (prompt == null || prompt.trim() === '') {
|
||
return;
|
||
}
|
||
const trimmed = prompt.trim();
|
||
const isIntent = hasLeadIntent(trimmed);
|
||
const zh = isZhLocale();
|
||
|
||
let fullReply = '';
|
||
if (isIntent) {
|
||
fullReply = zh
|
||
? '关于方案与合作细节,已为您准备相关资料。请在下方填写您的联系方式,我们将尽快与您对接。'
|
||
: 'For pricing and partnership details, we have tailored options. Please leave your details below to receive our pricing sheet.';
|
||
} else {
|
||
fullReply = zh
|
||
? '您好,欢迎咨询。您可以了解业务介绍、服务案例或提交合作需求,我们将尽快与您联系。'
|
||
: 'Welcome! I can introduce the profile, business blocks, and our capabilities. Feel free to ask.';
|
||
}
|
||
|
||
const step = 3;
|
||
for (let i = 0; i < fullReply.length; i += step) {
|
||
const chunk = fullReply.slice(i, i + step);
|
||
if (intervalMs > 0) {
|
||
await new Promise<void>((resolve) => setTimeout(resolve, intervalMs));
|
||
}
|
||
onChunk(chunk);
|
||
}
|
||
onComplete();
|
||
}
|
||
|
||
/**
|
||
* 留资卡片负载生成
|
||
*/
|
||
export function createLeadCardPayload(locale?: string): ChatLeadPayload {
|
||
const loc = locale != null && locale !== '' ? locale : getCurrentLocale();
|
||
const zh = loc.startsWith('zh');
|
||
return {
|
||
title: zh ? '获取方案与报价' : 'Proposal & Pricing',
|
||
description: zh
|
||
? '请留下您的联系方式,商务人员将尽快与您对接。'
|
||
: 'Leave your contact info and our team will get in touch.',
|
||
name: '',
|
||
contact: '',
|
||
submitted: false,
|
||
submitting: false,
|
||
errorMessage: '',
|
||
successMessage: '',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建留资卡片消息
|
||
*/
|
||
export function createLeadCardMessage(locale?: string): ChatMessage {
|
||
return {
|
||
role: 'assistant',
|
||
content: '',
|
||
type: 'lead_card',
|
||
cardPayload: createLeadCardPayload(locale),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 模拟流式对话发送
|
||
*/
|
||
export async function sendStreamChat(
|
||
state: AiChatState,
|
||
text?: string,
|
||
options?: { intervalMs?: number }
|
||
): Promise<boolean> {
|
||
if (state.sending) {
|
||
return false;
|
||
}
|
||
const rawText = text != null ? text : state.draft;
|
||
const prompt = rawText != null ? rawText.trim() : '';
|
||
if (prompt === '' || state.cardId === '') {
|
||
return false;
|
||
}
|
||
|
||
state.sending = true;
|
||
state.draft = '';
|
||
state.errorMessage = '';
|
||
|
||
// 1. 用户消息
|
||
const userMsg: ChatMessage = {
|
||
role: 'user',
|
||
content: prompt,
|
||
type: 'text',
|
||
};
|
||
state.messages.push(userMsg);
|
||
|
||
// 2. 意向检测
|
||
const isIntent = hasLeadIntent(prompt);
|
||
|
||
// 3. 初始 assistant 消息
|
||
const assistantMsg: ChatMessage = {
|
||
role: 'assistant',
|
||
content: '',
|
||
type: 'text',
|
||
};
|
||
state.messages.push(assistantMsg);
|
||
|
||
// 4. 流式推送
|
||
const intervalMs = options != null && options.intervalMs != null ? options.intervalMs : 20;
|
||
await sendStreamMessage(
|
||
prompt,
|
||
(chunk: string) => {
|
||
assistantMsg.content += chunk;
|
||
},
|
||
() => {},
|
||
intervalMs
|
||
);
|
||
|
||
// 5. 命中意向词时自动追加留资卡片
|
||
if (isIntent) {
|
||
state.messages.push(createLeadCardMessage(state.locale));
|
||
state.showLeadHint = true;
|
||
state.leadHint = resolveAiChatText('leadHint');
|
||
}
|
||
|
||
state.sending = false;
|
||
return true;
|
||
}
|
||
|
||
export type SubmitLeadApi = {
|
||
submitLead: (params: SubmitLeadParams) => Promise<unknown>;
|
||
};
|
||
|
||
/**
|
||
* 提交留资卡片线索
|
||
*/
|
||
export async function submitLeadCard(
|
||
cardId: string | number,
|
||
payload: ChatLeadPayload,
|
||
api?: SubmitLeadApi
|
||
): Promise<boolean> {
|
||
if (payload == null) {
|
||
return false;
|
||
}
|
||
const name = payload.name != null ? payload.name.trim() : '';
|
||
const contact = payload.contact != null ? payload.contact.trim() : '';
|
||
if (name === '' || contact === '') {
|
||
return false;
|
||
}
|
||
|
||
payload.submitting = true;
|
||
payload.errorMessage = '';
|
||
|
||
const submitFn = api != null ? api.submitLead : submitLead;
|
||
|
||
try {
|
||
const isEmail = contact.includes('@');
|
||
const params: SubmitLeadParams = {
|
||
cardId,
|
||
name,
|
||
source: 'ai_chat',
|
||
notes: 'AI顾问留资卡片线索',
|
||
};
|
||
if (isEmail) {
|
||
params.email = contact;
|
||
} else {
|
||
params.phone = contact;
|
||
}
|
||
|
||
await submitFn(params);
|
||
payload.submitted = true;
|
||
payload.submitting = false;
|
||
payload.successMessage = resolveAiChatText('submittedSuccess');
|
||
return true;
|
||
} catch (_e) {
|
||
payload.submitting = false;
|
||
payload.errorMessage = resolveAiChatText('submitFailed');
|
||
return false;
|
||
}
|
||
}
|