整合体重和饮食指令处理逻辑,优化AI教练服务,支持通过指令解析用户输入。更新系统提示以提供个性化健康建议,并增强饮食图片分析功能。移除冗余的体重识别逻辑,简化代码结构。
This commit is contained in:
@@ -33,39 +33,8 @@ export class AiCoachController {
|
|||||||
userContent,
|
userContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
let weightInfo: { weightKg?: number; systemNotice?: string } = {};
|
// 体重和饮食指令处理现在已经集成到 streamChat 方法中
|
||||||
|
// 通过 # 字符开头的指令系统进行统一处理
|
||||||
// 体重识别逻辑优化:
|
|
||||||
// 1. 如果有图片URL,使用原有的图片识别逻辑
|
|
||||||
// 2. 如果没有图片URL,但文本中包含体重信息,使用新的文本识别逻辑
|
|
||||||
try {
|
|
||||||
if (body.imageUrl) {
|
|
||||||
// 原有逻辑:从图片识别体重
|
|
||||||
const imageWeightInfo = await this.aiCoachService.maybeExtractAndUpdateWeight(
|
|
||||||
userId,
|
|
||||||
body.imageUrl,
|
|
||||||
userContent,
|
|
||||||
);
|
|
||||||
if (imageWeightInfo.weightKg) {
|
|
||||||
weightInfo = {
|
|
||||||
weightKg: imageWeightInfo.weightKg,
|
|
||||||
systemNotice: `系统提示:已从图片识别体重为${imageWeightInfo.weightKg}kg,并已为你更新到个人资料。`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 新逻辑:从文本识别体重,并获取历史对比信息
|
|
||||||
const textWeightInfo = await this.aiCoachService.processWeightFromText(userId, userContent);
|
|
||||||
if (textWeightInfo.weightKg && textWeightInfo.systemNotice) {
|
|
||||||
weightInfo = {
|
|
||||||
weightKg: textWeightInfo.weightKg,
|
|
||||||
systemNotice: textWeightInfo.systemNotice
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 体重识别失败不影响正常对话
|
|
||||||
console.error('体重识别失败:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
// 非流式:聚合后一次性返回文本
|
// 非流式:聚合后一次性返回文本
|
||||||
@@ -73,14 +42,14 @@ export class AiCoachController {
|
|||||||
userId,
|
userId,
|
||||||
conversationId,
|
conversationId,
|
||||||
userContent,
|
userContent,
|
||||||
systemNotice: weightInfo.systemNotice,
|
imageUrl: body.imageUrl,
|
||||||
});
|
});
|
||||||
let text = '';
|
let text = '';
|
||||||
for await (const chunk of readable) {
|
for await (const chunk of readable) {
|
||||||
text += chunk.toString();
|
text += chunk.toString();
|
||||||
}
|
}
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
res.send({ conversationId, text, weightKg: weightInfo.weightKg });
|
res.send({ conversationId, text });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +62,7 @@ export class AiCoachController {
|
|||||||
userId,
|
userId,
|
||||||
conversationId,
|
conversationId,
|
||||||
userContent,
|
userContent,
|
||||||
systemNotice: weightInfo.systemNotice,
|
imageUrl: body.imageUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
readable.on('data', (chunk) => {
|
readable.on('data', (chunk) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { PostureAssessment } from './models/posture-assessment.model';
|
|||||||
import { UserProfile } from '../users/models/user-profile.model';
|
import { UserProfile } from '../users/models/user-profile.model';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `作为一名资深的普拉提与运动康复教练(Pilates Coach)兼营养分析师(Nutrition Analyst),我拥有丰富的专业知识,包括但不限于:
|
const SYSTEM_PROMPT = `作为一名资深的健康管家兼营养分析师(Nutrition Analyst)和健身教练,我拥有丰富的专业知识,包括但不限于:
|
||||||
|
|
||||||
运动领域:运动解剖学、体态评估、疼痛预防、功能性训练、力量与柔韧性训练、运动损伤预防与恢复。
|
运动领域:运动解剖学、体态评估、疼痛预防、功能性训练、力量与柔韧性训练、运动损伤预防与恢复。
|
||||||
|
|
||||||
@@ -76,6 +76,16 @@ const NUTRITION_ANALYST_PROMPT = `营养分析师模式(仅在检测为营养/
|
|||||||
- 无法确定时给出区间与假设,并提示用户完善信息。
|
- 无法确定时给出区间与假设,并提示用户完善信息。
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指令解析结果接口
|
||||||
|
*/
|
||||||
|
interface CommandResult {
|
||||||
|
isCommand: boolean;
|
||||||
|
command?: 'weight' | 'diet';
|
||||||
|
originalText: string;
|
||||||
|
cleanText: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiCoachService {
|
export class AiCoachService {
|
||||||
private readonly logger = new Logger(AiCoachService.name);
|
private readonly logger = new Logger(AiCoachService.name);
|
||||||
@@ -131,27 +141,114 @@ export class AiCoachService {
|
|||||||
return messages;
|
return messages;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建包含用户体重信息的系统提示
|
||||||
|
*/
|
||||||
|
private async buildUserWeightContext(userId: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const { currentWeight, history } = await this.getUserWeightHistory(userId, 10);
|
||||||
|
|
||||||
|
if (!currentWeight && history.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = '\n\n=== 用户体重信息 ===\n';
|
||||||
|
|
||||||
|
if (currentWeight) {
|
||||||
|
context += `当前体重:${currentWeight}kg\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history.length > 0) {
|
||||||
|
context += `体重历史记录(最近${Math.min(history.length, 10)}次):\n`;
|
||||||
|
|
||||||
|
// 按时间倒序显示最近的记录
|
||||||
|
const recentHistory = history.slice(0, 10).reverse();
|
||||||
|
recentHistory.forEach((record, index) => {
|
||||||
|
const date = new Date(record.createdAt).toLocaleDateString('zh-CN');
|
||||||
|
context += `${index + 1}. ${date}: ${record.weight}kg (${record.source === 'manual' ? '手动记录' : record.source === 'vision' ? '视觉识别' : '其他'})\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分析体重趋势
|
||||||
|
if (recentHistory.length >= 2) {
|
||||||
|
const trend = this.analyzeWeightTrend(recentHistory, currentWeight || recentHistory[0].weight);
|
||||||
|
context += `\n体重趋势分析:${trend}\n`;
|
||||||
|
|
||||||
|
// 计算变化幅度
|
||||||
|
const firstWeight = recentHistory[recentHistory.length - 1].weight;
|
||||||
|
const lastWeight = recentHistory[0].weight;
|
||||||
|
const totalChange = lastWeight - firstWeight;
|
||||||
|
const changePercent = ((totalChange / firstWeight) * 100).toFixed(1);
|
||||||
|
|
||||||
|
if (Math.abs(totalChange) > 0.5) {
|
||||||
|
const changeDirection = totalChange > 0 ? '增加' : '减少';
|
||||||
|
context += `总体变化:${changeDirection}${Math.abs(totalChange).toFixed(1)}kg (${changePercent}%)\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context += '\n请根据用户的体重记录趋势,提供个性化的健康建议。';
|
||||||
|
|
||||||
|
return context;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`构建用户体重上下文失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async streamChat(params: {
|
async streamChat(params: {
|
||||||
userId: string;
|
userId: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
userContent: string;
|
userContent: string;
|
||||||
systemNotice?: string;
|
systemNotice?: string;
|
||||||
|
imageUrl?: string;
|
||||||
}): Promise<Readable> {
|
}): Promise<Readable> {
|
||||||
|
// 解析指令(如果以 # 开头)
|
||||||
|
const commandResult = this.parseCommand(params.userContent);
|
||||||
|
|
||||||
// 上下文:系统提示 + 历史 + 当前用户消息
|
// 上下文:系统提示 + 历史 + 当前用户消息
|
||||||
const messages = await this.buildChatHistory(params.userId, params.conversationId);
|
const messages = await this.buildChatHistory(params.userId, params.conversationId);
|
||||||
if (params.systemNotice) {
|
if (params.systemNotice) {
|
||||||
messages.unshift({ role: 'system', content: params.systemNotice });
|
messages.unshift({ role: 'system', content: params.systemNotice });
|
||||||
}
|
}
|
||||||
if (this.isLikelyNutritionTopic(params.userContent, messages)) {
|
|
||||||
messages.unshift({ role: 'system', content: NUTRITION_ANALYST_PROMPT });
|
this.logger.log(`commandResult: ${JSON.stringify(commandResult, null, 2)}`);
|
||||||
|
|
||||||
|
// 处理体重记录指令
|
||||||
|
if (commandResult.command === 'weight') {
|
||||||
|
const textWeightInfo = await this.processWeightFromText(params.userId, params.userContent);
|
||||||
|
if (textWeightInfo.systemNotice) {
|
||||||
|
params.systemNotice = textWeightInfo.systemNotice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为体重相关话题提供用户体重信息上下文
|
||||||
|
const weightContext = await this.buildUserWeightContext(params.userId);
|
||||||
|
if (weightContext) {
|
||||||
|
messages.unshift({ role: 'system', content: weightContext });
|
||||||
|
}
|
||||||
|
} else if (commandResult.command === 'diet') {
|
||||||
|
// 使用视觉模型分析饮食图片
|
||||||
|
if (params.imageUrl) {
|
||||||
|
const dietAnalysis = await this.analyzeDietImage(params.imageUrl);
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: `用户通过拍照记录饮食,图片分析结果如下:\n${dietAnalysis}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
messages.unshift({ role: 'system', content: this.buildDietAnalysisPrompt() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// else if (this.isLikelyNutritionTopic(params.userContent, messages)) {
|
||||||
|
// messages.unshift({ role: 'system', content: NUTRITION_ANALYST_PROMPT });
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.logger.log(`messages: ${JSON.stringify(messages)}`);
|
||||||
|
|
||||||
const stream = await this.client.chat.completions.create({
|
const stream = await this.client.chat.completions.create({
|
||||||
model: this.model,
|
model: this.model,
|
||||||
messages,
|
messages,
|
||||||
stream: true,
|
stream: true,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
max_tokens: 800,
|
max_tokens: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
const readable = new Readable({ read() { } });
|
const readable = new Readable({ read() { } });
|
||||||
@@ -186,6 +283,8 @@ export class AiCoachService {
|
|||||||
return readable;
|
return readable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private isLikelyNutritionTopic(
|
private isLikelyNutritionTopic(
|
||||||
currentText: string | undefined,
|
currentText: string | undefined,
|
||||||
messages?: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>,
|
messages?: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>,
|
||||||
@@ -231,6 +330,137 @@ export class AiCoachService {
|
|||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析用户输入的指令(以 # 开头)
|
||||||
|
* @param text 用户输入文本
|
||||||
|
* @returns 指令解析结果
|
||||||
|
*/
|
||||||
|
private parseCommand(text: string): CommandResult {
|
||||||
|
if (!text || !text.trim().startsWith('#')) {
|
||||||
|
return {
|
||||||
|
isCommand: false,
|
||||||
|
originalText: text || '',
|
||||||
|
cleanText: text || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedText = text.trim();
|
||||||
|
const commandMatch = trimmedText.match(/^#([^\s::]+)[::]?\s*(.*)$/);
|
||||||
|
|
||||||
|
if (!commandMatch) {
|
||||||
|
return {
|
||||||
|
isCommand: false,
|
||||||
|
originalText: text,
|
||||||
|
cleanText: text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, commandPart, restText] = commandMatch;
|
||||||
|
const cleanText = restText.trim();
|
||||||
|
|
||||||
|
// 识别体重记录指令
|
||||||
|
if (/^记体重$|^体重$|^称重$/.test(commandPart)) {
|
||||||
|
return {
|
||||||
|
isCommand: true,
|
||||||
|
command: 'weight',
|
||||||
|
originalText: text,
|
||||||
|
cleanText: cleanText || '记录体重'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 识别饮食记录指令
|
||||||
|
if (/^记饮食$|^饮食$|^记录饮食$/.test(commandPart)) {
|
||||||
|
return {
|
||||||
|
isCommand: true,
|
||||||
|
command: 'diet',
|
||||||
|
originalText: text,
|
||||||
|
cleanText: cleanText || '记录饮食'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未识别的指令,当作普通文本处理
|
||||||
|
return {
|
||||||
|
isCommand: false,
|
||||||
|
originalText: text,
|
||||||
|
cleanText: text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建饮食分析提示
|
||||||
|
* @returns 饮食分析提示文本
|
||||||
|
*/
|
||||||
|
private buildDietAnalysisPrompt(): string {
|
||||||
|
return `饮食分析专家模式:
|
||||||
|
|
||||||
|
你是一位专业的营养分析师和饮食评估专家。用户将通过拍照记录饮食,你需要:
|
||||||
|
|
||||||
|
1. 饮食识别:
|
||||||
|
- 仔细观察图片中的食物种类、分量
|
||||||
|
- 识别主要食材和烹饪方式
|
||||||
|
- 估算食物的重量或体积
|
||||||
|
|
||||||
|
2. 热量分析:
|
||||||
|
- 基于识别的食物,计算总热量
|
||||||
|
- 分析三大营养素比例(蛋白质、碳水化合物、脂肪)
|
||||||
|
- 估算主要维生素和矿物质含量
|
||||||
|
|
||||||
|
3. 健康建议:
|
||||||
|
- 评估该餐的营养均衡性
|
||||||
|
- 提供个性化的饮食改进建议
|
||||||
|
- 针对用户的健康目标给出指导
|
||||||
|
|
||||||
|
请以结构化、清晰的方式输出结果,使用亲切专业的语言风格。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析饮食图片
|
||||||
|
* @param imageUrl 图片URL
|
||||||
|
* @returns 饮食分析结果
|
||||||
|
*/
|
||||||
|
private async analyzeDietImage(imageUrl: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const prompt = `请分析这张食物图片,识别其中的食物种类、分量,并提供以下信息:
|
||||||
|
|
||||||
|
1. 食物识别:
|
||||||
|
- 主要食材名称
|
||||||
|
- 烹饪方式
|
||||||
|
- 食物类型(主食、蛋白质、蔬菜、水果等)
|
||||||
|
|
||||||
|
2. 分量估算:
|
||||||
|
- 每种食物的大致重量或体积
|
||||||
|
- 使用常见单位描述(如:100g、1碗、2片等)
|
||||||
|
|
||||||
|
3. 营养分析:
|
||||||
|
- 估算总热量(kcal)
|
||||||
|
- 三大营养素含量(蛋白质、碳水化合物、脂肪)
|
||||||
|
- 主要维生素和矿物质
|
||||||
|
|
||||||
|
请以结构化、清晰的方式输出结果。`;
|
||||||
|
|
||||||
|
const completion = await this.client.chat.completions.create({
|
||||||
|
model: this.visionModel,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: prompt },
|
||||||
|
{ type: 'image_url', image_url: { url: imageUrl } as any },
|
||||||
|
] as any,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`diet image analysis result: ${completion.choices?.[0]?.message?.content}`);
|
||||||
|
|
||||||
|
return completion.choices?.[0]?.message?.content || '无法分析图片中的食物';
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`饮食图片分析失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
return '饮食图片分析失败';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private deriveTitleIfEmpty(assistantReply: string): string | null {
|
private deriveTitleIfEmpty(assistantReply: string): string | null {
|
||||||
if (!assistantReply) return null;
|
if (!assistantReply) return null;
|
||||||
const firstLine = assistantReply.split(/\r?\n/).find(Boolean) || '';
|
const firstLine = assistantReply.split(/\r?\n/).find(Boolean) || '';
|
||||||
@@ -376,51 +606,6 @@ export class AiCoachService {
|
|||||||
return { id: rec.id, overallScore, result };
|
return { id: rec.id, overallScore, result };
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLikelyWeightLogIntent(text: string | undefined): boolean {
|
|
||||||
if (!text) return false;
|
|
||||||
const t = text.toLowerCase();
|
|
||||||
return /体重|称重|秤|kg|公斤|weigh|weight/.test(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
async maybeExtractAndUpdateWeight(userId: string, imageUrl?: string, userText?: string): Promise<{ weightKg?: number }> {
|
|
||||||
if (!imageUrl || !this.isLikelyWeightLogIntent(userText)) return {};
|
|
||||||
try {
|
|
||||||
const sys = '从照片中读取电子秤的数字,单位通常为kg。仅返回JSON,例如 {"weightKg": 65.2},若无法识别,返回 {"weightKg": null}。不要添加其他文本。';
|
|
||||||
const completion = await this.client.chat.completions.create({
|
|
||||||
model: this.visionModel,
|
|
||||||
messages: [
|
|
||||||
{ role: 'system', content: sys },
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: [
|
|
||||||
{ type: 'text', text: '请从图片中提取体重(kg)。若图中单位为斤或lb,请换算为kg。' },
|
|
||||||
{ type: 'image_url', image_url: { url: imageUrl } as any },
|
|
||||||
] as any,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
temperature: 0,
|
|
||||||
response_format: { type: 'json_object' } as any,
|
|
||||||
});
|
|
||||||
const raw = completion.choices?.[0]?.message?.content || '';
|
|
||||||
let weightKg: number | undefined;
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(raw);
|
|
||||||
weightKg = typeof obj.weightKg === 'number' ? obj.weightKg : undefined;
|
|
||||||
} catch {
|
|
||||||
const m = raw.match(/\d+(?:\.\d+)?/);
|
|
||||||
weightKg = m ? parseFloat(m[0]) : undefined;
|
|
||||||
}
|
|
||||||
if (weightKg && isFinite(weightKg) && weightKg > 0 && weightKg < 400) {
|
|
||||||
await this.usersService.addWeightByVision(userId, weightKg);
|
|
||||||
return { weightKg };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(`maybeExtractAndUpdateWeight error: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从用户文本中识别体重信息
|
* 从用户文本中识别体重信息
|
||||||
* 支持多种格式:65kg、65公斤、65.5kg、体重65等
|
* 支持多种格式:65kg、65公斤、65.5kg、体重65等
|
||||||
@@ -432,16 +617,36 @@ export class AiCoachService {
|
|||||||
|
|
||||||
// 匹配各种体重格式的正则表达式
|
// 匹配各种体重格式的正则表达式
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/(?:体重|称重|秤|重量|weight).*?(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)/i,
|
// 匹配 "#记体重:80 kg" 格式
|
||||||
|
/#(?:记体重|体重|称重|记录体重)[::]\s*(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)/i,
|
||||||
|
|
||||||
|
// 匹配带单位的体重 "80kg", "80.5公斤", "80 kg"
|
||||||
/(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)/i,
|
/(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)/i,
|
||||||
/(?:体重|称重|秤|重量|weight).*?(\d+(?:\.\d+)?)/i,
|
|
||||||
/我(?:现在|今天)?(?:体重|重量|称重)?(?:是|为|有)?(\d+(?:\.\d+)?)/i,
|
// 匹配体重关键词后的数字 "体重65", "weight 70.5"
|
||||||
|
/(?:体重|称重|秤|重量|weight)[::]?\s*(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)?/i,
|
||||||
|
|
||||||
|
// 匹配口语化表达 "我体重65", "我现在70kg"
|
||||||
|
/我(?:现在|今天)?(?:体重|重量|称重)?(?:是|为|有|:|:)?\s*(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)?/i,
|
||||||
|
|
||||||
|
// 匹配简单数字+单位格式 "65.5kg"
|
||||||
|
/^(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)$/i,
|
||||||
|
|
||||||
|
// 匹配斤单位并转换为kg "130斤"
|
||||||
|
/(\d+(?:\.\d+)?)\s*斤/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
const match = t.match(pattern);
|
const match = t.match(pattern);
|
||||||
if (match) {
|
if (match) {
|
||||||
const weight = parseFloat(match[1]);
|
let weight = parseFloat(match[1]);
|
||||||
|
|
||||||
|
// 如果是斤单位,转换为kg (1斤 = 0.5kg)
|
||||||
|
// 只有专门匹配斤单位的模式才进行转换,避免"公斤"等词被误判
|
||||||
|
if (pattern.source.includes('斤') && !pattern.source.includes('公斤')) {
|
||||||
|
weight = weight * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
// 合理的体重范围检查 (20-400kg)
|
// 合理的体重范围检查 (20-400kg)
|
||||||
if (weight >= 20 && weight <= 400) {
|
if (weight >= 20 && weight <= 400) {
|
||||||
return weight;
|
return weight;
|
||||||
@@ -546,14 +751,12 @@ export class AiCoachService {
|
|||||||
}> {
|
}> {
|
||||||
if (!userText) return {};
|
if (!userText) return {};
|
||||||
|
|
||||||
// 检查是否是体重记录意图
|
|
||||||
if (!this.isLikelyWeightLogIntent(userText)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 从文本中提取体重
|
// 从文本中提取体重
|
||||||
const extractedWeight = this.extractWeightFromText(userText);
|
const extractedWeight = this.extractWeightFromText(userText);
|
||||||
|
this.logger.log(`extractedWeight: ${extractedWeight}`);
|
||||||
|
|
||||||
if (!extractedWeight) {
|
if (!extractedWeight) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -564,6 +767,9 @@ export class AiCoachService {
|
|||||||
// 更新体重到数据库
|
// 更新体重到数据库
|
||||||
await this.usersService.addWeightByVision(userId, extractedWeight);
|
await this.usersService.addWeightByVision(userId, extractedWeight);
|
||||||
|
|
||||||
|
this.logger.log(`currentWeight: ${currentWeight}`);
|
||||||
|
this.logger.log(`history: ${JSON.stringify(history)}`);
|
||||||
|
|
||||||
// 构建系统提示
|
// 构建系统提示
|
||||||
const systemNotice = this.buildWeightUpdateSystemNotice(
|
const systemNotice = this.buildWeightUpdateSystemNotice(
|
||||||
extractedWeight,
|
extractedWeight,
|
||||||
|
|||||||
Reference in New Issue
Block a user