feat(round-16): AI意图情绪分析/展会名片双向互换/雷达爬虫清洗/离线多媒体缓存 (103套件1101测试全绿)

这个提交包含在:
wikiboss
2026-09-21 02:17:06 +08:00
父节点 e6d06f762c
当前提交 8c003b70cd
共有 8 个文件被更改,包括 805 次插入 和 0 次删除

查看文件

@@ -0,0 +1,151 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type CommercialIntent = 'PURCHASE_INTENT' | 'PARTNER_INQUIRY' | 'TECH_SUPPORT' | 'GENERAL_CHAT';
export type SentimentPolarity = 'positive' | 'neutral' | 'negative';
export interface IntentAnalysisResult {
primaryIntent: CommercialIntent;
confidence: number;
matchedKeywords: string[];
}
export interface SentimentAnalysisResult {
polarity: SentimentPolarity;
score: number; // -1.0 到 1.0
}
export interface ConversationAnalysisResult {
intent: IntentAnalysisResult;
sentiment: SentimentAnalysisResult;
recommendedLeadScoreBoost: number;
}
const PURCHASE_KEYWORDS = ['buy', 'purchase', 'price', 'pricing', 'quote', 'quotation', 'cost', 'moq', 'wholesale', 'order', 'contract', '订购', '采购', '价格', '报价'];
const PARTNER_KEYWORDS = ['partner', 'partnership', 'reseller', 'distributor', 'agency', 'agent', 'cooperate', '代理', '分销', '合作'];
const TECH_KEYWORDS = ['api', 'sdk', 'tech', 'integration', 'developer', 'spec', 'chip', 'ntag', '接口', '开发'];
const POSITIVE_KEYWORDS = ['great', 'excellent', 'love', 'perfect', 'awesome', 'good', 'impressive', 'forward to', 'pleasure', '太棒', '满意', '喜欢', '期待'];
const NEGATIVE_KEYWORDS = ['bad', 'expensive', 'too high', 'slow', 'poor', 'disappointed', 'terrible', 'not happy', '差', '贵', '不满意', '太慢'];
/**
* 识别对话核心商务意图
*/
export function analyzeConversationIntent(messages: string[]): IntentAnalysisResult {
const fullText = messages.join(' ').toLowerCase();
let purchaseHits = 0;
let partnerHits = 0;
let techHits = 0;
const matchedKeywords: string[] = [];
for (const kw of PURCHASE_KEYWORDS) {
if (fullText.includes(kw)) {
purchaseHits++;
matchedKeywords.push(kw);
}
}
for (const kw of PARTNER_KEYWORDS) {
if (fullText.includes(kw)) {
partnerHits++;
matchedKeywords.push(kw);
}
}
for (const kw of TECH_KEYWORDS) {
if (fullText.includes(kw)) {
techHits++;
matchedKeywords.push(kw);
}
}
if (purchaseHits > 0 && purchaseHits >= partnerHits && purchaseHits >= techHits) {
return {
primaryIntent: 'PURCHASE_INTENT',
confidence: Math.min(1.0, 0.6 + purchaseHits * 0.15),
matchedKeywords,
};
}
if (partnerHits > 0 && partnerHits >= techHits) {
return {
primaryIntent: 'PARTNER_INQUIRY',
confidence: Math.min(1.0, 0.6 + partnerHits * 0.15),
matchedKeywords,
};
}
if (techHits > 0) {
return {
primaryIntent: 'TECH_SUPPORT',
confidence: Math.min(1.0, 0.6 + techHits * 0.15),
matchedKeywords,
};
}
return {
primaryIntent: 'GENERAL_CHAT',
confidence: 0.5,
matchedKeywords: [],
};
}
/**
* 客商情绪极性分析
*/
export function analyzeConversationSentiment(messages: string[]): SentimentAnalysisResult {
const fullText = messages.join(' ').toLowerCase();
let posCount = 0;
let negCount = 0;
for (const kw of POSITIVE_KEYWORDS) {
if (fullText.includes(kw)) posCount++;
}
for (const kw of NEGATIVE_KEYWORDS) {
if (fullText.includes(kw)) negCount++;
}
if (posCount > negCount) {
return {
polarity: 'positive',
score: Math.min(1.0, 0.4 + (posCount - negCount) * 0.3),
};
}
if (negCount > posCount) {
return {
polarity: 'negative',
score: Math.max(-1.0, -0.4 - (negCount - posCount) * 0.3),
};
}
return {
polarity: 'neutral',
score: 0.0,
};
}
/**
* AI 客商意图与商业情绪分析引擎服务
*/
@Provide()
export class AiSentimentIntentAnalyzer extends BaseService {
async evaluateConversationSession(
messages: string[]
): Promise<ConversationAnalysisResult> {
const intent = analyzeConversationIntent(messages);
const sentiment = analyzeConversationSentiment(messages);
let boost = 0;
if (intent.primaryIntent === 'PURCHASE_INTENT') boost += 20;
if (intent.primaryIntent === 'PARTNER_INQUIRY') boost += 15;
if (sentiment.polarity === 'positive') boost += 10;
if (sentiment.polarity === 'negative') boost -= 10;
return {
intent,
sentiment,
recommendedLeadScoreBoost: Math.max(0, boost),
};
}
}

查看文件

