/** * 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; 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 { 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 { 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 { 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((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 { 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; }; /** * 提交留资卡片线索 */ export async function submitLeadCard( cardId: string | number, payload: ChatLeadPayload, api?: SubmitLeadApi ): Promise { 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; } }