1050 lines
32 KiB
TypeScript
1050 lines
32 KiB
TypeScript
import { Injectable, Optional } from '@nestjs/common';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import { ConversationContext } from '../claude-agent.service';
|
||
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
|
||
import { PaymentClientService } from '../../payment/payment-client.service';
|
||
|
||
export interface Tool {
|
||
name: string;
|
||
description: string;
|
||
input_schema: {
|
||
type: string;
|
||
properties: Record<string, unknown>;
|
||
required?: string[];
|
||
};
|
||
}
|
||
|
||
@Injectable()
|
||
export class ImmigrationToolsService {
|
||
constructor(
|
||
private knowledgeClient: KnowledgeClientService,
|
||
private configService: ConfigService,
|
||
@Optional() private paymentClient?: PaymentClientService,
|
||
) {}
|
||
|
||
/**
|
||
* 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<string, unknown>,
|
||
context: ConversationContext,
|
||
): Promise<unknown> {
|
||
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);
|
||
|
||
default:
|
||
return { error: `Unknown tool: ${toolName}` };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Search knowledge base - 调用 knowledge-service RAG API
|
||
* 级联 Fallback: 知识库(置信度检查) → 网络搜索 → 标注来源的内置知识
|
||
*/
|
||
private async searchKnowledge(
|
||
input: Record<string, unknown>,
|
||
context: ConversationContext,
|
||
): Promise<unknown> {
|
||
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<string, unknown>): Promise<unknown> {
|
||
// 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<string, unknown>,
|
||
context: ConversationContext,
|
||
): Promise<unknown> {
|
||
// coordinator-tools.ts sends { userId, field, value } (single-field mode)
|
||
// Legacy getTools() sent { category, age, education, ... } (batch mode)
|
||
// Support both formats
|
||
let info: Record<string, unknown>;
|
||
|
||
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<string, unknown>,
|
||
context: ConversationContext,
|
||
): Promise<unknown> {
|
||
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 };
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
orderId: order.id,
|
||
paymentId: payment.paymentId,
|
||
amount: payment.amount,
|
||
currency: order.currency,
|
||
paymentMethod: payment.method,
|
||
qrCodeUrl: payment.qrCodeUrl,
|
||
paymentUrl: payment.paymentUrl,
|
||
expiresAt: payment.expiresAt,
|
||
message: `请扫描二维码支付 ¥${payment.amount} 完成${category}类别的移民资格评估服务`,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Check payment status — 查询订单支付状态
|
||
*/
|
||
private async checkPaymentStatus(
|
||
input: Record<string, unknown>,
|
||
): Promise<unknown> {
|
||
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<string, string> = {
|
||
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<unknown> {
|
||
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<string, unknown>,
|
||
): Promise<unknown> {
|
||
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<string, unknown>,
|
||
context: ConversationContext,
|
||
): Promise<unknown> {
|
||
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<string, unknown>,
|
||
context: ConversationContext,
|
||
): Promise<unknown> {
|
||
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<string, unknown>): 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<string, unknown>): Promise<unknown> {
|
||
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<string>('GOOGLE_SEARCH_API_KEY');
|
||
const googleCseId = this.configService.get<string>('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<string, unknown>): Promise<unknown> {
|
||
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<string>('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<string, number>;
|
||
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<string, Record<string, number>> = {
|
||
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<string, unknown>): Promise<unknown> {
|
||
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<string>('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<unknown> };
|
||
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<string, Array<{ title: string; summary: string; date: string; source: string }>> = {
|
||
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' },
|
||
],
|
||
};
|
||
}
|
||
}
|