import { Injectable, Optional } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Not } from 'typeorm'; import { TenantContextService } from '@iconsulting/shared'; import { ConversationContext } from '../claude-agent.service'; import { KnowledgeClientService } from '../../knowledge/knowledge-client.service'; import { PaymentClientService } from '../../payment/payment-client.service'; import { AssessmentExpertService } from '../../agents/specialists/assessment-expert.service'; import { ConversationORM } from '../../database/postgres/entities/conversation.orm'; import { UserArtifactORM } from '../../database/postgres/entities/user-artifact.orm'; export interface Tool { name: string; description: string; input_schema: { type: string; properties: Record; required?: string[]; }; } @Injectable() export class ImmigrationToolsService { constructor( private knowledgeClient: KnowledgeClientService, private configService: ConfigService, @InjectRepository(ConversationORM) private conversationRepo: Repository, @InjectRepository(UserArtifactORM) private artifactRepo: Repository, private tenantContext: TenantContextService, @Optional() private paymentClient?: PaymentClientService, @Optional() private assessmentExpert?: AssessmentExpertService, ) {} /** * Get all available tools for the agent */ getTools(): Tool[] { return [ { name: 'search_knowledge', description: '搜索香港移民相关知识库,获取最新的政策信息和常见问题解答', input_schema: { type: 'object', properties: { query: { type: 'string', description: '搜索查询内容', }, category: { type: 'string', enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'], description: '移民类别代码(可选)', }, }, required: ['query'], }, }, { name: 'check_off_topic', description: '检查用户的问题是否与香港移民相关,用于判断是否需要拒绝回答', input_schema: { type: 'object', properties: { question: { type: 'string', description: '用户的问题', }, }, required: ['question'], }, }, { name: 'collect_assessment_info', description: '收集用户的个人信息用于移民资格评估', input_schema: { type: 'object', properties: { category: { type: 'string', enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'], description: '用户感兴趣的移民类别', }, age: { type: 'number', description: '用户年龄', }, education: { type: 'string', description: '最高学历', }, university: { type: 'string', description: '毕业院校', }, yearsOfExperience: { type: 'number', description: '工作年限', }, currentJobTitle: { type: 'string', description: '当前职位', }, industry: { type: 'string', description: '所属行业', }, annualIncome: { type: 'number', description: '年收入(人民币)', }, hasHKEmployer: { type: 'boolean', description: '是否有香港雇主', }, }, required: ['category'], }, }, { name: 'generate_payment', description: '为付费评估服务生成支付二维码', input_schema: { type: 'object', properties: { serviceType: { type: 'string', enum: ['ASSESSMENT'], description: '服务类型', }, category: { type: 'string', enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'], description: '移民类别', }, paymentMethod: { type: 'string', enum: ['ALIPAY', 'WECHAT', 'CREDIT_CARD'], description: '支付方式', }, }, required: ['serviceType', 'category', 'paymentMethod'], }, }, { name: 'check_payment_status', description: '查询订单支付状态。当用户询问支付结果、是否支付成功时使用。', input_schema: { type: 'object', properties: { orderId: { type: 'string', description: '订单ID', }, }, required: ['orderId'], }, }, { name: 'query_order_history', description: '查询用户的历史订单列表。当用户想查看购买记录、订单状态时使用。', input_schema: { type: 'object', properties: {}, }, }, { name: 'cancel_order', description: '取消未支付的订单。仅限状态为待支付的订单可以取消。当用户明确表示要取消订单时使用。', input_schema: { type: 'object', properties: { orderId: { type: 'string', description: '要取消的订单ID', }, }, required: ['orderId'], }, }, { name: 'save_user_memory', description: '保存用户的重要信息到长期记忆,以便后续对话中记住用户情况', input_schema: { type: 'object', properties: { memoryType: { type: 'string', enum: ['FACT', 'PREFERENCE', 'INTENT'], description: '记忆类型:FACT-用户陈述的事实,PREFERENCE-用户偏好,INTENT-用户意图', }, content: { type: 'string', description: '要记住的内容', }, category: { type: 'string', enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'], description: '相关的移民类别(可选)', }, }, required: ['memoryType', 'content'], }, }, { name: 'get_user_context', description: '获取用户的历史背景信息和之前的对话记忆,帮助提供更个性化的建议', input_schema: { type: 'object', properties: { query: { type: 'string', description: '当前问题,用于检索相关记忆', }, }, required: ['query'], }, }, // ========== 实时工具 ========== { name: 'get_current_datetime', description: '获取当前的日期和时间,用于回答与时间相关的问题', input_schema: { type: 'object', properties: { timezone: { type: 'string', description: '时区,默认为 Asia/Hong_Kong', }, format: { type: 'string', enum: ['full', 'date', 'time'], description: '返回格式:full-完整日期时间,date-仅日期,time-仅时间', }, }, }, }, { name: 'web_search', description: '搜索互联网获取最新信息,用于查询移民政策变更、新闻动态等实时内容', input_schema: { type: 'object', properties: { query: { type: 'string', description: '搜索关键词', }, site: { type: 'string', description: '限定搜索网站,如 immd.gov.hk(香港入境处)', }, language: { type: 'string', enum: ['zh-CN', 'zh-TW', 'en'], description: '搜索语言,默认 zh-CN', }, }, required: ['query'], }, }, { name: 'get_exchange_rate', description: '查询实时货币汇率,用于投资移民金额换算等场景', input_schema: { type: 'object', properties: { from: { type: 'string', description: '源货币代码,如 CNY(人民币)、USD(美元)', }, to: { type: 'string', description: '目标货币代码,如 HKD(港币)', }, amount: { type: 'number', description: '要转换的金额(可选)', }, }, required: ['from', 'to'], }, }, { name: 'fetch_immigration_news', description: '获取香港移民相关的最新新闻和政策公告', input_schema: { type: 'object', properties: { category: { type: 'string', enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS', 'ALL'], description: '移民类别,ALL表示所有类别', }, limit: { type: 'number', description: '返回条数,默认5条', }, }, }, }, ]; } /** * Execute a tool and return the result */ async executeTool( toolName: string, input: Record, context: ConversationContext, onProgress?: (text: string) => void, abortSignal?: AbortSignal, ): Promise { console.log(`[Tool] Executing ${toolName} with input:`, JSON.stringify(input)); switch (toolName) { case 'search_knowledge': return this.searchKnowledge(input, context); case 'check_off_topic': return this.checkOffTopic(input); case 'collect_assessment_info': return this.collectAssessmentInfo(input, context); case 'generate_payment': return this.generatePayment(input, context); case 'check_payment_status': return this.checkPaymentStatus(input); case 'query_order_history': return this.queryOrderHistory(context); case 'cancel_order': return this.cancelOrder(input); case 'save_user_memory': return this.saveUserMemory(input, context); case 'get_user_context': return this.getUserContext(input, context); case 'get_current_datetime': return this.getCurrentDatetime(input); case 'web_search': return this.webSearch(input); case 'get_exchange_rate': return this.getExchangeRate(input); case 'fetch_immigration_news': return this.fetchImmigrationNews(input); case 'query_user_profile': return this.queryUserProfile(context); case 'generate_document': return this.generateDocument(input, context); case 'manage_checklist': return this.manageChecklist(input, context); case 'create_timeline': return this.createTimeline(input, context); case 'query_user_artifacts': return this.queryUserArtifacts(input, context); case 'run_professional_assessment': return this.runProfessionalAssessment(input, context, onProgress, abortSignal); default: return { error: `Unknown tool: ${toolName}` }; } } /** * Search knowledge base - 调用 knowledge-service RAG API * 级联 Fallback: 知识库(置信度检查) → 网络搜索 → 标注来源的内置知识 */ private async searchKnowledge( input: Record, context: ConversationContext, ): Promise { const { query, category } = input as { query: string; category?: string }; const RAG_CONFIDENCE_THRESHOLD = 0.55; console.log(`[Tool:search_knowledge] Query: "${query}", Category: ${category || 'all'}`); // ── Step 1: 知识库 RAG 搜索 ── try { const result = await this.knowledgeClient.retrieveKnowledge({ query, userId: context.userId, category, includeMemories: true, includeExperiences: true, }); if (result && result.content) { const hasConfidentSource = result.sources?.some( (s) => s.similarity >= RAG_CONFIDENCE_THRESHOLD, ); if (hasConfidentSource) { return { success: true, content: result.content, sources: result.sources, sourceType: 'KNOWLEDGE_BASE', userContext: result.userMemories, relatedExperiences: result.systemExperiences, message: `[来源: 知识库] 找到 ${result.sources?.length || 0} 条相关知识`, }; } console.log(`[Tool:search_knowledge] Low confidence scores, cascading to web_search`); } } catch (error) { console.error('[Tool:search_knowledge] Knowledge base error:', error); } // ── Step 2: 网络搜索 Fallback ── try { const webResult = await this.webSearch({ query: `香港移民 ${query}`, language: 'zh-CN' }); const web = webResult as { success?: boolean; results?: Array<{ title: string; url: string; snippet: string }> }; if (web.success && web.results && web.results.length > 0) { return { success: true, content: web.results .map((r) => `**${r.title}**\n${r.snippet}\n来源: ${r.url}`) .join('\n\n'), sources: web.results.map((r) => ({ title: r.title, url: r.url, type: 'web', })), sourceType: 'WEB_SEARCH', message: '[来源: 网络搜索] 知识库未找到高置信度内容,以下来自网络搜索结果,信息仅供参考,请注意核实。', }; } } catch (error) { console.error('[Tool:search_knowledge] Web search fallback failed:', error); } // ── Step 3: 内置知识 Fallback(明确标注来源) ── return { success: false, content: null, sourceType: 'BUILT_IN_KNOWLEDGE', message: '[来源: AI内置知识] 知识库和网络搜索均未找到相关信息。如需回答,请务必在回复中明确告知用户:此信息基于AI训练数据,可能不是最新信息,仅供参考。', }; } /** * Check if question is off-topic - 调用 knowledge-service API */ private async checkOffTopic(input: Record): Promise { // coordinator-tools.ts sends `query`, legacy getTools() sent `question` const question = (input.query as string) || (input.question as string) || ''; console.log(`[Tool:check_off_topic] Checking: "${question}"`); // 调用 knowledge-service 离题检测 API const result = await this.knowledgeClient.checkOffTopic(question); if (result.isOffTopic) { return { isOffTopic: true, confidence: result.confidence, suggestion: result.reason || '这个问题似乎与香港移民无关。作为移民咨询顾问,我专注于香港各类移民政策的咨询。请问您有香港移民相关的问题吗?', }; } return { isOffTopic: false, confidence: result.confidence, suggestion: null, }; } /** * Collect assessment info - 收集评估信息并保存到记忆 */ private async collectAssessmentInfo( input: Record, context: ConversationContext, ): Promise { // coordinator-tools.ts sends { userId, field, value } (single-field mode) // Legacy getTools() sent { category, age, education, ... } (batch mode) // Support both formats let info: Record; if (input.field && input.value) { // Single-field mode from coordinator-tools.ts info = { [input.field as string]: input.value }; } else { // Batch mode from legacy getTools() const { userId: _userId, ...rest } = input; info = rest; } const category = (info.category as string) || ''; console.log(`[Tool:collect_assessment_info] User ${context.userId} - Fields: ${Object.keys(info).join(', ')}`); // 保存收集到的信息到用户记忆 const memoryContent = Object.entries(info) .filter(([, v]) => v !== undefined) .map(([k, v]) => `${k}: ${v}`) .join(', '); if (memoryContent) { await this.knowledgeClient.saveUserMemory({ userId: context.userId, memoryType: 'FACT', content: `用户评估信息 - ${memoryContent}`, sourceConversationId: context.conversationId, relatedCategory: category, importance: 80, }); } return { success: true, collectedInfo: info, message: '已记录您的信息。如需完整评估,请选择付费评估服务。', nextStep: 'payment', }; } /** * Generate payment QR code — 调用 payment-service 创建订单并生成支付 */ private async generatePayment( input: Record, context: ConversationContext, ): Promise { const { serviceType, category, paymentMethod } = input as { serviceType: string; category: string; paymentMethod: string; }; console.log( `[Tool:generate_payment] User ${context.userId} - ${serviceType} for ${category} via ${paymentMethod}`, ); // 记录用户付费意向 await this.knowledgeClient.saveUserMemory({ userId: context.userId, memoryType: 'INTENT', content: `用户希望购买${category}类别的${serviceType}服务,选择${paymentMethod}支付`, sourceConversationId: context.conversationId, relatedCategory: category, importance: 90, }); // 调用 payment-service 创建订单 + 生成支付 if (!this.paymentClient) { return { success: false, error: '支付服务暂不可用,请稍后重试' }; } // Step 1: 创建订单 const order = await this.paymentClient.createOrder({ userId: context.userId, serviceType, serviceCategory: category, conversationId: context.conversationId, }); if (!order) { return { success: false, error: '创建订单失败,请稍后重试' }; } // Step 2: 创建支付(生成二维码/支付链接) const payment = await this.paymentClient.createPayment({ orderId: order.id, method: paymentMethod, }); if (!payment) { return { success: false, error: '生成支付失败,请稍后重试', orderId: order.id }; } // paymentUrl 是支付链接(短文本),前端用 QRCodeSVG 从它生成二维码 // qrCodeUrl 是 base64 data URL(~数KB),绝不能传给 AI(会被塞进文本回复) // 只返回 paymentUrl,永远不返回 qrCodeUrl(base64) return { success: true, orderId: order.id, paymentId: payment.paymentId, amount: payment.amount, currency: order.currency, paymentMethod: payment.method, paymentUrl: payment.paymentUrl, expiresAt: payment.expiresAt, _ui_hint: '前端已自动渲染支付二维码,回复中不要包含任何链接、URL或二维码数据', }; } /** * Check payment status — 查询订单支付状态 */ private async checkPaymentStatus( input: Record, ): Promise { const { orderId } = input as { orderId: string }; console.log(`[Tool:check_payment_status] Order: ${orderId}`); if (!this.paymentClient) { return { success: false, error: '支付服务暂不可用' }; } const status = await this.paymentClient.getOrderStatus(orderId); if (!status) { return { success: false, error: '查询订单状态失败,请确认订单号是否正确' }; } const statusLabels: Record = { CREATED: '已创建,等待支付', PENDING_PAYMENT: '等待支付中', PAID: '已支付成功', PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消', REFUNDED: '已退款', }; return { success: true, orderId: status.orderId, status: status.status, statusLabel: statusLabels[status.status] || status.status, paidAt: status.paidAt, completedAt: status.completedAt, }; } /** * Query order history — 查询用户历史订单 */ private async queryOrderHistory( context: ConversationContext, ): Promise { console.log(`[Tool:query_order_history] User: ${context.userId}`); if (!this.paymentClient) { return { success: false, error: '支付服务暂不可用' }; } const orders = await this.paymentClient.getUserOrders(context.userId); return { success: true, totalOrders: orders.length, orders: orders.map((o) => ({ orderId: o.id, serviceType: o.serviceType, serviceCategory: o.serviceCategory, amount: o.amount, currency: o.currency, status: o.status, paidAt: o.paidAt, createdAt: o.createdAt, })), }; } /** * Cancel order — 取消未支付的订单 */ private async cancelOrder( input: Record, ): Promise { const { orderId } = input as { orderId: string }; console.log(`[Tool:cancel_order] Order: ${orderId}`); if (!this.paymentClient) { return { success: false, error: '支付服务暂不可用' }; } const result = await this.paymentClient.cancelOrder(orderId); if (!result) { return { success: false, error: '取消订单失败。只有未支付的订单才能取消,请确认订单状态。', }; } return { success: true, orderId: result.orderId, status: result.status, message: '订单已成功取消', }; } /** * Save user memory - 调用 knowledge-service Memory API */ private async saveUserMemory( input: Record, context: ConversationContext, ): Promise { const { memoryType, content, category } = input as { memoryType: 'FACT' | 'PREFERENCE' | 'INTENT'; content: string; category?: string; }; console.log( `[Tool:save_user_memory] User ${context.userId} - Type: ${memoryType}, Content: "${content}"`, ); // 调用 knowledge-service Memory API const memory = await this.knowledgeClient.saveUserMemory({ userId: context.userId, memoryType, content, sourceConversationId: context.conversationId, relatedCategory: category, importance: memoryType === 'FACT' ? 70 : memoryType === 'INTENT' ? 80 : 60, }); if (memory) { return { success: true, memoryId: memory.id, message: '已记住您的信息,下次对话时我会记得', }; } return { success: false, message: '记忆保存失败,但不影响当前对话', }; } /** * Get user context - 获取用户历史背景 */ private async getUserContext( input: Record, context: ConversationContext, ): Promise { const { query } = input as { query: string }; console.log(`[Tool:get_user_context] User ${context.userId} - Query: "${query}"`); // 并行获取相关记忆和重要记忆 const [relevantMemories, topMemories] = await Promise.all([ this.knowledgeClient.searchUserMemories({ userId: context.userId, query, limit: 3, }), this.knowledgeClient.getUserTopMemories(context.userId, 5), ]); const allMemories = [...relevantMemories]; // 添加不重复的重要记忆 for (const mem of topMemories) { if (!allMemories.find(m => m.id === mem.id)) { allMemories.push(mem); } } if (allMemories.length > 0) { return { success: true, memories: allMemories.map(m => ({ type: m.memoryType, content: m.content, importance: m.importance, })), message: `找到 ${allMemories.length} 条用户背景信息`, }; } return { success: true, memories: [], message: '这是新用户,暂无历史信息', }; } // ========== 实时工具实现 ========== /** * Get current datetime - 获取当前日期时间 */ private getCurrentDatetime(input: Record): unknown { const { timezone = 'Asia/Hong_Kong', format = 'full' } = input as { timezone?: string; format?: 'full' | 'date' | 'time'; }; console.log(`[Tool:get_current_datetime] Timezone: ${timezone}, Format: ${format}`); const now = new Date(); const options: Intl.DateTimeFormatOptions = { timeZone: timezone, }; let result: string; switch (format) { case 'date': options.year = 'numeric'; options.month = 'long'; options.day = 'numeric'; options.weekday = 'long'; result = now.toLocaleDateString('zh-CN', options); break; case 'time': options.hour = '2-digit'; options.minute = '2-digit'; options.second = '2-digit'; options.hour12 = false; result = now.toLocaleTimeString('zh-CN', options); break; case 'full': default: options.year = 'numeric'; options.month = 'long'; options.day = 'numeric'; options.weekday = 'long'; options.hour = '2-digit'; options.minute = '2-digit'; options.second = '2-digit'; options.hour12 = false; result = now.toLocaleString('zh-CN', options); break; } return { success: true, datetime: result, timezone, timestamp: now.toISOString(), message: `当前${timezone}时间: ${result}`, }; } /** * Web search - 搜索互联网获取最新信息 * 使用 Google Custom Search API 或其他搜索服务 */ private async webSearch(input: Record): Promise { const { query, site, language = 'zh-CN' } = input as { query: string; site?: string; language?: string; }; console.log(`[Tool:web_search] Query: "${query}", Site: ${site || 'any'}, Language: ${language}`); // 构建搜索查询 let searchQuery = query; if (site) { searchQuery = `site:${site} ${query}`; } // 尝试使用 Google Custom Search API const googleApiKey = this.configService.get('GOOGLE_SEARCH_API_KEY'); const googleCseId = this.configService.get('GOOGLE_CSE_ID'); if (googleApiKey && googleCseId) { try { const url = new URL('https://www.googleapis.com/customsearch/v1'); url.searchParams.set('key', googleApiKey); url.searchParams.set('cx', googleCseId); url.searchParams.set('q', searchQuery); url.searchParams.set('lr', `lang_${language.split('-')[0]}`); url.searchParams.set('num', '5'); const response = await fetch(url.toString()); if (response.ok) { const data = (await response.json()) as { items?: Array<{ title: string; link: string; snippet: string }> }; const results = (data.items || []).map((item: { title: string; link: string; snippet: string }) => ({ title: item.title, url: item.link, snippet: item.snippet, })); return { success: true, query: searchQuery, results, resultCount: results.length, message: `找到 ${results.length} 条相关结果`, }; } } catch (error) { console.error('[Tool:web_search] Google API error:', error); } } // 降级:返回建议用户自行搜索的信息 return { success: false, query: searchQuery, results: [], message: '搜索服务暂时不可用。建议您访问以下官方网站获取最新信息:', suggestedSites: [ { name: '香港入境事务处', url: 'https://www.immd.gov.hk' }, { name: '香港人才服务窗口', url: 'https://www.talentservicewindow.gov.hk' }, { name: '投资推广署', url: 'https://www.investhk.gov.hk' }, ], }; } /** * Get exchange rate - 获取实时汇率 * 使用免费的汇率 API */ private async getExchangeRate(input: Record): Promise { const { from, to, amount } = input as { from: string; to: string; amount?: number; }; console.log(`[Tool:get_exchange_rate] From: ${from}, To: ${to}, Amount: ${amount || 'N/A'}`); // 使用 Exchange Rate API (免费层) const apiKey = this.configService.get('EXCHANGE_RATE_API_KEY'); const baseUrl = apiKey ? `https://v6.exchangerate-api.com/v6/${apiKey}/pair/${from}/${to}` : `https://api.exchangerate-api.com/v4/latest/${from}`; try { const response = await fetch(baseUrl); if (response.ok) { const data = (await response.json()) as { conversion_rate?: number; rates?: Record; time_last_update_utc?: string; }; let rate: number | undefined; if (apiKey) { // v6 API response rate = data.conversion_rate; } else { // v4 API response (free, no key needed) rate = data.rates?.[to]; } if (rate) { const convertedAmount = amount ? amount * rate : null; return { success: true, from, to, rate, amount: amount || null, convertedAmount, lastUpdate: data.time_last_update_utc || new Date().toISOString(), message: amount ? `${amount} ${from} = ${convertedAmount?.toFixed(2)} ${to} (汇率: ${rate.toFixed(4)})` : `1 ${from} = ${rate.toFixed(4)} ${to}`, }; } } } catch (error) { console.error('[Tool:get_exchange_rate] API error:', error); } // 降级:使用固定参考汇率 const fallbackRates: Record> = { CNY: { HKD: 1.08, USD: 0.14 }, USD: { HKD: 7.78, CNY: 7.24 }, HKD: { CNY: 0.93, USD: 0.13 }, }; const fallbackRate = fallbackRates[from]?.[to]; if (fallbackRate) { const convertedAmount = amount ? amount * fallbackRate : null; return { success: true, from, to, rate: fallbackRate, amount: amount || null, convertedAmount, isEstimate: true, message: `参考汇率 (可能有偏差): 1 ${from} ≈ ${fallbackRate} ${to}${amount ? `,${amount} ${from} ≈ ${convertedAmount?.toFixed(2)} ${to}` : ''}`, }; } return { success: false, message: `暂不支持 ${from} 到 ${to} 的汇率查询`, }; } /** * Fetch immigration news - 获取移民相关新闻 * 从官方渠道或新闻源获取 */ private async fetchImmigrationNews(input: Record): Promise { const { category = 'ALL', limit = 5 } = input as { category?: string; limit?: number; }; console.log(`[Tool:fetch_immigration_news] Category: ${category}, Limit: ${limit}`); // 尝试从 knowledge-service 获取最新新闻(如果有此功能) try { const response = await fetch( `${this.configService.get('KNOWLEDGE_SERVICE_URL') || 'http://knowledge-service:3003'}/api/v1/news/immigration?category=${category}&limit=${limit}`, ); if (response.ok) { const data = (await response.json()) as { success?: boolean; data?: Array }; if (data.success && data.data) { return { success: true, category, news: data.data, message: `获取到 ${data.data.length} 条${category === 'ALL' ? '' : category + '相关'}移民新闻`, }; } } } catch { console.log('[Tool:fetch_immigration_news] Knowledge service news not available'); } // 降级:返回静态参考信息和官方渠道 const categoryNews: Record> = { QMAS: [ { title: '优才计划持续接受申请', summary: '香港优才计划无配额限制,全年接受申请。申请人需符合基本资格要求,并通过计分制评核。', date: '2024-01', source: '香港入境事务处', }, ], TTPS: [ { title: '高才通计划最新动态', summary: '高端人才通行证计划持续运作,A/B/C三类申请通道开放。', date: '2024-01', source: '香港入境事务处', }, ], CIES: [ { title: '新资本投资者入境计划', summary: '投资门槛为3000万港币,需投资于许可资产类别。', date: '2024-03', source: '香港入境事务处', }, ], }; const newsItems = category === 'ALL' ? Object.values(categoryNews).flat() : categoryNews[category] || []; return { success: true, category, news: newsItems.slice(0, limit), isStatic: true, message: newsItems.length > 0 ? `以下是${category === 'ALL' ? '' : category + '类别'}的参考信息。如需最新资讯,请访问香港入境事务处官网。` : '暂无相关新闻,建议访问香港入境事务处官网获取最新信息。', officialSources: [ { name: '香港入境事务处', url: 'https://www.immd.gov.hk/hks/services/visas/admission_talents.html' }, { name: '香港人才服务窗口', url: 'https://www.talentservicewindow.gov.hk' }, ], }; } /** * Query user profile — 查询用户在系统中的完整信息档案 * 包括咨询次数、对话主题、用户记忆等 */ private async queryUserProfile( context: ConversationContext, ): Promise { console.log(`[Tool:query_user_profile] User: ${context.userId}`); const tenantId = this.tenantContext.getCurrentTenantId() || ''; // 并行查询:对话历史 + 用户记忆 + 订单历史 const [conversations, topMemories, orders] = await Promise.all([ // 查询该用户的所有非删除对话 this.conversationRepo.find({ where: { userId: context.userId, tenantId, status: Not('DELETED'), }, order: { createdAt: 'DESC' }, select: ['id', 'title', 'status', 'category', 'messageCount', 'consultingStage', 'createdAt', 'updatedAt'], }), // 获取用户最重要的记忆 this.knowledgeClient.getUserTopMemories(context.userId, 15).catch(() => []), // 获取订单历史 this.paymentClient?.getUserOrders(context.userId).catch(() => []) ?? Promise.resolve([]), ]); // 统计信息 const totalConsultations = conversations.length; const firstConsultation = conversations.length > 0 ? conversations[conversations.length - 1].createdAt : null; const lastConsultation = conversations.length > 0 ? conversations[0].createdAt : null; // 对话主题摘要(最近 10 个) const recentConversations = conversations.slice(0, 10).map(c => ({ title: c.title, category: c.category || '未分类', stage: c.consultingStage || '未知', messageCount: c.messageCount, date: c.createdAt, })); // 分类统计 const categoryStats: Record = {}; for (const c of conversations) { const cat = c.category || '未分类'; categoryStats[cat] = (categoryStats[cat] || 0) + 1; } // 用户记忆分类 const facts = topMemories.filter(m => m.memoryType === 'FACT'); const preferences = topMemories.filter(m => m.memoryType === 'PREFERENCE'); const intents = topMemories.filter(m => m.memoryType === 'INTENT'); // 订单统计 const orderStats = { total: orders.length, paid: orders.filter((o: any) => o.status === 'PAID' || o.status === 'COMPLETED').length, pending: orders.filter((o: any) => o.status === 'CREATED' || o.status === 'PENDING_PAYMENT').length, }; return { success: true, profile: { // 咨询统计 totalConsultations, currentConsultationNumber: totalConsultations, // "这是第 N 次咨询" firstConsultationDate: firstConsultation, lastConsultationDate: lastConsultation, categoryDistribution: categoryStats, // 最近对话 recentConversations, // 用户画像(来自记忆系统) userFacts: facts.map(m => m.content), userPreferences: preferences.map(m => m.content), userIntents: intents.map(m => m.content), // 订单统计 orderStats, }, message: `用户已累计咨询 ${totalConsultations} 次,系统记录了 ${topMemories.length} 条用户信息`, }; } // ========== 任务执行工具 ========== /** * Generate document — 生成结构化文档并持久化 */ private async generateDocument( input: Record, context: ConversationContext, ): Promise { const { title, content, documentType } = input as { title: string; content: string; documentType: string; }; console.log(`[Tool:generate_document] User ${context.userId} - "${title}" (${documentType})`); const tenantId = this.tenantContext.getCurrentTenantId() || ''; // 检查是否已存在同标题同类型的工件 → 更新而非重复创建 const existing = await this.artifactRepo.findOne({ where: { userId: context.userId, tenantId, artifactType: 'document', title }, }); if (existing) { existing.content = content; existing.documentType = documentType; existing.conversationId = context.conversationId; await this.artifactRepo.save(existing); return { success: true, artifactId: existing.id, title, content, documentType, updated: true, createdAt: existing.createdAt.toISOString(), updatedAt: new Date().toISOString(), _ui_hint: '前端已渲染文档卡片,回复中简要说明文档内容即可,不要重复文档全文', }; } const artifact = this.artifactRepo.create({ tenantId, userId: context.userId, conversationId: context.conversationId, artifactType: 'document', title, documentType, content, }); const saved = await this.artifactRepo.save(artifact); return { success: true, artifactId: saved.id, title, content, documentType, updated: false, createdAt: saved.createdAt.toISOString(), _ui_hint: '前端已渲染文档卡片,回复中简要说明文档内容即可,不要重复文档全文', }; } /** * Manage checklist — 创建/更新待办事项清单并持久化 */ private async manageChecklist( input: Record, context: ConversationContext, ): Promise { const { title, items } = input as { title: string; items: Array<{ text: string; completed?: boolean; category?: string; note?: string }>; }; console.log(`[Tool:manage_checklist] User ${context.userId} - "${title}" (${items.length} items)`); const tenantId = this.tenantContext.getCurrentTenantId() || ''; const normalized = items.map(item => ({ ...item, completed: item.completed ?? false, })); // 检查是否已存在同标题的清单 → 更新 const existing = await this.artifactRepo.findOne({ where: { userId: context.userId, tenantId, artifactType: 'checklist', title }, }); if (existing) { existing.content = JSON.stringify(normalized); existing.conversationId = context.conversationId; await this.artifactRepo.save(existing); return { success: true, artifactId: existing.id, title, items: normalized, totalItems: normalized.length, completedCount: normalized.filter(i => i.completed).length, updated: true, _ui_hint: '前端已渲染清单卡片,回复中简要说明清单用途即可', }; } const artifact = this.artifactRepo.create({ tenantId, userId: context.userId, conversationId: context.conversationId, artifactType: 'checklist', title, documentType: null, content: JSON.stringify(normalized), }); const saved = await this.artifactRepo.save(artifact); return { success: true, artifactId: saved.id, title, items: normalized, totalItems: normalized.length, completedCount: normalized.filter(i => i.completed).length, updated: false, _ui_hint: '前端已渲染清单卡片,回复中简要说明清单用途即可', }; } /** * Create timeline — 创建/更新时间线并持久化 */ private async createTimeline( input: Record, context: ConversationContext, ): Promise { const { title, milestones } = input as { title: string; milestones: Array<{ title: string; description: string; duration?: string; status?: string }>; }; console.log(`[Tool:create_timeline] User ${context.userId} - "${title}" (${milestones.length} milestones)`); const tenantId = this.tenantContext.getCurrentTenantId() || ''; const normalized = milestones.map(m => ({ ...m, status: m.status || 'pending', })); // 检查是否已存在同标题的时间线 → 更新 const existing = await this.artifactRepo.findOne({ where: { userId: context.userId, tenantId, artifactType: 'timeline', title }, }); if (existing) { existing.content = JSON.stringify(normalized); existing.conversationId = context.conversationId; await this.artifactRepo.save(existing); return { success: true, artifactId: existing.id, title, milestones: normalized, totalSteps: normalized.length, updated: true, _ui_hint: '前端已渲染时间线卡片,回复中简要总结时间规划即可', }; } const artifact = this.artifactRepo.create({ tenantId, userId: context.userId, conversationId: context.conversationId, artifactType: 'timeline', title, documentType: null, content: JSON.stringify(normalized), }); const saved = await this.artifactRepo.save(artifact); return { success: true, artifactId: saved.id, title, milestones: normalized, totalSteps: normalized.length, updated: false, _ui_hint: '前端已渲染时间线卡片,回复中简要总结时间规划即可', }; } /** * Query user artifacts — 查询用户的所有工件(文档、清单、时间线) */ private async queryUserArtifacts( input: Record, context: ConversationContext, ): Promise { const { artifactType } = input as { artifactType?: string }; console.log(`[Tool:query_user_artifacts] User ${context.userId} - Type: ${artifactType || 'all'}`); const tenantId = this.tenantContext.getCurrentTenantId() || ''; const where: Record = { userId: context.userId, tenantId, }; if (artifactType) { where.artifactType = artifactType; } const artifacts = await this.artifactRepo.find({ where, order: { updatedAt: 'DESC' }, take: 20, }); return { success: true, totalArtifacts: artifacts.length, artifacts: artifacts.map(a => ({ artifactId: a.id, artifactType: a.artifactType, title: a.title, documentType: a.documentType, updatedAt: a.updatedAt.toISOString(), createdAt: a.createdAt.toISOString(), // For checklists/timelines, parse the JSON content to provide summary ...(a.artifactType === 'checklist' ? (() => { try { const items = JSON.parse(a.content) as Array<{ completed?: boolean }>; return { totalItems: items.length, completedCount: items.filter(i => i.completed).length }; } catch { return {}; } })() : {}), ...(a.artifactType === 'timeline' ? (() => { try { const milestones = JSON.parse(a.content) as Array; return { totalSteps: milestones.length }; } catch { return {}; } })() : {}), ...(a.artifactType === 'document' ? { contentPreview: a.content.slice(0, 100) + (a.content.length > 100 ? '...' : '') } : {}), })), message: artifacts.length > 0 ? `找到 ${artifacts.length} 个已保存的工件` : '暂无已保存的工件', }; } /** * Run professional assessment — 完整的付费评估管线 * * Pipeline: * 1. Check existing assessment (avoid duplicate within 30 days) * 2. Validate payment (ASSESSMENT order with PAID/COMPLETED status) * 3. Validate info completeness (age, nationality, education_level, work_experience_years) * 4. Call AssessmentExpertService.executeAssessment() * 5. Persist result as UserArtifact (artifactType: 'assessment_report') */ private async runProfessionalAssessment( input: Record, context: ConversationContext, onProgress?: (text: string) => void, abortSignal?: AbortSignal, ): Promise { const { userInfo, targetCategories, conversationContext, forceReassess } = input as { userInfo: Record; targetCategories?: string[]; conversationContext?: string; forceReassess?: boolean; }; const tenantId = this.tenantContext.getCurrentTenantId() || ''; const userId = context.userId; console.log(`[Tool:run_professional_assessment] User ${userId} - Starting assessment pipeline (force=${!!forceReassess})`); // ── Step 1: Check existing assessment ── const existingReport = await this.artifactRepo.findOne({ where: { userId, tenantId, artifactType: 'assessment_report' }, order: { updatedAt: 'DESC' }, }); if (existingReport && !forceReassess) { const daysSinceReport = (Date.now() - existingReport.updatedAt.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceReport < 30) { let parsedContent: unknown = null; try { parsedContent = JSON.parse(existingReport.content); } catch { /* leave null */ } console.log(`[Tool:run_professional_assessment] Existing report found (${daysSinceReport.toFixed(1)} days old)`); return { status: 'already_assessed', message: `您在 ${Math.floor(daysSinceReport)} 天前已完成专业评估。如需重新评估,请30天后再试,或联系顾问手动更新。`, existingReport: parsedContent, artifactId: existingReport.id, assessedAt: existingReport.updatedAt.toISOString(), _ui_hint: '前端已渲染评估报告卡片', }; } console.log(`[Tool:run_professional_assessment] Existing report is ${daysSinceReport.toFixed(1)} days old, allowing re-assessment`); } if (forceReassess && existingReport) { console.log(`[Tool:run_professional_assessment] Force re-assessment requested — skipping 30-day check`); } // ── Step 2: Validate payment ── if (!this.paymentClient) { return { status: 'payment_service_unavailable', message: '支付服务暂时不可用,请稍后再试。', }; } const orders = await this.paymentClient.getUserOrders(userId); const paidAssessmentOrder = orders.find( (o) => o.serviceType === 'ASSESSMENT' && (o.status === 'PAID' || o.status === 'COMPLETED'), ); if (!paidAssessmentOrder) { const pendingOrder = orders.find( (o) => o.serviceType === 'ASSESSMENT' && (o.status === 'CREATED' || o.status === 'PENDING_PAYMENT'), ); return { status: 'payment_required', message: '专业评估服务需要支付99元评估费(一次性,终身有效)。请先完成支付后再进行评估。', hasPendingOrder: !!pendingOrder, pendingOrderId: pendingOrder?.id || null, _ui_hint: '提示用户付费,可使用 generate_payment 工具生成支付链接', }; } // ── Step 3: Validate info completeness ── const REQUIRED_FIELDS = ['age', 'nationality', 'education_level', 'work_experience_years']; const FIELD_LABELS: Record = { age: '年龄', nationality: '国籍/户籍', education_level: '最高学历', work_experience_years: '工作年限', }; const missingFields: string[] = []; const collectedFields: string[] = []; for (const field of REQUIRED_FIELDS) { if (userInfo[field] !== undefined && userInfo[field] !== null && userInfo[field] !== '') { collectedFields.push(field); } else { missingFields.push(field); } } if (missingFields.length > 0) { return { status: 'info_incomplete', message: `还需要以下信息才能进行准确评估:${missingFields.map(f => FIELD_LABELS[f] || f).join('、')}`, missingFields, missingFieldLabels: missingFields.map(f => FIELD_LABELS[f] || f), collectedFields, _ui_hint: '提示用户提供缺失信息', }; } // ── Step 4: Run assessment expert ── if (!this.assessmentExpert) { return { status: 'assessment_error', message: '评估服务暂时不可用,请稍后再试。', }; } console.log(`[Tool:run_professional_assessment] Payment verified, info complete. Running assessment...`); let assessmentResult: string; try { const opts = (onProgress || abortSignal) ? { onProgress, abortSignal } : undefined; assessmentResult = await this.assessmentExpert.executeAssessment({ userInfo, targetCategories, conversationContext, }, opts); } catch (error) { console.error('[Tool:run_professional_assessment] Assessment expert failed:', error); return { status: 'assessment_error', message: '评估过程中出现错误,请稍后重试。如问题持续,请联系客服。', }; } // Parse the JSON result from assessment expert let parsedResult: unknown; try { parsedResult = typeof assessmentResult === 'string' ? JSON.parse(assessmentResult) : assessmentResult; } catch { console.error('[Tool:run_professional_assessment] Failed to parse assessment result'); parsedResult = null; } // ── Step 4.5: QMAS Threshold Post-validation ── // Safety net: ensure assessment expert's QMAS score is consistent with 12-item threshold check const pr = parsedResult as Record | null; if (pr?.assessments && Array.isArray(pr.assessments)) { const qmasAssessment = pr.assessments.find((a: any) => a.category === 'QMAS'); if (qmasAssessment) { const tc = qmasAssessment.thresholdCheck; if (tc && typeof tc.metCount === 'number' && typeof tc.requiredCount === 'number') { const passed = tc.metCount >= tc.requiredCount; tc.passed = passed; if (!passed && qmasAssessment.score > 29) { console.warn( `[QMAS Threshold] Score ${qmasAssessment.score} inconsistent with failed threshold ` + `(${tc.metCount}/${tc.requiredCount}). Capping to 29.`, ); qmasAssessment.score = 29; qmasAssessment.eligible = false; if (!qmasAssessment.concerns) qmasAssessment.concerns = []; qmasAssessment.concerns.unshift( `基本门槛评核未通过:仅满足 ${tc.metCount}/12 项(需至少6项)`, ); } if (passed && !qmasAssessment.eligible && qmasAssessment.score > 29) { qmasAssessment.eligible = true; } } else { console.warn('[QMAS Threshold] Assessment expert did not return valid thresholdCheck for QMAS'); } } } // ── Step 5: Persist as artifact ── const contentToSave = parsedResult ? JSON.stringify(parsedResult) : assessmentResult; const title = '移民资格评估报告'; if (existingReport) { // Update existing report (re-assessment after 30+ days) existingReport.content = contentToSave; existingReport.conversationId = context.conversationId; await this.artifactRepo.save(existingReport); console.log(`[Tool:run_professional_assessment] Updated existing report: ${existingReport.id}`); return { status: 'completed', assessment: parsedResult || assessmentResult, artifactId: existingReport.id, isReassessment: true, paidOrderId: paidAssessmentOrder.id, assessedAt: new Date().toISOString(), _ui_hint: '前端已渲染评估报告卡片。评估完成后建议自动创建材料清单和时间线。', }; } const artifact = this.artifactRepo.create({ tenantId, userId, conversationId: context.conversationId, artifactType: 'assessment_report', title, documentType: 'report', content: contentToSave, }); const saved = await this.artifactRepo.save(artifact); console.log(`[Tool:run_professional_assessment] Assessment saved: ${saved.id}`); return { status: 'completed', assessment: parsedResult || assessmentResult, artifactId: saved.id, isReassessment: false, paidOrderId: paidAssessmentOrder.id, assessedAt: new Date().toISOString(), _ui_hint: '前端已渲染评估报告卡片。评估完成后建议自动创建材料清单和时间线。', }; } }