diff --git a/packages/services/conversation-service/src/domain/entities/conversation.entity.ts b/packages/services/conversation-service/src/domain/entities/conversation.entity.ts index 655296e..355928e 100644 --- a/packages/services/conversation-service/src/domain/entities/conversation.entity.ts +++ b/packages/services/conversation-service/src/domain/entities/conversation.entity.ts @@ -20,6 +20,46 @@ export const ConversationStatus = { export type ConversationStatusType = (typeof ConversationStatus)[keyof typeof ConversationStatus]; +/** + * 咨询阶段常量 + */ +export const ConsultingStage = { + GREETING: 'greeting', + NEEDS_DISCOVERY: 'needs_discovery', + INFO_COLLECTION: 'info_collection', + ASSESSMENT: 'assessment', + RECOMMENDATION: 'recommendation', + OBJECTION_HANDLING: 'objection_handling', + CONVERSION: 'conversion', + HANDOFF: 'handoff', +} as const; + +export type ConsultingStageType = + (typeof ConsultingStage)[keyof typeof ConsultingStage]; + +/** + * 咨询状态结构(存储为JSON) + */ +export interface ConsultingStateJson { + strategyId: string; + currentStageId: string; + stageTurnCount: number; + collectedInfo: Record; + assessmentResult?: { + recommendedPrograms: string[]; + suitabilityScore: number; + highlights: string[]; + concerns: string[]; + }; + conversionPath?: string; + stageHistory: Array<{ + stageId: string; + enteredAt: string; + exitedAt?: string; + turnsInStage: number; + }>; +} + @Entity('conversations') export class ConversationEntity { @PrimaryGeneratedColumn('uuid') @@ -43,6 +83,77 @@ export class ConversationEntity { @Column({ name: 'message_count', default: 0 }) messageCount: number; + // ========== V2新增:咨询流程字段 ========== + + /** + * 当前咨询阶段 + */ + @Column({ + name: 'consulting_stage', + length: 30, + default: 'greeting', + nullable: true, + }) + consultingStage: ConsultingStageType; + + /** + * 咨询状态(完整的状态对象,包含收集的信息、评估结果等) + */ + @Column({ + name: 'consulting_state', + type: 'jsonb', + nullable: true, + }) + consultingState: ConsultingStateJson; + + /** + * 已收集的用户信息(快速查询用,与consultingState中的collectedInfo同步) + */ + @Column({ + name: 'collected_info', + type: 'jsonb', + nullable: true, + }) + collectedInfo: Record; + + /** + * 推荐的移民方案(评估后填充) + */ + @Column({ + name: 'recommended_programs', + type: 'text', + array: true, + nullable: true, + }) + recommendedPrograms: string[]; + + /** + * 转化路径(用户选择的下一步) + */ + @Column({ + name: 'conversion_path', + length: 30, + nullable: true, + }) + conversionPath: string; + + /** + * 用户设备信息(用于破冰) + */ + @Column({ + name: 'device_info', + type: 'jsonb', + nullable: true, + }) + deviceInfo: { + ip?: string; + userAgent?: string; + fingerprint?: string; + region?: string; + }; + + // ========== 原有字段 ========== + @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/packages/services/conversation-service/src/infrastructure/claude/claude-agent-v2.service.ts b/packages/services/conversation-service/src/infrastructure/claude/claude-agent-v2.service.ts new file mode 100644 index 0000000..76efc2e --- /dev/null +++ b/packages/services/conversation-service/src/infrastructure/claude/claude-agent-v2.service.ts @@ -0,0 +1,608 @@ +/** + * Claude Agent Service V2 - 集成策略引擎 + * + * 相比V1的主要改进: + * 1. 集成策略引擎,按老板定义的流程引导用户 + * 2. 对话有明确的阶段和目标 + * 3. 自动收集和保存用户信息 + * 4. 在关键节点自动调用必需的工具 + * 5. 支持老用户识别和个性化破冰 + */ + +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Anthropic from '@anthropic-ai/sdk'; +import { ImmigrationToolsService } from './tools/immigration-tools.service'; +import { TokenUsageService } from './token-usage.service'; +import { buildSystemPrompt, SystemPromptConfig } from './prompts/system-prompt'; +import { KnowledgeClientService } from '../knowledge/knowledge-client.service'; +import { intentClassifier, IntentResult, IntentType } from './intent-classifier'; +import { responseGate } from './response-gate'; +import { + StrategyEngineService, + ConsultingState, +} from './strategy/strategy-engine.service'; +import { + StageId, + getStageById, + DEFAULT_CONSULTING_STRATEGY, +} from './strategy/default-strategy'; + +export interface FileAttachment { + id: string; + originalName: string; + mimeType: string; + type: 'image' | 'document' | 'audio' | 'video' | 'other'; + size: number; + downloadUrl?: string; + thumbnailUrl?: string; +} + +export interface ConversationContext { + userId: string; + conversationId: string; + userMemory?: string[]; + previousMessages?: Array<{ + role: 'user' | 'assistant'; + content: string; + attachments?: FileAttachment[]; + }>; + // V2新增:咨询状态 + consultingState?: ConsultingState; + // V2新增:用户设备信息(用于新用户破冰) + deviceInfo?: { + ip?: string; + userAgent?: string; + fingerprint?: string; + }; +} + +export interface StreamChunk { + type: 'text' | 'tool_use' | 'tool_result' | 'end' | 'stage_change' | 'state_update'; + content?: string; + toolName?: string; + toolInput?: Record; + toolResult?: unknown; + // V2新增 + stageName?: string; + newState?: ConsultingState; +} + +@Injectable() +export class ClaudeAgentServiceV2 implements OnModuleInit { + private client: Anthropic; + private systemPromptConfig: SystemPromptConfig; + + constructor( + private configService: ConfigService, + private immigrationToolsService: ImmigrationToolsService, + private knowledgeClient: KnowledgeClientService, + private tokenUsageService: TokenUsageService, + private strategyEngine: StrategyEngineService, + ) {} + + onModuleInit() { + const baseUrl = this.configService.get('ANTHROPIC_BASE_URL'); + const isProxyUrl = baseUrl && (baseUrl.includes('67.223.119.33') || baseUrl.match(/^\d+\.\d+\.\d+\.\d+/)); + + if (isProxyUrl) { + console.log(`Using Anthropic proxy (TLS verification disabled): ${baseUrl}`); + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + + this.client = new Anthropic({ + apiKey: this.configService.get('ANTHROPIC_API_KEY'), + baseURL: baseUrl || undefined, + }); + + this.systemPromptConfig = { + identity: '专业、友善、耐心的香港移民顾问', + conversationStyle: '专业但不生硬,用简洁明了的语言解答', + }; + } + + /** + * 初始化或获取咨询状态 + */ + initializeOrGetState(context: ConversationContext): ConsultingState { + if (context.consultingState) { + return context.consultingState; + } + return this.strategyEngine.initializeState(); + } + + /** + * 发送消息并获取流式响应 - V2版本 + * + * 核心改进: + * 1. 在对话开始时检查用户历史(老用户/新用户) + * 2. 根据当前阶段注入行为指令 + * 3. 自动判断阶段转移 + * 4. 从对话中提取用户信息 + */ + async *sendMessage( + message: string, + context: ConversationContext, + attachments?: FileAttachment[], + ): AsyncGenerator { + // ========== 1. 初始化咨询状态 ========== + let state = this.initializeOrGetState(context); + const isFirstMessage = !context.previousMessages || context.previousMessages.length === 0; + + console.log(`[ClaudeAgentV2] Stage: ${state.currentStageId}, Turn: ${state.stageTurnCount}, IsFirst: ${isFirstMessage}`); + + // ========== 2. 检查阶段转移 ========== + if (!isFirstMessage) { + const transitionResult = await this.strategyEngine.evaluateTransition(state, message); + if (transitionResult.shouldTransition && transitionResult.targetStageId) { + console.log(`[ClaudeAgentV2] Stage transition: ${state.currentStageId} -> ${transitionResult.targetStageId} (${transitionResult.reason})`); + state = this.strategyEngine.transitionToStage(state, transitionResult.targetStageId); + yield { + type: 'stage_change', + stageName: getStageById(transitionResult.targetStageId)?.name, + }; + } + } + + // ========== 3. 意图分类(保留原有能力) ========== + const conversationHistory = context.previousMessages?.map(msg => ({ + role: msg.role, + content: msg.content, + })) || []; + const intent = intentClassifier.classify(message, conversationHistory); + + console.log(`[ClaudeAgentV2] Intent: ${intent.type}, Stage: ${state.currentStageId}`); + + // ========== 4. 构建增强的System Prompt ========== + const tools = this.immigrationToolsService.getTools(); + const accumulatedExperience = await this.getAccumulatedExperience(message); + + // 构建阶段引导 + const stageGuidance = this.strategyEngine.buildStageGuidance(state); + + // 构建设备信息提示(用于新用户破冰) + const deviceContext = this.buildDeviceContext(context, isFirstMessage); + + const dynamicConfig: SystemPromptConfig = { + ...this.systemPromptConfig, + accumulatedExperience, + intentHint: intent, + }; + let systemPrompt = buildSystemPrompt(dynamicConfig); + + // 注入策略引导到System Prompt + systemPrompt = this.injectStrategyGuidance(systemPrompt, stageGuidance, deviceContext, state); + + // ========== 5. 构建消息数组 ========== + const messages: Anthropic.MessageParam[] = []; + + if (context.previousMessages) { + for (const msg of context.previousMessages) { + if (msg.attachments && msg.attachments.length > 0 && msg.role === 'user') { + const multimodalContent = await this.buildMultimodalContent(msg.content, msg.attachments); + messages.push({ role: msg.role, content: multimodalContent }); + } else { + messages.push({ role: msg.role, content: msg.content }); + } + } + } + + if (attachments && attachments.length > 0) { + const multimodalContent = await this.buildMultimodalContent(message, attachments); + messages.push({ role: 'user', content: multimodalContent }); + } else { + messages.push({ role: 'user', content: message }); + } + + // ========== 6. 调用Claude API ========== + const maxIterations = 10; + let iterations = 0; + const maxTokens = this.calculateMaxTokens(intent, state); + + const systemWithCache: Anthropic.TextBlockParam[] = [ + { + type: 'text', + text: systemPrompt, + cache_control: { type: 'ephemeral' }, + }, + ]; + + let fullResponseText = ''; + const startTime = Date.now(); + let totalInputTokens = 0; + let totalOutputTokens = 0; + let totalCacheCreationTokens = 0; + let totalCacheReadTokens = 0; + let toolCallCount = 0; + + while (iterations < maxIterations) { + iterations++; + + try { + const stream = await this.client.messages.stream({ + model: 'claude-sonnet-4-20250514', + max_tokens: maxTokens, + system: systemWithCache, + messages, + tools: tools as Anthropic.Tool[], + }); + + let currentToolUse: { + id: string; + name: string; + inputJson: string; + input: Record; + } | null = null; + + const toolUses: Array<{ id: string; name: string; input: Record }> = []; + const assistantContent: Anthropic.ContentBlockParam[] = []; + + for await (const event of stream) { + if (event.type === 'content_block_start') { + if (event.content_block.type === 'tool_use') { + currentToolUse = { + id: event.content_block.id, + name: event.content_block.name, + inputJson: '', + input: {}, + }; + } + } else if (event.type === 'content_block_delta') { + if (event.delta.type === 'text_delta') { + fullResponseText += event.delta.text; + yield { type: 'text', content: event.delta.text }; + } else if (event.delta.type === 'input_json_delta' && currentToolUse) { + currentToolUse.inputJson += event.delta.partial_json || ''; + } + } else if (event.type === 'content_block_stop') { + if (currentToolUse) { + try { + currentToolUse.input = JSON.parse(currentToolUse.inputJson || '{}'); + } catch { + currentToolUse.input = {}; + } + + toolUses.push({ + id: currentToolUse.id, + name: currentToolUse.name, + input: currentToolUse.input, + }); + + yield { + type: 'tool_use', + toolName: currentToolUse.name, + toolInput: currentToolUse.input, + }; + + currentToolUse = null; + } + } + } + + const finalMsg = await stream.finalMessage(); + + if (finalMsg.usage) { + totalInputTokens += finalMsg.usage.input_tokens || 0; + totalOutputTokens += finalMsg.usage.output_tokens || 0; + const usage = finalMsg.usage as unknown as Record; + totalCacheCreationTokens += usage.cache_creation_input_tokens || 0; + totalCacheReadTokens += usage.cache_read_input_tokens || 0; + } + + // 如果没有工具调用,完成 + if (toolUses.length === 0) { + // ========== 7. 回复后处理 ========== + + // 7.1 质量门控 + if (fullResponseText) { + const gateResult = responseGate.check(fullResponseText, intent, message); + console.log(`[ClaudeAgentV2] Response gate: passed=${gateResult.passed}, length=${fullResponseText.length}`); + } + + // 7.2 提取用户信息 + const newInfo = await this.strategyEngine.extractUserInfo( + message, + fullResponseText, + state.collectedInfo, + ); + if (Object.keys(newInfo).length > 0) { + console.log(`[ClaudeAgentV2] Extracted user info:`, newInfo); + state = this.strategyEngine.updateCollectedInfo(state, newInfo); + } + + // 7.3 如果在评估阶段且信息充足,执行评估 + if (state.currentStageId === StageId.ASSESSMENT && !state.assessmentResult) { + const coreInfoCount = ['age', 'education', 'workYears', 'annualIncome'] + .filter(key => state.collectedInfo[key] !== undefined).length; + if (coreInfoCount >= 3) { + const assessmentResult = await this.strategyEngine.performAssessment(state.collectedInfo); + state = this.strategyEngine.setAssessmentResult(state, assessmentResult); + console.log(`[ClaudeAgentV2] Assessment completed:`, assessmentResult); + } + } + + // 7.4 增加轮次计数 + state = this.strategyEngine.incrementTurnCount(state); + + // 7.5 记录Token使用量 + const latencyMs = Date.now() - startTime; + this.tokenUsageService.recordUsage({ + userId: context.userId, + conversationId: context.conversationId, + model: 'claude-sonnet-4-20250514', + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + cacheCreationTokens: totalCacheCreationTokens, + cacheReadTokens: totalCacheReadTokens, + intentType: intent.type, + toolCalls: toolCallCount, + responseLength: fullResponseText.length, + latencyMs, + }).catch(err => console.error('[ClaudeAgentV2] Failed to record token usage:', err)); + + // 7.6 返回更新后的状态 + yield { type: 'state_update', newState: state }; + yield { type: 'end' }; + return; + } + + // 处理工具调用 + toolCallCount += toolUses.length; + + for (const block of finalMsg.content) { + if (block.type === 'text') { + assistantContent.push({ type: 'text', text: block.text }); + } else if (block.type === 'tool_use') { + assistantContent.push({ + type: 'tool_use', + id: block.id, + name: block.name, + input: block.input as Record, + }); + } + } + + messages.push({ role: 'assistant', content: assistantContent }); + + const toolResults: Anthropic.ToolResultBlockParam[] = []; + for (const toolUse of toolUses) { + const result = await this.immigrationToolsService.executeTool( + toolUse.name, + toolUse.input, + context, + ); + + yield { + type: 'tool_result', + toolName: toolUse.name, + toolResult: result, + }; + + toolResults.push({ + type: 'tool_result', + tool_use_id: toolUse.id, + content: JSON.stringify(result), + }); + + // 如果是get_user_context工具,从结果中提取已有信息 + if (toolUse.name === 'get_user_context' && result) { + const userContextResult = result as { memories?: Array<{ type: string; content: string }> }; + if (userContextResult.memories && userContextResult.memories.length > 0) { + console.log(`[ClaudeAgentV2] Found existing user memories:`, userContextResult.memories.length); + // 可以从记忆中提取已知信息更新state + } + } + } + + messages.push({ role: 'user', content: toolResults }); + + } catch (error) { + console.error('[ClaudeAgentV2] Claude API error:', error); + throw error; + } + } + + console.error('[ClaudeAgentV2] Tool loop exceeded maximum iterations'); + yield { type: 'end' }; + } + + /** + * 注入策略引导到System Prompt + */ + private injectStrategyGuidance( + basePrompt: string, + stageGuidance: string, + deviceContext: string, + state: ConsultingState, + ): string { + const strategy = this.strategyEngine.getStrategy(); + const stage = this.strategyEngine.getCurrentStage(state); + + const injectedContent = ` +${basePrompt} + +================================================================================ +【重要】咨询流程控制 - 你必须按照以下策略执行 +================================================================================ + +${stageGuidance} + +${deviceContext} + +## 工具调用要求 +当前阶段建议使用的工具: +${stage.suggestedTools.map(t => { + const priorityLabel = t.priority === 'required' ? '【必须调用】' : t.priority === 'recommended' ? '【建议调用】' : '【可选】'; + return `- ${priorityLabel} ${t.toolName}: ${t.description}\n 触发条件: ${t.triggerCondition}`; +}).join('\n')} + +## 非移民话题处理 +如果用户询问与香港移民无关的问题: +1. 调用 check_off_topic 工具确认是否离题 +2. 如果确认离题,礼貌拒绝并引导回移民话题 +3. 示例回复:"抱歉,我是专门的香港移民咨询顾问,对于[用户问题的领域]我不太了解。如果您有香港移民相关的问题,我很乐意帮您解答。" + +## 专家对接信息 +- 人类专家微信: ${strategy.expertContact.wechat} +- 专家电话: ${strategy.expertContact.phone} +- 工作时间: ${strategy.expertContact.workingHours} + +## 付费服务信息 +- 评估服务价格: ¥${strategy.paidServices.assessmentPrice} +- 服务说明: ${strategy.paidServices.description} + +================================================================================ +`; + + return injectedContent; + } + + /** + * 构建设备上下文信息(用于新用户破冰) + */ + private buildDeviceContext(context: ConversationContext, isFirstMessage: boolean): string { + if (!isFirstMessage || !context.deviceInfo) { + return ''; + } + + const parts: string[] = ['## 用户设备信息(可用于破冰参考)']; + + if (context.deviceInfo.ip) { + // 可以根据IP判断地区 + parts.push(`- IP地址: ${context.deviceInfo.ip} (可用于判断用户所在地区)`); + } + + if (context.deviceInfo.userAgent) { + const ua = context.deviceInfo.userAgent; + if (ua.includes('Mobile')) { + parts.push('- 设备类型: 移动设备'); + } else { + parts.push('- 设备类型: 电脑'); + } + } + + if (context.deviceInfo.fingerprint) { + parts.push(`- 设备指纹: ${context.deviceInfo.fingerprint.slice(0, 8)}...`); + } + + parts.push(''); + parts.push('提示: 可以根据设备信息进行自然的寒暄,如"在手机上咨询呢"等'); + + return parts.join('\n'); + } + + /** + * 根据意图和阶段计算max_tokens + */ + private calculateMaxTokens(intent: IntentResult, state: ConsultingState): number { + const stage = this.strategyEngine.getCurrentStage(state); + const baseLength = stage.suggestedResponseLength; + + // 中文约 1.8 tokens/字符 + const tokensPerChar = 1.8; + const baseTokens = Math.round(baseLength * tokensPerChar); + + // 根据阶段调整 + switch (state.currentStageId) { + case StageId.GREETING: + return Math.min(400, baseTokens); + case StageId.NEEDS_DISCOVERY: + return Math.min(500, baseTokens); + case StageId.INFO_COLLECTION: + return Math.min(450, baseTokens); + case StageId.ASSESSMENT: + return Math.min(800, baseTokens); + case StageId.RECOMMENDATION: + return Math.min(1000, baseTokens); + case StageId.OBJECTION_HANDLING: + return Math.min(600, baseTokens); + case StageId.CONVERSION: + return Math.min(600, baseTokens); + case StageId.HANDOFF: + return Math.min(500, baseTokens); + default: + return 800; + } + } + + /** + * 获取已积累的经验 + */ + private async getAccumulatedExperience(query: string): Promise { + try { + const experiences = await this.knowledgeClient.searchExperiences({ + query, + activeOnly: true, + limit: 5, + }); + + if (experiences.length === 0) { + return '暂无'; + } + + return experiences + .map((exp, index) => `${index + 1}. [${exp.experienceType}] ${exp.content}`) + .join('\n'); + } catch (error) { + console.error('[ClaudeAgentV2] Failed to fetch experiences:', error); + return '暂无'; + } + } + + /** + * 构建多模态内容 + */ + private async buildMultimodalContent( + text: string, + attachments?: FileAttachment[], + ): Promise { + const content: Anthropic.ContentBlockParam[] = []; + + if (attachments && attachments.length > 0) { + for (const attachment of attachments) { + if (attachment.type === 'image' && attachment.downloadUrl) { + try { + const response = await fetch(attachment.downloadUrl); + if (response.ok) { + const buffer = await response.arrayBuffer(); + const base64Data = Buffer.from(buffer).toString('base64'); + const mediaType = attachment.mimeType as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp'; + + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: mediaType, + data: base64Data, + }, + }); + } + } catch (error) { + console.error(`Failed to fetch image ${attachment.originalName}:`, error); + } + } else if (attachment.type === 'document') { + content.push({ + type: 'text', + text: `[Attached document: ${attachment.originalName}]`, + }); + } + } + } + + if (text) { + content.push({ type: 'text', text }); + } + + return content; + } + + /** + * 更新系统提示配置 + */ + updateSystemPromptConfig(config: Partial) { + this.systemPromptConfig = { + ...this.systemPromptConfig, + ...config, + }; + } +} diff --git a/packages/services/conversation-service/src/infrastructure/claude/claude.module.ts b/packages/services/conversation-service/src/infrastructure/claude/claude.module.ts index e82661a..96ef1a4 100644 --- a/packages/services/conversation-service/src/infrastructure/claude/claude.module.ts +++ b/packages/services/conversation-service/src/infrastructure/claude/claude.module.ts @@ -2,8 +2,10 @@ import { Module, Global } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ClaudeAgentService } from './claude-agent.service'; +import { ClaudeAgentServiceV2 } from './claude-agent-v2.service'; import { ImmigrationToolsService } from './tools/immigration-tools.service'; import { TokenUsageService } from './token-usage.service'; +import { StrategyEngineService } from './strategy/strategy-engine.service'; import { TokenUsageEntity } from '../../domain/entities/token-usage.entity'; import { KnowledgeModule } from '../knowledge/knowledge.module'; @@ -14,7 +16,19 @@ import { KnowledgeModule } from '../knowledge/knowledge.module'; KnowledgeModule, TypeOrmModule.forFeature([TokenUsageEntity]), ], - providers: [ClaudeAgentService, ImmigrationToolsService, TokenUsageService], - exports: [ClaudeAgentService, ImmigrationToolsService, TokenUsageService], + providers: [ + ClaudeAgentService, + ClaudeAgentServiceV2, + ImmigrationToolsService, + TokenUsageService, + StrategyEngineService, + ], + exports: [ + ClaudeAgentService, + ClaudeAgentServiceV2, + ImmigrationToolsService, + TokenUsageService, + StrategyEngineService, + ], }) export class ClaudeModule {} diff --git a/packages/services/conversation-service/src/infrastructure/claude/strategy/default-strategy.ts b/packages/services/conversation-service/src/infrastructure/claude/strategy/default-strategy.ts new file mode 100644 index 0000000..48f37f6 --- /dev/null +++ b/packages/services/conversation-service/src/infrastructure/claude/strategy/default-strategy.ts @@ -0,0 +1,1158 @@ +/** + * 默认移民咨询策略 + * + * 这是系统的核心咨询流程,定义了Agent如何主动引导用户完成移民咨询。 + * 老板可以通过后台修改或创建新策略来覆盖此默认行为。 + */ + +// ============================================================================ +// 类型定义 +// ============================================================================ + +/** + * 咨询阶段ID + */ +export enum StageId { + GREETING = 'greeting', // 开场破冰 + NEEDS_DISCOVERY = 'needs_discovery', // 需求了解 + INFO_COLLECTION = 'info_collection', // 背景收集 + ASSESSMENT = 'assessment', // 资格评估 + RECOMMENDATION = 'recommendation', // 方案推荐 + OBJECTION_HANDLING = 'objection_handling', // 异议处理 + CONVERSION = 'conversion', // 转化促成 + HANDOFF = 'handoff', // 完成对接 +} + +/** + * 指令类型 + */ +export enum DirectiveType { + MUST_DO = 'MUST_DO', // 必须做 + SHOULD_DO = 'SHOULD_DO', // 建议做 + MUST_NOT = 'MUST_NOT', // 禁止做 + PREFER = 'PREFER', // 优先考虑 +} + +/** + * 转化路径类型 + */ +export enum ConversionPath { + PAID_ASSESSMENT = 'paid_assessment', // 付费评估 + HUMAN_EXPERT = 'human_expert', // 对接人类专家 + CONTINUE_FREE = 'continue_free', // 继续免费咨询 +} + +/** + * 行为指令 + */ +export interface Directive { + type: DirectiveType; + content: string; + priority: number; +} + +/** + * 需要收集的用户信息 + */ +export interface RequiredInfo { + key: string; + label: string; + description: string; + askingStrategy: string; // 建议的提问方式 + validation?: string; // 验证规则 + isRequired: boolean; + priority: number; // 收集优先级,数字越小越优先 +} + +/** + * 阶段转移条件 + */ +export interface TransitionCondition { + type: 'INFO_COLLECTED' | 'USER_INTENT' | 'TURNS_EXCEEDED' | 'KEYWORD' | 'ASSESSMENT_DONE' | 'CUSTOM'; + // INFO_COLLECTED: 指定信息已收集 + infoKeys?: string[]; + minCount?: number; // 至少收集几项 + // USER_INTENT: 用户表达了某种意图 + intents?: string[]; + // TURNS_EXCEEDED: 超过指定轮次 + maxTurns?: number; + // KEYWORD: 包含关键词 + keywords?: string[]; + // CUSTOM: 自定义条件描述(LLM判断) + customCondition?: string; +} + +/** + * 阶段转移 + */ +export interface StageTransition { + targetStageId: StageId; + conditions: TransitionCondition[]; + priority: number; // 优先级,数字越大越优先 + description: string; // 转移说明 +} + +/** + * 阶段建议使用的工具 + */ +export interface StageTool { + toolName: string; + description: string; + triggerCondition: string; // 什么情况下应该调用 + priority: 'required' | 'recommended' | 'optional'; +} + +/** + * 咨询阶段 + */ +export interface ConsultingStage { + id: StageId; + name: string; + description: string; + order: number; + + // 阶段目标 + goal: string; + + // 成功标准 + successCriteria: string[]; + + // 行为指令 + directives: Directive[]; + + // 需要收集的信息 + requiredInfo: RequiredInfo[]; + + // 建议使用的工具(确保信息准确) + suggestedTools: StageTool[]; + + // 阶段转移规则 + transitions: StageTransition[]; + + // 最大轮次(超过后强制转移) + maxTurns: number; + + // 建议的回复长度 + suggestedResponseLength: number; +} + +/** + * 完整咨询策略 + */ +export interface ConsultingStrategy { + id: string; + name: string; + description: string; + version: string; + isDefault: boolean; + + // 阶段定义 + stages: ConsultingStage[]; + + // 全局指令(适用于所有阶段) + globalDirectives: Directive[]; + + // 专家联系方式配置 + expertContact: { + wechat?: string; + phone?: string; + email?: string; + workingHours?: string; + }; + + // 付费服务配置 + paidServices: { + assessmentPrice: number; + currency: string; + description: string; + }; +} + +// ============================================================================ +// 默认策略定义 +// ============================================================================ + +/** + * 需要收集的核心用户信息 + */ +export const CORE_USER_INFO: RequiredInfo[] = [ + { + key: 'age', + label: '年龄', + description: '用户的年龄', + askingStrategy: '方便问一下您今年多大了吗?', + validation: '18-65之间的数字', + isRequired: true, + priority: 1, + }, + { + key: 'education', + label: '最高学历', + description: '用户的最高学历', + askingStrategy: '您的最高学历是什么呢?(如本科、硕士、博士等)', + isRequired: true, + priority: 2, + }, + { + key: 'university', + label: '毕业院校', + description: '用户毕业的学校', + askingStrategy: '请问您是哪所学校毕业的?', + isRequired: false, + priority: 3, + }, + { + key: 'major', + label: '专业', + description: '用户的专业', + askingStrategy: '您学的是什么专业?', + isRequired: false, + priority: 4, + }, + { + key: 'workYears', + label: '工作年限', + description: '用户的工作年限', + askingStrategy: '您工作几年了?', + isRequired: true, + priority: 5, + }, + { + key: 'industry', + label: '所在行业', + description: '用户所在的行业', + askingStrategy: '您目前在什么行业工作?', + isRequired: true, + priority: 6, + }, + { + key: 'jobTitle', + label: '职位', + description: '用户的职位', + askingStrategy: '您目前的职位是什么?', + isRequired: false, + priority: 7, + }, + { + key: 'annualIncome', + label: '年收入', + description: '用户的年收入(人民币)', + askingStrategy: '方便透露一下您的年收入大概在什么范围吗?(大概范围就行,比如30-50万、50-100万等)', + isRequired: true, + priority: 8, + }, + { + key: 'immigrationPurpose', + label: '移民目的', + description: '用户想移民的主要原因', + askingStrategy: '您考虑移民香港主要是出于什么考虑呢?(比如子女教育、事业发展、税务规划等)', + isRequired: true, + priority: 9, + }, + { + key: 'timeline', + label: '时间规划', + description: '用户的移民时间规划', + askingStrategy: '您计划什么时候开始办理?有没有比较紧迫的时间要求?', + isRequired: false, + priority: 10, + }, + { + key: 'hasHKConnection', + label: '香港联系', + description: '用户是否有香港的工作机会或雇主', + askingStrategy: '您目前有没有香港的工作机会或者认识的香港雇主?', + isRequired: false, + priority: 11, + }, +]; + +/** + * 默认咨询策略 + */ +export const DEFAULT_CONSULTING_STRATEGY: ConsultingStrategy = { + id: 'default-hk-immigration', + name: '香港移民咨询默认策略', + description: '标准的香港移民咨询流程,适用于大多数咨询场景', + version: '1.0.0', + isDefault: true, + + // ======================================== + // 全局指令 + // ======================================== + globalDirectives: [ + { + type: DirectiveType.MUST_DO, + content: '每次回复结尾都要有一个推进对话的问题或明确的下一步引导', + priority: 100, + }, + { + type: DirectiveType.MUST_DO, + content: '保持专业但亲切的语气,像一个经验丰富的朋友在给建议', + priority: 95, + }, + { + type: DirectiveType.MUST_NOT, + content: '绝对不能说"保证成功"、"100%通过"、"包过"之类的承诺', + priority: 100, + }, + { + type: DirectiveType.MUST_NOT, + content: '不要一次问超过2个问题,会让用户感到压力', + priority: 90, + }, + { + type: DirectiveType.MUST_NOT, + content: '不要回避价格、费用、难度等敏感问题,要诚实回答', + priority: 90, + }, + { + type: DirectiveType.SHOULD_DO, + content: '适时总结和确认已了解的信息,让用户感到被认真对待', + priority: 70, + }, + { + type: DirectiveType.SHOULD_DO, + content: '如果用户问了与移民无关的问题,礼貌引导回移民话题', + priority: 60, + }, + { + type: DirectiveType.PREFER, + content: '回复要简洁有力,避免长篇大论,重点突出', + priority: 50, + }, + ], + + // ======================================== + // 专家联系方式 + // ======================================== + expertContact: { + wechat: 'HK_Immigration_Expert', + phone: '+852-1234-5678', + email: 'expert@iconsulting.com', + workingHours: '周一至周五 9:00-18:00(香港时间)', + }, + + // ======================================== + // 付费服务配置 + // ======================================== + paidServices: { + assessmentPrice: 99, + currency: 'CNY', + description: '专业移民顾问1对1评估,包含详细的方案分析和申请建议', + }, + + // ======================================== + // 阶段定义 + // ======================================== + stages: [ + // ---------------------------------------- + // 阶段1: 开场破冰 + // ---------------------------------------- + { + id: StageId.GREETING, + name: '开场破冰', + description: '热情欢迎用户,建立初步信任,根据用户历史做个性化问候', + order: 1, + goal: '让用户感到被欢迎和被重视,愿意继续对话', + successCriteria: [ + '用户回复了至少一条消息', + '用户没有表现出不耐烦或想离开', + '如果是老用户,提及之前的咨询让用户感到被记住', + ], + suggestedResponseLength: 150, + maxTurns: 3, + + directives: [ + { + type: DirectiveType.MUST_DO, + content: '【首要】在对话开始时立即调用get_user_context工具,获取用户历史信息', + priority: 100, + }, + { + type: DirectiveType.MUST_DO, + content: '如果是老用户:提及上次咨询的内容,如"您好!上次您咨询的是高才通,后来考虑得怎么样了?"', + priority: 98, + }, + { + type: DirectiveType.MUST_DO, + content: '如果是新用户:热情但不过度,用"您好"开头,简单介绍自己是香港移民咨询顾问', + priority: 95, + }, + { + type: DirectiveType.SHOULD_DO, + content: '可以问一下用户是怎么了解到我们的(但不强求回答)', + priority: 50, + }, + { + type: DirectiveType.MUST_NOT, + content: '不要在开场就问太多个人信息,会让人有压力', + priority: 90, + }, + ], + + requiredInfo: [], + + suggestedTools: [ + { + toolName: 'get_user_context', + description: '获取用户历史信息和记忆,判断是否为老用户,提取历史咨询记录', + triggerCondition: '【必须】对话开始的第一轮就要调用,获取用户所有历史资料', + priority: 'required', + }, + ], + + transitions: [ + { + targetStageId: StageId.NEEDS_DISCOVERY, + conditions: [ + { type: 'USER_INTENT', intents: ['ask_about_immigration', 'want_to_immigrate', 'curious_about_hk'] }, + ], + priority: 10, + description: '用户表达了对移民的兴趣', + }, + { + targetStageId: StageId.INFO_COLLECTION, + conditions: [ + { type: 'CUSTOM', customCondition: '老用户已有足够背景信息(年龄、学历、工作年限、收入)' }, + ], + priority: 15, + description: '老用户信息充足,跳过需求了解直接进入信息确认', + }, + { + targetStageId: StageId.NEEDS_DISCOVERY, + conditions: [ + { type: 'TURNS_EXCEEDED', maxTurns: 2 }, + ], + priority: 5, + description: '开场超过2轮,主动进入需求了解', + }, + ], + }, + + // ---------------------------------------- + // 阶段2: 需求了解 + // ---------------------------------------- + { + id: StageId.NEEDS_DISCOVERY, + name: '需求了解', + description: '了解用户为什么想移民香港,有什么期望和顾虑', + order: 2, + goal: '理解用户的移民动机和核心需求', + successCriteria: [ + '了解用户想移民的主要原因', + '了解用户对香港移民的基本认知水平', + ], + suggestedResponseLength: 200, + maxTurns: 5, + + directives: [ + { + type: DirectiveType.MUST_DO, + content: '询问用户考虑移民香港的主要原因(子女教育/事业发展/税务规划/生活品质等)', + priority: 100, + }, + { + type: DirectiveType.SHOULD_DO, + content: '了解用户对香港移民政策了解多少,针对性地介绍', + priority: 80, + }, + { + type: DirectiveType.SHOULD_DO, + content: '如果用户提到具体的移民类型(如高才通),先调用search_knowledge获取准确信息再回应', + priority: 70, + }, + { + type: DirectiveType.MUST_DO, + content: '回答政策问题时,必须先调用search_knowledge从知识库获取准确信息,不要凭记忆回答', + priority: 95, + }, + ], + + requiredInfo: [ + CORE_USER_INFO.find(i => i.key === 'immigrationPurpose')!, + ], + + suggestedTools: [ + { + toolName: 'search_knowledge', + description: '搜索知识库获取移民政策的准确信息', + triggerCondition: '当用户询问具体政策、要求、流程时,必须调用以确保信息准确', + priority: 'required', + }, + { + toolName: 'web_search', + description: '搜索官方网站获取最新政策变化', + triggerCondition: '当用户问到"最新"、"现在"、"2024年"等时效性问题时调用', + priority: 'recommended', + }, + ], + + transitions: [ + { + targetStageId: StageId.INFO_COLLECTION, + conditions: [ + { type: 'INFO_COLLECTED', infoKeys: ['immigrationPurpose'] }, + ], + priority: 10, + description: '已了解用户移民目的,进入信息收集', + }, + { + targetStageId: StageId.INFO_COLLECTION, + conditions: [ + { type: 'USER_INTENT', intents: ['want_assessment', 'check_eligibility'] }, + ], + priority: 15, + description: '用户主动想做评估', + }, + { + targetStageId: StageId.INFO_COLLECTION, + conditions: [ + { type: 'TURNS_EXCEEDED', maxTurns: 4 }, + ], + priority: 5, + description: '需求了解超过4轮,进入信息收集', + }, + ], + }, + + // ---------------------------------------- + // 阶段3: 背景信息收集 + // ---------------------------------------- + { + id: StageId.INFO_COLLECTION, + name: '背景信息收集', + description: '收集用户的年龄、学历、工作、收入等评估所需信息', + order: 3, + goal: '收集足够的信息来判断用户适合哪种移民方案', + successCriteria: [ + '收集到年龄、学历、工作年限、年收入这4项核心信息', + '用户没有因为问题太多而流失', + ], + suggestedResponseLength: 180, + maxTurns: 10, + + directives: [ + { + type: DirectiveType.MUST_DO, + content: '每次只问1-2个问题,不要让用户感到被审问', + priority: 100, + }, + { + type: DirectiveType.MUST_DO, + content: '解释为什么要问这些信息:"为了帮您找到最合适的方案,我需要了解一下..."', + priority: 95, + }, + { + type: DirectiveType.SHOULD_DO, + content: '收集到一些信息后,可以给出初步的积极反馈:"您的条件看起来不错"', + priority: 70, + }, + { + type: DirectiveType.SHOULD_DO, + content: '如果用户不愿意透露某些信息,不要强求,跳过继续', + priority: 80, + }, + { + type: DirectiveType.PREFER, + content: '优先收集:年龄 → 学历/院校 → 工作年限 → 年收入', + priority: 60, + }, + ], + + requiredInfo: CORE_USER_INFO.filter(i => + ['age', 'education', 'workYears', 'annualIncome'].includes(i.key) + ), + + suggestedTools: [ + { + toolName: 'save_user_memory', + description: '保存用户提供的背景信息到长期记忆', + triggerCondition: '每当用户透露了个人信息(年龄、学历、收入等)时,立即保存', + priority: 'required', + }, + { + toolName: 'check_off_topic', + description: '检查用户问题是否与移民无关', + triggerCondition: '当用户问题明显偏离移民话题时调用', + priority: 'recommended', + }, + ], + + transitions: [ + { + targetStageId: StageId.ASSESSMENT, + conditions: [ + { + type: 'INFO_COLLECTED', + infoKeys: ['age', 'education', 'workYears', 'annualIncome'], + minCount: 4, + }, + ], + priority: 10, + description: '核心信息收集完成,进入评估', + }, + { + targetStageId: StageId.ASSESSMENT, + conditions: [ + { + type: 'INFO_COLLECTED', + infoKeys: ['age', 'education', 'workYears', 'annualIncome'], + minCount: 3, + }, + { type: 'TURNS_EXCEEDED', maxTurns: 8 }, + ], + priority: 8, + description: '收集了大部分信息且轮次较多,进入评估', + }, + { + targetStageId: StageId.ASSESSMENT, + conditions: [ + { type: 'USER_INTENT', intents: ['want_result_now', 'impatient'] }, + ], + priority: 15, + description: '用户急于知道结果', + }, + ], + }, + + // ---------------------------------------- + // 阶段4: 资格评估 + // ---------------------------------------- + { + id: StageId.ASSESSMENT, + name: '资格评估', + description: '根据收集的信息,评估用户适合哪些移民方案', + order: 4, + goal: '给出初步的评估结论,判断用户适合的移民类型', + successCriteria: [ + '明确告诉用户他最适合的1-2个方案', + '简要说明为什么适合', + ], + suggestedResponseLength: 300, + maxTurns: 3, + + directives: [ + { + type: DirectiveType.MUST_DO, + content: '基于已收集的信息,明确推荐最适合的移民方案(高才通/优才/专才等)', + priority: 100, + }, + { + type: DirectiveType.MUST_DO, + content: '【重要】给出评估结论前,必须调用search_knowledge获取各方案的最新要求,确保推荐准确', + priority: 98, + }, + { + type: DirectiveType.MUST_DO, + content: '解释为什么推荐这个方案,与用户的条件对应起来', + priority: 95, + }, + { + type: DirectiveType.SHOULD_DO, + content: '如果用户条件很好,要表达积极信号;如果条件一般,要诚实但给出希望', + priority: 80, + }, + { + type: DirectiveType.MUST_NOT, + content: '不要给出成功率的具体数字承诺', + priority: 100, + }, + ], + + requiredInfo: [], + + suggestedTools: [ + { + toolName: 'search_knowledge', + description: '搜索各移民方案的具体要求和标准', + triggerCondition: '【必须】在给出评估结论前,搜索推荐方案的官方要求', + priority: 'required', + }, + { + toolName: 'web_search', + description: '搜索最新政策,确保评估基于最新信息', + triggerCondition: '当知识库信息可能过时时,搜索入境处官网获取最新政策', + priority: 'recommended', + }, + { + toolName: 'collect_assessment_info', + description: '汇总收集的用户信息用于评估', + triggerCondition: '开始评估前,整理所有已收集的用户信息', + priority: 'recommended', + }, + ], + + transitions: [ + { + targetStageId: StageId.RECOMMENDATION, + conditions: [ + { type: 'TURNS_EXCEEDED', maxTurns: 2 }, + ], + priority: 10, + description: '评估完成,进入详细方案推荐', + }, + { + targetStageId: StageId.RECOMMENDATION, + conditions: [ + { type: 'USER_INTENT', intents: ['want_details', 'interested', 'tell_me_more'] }, + ], + priority: 15, + description: '用户想了解更多细节', + }, + { + targetStageId: StageId.OBJECTION_HANDLING, + conditions: [ + { type: 'USER_INTENT', intents: ['has_doubt', 'not_sure', 'worried'] }, + ], + priority: 12, + description: '用户有顾虑,进入异议处理', + }, + ], + }, + + // ---------------------------------------- + // 阶段5: 方案推荐 + // ---------------------------------------- + { + id: StageId.RECOMMENDATION, + name: '方案推荐', + description: '详细介绍推荐的移民方案,包括流程、材料、时间、费用等', + order: 5, + goal: '让用户充分了解推荐方案的具体内容', + successCriteria: [ + '用户了解了申请流程的大致步骤', + '用户了解了大概的时间和费用', + ], + suggestedResponseLength: 400, + maxTurns: 6, + + directives: [ + { + type: DirectiveType.MUST_DO, + content: '【重要】介绍流程前,必须调用search_knowledge获取该方案的详细流程、材料清单、费用等信息', + priority: 100, + }, + { + type: DirectiveType.MUST_DO, + content: '介绍推荐方案的基本流程(准备材料→递交申请→审批→获批)', + priority: 98, + }, + { + type: DirectiveType.SHOULD_DO, + content: '说明大致的时间周期和费用范围', + priority: 90, + }, + { + type: DirectiveType.SHOULD_DO, + content: '强调该方案对用户的优势(审批快/成功率高/要求匹配等)', + priority: 80, + }, + { + type: DirectiveType.MUST_NOT, + content: '不要一次把所有信息都倒出来,根据用户问题逐步展开', + priority: 85, + }, + ], + + requiredInfo: [], + + suggestedTools: [ + { + toolName: 'search_knowledge', + description: '搜索方案的详细流程、材料清单、费用信息', + triggerCondition: '【必须】介绍任何具体信息前,先从知识库获取准确数据', + priority: 'required', + }, + { + toolName: 'web_search', + description: '搜索入境处官网获取最新政策和流程', + triggerCondition: '当用户询问最新政策、审批时间、费用变化时调用', + priority: 'recommended', + }, + { + toolName: 'fetch_immigration_news', + description: '获取移民相关新闻', + triggerCondition: '当用户问到政策变化或新闻时调用', + priority: 'optional', + }, + ], + + transitions: [ + { + targetStageId: StageId.CONVERSION, + conditions: [ + { type: 'USER_INTENT', intents: ['want_to_proceed', 'how_to_start', 'next_step'] }, + ], + priority: 15, + description: '用户想进一步行动', + }, + { + targetStageId: StageId.OBJECTION_HANDLING, + conditions: [ + { type: 'USER_INTENT', intents: ['has_concern', 'too_expensive', 'too_difficult', 'not_confident'] }, + ], + priority: 12, + description: '用户有顾虑', + }, + { + targetStageId: StageId.CONVERSION, + conditions: [ + { type: 'TURNS_EXCEEDED', maxTurns: 5 }, + ], + priority: 8, + description: '介绍充分,引导转化', + }, + ], + }, + + // ---------------------------------------- + // 阶段6: 异议处理 + // ---------------------------------------- + { + id: StageId.OBJECTION_HANDLING, + name: '异议处理', + description: '解答用户的疑虑、担忧和反对意见', + order: 6, + goal: '消除用户的顾虑,建立信心', + successCriteria: [ + '用户的主要顾虑得到了回应', + '用户态度有所缓和或积极', + ], + suggestedResponseLength: 250, + maxTurns: 5, + + directives: [ + { + type: DirectiveType.MUST_DO, + content: '认真倾听用户的顾虑,先表示理解再回应', + priority: 100, + }, + { + type: DirectiveType.MUST_DO, + content: '用事实和逻辑回应,不要空洞安慰', + priority: 95, + }, + { + type: DirectiveType.SHOULD_DO, + content: '可以举一些类似情况的成功案例(不承诺具体结果)', + priority: 70, + }, + { + type: DirectiveType.SHOULD_DO, + content: '如果用户的顾虑确实有道理,诚实承认并提供替代方案', + priority: 80, + }, + { + type: DirectiveType.MUST_NOT, + content: '不要贬低用户的担忧,说"这没什么"之类的话', + priority: 90, + }, + ], + + requiredInfo: [], + + suggestedTools: [ + { + toolName: 'search_knowledge', + description: '搜索相关的案例和常见问题解答', + triggerCondition: '当用户提出具体顾虑时,搜索相关的解答和案例', + priority: 'recommended', + }, + { + toolName: 'get_exchange_rate', + description: '获取实时汇率', + triggerCondition: '当用户担心费用问题时,可以计算人民币/港币的实际金额', + priority: 'optional', + }, + ], + + transitions: [ + { + targetStageId: StageId.CONVERSION, + conditions: [ + { type: 'USER_INTENT', intents: ['satisfied', 'convinced', 'want_to_proceed'] }, + ], + priority: 15, + description: '顾虑消除,进入转化', + }, + { + targetStageId: StageId.RECOMMENDATION, + conditions: [ + { type: 'USER_INTENT', intents: ['want_other_options', 'try_another'] }, + ], + priority: 12, + description: '用户想了解其他方案', + }, + { + targetStageId: StageId.CONVERSION, + conditions: [ + { type: 'TURNS_EXCEEDED', maxTurns: 4 }, + ], + priority: 8, + description: '异议处理充分,尝试转化', + }, + { + targetStageId: StageId.HANDOFF, + conditions: [ + { type: 'USER_INTENT', intents: ['give_up', 'not_interested', 'maybe_later'] }, + ], + priority: 10, + description: '用户暂时不想继续,友好收尾', + }, + ], + }, + + // ---------------------------------------- + // 阶段7: 转化促成 + // ---------------------------------------- + { + id: StageId.CONVERSION, + name: '转化促成', + description: '引导用户选择下一步:付费评估、对接专家或其他', + order: 7, + goal: '促成用户采取行动(付费评估或对接专家)', + successCriteria: [ + '用户明确表示要付费评估,或要专家联系方式', + '或用户明确表示暂时不需要,但保持了良好关系', + ], + suggestedResponseLength: 250, + maxTurns: 5, + + directives: [ + { + type: DirectiveType.MUST_DO, + content: '给用户明确的下一步选择:1)付费专业评估 2)对接资深顾问 3)继续免费咨询', + priority: 100, + }, + { + type: DirectiveType.MUST_DO, + content: '说明付费评估的价值:专业团队一对一分析,比AI更深入准确', + priority: 95, + }, + { + type: DirectiveType.SHOULD_DO, + content: '强调时间价值:移民政策经常变化,早行动早受益', + priority: 70, + }, + { + type: DirectiveType.MUST_NOT, + content: '不要过度推销让用户反感,一次最多引导两次', + priority: 90, + }, + { + type: DirectiveType.SHOULD_DO, + content: '如果用户犹豫,可以提供限时优惠或免费咨询名额', + priority: 60, + }, + ], + + requiredInfo: [], + + suggestedTools: [ + { + toolName: 'generate_payment', + description: '生成付费评估的支付二维码', + triggerCondition: '当用户明确表示要付费评估时,立即调用生成支付码', + priority: 'required', + }, + { + toolName: 'save_user_memory', + description: '保存用户的转化意向和偏好', + triggerCondition: '当用户表达明确意向时,记录下来以便后续跟进', + priority: 'recommended', + }, + ], + + transitions: [ + { + targetStageId: StageId.HANDOFF, + conditions: [ + { type: 'USER_INTENT', intents: ['want_to_pay', 'buy_assessment'] }, + ], + priority: 20, + description: '用户决定付费评估', + }, + { + targetStageId: StageId.HANDOFF, + conditions: [ + { type: 'USER_INTENT', intents: ['want_expert', 'talk_to_human', 'contact_info'] }, + ], + priority: 18, + description: '用户想对接人类专家', + }, + { + targetStageId: StageId.NEEDS_DISCOVERY, + conditions: [ + { type: 'USER_INTENT', intents: ['more_questions', 'want_to_know_more'] }, + ], + priority: 12, + description: '用户还有更多问题', + }, + { + targetStageId: StageId.HANDOFF, + conditions: [ + { type: 'TURNS_EXCEEDED', maxTurns: 4 }, + ], + priority: 8, + description: '转化尝试充分,进入收尾', + }, + ], + }, + + // ---------------------------------------- + // 阶段8: 完成对接 + // ---------------------------------------- + { + id: StageId.HANDOFF, + name: '完成对接', + description: '根据用户选择,完成付费、提供专家联系方式或友好收尾', + order: 8, + goal: '完成本次咨询的闭环', + successCriteria: [ + '如果付费:成功生成支付链接', + '如果对接专家:提供了联系方式', + '如果暂不需要:友好告别并留下再次咨询的可能', + ], + suggestedResponseLength: 200, + maxTurns: 5, + + directives: [ + { + type: DirectiveType.MUST_DO, + content: '根据用户选择执行:生成支付码/提供专家联系方式/友好告别', + priority: 100, + }, + { + type: DirectiveType.MUST_DO, + content: '感谢用户的咨询,表示随时欢迎再来', + priority: 90, + }, + { + type: DirectiveType.SHOULD_DO, + content: '如果用户暂时不需要服务,告知我们的公众号/官网,保持联系', + priority: 70, + }, + ], + + requiredInfo: [], + + suggestedTools: [ + { + toolName: 'generate_payment', + description: '生成支付二维码', + triggerCondition: '当用户确认要付费时调用', + priority: 'required', + }, + { + toolName: 'save_user_memory', + description: '保存本次咨询的关键信息和用户决定', + triggerCondition: '对话结束前,保存用户的决定和后续跟进信息', + priority: 'required', + }, + ], + + transitions: [ + { + targetStageId: StageId.NEEDS_DISCOVERY, + conditions: [ + { type: 'USER_INTENT', intents: ['new_question', 'another_topic'] }, + ], + priority: 10, + description: '用户有新问题,重新开始', + }, + ], + }, + ], +}; + +// ============================================================================ +// 辅助函数 +// ============================================================================ + +/** + * 获取阶段配置 + */ +export function getStageById(stageId: StageId): ConsultingStage | undefined { + return DEFAULT_CONSULTING_STRATEGY.stages.find(s => s.id === stageId); +} + +/** + * 获取下一个默认阶段 + */ +export function getNextStage(currentStageId: StageId): ConsultingStage | undefined { + const currentStage = getStageById(currentStageId); + if (!currentStage) return undefined; + + return DEFAULT_CONSULTING_STRATEGY.stages.find(s => s.order === currentStage.order + 1); +} + +/** + * 获取阶段需要收集的信息(未收集的) + */ +export function getUncollectedInfo( + stageId: StageId, + collectedInfo: Record +): RequiredInfo[] { + const stage = getStageById(stageId); + if (!stage) return []; + + return stage.requiredInfo + .filter(info => !collectedInfo[info.key]) + .sort((a, b) => a.priority - b.priority); +} + +/** + * 判断用户意图(简单关键词匹配) + */ +export function detectUserIntents(message: string): string[] { + const intents: string[] = []; + const text = message.toLowerCase(); + + // 想移民/咨询相关 + if (/移民|香港|想去|考虑|咨询|了解/.test(text)) { + intents.push('ask_about_immigration', 'want_to_immigrate'); + } + + // 想评估 + if (/评估|测试|看看|够不够|资格|条件/.test(text)) { + intents.push('want_assessment', 'check_eligibility'); + } + + // 想了解更多 + if (/详细|具体|更多|展开|说说|介绍/.test(text)) { + intents.push('want_details', 'tell_me_more', 'interested'); + } + + // 有顾虑 + if (/担心|顾虑|不确定|难|复杂|麻烦|贵|费用高/.test(text)) { + intents.push('has_concern', 'has_doubt', 'worried'); + } + + // 满意/想继续 + if (/好的|可以|明白|清楚|懂了|想继续|下一步|怎么办/.test(text)) { + intents.push('satisfied', 'want_to_proceed', 'next_step'); + } + + // 想付费 + if (/付费|付款|购买|买|支付|下单/.test(text)) { + intents.push('want_to_pay', 'buy_assessment'); + } + + // 想对接专家 + if (/专家|顾问|人工|真人|联系方式|微信|电话/.test(text)) { + intents.push('want_expert', 'talk_to_human', 'contact_info'); + } + + // 暂时不需要 + if (/再说|以后|考虑一下|不急|暂时不|再看看/.test(text)) { + intents.push('maybe_later', 'not_interested'); + } + + // 不耐烦 + if (/直接|快点|别啰嗦|说重点/.test(text)) { + intents.push('impatient', 'want_result_now'); + } + + return intents; +} diff --git a/packages/services/conversation-service/src/infrastructure/claude/strategy/index.ts b/packages/services/conversation-service/src/infrastructure/claude/strategy/index.ts new file mode 100644 index 0000000..3fbeafc --- /dev/null +++ b/packages/services/conversation-service/src/infrastructure/claude/strategy/index.ts @@ -0,0 +1,6 @@ +/** + * 策略模块导出 + */ + +export * from './default-strategy'; +export * from './strategy-engine.service'; diff --git a/packages/services/conversation-service/src/infrastructure/claude/strategy/strategy-engine.service.ts b/packages/services/conversation-service/src/infrastructure/claude/strategy/strategy-engine.service.ts new file mode 100644 index 0000000..4ccdfcb --- /dev/null +++ b/packages/services/conversation-service/src/infrastructure/claude/strategy/strategy-engine.service.ts @@ -0,0 +1,575 @@ +/** + * 策略执行引擎 + * + * 负责: + * 1. 管理对话的咨询阶段状态 + * 2. 判断阶段转移 + * 3. 生成阶段引导指令(注入到System Prompt) + * 4. 从对话中提取用户信息 + */ + +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Anthropic from '@anthropic-ai/sdk'; +import { + DEFAULT_CONSULTING_STRATEGY, + ConsultingStage, + ConsultingStrategy, + StageId, + DirectiveType, + RequiredInfo, + TransitionCondition, + ConversionPath, + getStageById, + getNextStage, + getUncollectedInfo, + detectUserIntents, + CORE_USER_INFO, +} from './default-strategy'; + +/** + * 对话的咨询状态 + */ +export interface ConsultingState { + // 当前使用的策略ID + strategyId: string; + + // 当前阶段 + currentStageId: StageId; + + // 当前阶段已进行的轮次 + stageTurnCount: number; + + // 已收集的用户信息 + collectedInfo: Record; + + // 评估结果(如果已评估) + assessmentResult?: { + recommendedPrograms: string[]; // 推荐的移民类型 + suitabilityScore: number; // 匹配度 0-100 + highlights: string[]; // 优势 + concerns: string[]; // 需要注意的点 + }; + + // 用户选择的转化路径 + conversionPath?: ConversionPath; + + // 阶段历史 + stageHistory: Array<{ + stageId: StageId; + enteredAt: Date; + exitedAt?: Date; + turnsInStage: number; + }>; +} + +/** + * 策略引擎服务 + */ +@Injectable() +export class StrategyEngineService { + private client: Anthropic; + + constructor(private configService: ConfigService) { + const baseUrl = this.configService.get('ANTHROPIC_BASE_URL'); + const isProxyUrl = baseUrl && baseUrl.match(/^\d+\.\d+\.\d+\.\d+/); + if (isProxyUrl) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + + this.client = new Anthropic({ + apiKey: this.configService.get('ANTHROPIC_API_KEY'), + baseURL: baseUrl || undefined, + }); + } + + /** + * 初始化新对话的咨询状态 + */ + initializeState(): ConsultingState { + return { + strategyId: DEFAULT_CONSULTING_STRATEGY.id, + currentStageId: StageId.GREETING, + stageTurnCount: 0, + collectedInfo: {}, + stageHistory: [ + { + stageId: StageId.GREETING, + enteredAt: new Date(), + turnsInStage: 0, + }, + ], + }; + } + + /** + * 获取当前策略 + */ + getStrategy(): ConsultingStrategy { + // 目前只有默认策略,后续可从数据库加载 + return DEFAULT_CONSULTING_STRATEGY; + } + + /** + * 获取当前阶段 + */ + getCurrentStage(state: ConsultingState): ConsultingStage { + const stage = getStageById(state.currentStageId); + if (!stage) { + // 如果阶段无效,回到开场 + return getStageById(StageId.GREETING)!; + } + return stage; + } + + /** + * 评估是否需要转移阶段 + */ + async evaluateTransition( + state: ConsultingState, + userMessage: string, + ): Promise<{ shouldTransition: boolean; targetStageId?: StageId; reason?: string }> { + const currentStage = this.getCurrentStage(state); + const userIntents = detectUserIntents(userMessage); + + // 按优先级检查每个转移条件 + const sortedTransitions = [...currentStage.transitions].sort((a, b) => b.priority - a.priority); + + for (const transition of sortedTransitions) { + const conditionsMet = await this.checkConditions( + transition.conditions, + state, + userMessage, + userIntents, + ); + + if (conditionsMet) { + return { + shouldTransition: true, + targetStageId: transition.targetStageId, + reason: transition.description, + }; + } + } + + return { shouldTransition: false }; + } + + /** + * 检查转移条件是否满足 + */ + private async checkConditions( + conditions: TransitionCondition[], + state: ConsultingState, + userMessage: string, + userIntents: string[], + ): Promise { + // 所有条件都要满足(AND 逻辑) + for (const condition of conditions) { + const met = await this.checkSingleCondition(condition, state, userMessage, userIntents); + if (!met) return false; + } + return true; + } + + /** + * 检查单个条件 + */ + private async checkSingleCondition( + condition: TransitionCondition, + state: ConsultingState, + userMessage: string, + userIntents: string[], + ): Promise { + switch (condition.type) { + case 'INFO_COLLECTED': + if (condition.infoKeys) { + const collectedCount = condition.infoKeys.filter( + key => state.collectedInfo[key] !== undefined + ).length; + + if (condition.minCount) { + return collectedCount >= condition.minCount; + } + return collectedCount === condition.infoKeys.length; + } + return false; + + case 'USER_INTENT': + if (condition.intents) { + return condition.intents.some(intent => userIntents.includes(intent)); + } + return false; + + case 'TURNS_EXCEEDED': + if (condition.maxTurns !== undefined) { + return state.stageTurnCount >= condition.maxTurns; + } + return false; + + case 'KEYWORD': + if (condition.keywords) { + const lowerMessage = userMessage.toLowerCase(); + return condition.keywords.some(kw => lowerMessage.includes(kw.toLowerCase())); + } + return false; + + case 'ASSESSMENT_DONE': + return state.assessmentResult !== undefined; + + case 'CUSTOM': + // 自定义条件用LLM判断 + if (condition.customCondition) { + return this.evaluateCustomCondition(condition.customCondition, userMessage, state); + } + return false; + + default: + return false; + } + } + + /** + * 用LLM评估自定义条件 + */ + private async evaluateCustomCondition( + condition: string, + userMessage: string, + state: ConsultingState, + ): Promise { + try { + const response = await this.client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 10, + messages: [ + { + role: 'user', + content: `判断以下条件是否满足,只回答 true 或 false: + +条件:${condition} + +用户最新消息:${userMessage} + +已收集的用户信息:${JSON.stringify(state.collectedInfo)} + +回答(true/false):`, + }, + ], + }); + + const text = response.content[0].type === 'text' ? response.content[0].text : ''; + return text.toLowerCase().includes('true'); + } catch { + return false; + } + } + + /** + * 执行阶段转移 + */ + transitionToStage(state: ConsultingState, targetStageId: StageId): ConsultingState { + // 更新历史记录 + const currentHistory = state.stageHistory[state.stageHistory.length - 1]; + if (currentHistory) { + currentHistory.exitedAt = new Date(); + currentHistory.turnsInStage = state.stageTurnCount; + } + + // 创建新状态 + return { + ...state, + currentStageId: targetStageId, + stageTurnCount: 0, + stageHistory: [ + ...state.stageHistory, + { + stageId: targetStageId, + enteredAt: new Date(), + turnsInStage: 0, + }, + ], + }; + } + + /** + * 增加阶段轮次 + */ + incrementTurnCount(state: ConsultingState): ConsultingState { + return { + ...state, + stageTurnCount: state.stageTurnCount + 1, + }; + } + + /** + * 从对话中提取用户信息 + */ + async extractUserInfo( + userMessage: string, + assistantMessage: string, + existingInfo: Record, + ): Promise> { + const infoToExtract = CORE_USER_INFO.filter(info => !existingInfo[info.key]); + + if (infoToExtract.length === 0) { + return {}; + } + + try { + const response = await this.client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 500, + messages: [ + { + role: 'user', + content: `从以下对话中提取用户信息。只提取能明确确定的信息,不要猜测。 + +用户消息:${userMessage} +助手回复:${assistantMessage} + +需要提取的字段: +${infoToExtract.map(i => `- ${i.key}: ${i.description}`).join('\n')} + +返回JSON格式,只包含能确定的字段。如果没有任何信息可提取,返回 {} +示例:{"age": 35, "education": "硕士"} + +JSON:`, + }, + ], + }); + + const text = response.content[0].type === 'text' ? response.content[0].text : '{}'; + // 提取JSON部分 + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (jsonMatch) { + return JSON.parse(jsonMatch[0]); + } + return {}; + } catch (error) { + console.error('[StrategyEngine] Failed to extract user info:', error); + return {}; + } + } + + /** + * 执行资格评估 + */ + async performAssessment( + collectedInfo: Record, + ): Promise { + const age = collectedInfo.age as number | undefined; + const education = collectedInfo.education as string | undefined; + const university = collectedInfo.university as string | undefined; + const workYears = collectedInfo.workYears as number | undefined; + const annualIncome = collectedInfo.annualIncome as number | undefined; + + const recommendedPrograms: string[] = []; + const highlights: string[] = []; + const concerns: string[] = []; + let suitabilityScore = 50; + + // 高才通A类:年薪250万港币以上(约220万人民币) + if (annualIncome && annualIncome >= 2200000) { + recommendedPrograms.push('高才通A类'); + highlights.push('年薪达到高才通A类标准(250万港币)'); + suitabilityScore += 30; + } + + // 高才通B类:全球百强大学+3年以上工作经验 + const top100Universities = ['清华', '北大', '复旦', '上海交通', '浙大', '中科大', '南京大学', '香港大学', '港中文', '港科大']; + const isTop100 = university && top100Universities.some(u => university.includes(u)); + if (isTop100 && workYears && workYears >= 3) { + recommendedPrograms.push('高才通B类'); + highlights.push('名校毕业+工作经验充足'); + suitabilityScore += 25; + } + + // 高才通C类:全球百强大学毕业,工作经验不足3年 + if (isTop100 && (!workYears || workYears < 3)) { + recommendedPrograms.push('高才通C类'); + highlights.push('名校毕业'); + concerns.push('工作经验较少,C类有年度名额限制'); + suitabilityScore += 15; + } + + // 优才计划 + if (education && ['硕士', '博士', 'MBA'].some(e => education.includes(e))) { + recommendedPrograms.push('优才计划'); + highlights.push('高学历加分'); + suitabilityScore += 15; + } + if (workYears && workYears >= 5) { + if (!recommendedPrograms.includes('优才计划')) { + recommendedPrograms.push('优才计划'); + } + highlights.push('工作经验丰富'); + suitabilityScore += 10; + } + + // 专才计划:需要香港雇主 + if (collectedInfo.hasHKConnection) { + recommendedPrograms.push('专才计划'); + highlights.push('有香港雇主/工作机会'); + suitabilityScore += 20; + } + + // 年龄因素 + if (age) { + if (age >= 18 && age <= 39) { + highlights.push('年龄优势明显'); + suitabilityScore += 10; + } else if (age >= 40 && age <= 50) { + concerns.push('年龄在优才计划中扣分'); + } else if (age > 50) { + concerns.push('年龄偏大,建议尽快申请'); + } + } + + // 如果没有明确推荐,默认推荐优才 + if (recommendedPrograms.length === 0) { + recommendedPrograms.push('优才计划'); + concerns.push('需要更多信息做精准评估'); + } + + return { + recommendedPrograms: [...new Set(recommendedPrograms)], // 去重 + suitabilityScore: Math.min(100, suitabilityScore), + highlights, + concerns, + }; + } + + /** + * 构建阶段引导(注入到System Prompt) + */ + buildStageGuidance(state: ConsultingState): string { + const strategy = this.getStrategy(); + const stage = this.getCurrentStage(state); + const uncollectedInfo = getUncollectedInfo(stage.id, state.collectedInfo); + + const parts: string[] = []; + + // ========== 当前阶段信息 ========== + parts.push('## 【咨询流程控制】'); + parts.push(''); + parts.push(`### 当前阶段:${stage.name}(第${stage.order}阶段,共8阶段)`); + parts.push(`**阶段目标**:${stage.goal}`); + parts.push(`**当前轮次**:${state.stageTurnCount + 1}/${stage.maxTurns}`); + parts.push(''); + + // ========== 行为指令 ========== + parts.push('### 行为指令(必须遵守)'); + const allDirectives = [...strategy.globalDirectives, ...stage.directives] + .sort((a, b) => b.priority - a.priority); + + for (const directive of allDirectives) { + const prefix = { + [DirectiveType.MUST_DO]: '✓ 必须', + [DirectiveType.SHOULD_DO]: '○ 建议', + [DirectiveType.MUST_NOT]: '✗ 禁止', + [DirectiveType.PREFER]: '☆ 优先', + }[directive.type]; + parts.push(`${prefix}:${directive.content}`); + } + parts.push(''); + + // ========== 信息收集进度 ========== + if (Object.keys(state.collectedInfo).length > 0 || uncollectedInfo.length > 0) { + parts.push('### 用户信息收集进度'); + + // 已收集的 + for (const [key, value] of Object.entries(state.collectedInfo)) { + const info = CORE_USER_INFO.find(i => i.key === key); + parts.push(`✓ ${info?.label || key}:${value}`); + } + + // 未收集的(当前阶段需要的) + for (const info of uncollectedInfo.slice(0, 3)) { // 最多显示3个 + parts.push(`○ ${info.label}:待收集(建议问法:"${info.askingStrategy}")`); + } + parts.push(''); + } + + // ========== 评估结果 ========== + if (state.assessmentResult) { + parts.push('### 评估结果(基于已收集信息)'); + parts.push(`**推荐方案**:${state.assessmentResult.recommendedPrograms.join('、')}`); + parts.push(`**匹配度**:${state.assessmentResult.suitabilityScore}分`); + if (state.assessmentResult.highlights.length > 0) { + parts.push(`**优势**:${state.assessmentResult.highlights.join(';')}`); + } + if (state.assessmentResult.concerns.length > 0) { + parts.push(`**注意事项**:${state.assessmentResult.concerns.join(';')}`); + } + parts.push(''); + } + + // ========== 阶段转移提示 ========== + parts.push('### 阶段转移判断'); + parts.push('根据用户回复,判断是否满足以下转移条件:'); + for (const transition of stage.transitions.slice(0, 3)) { + parts.push(`- 如果【${transition.description}】→ 进入【${getStageById(transition.targetStageId)?.name}】阶段`); + } + parts.push(''); + + // ========== 专家联系方式(如果在转化/对接阶段)========== + if ([StageId.CONVERSION, StageId.HANDOFF].includes(state.currentStageId)) { + parts.push('### 可用资源'); + parts.push(`- 付费评估服务:¥${strategy.paidServices.assessmentPrice},${strategy.paidServices.description}`); + parts.push(`- 人类专家微信:${strategy.expertContact.wechat}`); + parts.push(`- 专家电话:${strategy.expertContact.phone}`); + parts.push(`- 工作时间:${strategy.expertContact.workingHours}`); + parts.push(''); + } + + // ========== 回复要求 ========== + parts.push('### 回复要求'); + parts.push(`- 建议长度:${stage.suggestedResponseLength}字左右`); + parts.push('- 结尾必须有推进对话的问题或明确的下一步引导'); + parts.push('- 不要一次给太多信息,要循序渐进'); + + return parts.join('\n'); + } + + /** + * 更新收集的信息 + */ + updateCollectedInfo( + state: ConsultingState, + newInfo: Record, + ): ConsultingState { + return { + ...state, + collectedInfo: { + ...state.collectedInfo, + ...newInfo, + }, + }; + } + + /** + * 设置评估结果 + */ + setAssessmentResult( + state: ConsultingState, + result: ConsultingState['assessmentResult'], + ): ConsultingState { + return { + ...state, + assessmentResult: result, + }; + } + + /** + * 设置转化路径 + */ + setConversionPath( + state: ConsultingState, + path: ConversionPath, + ): ConsultingState { + return { + ...state, + conversionPath: path, + }; + } +} diff --git a/packages/services/conversation-service/src/migrations/AddConsultingStateToConversation.ts b/packages/services/conversation-service/src/migrations/AddConsultingStateToConversation.ts new file mode 100644 index 0000000..4bb46ed --- /dev/null +++ b/packages/services/conversation-service/src/migrations/AddConsultingStateToConversation.ts @@ -0,0 +1,87 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 添加咨询状态字段到conversations表 + * + * 新增字段: + * - consulting_stage: 当前咨询阶段 + * - consulting_state: 完整的咨询状态JSON + * - collected_info: 已收集的用户信息 + * - recommended_programs: 推荐的移民方案 + * - conversion_path: 转化路径 + * - device_info: 用户设备信息 + */ +export class AddConsultingStateToConversation1706100000000 implements MigrationInterface { + name = 'AddConsultingStateToConversation1706100000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 添加咨询阶段字段 + await queryRunner.query(` + ALTER TABLE "conversations" + ADD COLUMN IF NOT EXISTS "consulting_stage" VARCHAR(30) DEFAULT 'greeting' + `); + + // 添加咨询状态JSON字段 + await queryRunner.query(` + ALTER TABLE "conversations" + ADD COLUMN IF NOT EXISTS "consulting_state" JSONB + `); + + // 添加已收集信息字段 + await queryRunner.query(` + ALTER TABLE "conversations" + ADD COLUMN IF NOT EXISTS "collected_info" JSONB + `); + + // 添加推荐方案字段 + await queryRunner.query(` + ALTER TABLE "conversations" + ADD COLUMN IF NOT EXISTS "recommended_programs" TEXT[] + `); + + // 添加转化路径字段 + await queryRunner.query(` + ALTER TABLE "conversations" + ADD COLUMN IF NOT EXISTS "conversion_path" VARCHAR(30) + `); + + // 添加设备信息字段 + await queryRunner.query(` + ALTER TABLE "conversations" + ADD COLUMN IF NOT EXISTS "device_info" JSONB + `); + + // 创建索引:按咨询阶段查询 + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_conversations_consulting_stage" + ON "conversations" ("consulting_stage") + `); + + // 创建索引:按转化路径查询 + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_conversations_conversion_path" + ON "conversations" ("conversion_path") + `); + + // 创建GIN索引:按收集的信息查询(JSONB) + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_conversations_collected_info" + ON "conversations" USING GIN ("collected_info") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // 删除索引 + await queryRunner.query(`DROP INDEX IF EXISTS "idx_conversations_collected_info"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_conversations_conversion_path"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_conversations_consulting_stage"`); + + // 删除字段 + await queryRunner.query(`ALTER TABLE "conversations" DROP COLUMN IF EXISTS "device_info"`); + await queryRunner.query(`ALTER TABLE "conversations" DROP COLUMN IF EXISTS "conversion_path"`); + await queryRunner.query(`ALTER TABLE "conversations" DROP COLUMN IF EXISTS "recommended_programs"`); + await queryRunner.query(`ALTER TABLE "conversations" DROP COLUMN IF EXISTS "collected_info"`); + await queryRunner.query(`ALTER TABLE "conversations" DROP COLUMN IF EXISTS "consulting_state"`); + await queryRunner.query(`ALTER TABLE "conversations" DROP COLUMN IF EXISTS "consulting_stage"`); + } +}