文件
OmniCard/mpuniapp/pages/portal/useAiChat.uts
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

559 行
14 KiB
Plaintext

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 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';
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 ? '向顾问提问,了解业务与服务。' : 'Ask the advisor about this business.';
}
if (key === 'leadHint') {
return zh
? '留下姓名和联系方式,方便继续沟通。'
: 'Leave your name and contact so we can continue.';
}
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 ? '获取专属方案' : 'Get Custom Proposal';
}
if (key === 'submitting') {
return zh ? '提交中' : 'Submitting';
}
if (key === 'submittedSuccess') {
return zh
? '已收到您的信息,顾问将尽快与您联系。'
: 'Information received. Our advisor will reach out shortly.';
}
if (key === 'leadCardTitle') {
return zh ? '获取专属方案与报价' : 'Get Custom Proposal & Pricing';
}
if (key === 'leadCardDesc') {
return zh
? '留下您的称呼与联系方式(WhatsApp 或邮箱),商务团队将快速与您对接。'
: 'Leave your contact info and our team will get in touch.';
}
if (key === 'submitFailed') {
return zh ? '提交失败,请重试' : 'Submission failed, please retry';
}
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 {
const cardIdRaw = options != null ? options.cardId || options.id || '' : '';
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
? '关于方案价格与合作权益,已为您梳理核心细节。请在下方卡片留下您的联系信息,专属顾问将在15分钟内提供完整报价。'
: 'For pricing and partnership details, we have tailored options. Please leave your details below to receive our pricing sheet.';
} else {
fullReply = zh
? '我是名片智能顾问,很高兴为您服务。我可以为您介绍名片主页、Bento微官网与出海团队能力。请随时提问。'
: 'I am the digital concierge for this card. I can introduce the profile, Bento 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 ? '获取专属方案与报价' : 'Get Custom Proposal & Pricing',
description: zh
? '留下您的称呼与联系方式(WhatsApp 或邮箱),商务团队将快速与您对接。'
: '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;
}
}