stash
This commit is contained in:
456
docs/API-AI-HEALTH-REPORT.md
Normal file
456
docs/API-AI-HEALTH-REPORT.md
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
# AI 健康报告接口文档
|
||||||
|
|
||||||
|
## 接口概述
|
||||||
|
|
||||||
|
生成用户的 AI 健康报告图片接口,基于用户的健康数据(体重、饮食、运动等)生成可视化的健康分析报告图片。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口信息
|
||||||
|
|
||||||
|
- **接口名称**: 生成 AI 健康报告
|
||||||
|
- **接口路径**: `/users/ai-report`
|
||||||
|
- **请求方法**: `POST`
|
||||||
|
- **认证方式**: JWT Token (Bearer Token)
|
||||||
|
- **内容类型**: `application/json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 请求说明
|
||||||
|
|
||||||
|
### 请求头 (Headers)
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||||
|
| ------------- | ------ | ---- | ------------ | ------------------------------------------------ |
|
||||||
|
| Authorization | string | 是 | JWT 访问令牌 | `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` |
|
||||||
|
| Content-Type | string | 是 | 请求内容类型 | `application/json` |
|
||||||
|
|
||||||
|
### 请求体 (Body)
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||||
|
| ------ | ------ | ---- | ------------------------------------------------------------- | -------------- |
|
||||||
|
| date | string | 否 | 指定生成报告的日期,格式 YYYY-MM-DD。不传则默认生成今天的报告 | `"2024-01-15"` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
#### 示例 1: 生成今天的报告
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://api.example.com/users/ai-report' \
|
||||||
|
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 示例 2: 生成指定日期的报告
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://api.example.com/users/ai-report' \
|
||||||
|
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"date": "2024-01-15"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript/TypeScript 示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用 fetch
|
||||||
|
async function generateHealthReport(date?: string) {
|
||||||
|
const response = await fetch("https://api.example.com/users/ai-report", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(date ? { date } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 axios
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
async function generateHealthReport(date?: string) {
|
||||||
|
const response = await axios.post(
|
||||||
|
"https://api.example.com/users/ai-report",
|
||||||
|
date ? { date } : {},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Swift 示例
|
||||||
|
|
||||||
|
```swift
|
||||||
|
func generateHealthReport(date: String? = nil, completion: @escaping (Result<HealthReportResponse, Error>) -> Void) {
|
||||||
|
let url = URL(string: "https://api.example.com/users/ai-report")!
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
var body: [String: Any] = [:]
|
||||||
|
if let date = date {
|
||||||
|
body["date"] = date
|
||||||
|
}
|
||||||
|
|
||||||
|
if !body.isEmpty {
|
||||||
|
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||||
|
}
|
||||||
|
|
||||||
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = data else {
|
||||||
|
completion(.failure(NSError(domain: "", code: -1, userInfo: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let result = try decoder.decode(HealthReportResponse.self, from: data)
|
||||||
|
completion(.success(result))
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应模型
|
||||||
|
struct HealthReportResponse: Codable {
|
||||||
|
let code: Int
|
||||||
|
let message: String
|
||||||
|
let data: HealthReportData
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HealthReportData: Codable {
|
||||||
|
let imageUrl: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 响应说明
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
所有响应都遵循统一的响应格式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
code: number; // 响应码: 0-成功, 1-失败
|
||||||
|
message: string; // 响应消息
|
||||||
|
data: {
|
||||||
|
imageUrl: string; // 生成的报告图片 URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
|
||||||
|
**HTTP 状态码**: `200 OK`
|
||||||
|
|
||||||
|
**响应体示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "AI健康报告生成成功",
|
||||||
|
"data": {
|
||||||
|
"imageUrl": "https://pilates-1234567890.cos.ap-guangzhou.myqcloud.com/health-reports/user-123/2024-01-15/report-xxxxx.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| ------------- | ------ | -------------------------------------------- |
|
||||||
|
| code | number | 响应码,0 表示成功 |
|
||||||
|
| message | string | 响应消息,成功时为 "AI健康报告生成成功" |
|
||||||
|
| data.imageUrl | string | 生成的健康报告图片完整 URL,可直接访问和下载 |
|
||||||
|
|
||||||
|
### 失败响应
|
||||||
|
|
||||||
|
**HTTP 状态码**: `200 OK` (业务失败也返回 200,通过 code 字段判断)
|
||||||
|
|
||||||
|
**响应体示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 1,
|
||||||
|
"message": "生成失败: 用户健康数据不足",
|
||||||
|
"data": {
|
||||||
|
"imageUrl": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**常见错误消息**:
|
||||||
|
|
||||||
|
| 错误消息 | 说明 | 解决方案 |
|
||||||
|
| ----------------------------- | ------------------------------ | ---------------------------------------------- |
|
||||||
|
| `生成失败: 用户健康数据不足` | 用户没有足够的健康数据生成报告 | 引导用户添加更多健康数据(体重、饮食、运动等) |
|
||||||
|
| `生成失败: 日期格式不正确` | date 参数格式错误 | 确保日期格式为 YYYY-MM-DD |
|
||||||
|
| `生成失败: 未找到用户信息` | 用户不存在或 Token 无效 | 检查认证 Token 是否有效 |
|
||||||
|
| `生成失败: AI 服务暂时不可用` | AI 模型服务异常 | 稍后重试 |
|
||||||
|
|
||||||
|
### 认证失败响应
|
||||||
|
|
||||||
|
**HTTP 状态码**: `401 Unauthorized`
|
||||||
|
|
||||||
|
**响应体示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 401,
|
||||||
|
"message": "Unauthorized"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 业务逻辑说明
|
||||||
|
|
||||||
|
### 报告内容
|
||||||
|
|
||||||
|
AI 健康报告会基于用户的以下数据生成:
|
||||||
|
|
||||||
|
1. **体重数据**: 当天或最近的体重记录
|
||||||
|
2. **饮食数据**: 当天的饮食记录和营养摄入
|
||||||
|
3. **运动数据**: 当天的运动记录和卡路里消耗
|
||||||
|
4. **围度数据**: 身体各部位的围度测量数据
|
||||||
|
5. **目标进度**: 用户设定的健康目标完成情况
|
||||||
|
|
||||||
|
### 报告生成逻辑
|
||||||
|
|
||||||
|
- 如果不传 `date` 参数,默认生成今天的报告
|
||||||
|
- 如果指定日期没有数据,会返回数据不足的提示
|
||||||
|
- 报告图片为 PNG 格式,尺寸适配移动端展示
|
||||||
|
- 图片会存储在腾讯云 COS,有效期永久(或根据策略定期清理)
|
||||||
|
|
||||||
|
### 缓存策略
|
||||||
|
|
||||||
|
- 同一天同一用户的报告会缓存,重复请求会返回相同的图片 URL
|
||||||
|
- 如果用户更新了当天的数据,可以重新生成覆盖旧报告
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 客户端错误处理示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function fetchHealthReport(date?: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.example.com/users/ai-report", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(date ? { date } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查 HTTP 状态码
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Token 失效,需要重新登录
|
||||||
|
throw new Error("认证失败,请重新登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// 检查业务状态码
|
||||||
|
if (result.code !== 0) {
|
||||||
|
// 业务失败
|
||||||
|
throw new Error(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功,返回图片 URL
|
||||||
|
return result.data.imageUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("生成健康报告失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
try {
|
||||||
|
const imageUrl = await fetchHealthReport();
|
||||||
|
console.log("报告生成成功:", imageUrl);
|
||||||
|
// 在 UI 中展示图片
|
||||||
|
} catch (error) {
|
||||||
|
// 向用户展示错误提示
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用建议
|
||||||
|
|
||||||
|
### 1. 前置检查
|
||||||
|
|
||||||
|
在调用接口前,建议先检查:
|
||||||
|
|
||||||
|
- 用户是否已登录(有有效的 JWT Token)
|
||||||
|
- 用户是否有足够的健康数据(可通过其他接口查询)
|
||||||
|
- 网络连接是否正常
|
||||||
|
|
||||||
|
### 2. 加载提示
|
||||||
|
|
||||||
|
由于报告生成需要调用 AI 服务,可能需要几秒钟时间,建议:
|
||||||
|
|
||||||
|
- 显示加载动画或进度提示
|
||||||
|
- 设置合理的超时时间(建议 30-60 秒)
|
||||||
|
- 提供取消操作的选项
|
||||||
|
|
||||||
|
### 3. 图片展示
|
||||||
|
|
||||||
|
获取到图片 URL 后:
|
||||||
|
|
||||||
|
- 可以直接在 Image 组件中使用该 URL
|
||||||
|
- 支持下载保存到本地相册
|
||||||
|
- 支持分享到社交媒体
|
||||||
|
|
||||||
|
### 4. 错误处理
|
||||||
|
|
||||||
|
- 网络错误:提示用户检查网络连接
|
||||||
|
- 数据不足:引导用户添加健康数据
|
||||||
|
- Token 过期:自动刷新 Token 或引导重新登录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
### React Native 完整示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Image, Button, Text, ActivityIndicator } from 'react-native';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const HealthReportScreen = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const generateReport = async (date?: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'https://api.example.com/users/ai-report',
|
||||||
|
date ? { date } : {},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
timeout: 60000, // 60秒超时
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
setImageUrl(response.data.data.imageUrl);
|
||||||
|
} else {
|
||||||
|
setError(response.data.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
setError('登录已过期,请重新登录');
|
||||||
|
} else if (err.code === 'ECONNABORTED') {
|
||||||
|
setError('请求超时,请检查网络连接');
|
||||||
|
} else {
|
||||||
|
setError(err.response?.data?.message || '生成报告失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError('未知错误');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, padding: 20 }}>
|
||||||
|
<Button
|
||||||
|
title="生成今天的健康报告"
|
||||||
|
onPressed={() => generateReport()}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<View style={{ marginTop: 20, alignItems: 'center' }}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
<Text style={{ marginTop: 10 }}>正在生成报告...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Text style={{ color: 'red', marginTop: 20 }}>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{imageUrl && (
|
||||||
|
<Image
|
||||||
|
source={{ uri: imageUrl }}
|
||||||
|
style={{ width: '100%', height: 400, marginTop: 20 }}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HealthReportScreen;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **认证要求**: 必须携带有效的 JWT Token,否则返回 401
|
||||||
|
2. **请求频率**: 建议不要频繁调用,同一用户同一天建议缓存结果
|
||||||
|
3. **图片有效期**: 图片 URL 长期有效,可以缓存在客户端
|
||||||
|
4. **数据依赖**: 需要用户有足够的健康数据才能生成有意义的报告
|
||||||
|
5. **网络要求**: AI 报告生成需要调用外部服务,需要稳定的网络连接
|
||||||
|
6. **超时设置**: 建议设置 30-60 秒的请求超时时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关接口
|
||||||
|
|
||||||
|
- [用户信息接口](./API-USER-INFO.md)
|
||||||
|
- [体重记录接口](./API-WEIGHT-RECORDS.md)
|
||||||
|
- [饮食记录接口](./API-DIET-RECORDS.md)
|
||||||
|
- [运动记录接口](./API-WORKOUT-RECORDS.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
| 版本 | 日期 | 更新内容 |
|
||||||
|
| ---- | ---------- | ------------------------------ |
|
||||||
|
| v1.0 | 2024-01-15 | 初始版本,支持基础健康报告生成 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术支持
|
||||||
|
|
||||||
|
如有问题,请联系技术支持团队。
|
||||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@nestjs/schedule": "^6.0.1",
|
"@nestjs/schedule": "^6.0.1",
|
||||||
"@nestjs/sequelize": "^11.0.0",
|
"@nestjs/sequelize": "^11.0.0",
|
||||||
"@nestjs/swagger": "^11.1.0",
|
"@nestjs/swagger": "^11.1.0",
|
||||||
|
"@openrouter/sdk": "^0.1.27",
|
||||||
"@parse/node-apn": "^5.0.0",
|
"@parse/node-apn": "^5.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
@@ -2612,6 +2613,15 @@
|
|||||||
"npm": ">=5.10.0"
|
"npm": ">=5.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@openrouter/sdk": {
|
||||||
|
"version": "0.1.27",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@openrouter/sdk/-/sdk-0.1.27.tgz",
|
||||||
|
"integrity": "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@parse/node-apn": {
|
"node_modules/@parse/node-apn": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/@parse/node-apn/-/node-apn-5.2.3.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/@parse/node-apn/-/node-apn-5.2.3.tgz",
|
||||||
@@ -13742,6 +13752,15 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.25.76",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/zod/-/zod-3.25.76.tgz",
|
||||||
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"@nestjs/schedule": "^6.0.1",
|
"@nestjs/schedule": "^6.0.1",
|
||||||
"@nestjs/sequelize": "^11.0.0",
|
"@nestjs/sequelize": "^11.0.0",
|
||||||
"@nestjs/swagger": "^11.1.0",
|
"@nestjs/swagger": "^11.1.0",
|
||||||
|
"@openrouter/sdk": "^0.1.27",
|
||||||
"@parse/node-apn": "^5.0.0",
|
"@parse/node-apn": "^5.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
|||||||
@@ -4,22 +4,32 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { AiCoachController } from './ai-coach.controller';
|
import { AiCoachController } from './ai-coach.controller';
|
||||||
import { AiCoachService } from './ai-coach.service';
|
import { AiCoachService } from './ai-coach.service';
|
||||||
import { DietAnalysisService } from './services/diet-analysis.service';
|
import { DietAnalysisService } from './services/diet-analysis.service';
|
||||||
|
import { AiReportService } from './services/ai-report.service';
|
||||||
import { AiMessage } from './models/ai-message.model';
|
import { AiMessage } from './models/ai-message.model';
|
||||||
import { AiConversation } from './models/ai-conversation.model';
|
import { AiConversation } from './models/ai-conversation.model';
|
||||||
import { PostureAssessment } from './models/posture-assessment.model';
|
import { PostureAssessment } from './models/posture-assessment.model';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
import { DietRecordsModule } from '../diet-records/diet-records.module';
|
import { DietRecordsModule } from '../diet-records/diet-records.module';
|
||||||
|
import { MedicationsModule } from '../medications/medications.module';
|
||||||
|
import { WorkoutsModule } from '../workouts/workouts.module';
|
||||||
|
import { MoodCheckinsModule } from '../mood-checkins/mood-checkins.module';
|
||||||
|
import { WaterRecordsModule } from '../water-records/water-records.module';
|
||||||
|
import { CosService } from '../users/cos.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
UsersModule,
|
forwardRef(() => UsersModule),
|
||||||
forwardRef(() => DietRecordsModule),
|
forwardRef(() => DietRecordsModule),
|
||||||
|
forwardRef(() => MedicationsModule),
|
||||||
|
forwardRef(() => WorkoutsModule),
|
||||||
|
forwardRef(() => MoodCheckinsModule),
|
||||||
|
forwardRef(() => WaterRecordsModule),
|
||||||
SequelizeModule.forFeature([AiConversation, AiMessage, PostureAssessment]),
|
SequelizeModule.forFeature([AiConversation, AiMessage, PostureAssessment]),
|
||||||
],
|
],
|
||||||
controllers: [AiCoachController],
|
controllers: [AiCoachController],
|
||||||
providers: [AiCoachService, DietAnalysisService],
|
providers: [AiCoachService, DietAnalysisService, AiReportService, CosService],
|
||||||
exports: [DietAnalysisService],
|
exports: [DietAnalysisService, AiReportService],
|
||||||
})
|
})
|
||||||
export class AiCoachModule { }
|
export class AiCoachModule { }
|
||||||
|
|
||||||
|
|||||||
429
src/ai-coach/services/ai-report.service.ts
Normal file
429
src/ai-coach/services/ai-report.service.ts
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AxiosResponse, AxiosRequestConfig, AxiosResponseHeaders } from 'axios';
|
||||||
|
import * as dayjs from 'dayjs';
|
||||||
|
import { OpenRouter } from '@openrouter/sdk';
|
||||||
|
import { CosService } from '../../users/cos.service';
|
||||||
|
|
||||||
|
// 假设各个模块的服务都已正确导出
|
||||||
|
import { UsersService } from '../../users/users.service';
|
||||||
|
import { MedicationStatsService } from '../../medications/medication-stats.service';
|
||||||
|
import { DietRecordsService } from '../../diet-records/diet-records.service';
|
||||||
|
import { WaterRecordsService } from '../../water-records/water-records.service';
|
||||||
|
import { MoodCheckinsService } from '../../mood-checkins/mood-checkins.service';
|
||||||
|
import { WorkoutsService } from '../../workouts/workouts.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聚合的每日健康数据接口
|
||||||
|
*/
|
||||||
|
interface DailyHealthData {
|
||||||
|
date: string;
|
||||||
|
user?: {
|
||||||
|
name: string;
|
||||||
|
avatar?: string;
|
||||||
|
gender?: string;
|
||||||
|
};
|
||||||
|
medications: {
|
||||||
|
totalScheduled: number;
|
||||||
|
taken: number;
|
||||||
|
missed: number;
|
||||||
|
completionRate: number;
|
||||||
|
};
|
||||||
|
diet: {
|
||||||
|
totalCalories: number;
|
||||||
|
totalProtein: number;
|
||||||
|
totalCarbohydrates: number;
|
||||||
|
totalFat: number;
|
||||||
|
averageCaloriesPerMeal: number;
|
||||||
|
mealTypeDistribution: Record<string, number>;
|
||||||
|
};
|
||||||
|
mood: {
|
||||||
|
primaryMood?: string;
|
||||||
|
averageIntensity?: number;
|
||||||
|
totalCheckins: number;
|
||||||
|
};
|
||||||
|
water: {
|
||||||
|
totalAmount: number;
|
||||||
|
dailyGoal: number;
|
||||||
|
completionRate: number;
|
||||||
|
};
|
||||||
|
bodyMeasurements: {
|
||||||
|
weight?: number;
|
||||||
|
latestChest?: number;
|
||||||
|
latestWaist?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AiReportService {
|
||||||
|
private readonly logger = new Logger(AiReportService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
@Inject(forwardRef(() => UsersService))
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
@Inject(forwardRef(() => MedicationStatsService))
|
||||||
|
private readonly medicationStatsService: MedicationStatsService,
|
||||||
|
@Inject(forwardRef(() => DietRecordsService))
|
||||||
|
private readonly dietRecordsService: DietRecordsService,
|
||||||
|
@Inject(forwardRef(() => WaterRecordsService))
|
||||||
|
private readonly waterRecordsService: WaterRecordsService,
|
||||||
|
@Inject(forwardRef(() => MoodCheckinsService))
|
||||||
|
private readonly moodCheckinsService: MoodCheckinsService,
|
||||||
|
@Inject(forwardRef(() => WorkoutsService))
|
||||||
|
private readonly workoutsService: WorkoutsService,
|
||||||
|
private readonly cosService: CosService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主入口:生成用户的AI健康报告图片
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param date 目标日期,格式 YYYY-MM-DD,默认为今天
|
||||||
|
* @returns 图片的 URL 或 Base64 数据
|
||||||
|
*/
|
||||||
|
async generateHealthReportImage(userId: string, date?: string): Promise<{ imageUrl: string }> {
|
||||||
|
const targetDate = date || dayjs().format('YYYY-MM-DD');
|
||||||
|
this.logger.log(`开始为用户 ${userId} 生成 ${targetDate} 的AI健康报告`);
|
||||||
|
|
||||||
|
// 1. 聚合数据
|
||||||
|
const dailyData = await this.gatherDailyData(userId, targetDate);
|
||||||
|
|
||||||
|
// 2. 生成图像生成Prompt
|
||||||
|
const imagePrompt = await this.generateImagePrompt(dailyData);
|
||||||
|
this.logger.log(`为用户 ${userId} 生成了图像Prompt: ${imagePrompt}`);
|
||||||
|
|
||||||
|
// 3. 调用图像生成API
|
||||||
|
const imageUrl = await this.callImageGenerationApi(imagePrompt);
|
||||||
|
this.logger.log(`为用户 ${userId} 成功生成图像: ${imageUrl}`);
|
||||||
|
|
||||||
|
return { imageUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. 聚合用户指定日期的各项健康数据
|
||||||
|
*/
|
||||||
|
private async gatherDailyData(userId: string, date: string): Promise<DailyHealthData> {
|
||||||
|
const dayStart = dayjs(date).startOf('day').toISOString();
|
||||||
|
const dayEnd = dayjs(date).endOf('day').toISOString();
|
||||||
|
|
||||||
|
const [userProfile, medicationStats, dietHistory, waterStats, moodStats] = await Promise.all([
|
||||||
|
this.usersService.getProfile({ sub: userId, email: '', isGuest: false } as any).then(p => p.data).catch(() => null),
|
||||||
|
this.medicationStatsService.getDailyStats(userId, date).catch(() => null),
|
||||||
|
// 获取当日饮食记录并聚合
|
||||||
|
this.dietRecordsService.getDietHistory(userId, { startDate: dayStart, endDate: dayEnd, limit: 100 }).catch(() => ({ records: [] })),
|
||||||
|
this.waterRecordsService.getWaterStats(userId, date).catch(() => null),
|
||||||
|
this.moodCheckinsService.getDaily(userId, date).catch(() => null),
|
||||||
|
// 获取最近的训练会话,并在后续筛选
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 处理饮食数据聚合
|
||||||
|
const dietRecords: any[] = (dietHistory as any).records || [];
|
||||||
|
const dietStats = {
|
||||||
|
totalCalories: dietRecords.reduce((sum, r) => sum + (r.estimatedCalories || 0), 0),
|
||||||
|
totalProtein: dietRecords.reduce((sum, r) => sum + (r.proteinGrams || 0), 0),
|
||||||
|
totalCarbohydrates: dietRecords.reduce((sum, r) => sum + (r.carbohydrateGrams || 0), 0),
|
||||||
|
totalFat: dietRecords.reduce((sum, r) => sum + (r.fatGrams || 0), 0),
|
||||||
|
averageCaloriesPerMeal: dietRecords.length > 0 ? (dietRecords.reduce((sum, r) => sum + (r.estimatedCalories || 0), 0) / dietRecords.length) : 0,
|
||||||
|
mealTypeDistribution: dietRecords.reduce((acc, r) => {
|
||||||
|
acc[r.mealType] = (acc[r.mealType] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 获取身体测量数据(最新的体重和围度)
|
||||||
|
const bodyMeasurements = await this.usersService.getBodyMeasurementHistory(userId, undefined).then(res => res.data).catch(() => []);
|
||||||
|
const latestWeight = await this.usersService.getWeightHistory(userId, { limit: 1 }).then(res => res[0]).catch(() => null);
|
||||||
|
const latestChest = bodyMeasurements.find(m => m.measurementType === 'chestCircumference');
|
||||||
|
const latestWaist = bodyMeasurements.find(m => m.measurementType === 'waistCircumference');
|
||||||
|
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
user: userProfile ? {
|
||||||
|
name: userProfile.name,
|
||||||
|
avatar: userProfile.avatar,
|
||||||
|
gender: (userProfile as any).gender,
|
||||||
|
} : undefined,
|
||||||
|
medications: medicationStats ? {
|
||||||
|
totalScheduled: medicationStats.totalScheduled,
|
||||||
|
taken: medicationStats.taken,
|
||||||
|
missed: medicationStats.missed,
|
||||||
|
completionRate: medicationStats.completionRate,
|
||||||
|
} : { totalScheduled: 0, taken: 0, missed: 0, completionRate: 0 },
|
||||||
|
diet: {
|
||||||
|
totalCalories: dietStats.totalCalories,
|
||||||
|
totalProtein: dietStats.totalProtein,
|
||||||
|
totalCarbohydrates: dietStats.totalCarbohydrates,
|
||||||
|
totalFat: dietStats.totalFat,
|
||||||
|
averageCaloriesPerMeal: dietStats.averageCaloriesPerMeal,
|
||||||
|
mealTypeDistribution: dietStats.mealTypeDistribution,
|
||||||
|
},
|
||||||
|
water: waterStats?.data ? {
|
||||||
|
totalAmount: waterStats.data.totalAmount,
|
||||||
|
dailyGoal: waterStats.data.dailyGoal,
|
||||||
|
completionRate: waterStats.data.completionRate,
|
||||||
|
} : { totalAmount: 0, dailyGoal: 0, completionRate: 0 },
|
||||||
|
mood: moodStats?.data && Array.isArray(moodStats.data) && moodStats.data.length > 0 ? {
|
||||||
|
primaryMood: moodStats.data[0].moodType,
|
||||||
|
averageIntensity: moodStats.data.reduce((sum: number, m: any) => sum + m.intensity, 0) / moodStats.data.length,
|
||||||
|
totalCheckins: moodStats.data.length,
|
||||||
|
} : { totalCheckins: 0 },
|
||||||
|
bodyMeasurements: {
|
||||||
|
weight: latestWeight?.weight,
|
||||||
|
latestChest: latestChest?.value,
|
||||||
|
latestWaist: latestWaist?.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2. 生成固定格式的 prompt,适用于 Nano Banana Pro 模型
|
||||||
|
* 优化:不再使用 LLM 生成 prompt,而是使用固定模版,并确保包含准确的中文文本
|
||||||
|
*/
|
||||||
|
private async generateImagePrompt(data: DailyHealthData): Promise<string> {
|
||||||
|
// 格式化日期为 "12月01日"
|
||||||
|
const dateStr = dayjs(data.date).format('MM月DD日');
|
||||||
|
|
||||||
|
// 准备数据文本
|
||||||
|
const moodText = this.translateMood(data.mood.primaryMood);
|
||||||
|
const medRate = Math.round(data.medications.completionRate); // 取整
|
||||||
|
const calories = Math.round(data.diet.totalCalories);
|
||||||
|
const water = Math.round(data.water.totalAmount);
|
||||||
|
|
||||||
|
// 根据性别调整角色描述
|
||||||
|
let characterDesc = 'A happy cute character or animal mascot';
|
||||||
|
if (data.user?.gender === 'male') {
|
||||||
|
characterDesc = 'A happy cute boy character in casual sportswear';
|
||||||
|
} else if (data.user?.gender === 'female') {
|
||||||
|
characterDesc = 'A happy cute girl character in yoga outfit';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 Prompt
|
||||||
|
// 格式:[风格描述] + [主体内容] + [文本渲染指令] + [细节描述]
|
||||||
|
const prompt = `
|
||||||
|
A cute, hand-drawn style health journal page illustration, kawaii aesthetic, soft pastel colors, warm lighting. Vertical 9:16 aspect ratio. High quality, 1k resolution.
|
||||||
|
|
||||||
|
The image features a cute layout with icons and text boxes.
|
||||||
|
Please render the following specific text in Chinese correctly:
|
||||||
|
- Title text: "${dateStr} 健康日报"
|
||||||
|
- Medication section text: "用药: ${medRate}%"
|
||||||
|
- Diet section text: "热量: ${calories}千卡"
|
||||||
|
- Water section text: "饮水: ${water}ml"
|
||||||
|
- Mood section text: "心情: ${moodText}"
|
||||||
|
|
||||||
|
Visual elements:
|
||||||
|
- ${characterDesc} representing the user.
|
||||||
|
- Icon for medication (pill bottle or pills).
|
||||||
|
- Icon for diet (healthy food bowl or apple).
|
||||||
|
- Icon for water (water glass or drop).
|
||||||
|
- Icon for exercise (sneakers or dumbbell).
|
||||||
|
- Icon for mood (a smiley face representing ${moodText}).
|
||||||
|
|
||||||
|
Composition: Clean, organized, magazine layout style, decorative stickers and washi tape effects.
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将心情类型翻译成中文(为了Prompt生成)
|
||||||
|
*/
|
||||||
|
private translateMood(moodType?: string): string {
|
||||||
|
const moodMap: Record<string, string> = {
|
||||||
|
HAPPY: '开心',
|
||||||
|
EXCITED: '兴奋',
|
||||||
|
THRILLED: '激动',
|
||||||
|
CALM: '平静',
|
||||||
|
ANXIOUS: '焦虑',
|
||||||
|
SAD: '难过',
|
||||||
|
LONELY: '孤独',
|
||||||
|
WRONGED: '委屈',
|
||||||
|
ANGRY: '生气',
|
||||||
|
TIRED: '心累',
|
||||||
|
};
|
||||||
|
// 如果心情未知,返回"开心"以保持积极的氛围,或者使用"平和"
|
||||||
|
return moodMap[moodType || ''] || '开心';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. 调用 OpenRouter SDK 生成图片并上传到 COS
|
||||||
|
*/
|
||||||
|
private async callImageGenerationApi(prompt: string): Promise<string> {
|
||||||
|
this.logger.log(`准备调用 OpenRouter SDK 生成图像`);
|
||||||
|
this.logger.log(`使用Prompt: ${prompt}`);
|
||||||
|
|
||||||
|
const openRouterApiKey = this.configService.get<string>('OPENROUTER_API_KEY');
|
||||||
|
if (!openRouterApiKey) {
|
||||||
|
this.logger.error('OpenRouter API Key 未配置');
|
||||||
|
throw new Error('OpenRouter API Key 未配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 初始化 OpenRouter 客户端
|
||||||
|
const openrouter = new OpenRouter({
|
||||||
|
apiKey: openRouterApiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 调用图像生成API
|
||||||
|
const result = await openrouter.chat.send({
|
||||||
|
model: "google/gemini-3-pro-image-preview",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: prompt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = result.choices[0].message;
|
||||||
|
|
||||||
|
// 处理不同格式的响应内容
|
||||||
|
let imageData: string | undefined;
|
||||||
|
let isBase64 = false;
|
||||||
|
|
||||||
|
if (typeof message.content === 'string') {
|
||||||
|
// 检查是否为 base64 数据
|
||||||
|
// base64 图像通常以 data:image/ 开头,或者是纯 base64 字符串
|
||||||
|
if (message.content.startsWith('data:image/')) {
|
||||||
|
// 完整的 data URL 格式:data:image/png;base64,xxxxx
|
||||||
|
imageData = message.content;
|
||||||
|
isBase64 = true;
|
||||||
|
this.logger.log('检测到 Data URL 格式的 base64 图像');
|
||||||
|
} else if (/^[A-Za-z0-9+/=]+$/.test(message.content.substring(0, 100))) {
|
||||||
|
// 纯 base64 字符串(检查前100个字符)
|
||||||
|
imageData = message.content;
|
||||||
|
isBase64 = true;
|
||||||
|
this.logger.log('检测到纯 base64 格式的图像数据');
|
||||||
|
} else {
|
||||||
|
// 尝试提取 HTTP URL
|
||||||
|
const urlMatch = message.content.match(/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp)/i);
|
||||||
|
if (urlMatch) {
|
||||||
|
imageData = urlMatch[0];
|
||||||
|
isBase64 = false;
|
||||||
|
this.logger.log(`检测到 HTTP URL: ${imageData}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(message.content)) {
|
||||||
|
// 检查内容数组中是否有图像项
|
||||||
|
const imageItem = message.content.find(item => item.type === 'image_url');
|
||||||
|
if (imageItem && imageItem.imageUrl) {
|
||||||
|
imageData = imageItem.imageUrl.url;
|
||||||
|
// 判断是 URL 还是 base64
|
||||||
|
isBase64 = imageData.startsWith('data:image/') || /^[A-Za-z0-9+/=]+$/.test(imageData.substring(0, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageData) {
|
||||||
|
if (isBase64) {
|
||||||
|
// 处理 base64 数据并上传到 COS
|
||||||
|
this.logger.log('开始处理 base64 图像数据');
|
||||||
|
const cosImageUrl = await this.uploadBase64ToCos(imageData);
|
||||||
|
this.logger.log(`Base64 图像上传到 COS 成功: ${cosImageUrl}`);
|
||||||
|
return cosImageUrl;
|
||||||
|
} else {
|
||||||
|
// 下载 HTTP URL 图像并上传到 COS
|
||||||
|
this.logger.log(`OpenRouter 返回图像 URL: ${imageData}`);
|
||||||
|
const cosImageUrl = await this.downloadAndUploadToCos(imageData);
|
||||||
|
this.logger.log(`图像上传到 COS 成功: ${cosImageUrl}`);
|
||||||
|
return cosImageUrl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.error('OpenRouter 响应中未包含图像数据');
|
||||||
|
this.logger.error(`实际响应内容类型: ${typeof message.content}`);
|
||||||
|
this.logger.error(`实际响应内容: ${JSON.stringify(message.content).substring(0, 500)}`);
|
||||||
|
throw new Error('图像生成失败:响应中未包含图像数据');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`调用 OpenRouter SDK 失败: ${error.message}`);
|
||||||
|
if (error.response) {
|
||||||
|
this.logger.error(`OpenRouter 错误详情: ${JSON.stringify(error.response.data)}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 base64 图像数据上传到 COS
|
||||||
|
*/
|
||||||
|
private async uploadBase64ToCos(base64Data: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
this.logger.log('开始处理 base64 图像数据');
|
||||||
|
|
||||||
|
let base64String = base64Data;
|
||||||
|
let mimeType = 'image/png'; // 默认类型
|
||||||
|
|
||||||
|
// 如果是 data URL 格式,提取 MIME 类型和纯 base64 数据
|
||||||
|
if (base64Data.startsWith('data:')) {
|
||||||
|
const matches = base64Data.match(/^data:([^;]+);base64,(.+)$/);
|
||||||
|
if (matches) {
|
||||||
|
mimeType = matches[1];
|
||||||
|
base64String = matches[2];
|
||||||
|
this.logger.log(`检测到 MIME 类型: ${mimeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 base64 转换为 Buffer
|
||||||
|
const imageBuffer = Buffer.from(base64String, 'base64');
|
||||||
|
this.logger.log(`Base64 数据转换成功,大小: ${imageBuffer.length} bytes`);
|
||||||
|
|
||||||
|
// 根据 MIME 类型确定文件扩展名
|
||||||
|
const ext = mimeType.split('/')[1] || 'png';
|
||||||
|
const fileName = `ai-health-report-${Date.now()}.${ext}`;
|
||||||
|
|
||||||
|
// 使用 CosService 的 uploadBuffer 方法上传
|
||||||
|
const uploadResult = await this.cosService.uploadBuffer('ai-report', imageBuffer, fileName, mimeType);
|
||||||
|
this.logger.log(`Base64 图像上传成功: ${uploadResult.fileUrl}`);
|
||||||
|
|
||||||
|
return uploadResult.fileUrl;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`上传 base64 图像到 COS 失败: ${error.message}`);
|
||||||
|
throw new Error(`Base64 图像处理失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载图像并上传到 COS
|
||||||
|
*/
|
||||||
|
private async downloadAndUploadToCos(imageUrl: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`开始下载图像: ${imageUrl}`);
|
||||||
|
|
||||||
|
// 下载图像
|
||||||
|
const axios = require('axios');
|
||||||
|
const response = await axios.get(imageUrl, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 30000, // 30秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`图像下载失败,HTTP状态码: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查响应数据
|
||||||
|
if (!response.data) {
|
||||||
|
throw new Error('图像下载失败:响应数据为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`图像下载成功,大小: ${response.data.length} bytes`);
|
||||||
|
|
||||||
|
// 创建模拟文件对象用于上传
|
||||||
|
const imageBuffer = Buffer.from(response.data);
|
||||||
|
const fileName = `ai-health-report-${Date.now()}.png`;
|
||||||
|
|
||||||
|
// 检测 MIME 类型
|
||||||
|
let mimeType = 'image/png';
|
||||||
|
if (response.headers['content-type']) {
|
||||||
|
mimeType = response.headers['content-type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 CosService 的 uploadBuffer 方法上传
|
||||||
|
const uploadResult = await this.cosService.uploadBuffer('ai-report', imageBuffer, fileName, mimeType);
|
||||||
|
return uploadResult.fileUrl;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`下载或上传图像到COS失败: ${error.message}`);
|
||||||
|
throw new Error(`图像处理失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import { DietRecordsService } from '../../diet-records/diet-records.service';
|
import { DietRecordsService } from '../../diet-records/diet-records.service';
|
||||||
@@ -119,6 +119,7 @@ export class DietAnalysisService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
@Inject(forwardRef(() => DietRecordsService))
|
||||||
private readonly dietRecordsService: DietRecordsService,
|
private readonly dietRecordsService: DietRecordsService,
|
||||||
) {
|
) {
|
||||||
// Support both GLM-4.5V and DashScope (Qwen) models
|
// Support both GLM-4.5V and DashScope (Qwen) models
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { SequelizeModule } from '@nestjs/sequelize';
|
import { SequelizeModule } from '@nestjs/sequelize';
|
||||||
import { ChallengesController } from './challenges.controller';
|
import { ChallengesController } from './challenges.controller';
|
||||||
import { ChallengesService } from './challenges.service';
|
import { ChallengesService } from './challenges.service';
|
||||||
@@ -12,7 +12,7 @@ import { BadgeConfig } from '../users/models/badge-config.model';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
SequelizeModule.forFeature([Challenge, ChallengeParticipant, ChallengeProgressReport, User, BadgeConfig]),
|
SequelizeModule.forFeature([Challenge, ChallengeParticipant, ChallengeProgressReport, User, BadgeConfig]),
|
||||||
UsersModule,
|
forwardRef(() => UsersModule),
|
||||||
],
|
],
|
||||||
controllers: [ChallengesController],
|
controllers: [ChallengesController],
|
||||||
providers: [ChallengesService],
|
providers: [ChallengesService],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AiCoachModule } from '../ai-coach/ai-coach.module';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
SequelizeModule.forFeature([UserDietHistory, ActivityLog, NutritionAnalysisRecord]),
|
SequelizeModule.forFeature([UserDietHistory, ActivityLog, NutritionAnalysisRecord]),
|
||||||
UsersModule,
|
forwardRef(() => UsersModule),
|
||||||
forwardRef(() => AiCoachModule),
|
forwardRef(() => AiCoachModule),
|
||||||
],
|
],
|
||||||
controllers: [DietRecordsController],
|
controllers: [DietRecordsController],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { SequelizeModule } from '@nestjs/sequelize';
|
import { SequelizeModule } from '@nestjs/sequelize';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
@@ -42,8 +42,8 @@ import { UsersModule } from '../users/users.module';
|
|||||||
MedicationAiSummary,
|
MedicationAiSummary,
|
||||||
]),
|
]),
|
||||||
ScheduleModule.forRoot(), // 启用定时任务
|
ScheduleModule.forRoot(), // 启用定时任务
|
||||||
PushNotificationsModule, // 推送通知功能
|
forwardRef(() => PushNotificationsModule), // 推送通知功能
|
||||||
UsersModule, // 用户认证服务
|
forwardRef(() => UsersModule), // 用户认证服务
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
MedicationsController,
|
MedicationsController,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { SequelizeModule } from '@nestjs/sequelize';
|
import { SequelizeModule } from '@nestjs/sequelize';
|
||||||
import { MoodCheckinsService } from './mood-checkins.service';
|
import { MoodCheckinsService } from './mood-checkins.service';
|
||||||
import { MoodCheckinsController } from './mood-checkins.controller';
|
import { MoodCheckinsController } from './mood-checkins.controller';
|
||||||
@@ -7,7 +7,7 @@ import { UsersModule } from '../users/users.module';
|
|||||||
import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
|
import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [SequelizeModule.forFeature([MoodCheckin]), UsersModule, ActivityLogsModule],
|
imports: [SequelizeModule.forFeature([MoodCheckin]), forwardRef(() => UsersModule), forwardRef(() => ActivityLogsModule)],
|
||||||
providers: [MoodCheckinsService],
|
providers: [MoodCheckinsService],
|
||||||
controllers: [MoodCheckinsController],
|
controllers: [MoodCheckinsController],
|
||||||
exports: [MoodCheckinsService],
|
exports: [MoodCheckinsService],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { SequelizeModule } from '@nestjs/sequelize';
|
import { SequelizeModule } from '@nestjs/sequelize';
|
||||||
import { PushNotificationsController } from './push-notifications.controller';
|
import { PushNotificationsController } from './push-notifications.controller';
|
||||||
import { PushTemplateController } from './push-template.controller';
|
import { PushTemplateController } from './push-template.controller';
|
||||||
@@ -22,8 +22,8 @@ import { ChallengeParticipant } from '../challenges/models/challenge-participant
|
|||||||
imports: [
|
imports: [
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
UsersModule,
|
forwardRef(() => UsersModule),
|
||||||
ChallengesModule,
|
forwardRef(() => ChallengesModule),
|
||||||
SequelizeModule.forFeature([
|
SequelizeModule.forFeature([
|
||||||
UserPushToken,
|
UserPushToken,
|
||||||
PushMessage,
|
PushMessage,
|
||||||
|
|||||||
@@ -297,4 +297,59 @@ export class CosService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传 Buffer 到 COS
|
||||||
|
*/
|
||||||
|
async uploadBuffer(
|
||||||
|
userId: string,
|
||||||
|
buffer: Buffer,
|
||||||
|
fileName: string,
|
||||||
|
mimeType: string = 'image/png'
|
||||||
|
): Promise<{ fileUrl: string; fileKey: string }> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`开始上传Buffer,用户ID: ${userId}, 文件名: ${fileName}`);
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
const fileExtension = this.getFileExtension(fileName);
|
||||||
|
if (!this.validateFileType('image', fileExtension)) {
|
||||||
|
throw new BadRequestException('不支持的图片格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小
|
||||||
|
const sizeLimit = this.getFileSizeLimit('image');
|
||||||
|
if (buffer.length > sizeLimit) {
|
||||||
|
throw new BadRequestException(`图片文件大小超过限制 (${sizeLimit / 1024 / 1024}MB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一文件名
|
||||||
|
const uniqueFileName = this.generateUniqueFileName(fileName, fileExtension);
|
||||||
|
const fileKey = `uploads/images/${uniqueFileName}`;
|
||||||
|
|
||||||
|
// 上传到COS
|
||||||
|
const uploadResult = await this.uploadToCos(fileKey, buffer, mimeType);
|
||||||
|
|
||||||
|
// 生成文件访问URL
|
||||||
|
const fileUrl = this.generateFileUrl(uploadResult);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
fileUrl,
|
||||||
|
fileKey,
|
||||||
|
originalName: fileName,
|
||||||
|
fileSize: buffer.length,
|
||||||
|
fileType: 'image',
|
||||||
|
mimeType: mimeType,
|
||||||
|
uploadTime: new Date(),
|
||||||
|
etag: uploadResult.ETag,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.log(`Buffer上传成功,用户ID: ${userId}, 文件URL: ${fileUrl}`);
|
||||||
|
return { fileUrl, fileKey };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`上传Buffer失败: ${error.message}`, error.stack);
|
||||||
|
throw new BadRequestException(`上传Buffer失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
|
forwardRef,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
@@ -45,6 +46,7 @@ import { AccessTokenPayload } from './services/apple-auth.service';
|
|||||||
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
|
||||||
import { ResponseCode } from 'src/base.dto';
|
import { ResponseCode } from 'src/base.dto';
|
||||||
import { CosService } from './cos.service';
|
import { CosService } from './cos.service';
|
||||||
|
import { AiReportService } from '../ai-coach/services/ai-report.service';
|
||||||
|
|
||||||
|
|
||||||
@ApiTags('users')
|
@ApiTags('users')
|
||||||
@@ -55,6 +57,8 @@ export class UsersController {
|
|||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly winstonLogger: WinstonLogger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly winstonLogger: WinstonLogger,
|
||||||
private readonly cosService: CosService,
|
private readonly cosService: CosService,
|
||||||
|
@Inject(forwardRef(() => AiReportService))
|
||||||
|
private readonly aiReportService: AiReportService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@@ -472,4 +476,65 @@ export class UsersController {
|
|||||||
return this.usersService.checkVersion(query);
|
return this.usersService.checkVersion(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== AI 健康报告 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成用户的 AI 健康报告图片
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Post('ai-report')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '生成用户的 AI 健康报告图片' })
|
||||||
|
@ApiBody({ type: Object, required: false, description: '请求体,可以传入 date 指定日期,格式 YYYY-MM-DD' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '成功生成 AI 报告图片',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
code: { type: 'number', example: 0 },
|
||||||
|
message: { type: 'string', example: 'success' },
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
imageUrl: { type: 'string', example: 'https://example.com/generated-image.png' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
async generateAiHealthReport(
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
@Body() body: { date?: string },
|
||||||
|
): Promise<{ code: ResponseCode; message: string; data: { imageUrl: string } }> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`生成AI健康报告请求 - 用户ID: ${user.sub}, 日期: ${body.date || '今天'}`);
|
||||||
|
|
||||||
|
const result = await this.aiReportService.generateHealthReportImage(user.sub, body.date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: ResponseCode.SUCCESS,
|
||||||
|
message: 'AI健康报告生成成功',
|
||||||
|
data: {
|
||||||
|
imageUrl: result.imageUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.winstonLogger.error('生成AI健康报告失败', {
|
||||||
|
context: 'UsersController',
|
||||||
|
userId: user?.sub,
|
||||||
|
error: (error as Error).message,
|
||||||
|
stack: (error as Error).stack,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: ResponseCode.ERROR,
|
||||||
|
message: `生成失败: ${(error as Error).message}`,
|
||||||
|
data: {
|
||||||
|
imageUrl: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { RevenueCatEvent } from "./models/revenue-cat-event.model";
|
|||||||
import { CosService } from './cos.service';
|
import { CosService } from './cos.service';
|
||||||
import { BadgeService } from './services/badge.service';
|
import { BadgeService } from './services/badge.service';
|
||||||
import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
|
import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
|
||||||
|
import { AiCoachModule } from '../ai-coach/ai-coach.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -43,6 +44,7 @@ import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
|
|||||||
UserActivity,
|
UserActivity,
|
||||||
]),
|
]),
|
||||||
forwardRef(() => ActivityLogsModule),
|
forwardRef(() => ActivityLogsModule),
|
||||||
|
forwardRef(() => AiCoachModule),
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_ACCESS_SECRET || 'your-access-token-secret-key',
|
secret: process.env.JWT_ACCESS_SECRET || 'your-access-token-secret-key',
|
||||||
signOptions: { expiresIn: '30d' },
|
signOptions: { expiresIn: '30d' },
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export class UsersService {
|
|||||||
...existingUser.toJSON(),
|
...existingUser.toJSON(),
|
||||||
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
|
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
|
||||||
isVip: existingUser.isVip,
|
isVip: existingUser.isVip,
|
||||||
|
gender: existingUser.gender,
|
||||||
dailyStepsGoal: profile?.dailyStepsGoal,
|
dailyStepsGoal: profile?.dailyStepsGoal,
|
||||||
dailyCaloriesGoal: profile?.dailyCaloriesGoal,
|
dailyCaloriesGoal: profile?.dailyCaloriesGoal,
|
||||||
pilatesPurposes: profile?.pilatesPurposes,
|
pilatesPurposes: profile?.pilatesPurposes,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { SequelizeModule } from '@nestjs/sequelize';
|
import { SequelizeModule } from '@nestjs/sequelize';
|
||||||
import { WorkoutsController } from './workouts.controller';
|
import { WorkoutsController } from './workouts.controller';
|
||||||
import { WorkoutsService } from './workouts.service';
|
import { WorkoutsService } from './workouts.service';
|
||||||
@@ -19,8 +19,8 @@ import { UsersModule } from '../users/users.module';
|
|||||||
ScheduleExercise,
|
ScheduleExercise,
|
||||||
Exercise,
|
Exercise,
|
||||||
]),
|
]),
|
||||||
ActivityLogsModule,
|
forwardRef(() => ActivityLogsModule),
|
||||||
UsersModule,
|
forwardRef(() => UsersModule),
|
||||||
],
|
],
|
||||||
controllers: [WorkoutsController],
|
controllers: [WorkoutsController],
|
||||||
providers: [WorkoutsService],
|
providers: [WorkoutsService],
|
||||||
|
|||||||
60
yarn.lock
60
yarn.lock
@@ -884,12 +884,12 @@
|
|||||||
|
|
||||||
"@napi-rs/nice-android-arm-eabi@1.0.1":
|
"@napi-rs/nice-android-arm-eabi@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936"
|
||||||
integrity sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==
|
integrity sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==
|
||||||
|
|
||||||
"@napi-rs/nice-android-arm64@1.0.1":
|
"@napi-rs/nice-android-arm64@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz#32fc32e9649bd759d2a39ad745e95766f6759d2f"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz#32fc32e9649bd759d2a39ad745e95766f6759d2f"
|
||||||
integrity sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==
|
integrity sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==
|
||||||
|
|
||||||
"@napi-rs/nice-darwin-arm64@1.0.1":
|
"@napi-rs/nice-darwin-arm64@1.0.1":
|
||||||
@@ -899,67 +899,67 @@
|
|||||||
|
|
||||||
"@napi-rs/nice-darwin-x64@1.0.1":
|
"@napi-rs/nice-darwin-x64@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz#f1b1365a8370c6a6957e90085a9b4873d0e6a957"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz#f1b1365a8370c6a6957e90085a9b4873d0e6a957"
|
||||||
integrity sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==
|
integrity sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==
|
||||||
|
|
||||||
"@napi-rs/nice-freebsd-x64@1.0.1":
|
"@napi-rs/nice-freebsd-x64@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz#4280f081efbe0b46c5165fdaea8b286e55a8f89e"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz#4280f081efbe0b46c5165fdaea8b286e55a8f89e"
|
||||||
integrity sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==
|
integrity sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==
|
||||||
|
|
||||||
"@napi-rs/nice-linux-arm-gnueabihf@1.0.1":
|
"@napi-rs/nice-linux-arm-gnueabihf@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz#07aec23a9467ed35eb7602af5e63d42c5d7bd473"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz#07aec23a9467ed35eb7602af5e63d42c5d7bd473"
|
||||||
integrity sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==
|
integrity sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==
|
||||||
|
|
||||||
"@napi-rs/nice-linux-arm64-gnu@1.0.1":
|
"@napi-rs/nice-linux-arm64-gnu@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz#038a77134cc6df3c48059d5a5e199d6f50fb9a90"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz#038a77134cc6df3c48059d5a5e199d6f50fb9a90"
|
||||||
integrity sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==
|
integrity sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==
|
||||||
|
|
||||||
"@napi-rs/nice-linux-arm64-musl@1.0.1":
|
"@napi-rs/nice-linux-arm64-musl@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz#715d0906582ba0cff025109f42e5b84ea68c2bcc"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz#715d0906582ba0cff025109f42e5b84ea68c2bcc"
|
||||||
integrity sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==
|
integrity sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==
|
||||||
|
|
||||||
"@napi-rs/nice-linux-ppc64-gnu@1.0.1":
|
"@napi-rs/nice-linux-ppc64-gnu@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz#ac1c8f781c67b0559fa7a1cd4ae3ca2299dc3d06"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz#ac1c8f781c67b0559fa7a1cd4ae3ca2299dc3d06"
|
||||||
integrity sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==
|
integrity sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==
|
||||||
|
|
||||||
"@napi-rs/nice-linux-riscv64-gnu@1.0.1":
|
"@napi-rs/nice-linux-riscv64-gnu@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz#b0a430549acfd3920ffd28ce544e2fe17833d263"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz#b0a430549acfd3920ffd28ce544e2fe17833d263"
|
||||||
integrity sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==
|
integrity sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==
|
||||||
|
|
||||||
"@napi-rs/nice-linux-s390x-gnu@1.0.1":
|
"@napi-rs/nice-linux-s390x-gnu@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz#5b95caf411ad72a965885217db378c4d09733e97"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz#5b95caf411ad72a965885217db378c4d09733e97"
|
||||||
integrity sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==
|
integrity sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==
|
||||||
|
|
||||||
"@napi-rs/nice-linux-x64-gnu@1.0.1":
|
"@napi-rs/nice-linux-x64-gnu@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz#a98cdef517549f8c17a83f0236a69418a90e77b7"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz#a98cdef517549f8c17a83f0236a69418a90e77b7"
|
||||||
integrity sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==
|
integrity sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==
|
||||||
|
|
||||||
"@napi-rs/nice-linux-x64-musl@1.0.1":
|
"@napi-rs/nice-linux-x64-musl@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz#5e26843eafa940138aed437c870cca751c8a8957"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz#5e26843eafa940138aed437c870cca751c8a8957"
|
||||||
integrity sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==
|
integrity sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==
|
||||||
|
|
||||||
"@napi-rs/nice-win32-arm64-msvc@1.0.1":
|
"@napi-rs/nice-win32-arm64-msvc@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz#bd62617d02f04aa30ab1e9081363856715f84cd8"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz#bd62617d02f04aa30ab1e9081363856715f84cd8"
|
||||||
integrity sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==
|
integrity sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==
|
||||||
|
|
||||||
"@napi-rs/nice-win32-ia32-msvc@1.0.1":
|
"@napi-rs/nice-win32-ia32-msvc@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz#b8b7aad552a24836027473d9b9f16edaeabecf18"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz#b8b7aad552a24836027473d9b9f16edaeabecf18"
|
||||||
integrity sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==
|
integrity sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==
|
||||||
|
|
||||||
"@napi-rs/nice-win32-x64-msvc@1.0.1":
|
"@napi-rs/nice-win32-x64-msvc@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz#37d8718b8f722f49067713e9f1e85540c9a3dd09"
|
resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz#37d8718b8f722f49067713e9f1e85540c9a3dd09"
|
||||||
integrity sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==
|
integrity sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==
|
||||||
|
|
||||||
"@napi-rs/nice@^1.0.1":
|
"@napi-rs/nice@^1.0.1":
|
||||||
@@ -1133,6 +1133,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
consola "^3.2.3"
|
consola "^3.2.3"
|
||||||
|
|
||||||
|
"@openrouter/sdk@^0.1.27":
|
||||||
|
version "0.1.27"
|
||||||
|
resolved "https://mirrors.tencent.com/npm/@openrouter/sdk/-/sdk-0.1.27.tgz"
|
||||||
|
integrity sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ==
|
||||||
|
dependencies:
|
||||||
|
zod "^3.25.0 || ^4.0.0"
|
||||||
|
|
||||||
"@parse/node-apn@^5.0.0":
|
"@parse/node-apn@^5.0.0":
|
||||||
version "5.2.3"
|
version "5.2.3"
|
||||||
resolved "https://mirrors.tencent.com/npm/@parse/node-apn/-/node-apn-5.2.3.tgz"
|
resolved "https://mirrors.tencent.com/npm/@parse/node-apn/-/node-apn-5.2.3.tgz"
|
||||||
@@ -1254,47 +1261,47 @@
|
|||||||
|
|
||||||
"@swc/core-darwin-x64@1.11.13":
|
"@swc/core-darwin-x64@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.13.tgz#9cad870d48ebff805e8946ddcbe3d8312182f70b"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-darwin-x64/-/core-darwin-x64-1.11.13.tgz#9cad870d48ebff805e8946ddcbe3d8312182f70b"
|
||||||
integrity sha512-uSA4UwgsDCIysUPfPS8OrQTH2h9spO7IYFd+1NB6dJlVGUuR6jLKuMBOP1IeLeax4cGHayvkcwSJ3OvxHwgcZQ==
|
integrity sha512-uSA4UwgsDCIysUPfPS8OrQTH2h9spO7IYFd+1NB6dJlVGUuR6jLKuMBOP1IeLeax4cGHayvkcwSJ3OvxHwgcZQ==
|
||||||
|
|
||||||
"@swc/core-linux-arm-gnueabihf@1.11.13":
|
"@swc/core-linux-arm-gnueabihf@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.13.tgz#51839e5a850bfa300e2c838fee8379e4dba1de78"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.13.tgz#51839e5a850bfa300e2c838fee8379e4dba1de78"
|
||||||
integrity sha512-boVtyJzS8g30iQfe8Q46W5QE/cmhKRln/7NMz/5sBP/am2Lce9NL0d05NnFwEWJp1e2AMGHFOdRr3Xg1cDiPKw==
|
integrity sha512-boVtyJzS8g30iQfe8Q46W5QE/cmhKRln/7NMz/5sBP/am2Lce9NL0d05NnFwEWJp1e2AMGHFOdRr3Xg1cDiPKw==
|
||||||
|
|
||||||
"@swc/core-linux-arm64-gnu@1.11.13":
|
"@swc/core-linux-arm64-gnu@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.13.tgz#4145f1e504bdfa92604aee883d777bc8c4fba5d7"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.13.tgz#4145f1e504bdfa92604aee883d777bc8c4fba5d7"
|
||||||
integrity sha512-+IK0jZ84zHUaKtwpV+T+wT0qIUBnK9v2xXD03vARubKF+eUqCsIvcVHXmLpFuap62dClMrhCiwW10X3RbXNlHw==
|
integrity sha512-+IK0jZ84zHUaKtwpV+T+wT0qIUBnK9v2xXD03vARubKF+eUqCsIvcVHXmLpFuap62dClMrhCiwW10X3RbXNlHw==
|
||||||
|
|
||||||
"@swc/core-linux-arm64-musl@1.11.13":
|
"@swc/core-linux-arm64-musl@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.13.tgz#b1813ae2e99e386ca16fff5af6601ac45ef57c5b"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.13.tgz#b1813ae2e99e386ca16fff5af6601ac45ef57c5b"
|
||||||
integrity sha512-+ukuB8RHD5BHPCUjQwuLP98z+VRfu+NkKQVBcLJGgp0/+w7y0IkaxLY/aKmrAS5ofCNEGqKL+AOVyRpX1aw+XA==
|
integrity sha512-+ukuB8RHD5BHPCUjQwuLP98z+VRfu+NkKQVBcLJGgp0/+w7y0IkaxLY/aKmrAS5ofCNEGqKL+AOVyRpX1aw+XA==
|
||||||
|
|
||||||
"@swc/core-linux-x64-gnu@1.11.13":
|
"@swc/core-linux-x64-gnu@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.13.tgz#13b89a0194c4033c01400e9c65d9c21c56a4a6cd"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.13.tgz#13b89a0194c4033c01400e9c65d9c21c56a4a6cd"
|
||||||
integrity sha512-q9H3WI3U3dfJ34tdv60zc8oTuWvSd5fOxytyAO9Pc5M82Hic3jjWaf2xBekUg07ubnMZpyfnv+MlD+EbUI3Llw==
|
integrity sha512-q9H3WI3U3dfJ34tdv60zc8oTuWvSd5fOxytyAO9Pc5M82Hic3jjWaf2xBekUg07ubnMZpyfnv+MlD+EbUI3Llw==
|
||||||
|
|
||||||
"@swc/core-linux-x64-musl@1.11.13":
|
"@swc/core-linux-x64-musl@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.13.tgz#0d0e5aa889dd4da69723e2287c3c1714d9bfd8aa"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.13.tgz#0d0e5aa889dd4da69723e2287c3c1714d9bfd8aa"
|
||||||
integrity sha512-9aaZnnq2pLdTbAzTSzy/q8dr7Woy3aYIcQISmw1+Q2/xHJg5y80ZzbWSWKYca/hKonDMjIbGR6dp299I5J0aeA==
|
integrity sha512-9aaZnnq2pLdTbAzTSzy/q8dr7Woy3aYIcQISmw1+Q2/xHJg5y80ZzbWSWKYca/hKonDMjIbGR6dp299I5J0aeA==
|
||||||
|
|
||||||
"@swc/core-win32-arm64-msvc@1.11.13":
|
"@swc/core-win32-arm64-msvc@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.13.tgz#ad7281f9467e3de09f52615afe2276a8ef738a9d"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.13.tgz#ad7281f9467e3de09f52615afe2276a8ef738a9d"
|
||||||
integrity sha512-n3QZmDewkHANcoHvtwvA6yJbmS4XJf0MBMmwLZoKDZ2dOnC9D/jHiXw7JOohEuzYcpLoL5tgbqmjxa3XNo9Oow==
|
integrity sha512-n3QZmDewkHANcoHvtwvA6yJbmS4XJf0MBMmwLZoKDZ2dOnC9D/jHiXw7JOohEuzYcpLoL5tgbqmjxa3XNo9Oow==
|
||||||
|
|
||||||
"@swc/core-win32-ia32-msvc@1.11.13":
|
"@swc/core-win32-ia32-msvc@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.13.tgz#046f6dbddb5b69a29bbaa98de104090a46088b74"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.13.tgz#046f6dbddb5b69a29bbaa98de104090a46088b74"
|
||||||
integrity sha512-wM+Nt4lc6YSJFthCx3W2dz0EwFNf++j0/2TQ0Js9QLJuIxUQAgukhNDVCDdq8TNcT0zuA399ALYbvj5lfIqG6g==
|
integrity sha512-wM+Nt4lc6YSJFthCx3W2dz0EwFNf++j0/2TQ0Js9QLJuIxUQAgukhNDVCDdq8TNcT0zuA399ALYbvj5lfIqG6g==
|
||||||
|
|
||||||
"@swc/core-win32-x64-msvc@1.11.13":
|
"@swc/core-win32-x64-msvc@1.11.13":
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.13.tgz#0412620d8594a7d3e482d3e79d9e89d80f9a14c0"
|
resolved "https://mirrors.tencent.com/npm/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.13.tgz#0412620d8594a7d3e482d3e79d9e89d80f9a14c0"
|
||||||
integrity sha512-+X5/uW3s1L5gK7wAo0E27YaAoidJDo51dnfKSfU7gF3mlEUuWH8H1bAy5OTt2mU4eXtfsdUMEVXSwhDlLtQkuA==
|
integrity sha512-+X5/uW3s1L5gK7wAo0E27YaAoidJDo51dnfKSfU7gF3mlEUuWH8H1bAy5OTt2mU4eXtfsdUMEVXSwhDlLtQkuA==
|
||||||
|
|
||||||
"@swc/core@^1.10.7":
|
"@swc/core@^1.10.7":
|
||||||
@@ -7575,3 +7582,8 @@ yoctocolors-cjs@^2.1.2:
|
|||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz"
|
resolved "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz"
|
||||||
integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==
|
integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==
|
||||||
|
|
||||||
|
"zod@^3.25.0 || ^4.0.0":
|
||||||
|
version "3.25.76"
|
||||||
|
resolved "https://mirrors.tencent.com/npm/zod/-/zod-3.25.76.tgz"
|
||||||
|
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||||
|
|||||||
Reference in New Issue
Block a user