@@ -0,0 +1,127 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type MutualExchangeStatus = 'REQUESTED' | 'ACCEPTED' | 'REJECTED' | 'EXPIRED';
export interface MutualExchangeInvitation {
id: string;
senderCardId: number;
recipientCardId: number;
status: MutualExchangeStatus;
exchangeCode: string;
notes?: string;
createdAt: number;
acceptedAt?: number;
}
export interface MutualExchangeSyncAction {
targetCardId: number;
sourceCardId: number;
action: 'CREATE_CRM_LEAD';
}
export interface AcceptExchangeResult {
invitation: MutualExchangeInvitation;
syncActions: MutualExchangeSyncAction[];
}
/**
* 发起名片双向互换邀约
*/
export function createMutualExchangeInvitation(
senderCardId: number,
recipientCardId: number,
notes?: string
): MutualExchangeInvitation {
if (senderCardId === recipientCardId) {
throw new Error('不能与自己互换名片');
}
const exchangeCode = String(Math.floor(100000 + Math.random() * 900000));
const id = `xchg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
return {
id,
senderCardId,
recipientCardId,
status: 'REQUESTED',
exchangeCode,
notes,
createdAt: Date.now(),
};
}
/**
* 接收方确认互换名片
*/
export function acceptMutualExchangeInvitation(
invitation: MutualExchangeInvitation,
actingCardId: number
): AcceptExchangeResult {
if (invitation.recipientCardId !== actingCardId) {
throw new Error('无权操作此名片互换邀约');
}
if (invitation.status !== 'REQUESTED') {
throw new Error(`当前互换邀约状态为 ${invitation.status},不可重复确认`);
}
invitation.status = 'ACCEPTED';
invitation.acceptedAt = Date.now();
const syncActions: MutualExchangeSyncAction[] = [
{
targetCardId: invitation.senderCardId,
sourceCardId: invitation.recipientCardId,
action: 'CREATE_CRM_LEAD',
},
{
targetCardId: invitation.recipientCardId,
sourceCardId: invitation.senderCardId,
action: 'CREATE_CRM_LEAD',
},
];
return {
invitation,
syncActions,
};
}
/**
* 展会名片双向互换服务
*/
@Provide()
export class CardMutualExchangeService extends BaseService {
private readonly invitations = new Map<string, MutualExchangeInvitation>();
async initiateExchange(params: {
senderCardId: number;
recipientCardId: number;
notes?: string;
}): Promise<MutualExchangeInvitation> {
const invite = createMutualExchangeInvitation(
params.senderCardId,
params.recipientCardId,
params.notes
);
this.invitations.set(invite.id, invite);
return invite;
}
async confirmExchange(
invitationId: string,
actingCardId: number
): Promise<{ status: MutualExchangeStatus; isDualSynced: boolean }> {
const invite = this.invitations.get(invitationId);
if (!invite) {
throw new Error('未找到该名片互换邀约');
}
const res = acceptMutualExchangeInvitation(invite, actingCardId);
return {
status: res.invitation.status,
isDualSynced: res.syncActions.length === 2,
};
}
}

查看文件

@@ -0,0 +1,102 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type BotClassification = 'SEARCH_CRAWLER' | 'SOCIAL_PREVIEW' | 'SCRIPTER';
export interface BotDetectResult {
isBot: boolean;
botType?: BotClassification;
botName?: string;
}
export interface TrafficFilterResult {
isBot: boolean;
category: 'HUMAN' | 'CRAWLER' | 'SUSPICIOUS_SPAM';
shouldRecordLead: boolean;
reason?: string;
}
const SEARCH_BOTS = ['googlebot', 'bingbot', 'baiduspider', 'yandexbot', 'duckduckbot', 'sogou'];
const SOCIAL_BOTS = ['whatsapp', 'twitterbot', 'facebookexternalhit', 'linkedinbot', 'telegrambot', 'slackbot'];
const SCRIPT_BOTS = ['curl', 'wget', 'python-requests', 'postman', 'httpclient', 'scrapy', 'axios'];
/**
* 基于 User-Agent 识别机器人与爬虫
*/
export function detectBotUserAgent(userAgent?: string): BotDetectResult {
if (!userAgent || typeof userAgent !== 'string') {
return { isBot: false };
}
const ua = userAgent.toLowerCase();
for (const bot of SEARCH_BOTS) {
if (ua.includes(bot)) {
return { isBot: true, botType: 'SEARCH_CRAWLER', botName: bot };
}
}
for (const bot of SOCIAL_BOTS) {
if (ua.includes(bot)) {
return { isBot: true, botType: 'SOCIAL_PREVIEW', botName: bot };
}
}
for (const bot of SCRIPT_BOTS) {
if (ua.includes(bot)) {
return { isBot: true, botType: 'SCRIPTER', botName: bot };
}
}
return { isBot: false };
}
/**
* 检测 1 秒内的频次是否异常超高
*/
export function isRateLimitSuspicious(timestamps: number[], thresholdPerSec = 10): boolean {
if (!timestamps || timestamps.length < thresholdPerSec) {
return false;
}
const sorted = [...timestamps].sort((a, b) => a - b);
const now = sorted[sorted.length - 1];
const countInLastSecond = sorted.filter(t => now - t <= 1000).length;
return countInLastSecond >= thresholdPerSec;
}
/**
* 访客雷达异常流量与爬虫清洗过滤服务
*/
@Provide()
export class RadarBotFilterService extends BaseService {
async filterIncomingTraffic(input: {
ip: string;
userAgent: string;
recentTimestamps?: number[];
}): Promise<TrafficFilterResult> {
const botCheck = detectBotUserAgent(input.userAgent);
if (botCheck.isBot) {
return {
isBot: true,
category: 'CRAWLER',
shouldRecordLead: false,
reason: `检测到 ${botCheck.botType}: ${botCheck.botName}`,
};
}
if (input.recentTimestamps && isRateLimitSuspicious(input.recentTimestamps)) {
return {
isBot: true,
category: 'SUSPICIOUS_SPAM',
shouldRecordLead: false,
reason: '1秒内突发异常高频请求,疑似自动化刷量攻击',
};
}
return {
isBot: false,
category: 'HUMAN',
shouldRecordLead: true,
};
}
}