feat: 实现减脂体重管理App完整功能

- 实现拍照识别食物功能(集成大语言模型视觉能力)
- 实现智能对话功能(集成大语言模型流式输出)
- 实现食物记录和卡路里管理功能
- 实现体重记录和统计功能
- 实现健康数据管理页面
- 配置数据库表结构(用户、食物记录、体重记录)
- 实现Express后端API路由
- 配置Tab导航和前端页面
- 采用健康运动配色方案
This commit is contained in:
jaystar
2026-02-02 15:17:50 +08:00
commit 28c4d7b3b4
82 changed files with 21891 additions and 0 deletions

21
server/build.js Normal file
View File

@@ -0,0 +1,21 @@
import * as esbuild from 'esbuild';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require('./package.json');
const dependencies = pkg.dependencies || {};
const externalList = Object.keys(dependencies).filter(dep => dep !== 'dayjs');
try {
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
platform: 'node',
format: 'esm',
outdir: 'dist',
external: externalList,
});
console.log('⚡ Build complete!');
} catch (e) {
console.error(e);
process.exit(1);
}

32
server/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "server",
"private": true,
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "bash ../.cozeproj/scripts/server_dev_run.sh",
"build": "node build.js",
"start": "NODE_ENV=production PORT=${PORT:-5000} node dist/index.js"
},
"dependencies": {
"express": "^4.22.1",
"cors": "^2.8.5",
"coze-coding-dev-sdk": "^0.7.2",
"dayjs": "^1.11.19",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"multer": "^2.0.2",
"pg": "^8.16.3",
"zod": "^4.2.1"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"tsx": "^4.21.0",
"@types/multer": "^2.0.0",
"@types/pg": "^8.16.0",
"esbuild": "0.27.2",
"typescript": "^5.8.3",
"drizzle-kit": "^0.31.8"
}
}

31
server/src/index.ts Normal file
View File

@@ -0,0 +1,31 @@
import express from "express";
import cors from "cors";
import usersRouter from './routes/users.js';
import foodRecordsRouter from './routes/food-records.js';
import weightRecordsRouter from './routes/weight-records.js';
import aiChatRouter from './routes/ai-chat.js';
const app = express();
const port = process.env.PORT || 9091;
// Middleware
app.use(cors());
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// Health check
app.get('/api/v1/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// API Routes
// 注意:静态路由必须在动态路由之前
app.use('/api/v1/users', usersRouter);
app.use('/api/v1/food-records', foodRecordsRouter);
app.use('/api/v1/weight-records', weightRecordsRouter);
app.use('/api/v1/ai-chat', aiChatRouter);
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}/`);
});

View File

@@ -0,0 +1,72 @@
import { Router } from 'express';
import { LLMClient, Config } from 'coze-coding-dev-sdk';
const router = Router();
// 初始化大语言模型
const llmConfig = new Config();
const llmClient = new LLMClient(llmConfig);
// AI 对话接口(流式)
router.post('/chat', async (req, res) => {
try {
const { message, conversationHistory } = req.body;
if (!message) {
return res.status(400).json({
success: false,
error: 'Message is required'
});
}
// 设置 SSE 响应头
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-store, no-transform, must-revalidate');
res.setHeader('Connection', 'keep-alive');
// 构建消息历史
const messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [
{
role: 'system',
content: '你是一个专业的健康饮食和减脂顾问助手。你的任务是帮助用户进行科学的体重管理,提供以下方面的建议:\n\n1. 饮食建议:根据用户的饮食习惯和目标,提供个性化的饮食方案\n2. 营养分析:分析食物的营养成分,推荐健康的食物选择\n3. 减脂指导:提供科学的减脂方法和运动建议\n4. 激励支持:鼓励用户坚持健康生活方式\n\n请用友好、专业、鼓舞人心的语气与用户交流避免提供极端或有害的减脂建议。'
}
];
// 如果有对话历史,添加到消息列表
if (conversationHistory && Array.isArray(conversationHistory)) {
messages.push(...conversationHistory);
}
// 添加当前用户消息
messages.push({
role: 'user',
content: message
});
// 流式生成响应
const stream = llmClient.stream(messages, {
model: 'doubao-seed-1-8-251228',
temperature: 0.7
});
for await (const chunk of stream) {
if (chunk.content) {
const text = chunk.content.toString();
// 发送 SSE 事件
res.write(`data: ${JSON.stringify({ content: text })}\n\n`);
}
}
// 发送结束标记
res.write('data: [DONE]\n\n');
res.end();
} catch (error: any) {
console.error('AI chat error:', error);
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.write('data: [DONE]\n\n');
res.end();
}
});
export default router;

View File

@@ -0,0 +1,229 @@
import { Router } from 'express';
import multer from 'multer';
import { foodRecordManager } from '../storage/database';
import { S3Storage } from 'coze-coding-dev-sdk';
import { LLMClient, Config } from 'coze-coding-dev-sdk';
const router = Router();
// 配置 multer 中间件
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 50 * 1024 * 1024 } // 限制 50MB
});
// 初始化对象存储
const storage = new S3Storage({
endpointUrl: process.env.COZE_BUCKET_ENDPOINT_URL,
accessKey: '',
secretKey: '',
bucketName: process.env.COZE_BUCKET_NAME,
region: 'cn-beijing',
});
// 初始化大语言模型
const llmConfig = new Config();
const llmClient = new LLMClient(llmConfig);
// 获取食物记录列表
router.get('/', async (req, res) => {
try {
const { userId } = req.query;
if (!userId || typeof userId !== 'string') {
return res.status(400).json({
success: false,
error: 'userId is required'
});
}
const { mealType, startDate, endDate } = req.query;
const records = await foodRecordManager.getFoodRecords({
userId,
mealType: mealType as string,
startDate: startDate ? new Date(startDate as string) : undefined,
endDate: endDate ? new Date(endDate as string) : undefined,
});
res.json({
success: true,
data: records
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 创建食物记录(手动添加)
router.post('/', async (req, res) => {
try {
const record = await foodRecordManager.createFoodRecord(req.body);
res.json({
success: true,
data: record
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 删除食物记录
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const success = await foodRecordManager.deleteFoodRecord(id);
res.json({
success,
message: success ? 'Record deleted successfully' : 'Record not found'
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 拍照识别食物
router.post('/recognize', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: 'No image file provided'
});
}
const { buffer, originalname, mimetype } = req.file;
// 1. 上传图片到对象存储
const imageKey = await storage.uploadFile({
fileContent: buffer,
fileName: `food-photos/${originalname}`,
contentType: mimetype,
});
// 生成图片访问 URL
const imageUrl = await storage.generatePresignedUrl({
key: imageKey,
expireTime: 86400 // 1天有效期
});
// 2. 调用大语言模型识别食物
const base64Image = buffer.toString('base64');
const dataUri = `data:${mimetype};base64,${base64Image}`;
const messages = [
{
role: 'system' as const,
content: '你是一个专业的食物营养分析助手。请分析图片中的食物识别食物名称、估算重量并计算热量。返回JSON格式包含foodName食物名称、weight估算重量单位克、calories估算热量单位千卡。'
},
{
role: 'user' as const,
content: [
{
type: 'text' as const,
text: '请识别这张图片中的食物,并告诉我食物名称、估算重量和热量。'
},
{
type: 'image_url' as const,
image_url: {
url: dataUri,
detail: 'high' as const
}
}
]
}
];
const response = await llmClient.invoke(messages, {
model: 'doubao-seed-1-6-vision-250815',
temperature: 0.3
});
// 3. 解析AI返回的结果
let foodInfo: any = {};
try {
// 尝试从响应中提取JSON
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
foodInfo = JSON.parse(jsonMatch[0]);
} else {
// 如果没有返回JSON使用默认值
foodInfo = {
foodName: '未知食物',
weight: 100,
calories: 100
};
}
} catch (parseError) {
// JSON解析失败使用默认值
foodInfo = {
foodName: '未知食物',
weight: 100,
calories: 100
};
}
// 4. 返回识别结果
res.json({
success: true,
data: {
foodName: foodInfo.foodName || '未知食物',
weight: Number(foodInfo.weight) || 100,
calories: Number(foodInfo.calories) || 100,
imageUrl,
imageKey,
aiResponse: response.content
}
});
} catch (error: any) {
console.error('Food recognition error:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to recognize food'
});
}
});
// 获取今日总热量
router.get('/total-calories', async (req, res) => {
try {
const { userId, date } = req.query;
if (!userId || typeof userId !== 'string') {
return res.status(400).json({
success: false,
error: 'userId is required'
});
}
const queryDate = date ? new Date(date as string) : new Date();
const totalCalories = await foodRecordManager.getTotalCaloriesByDate(userId, queryDate);
res.json({
success: true,
data: {
date: queryDate.toISOString().split('T')[0],
totalCalories
}
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
export default router;

View File

@@ -0,0 +1,81 @@
import { Router } from 'express';
import { userManager } from '../storage/database';
const router = Router();
// 获取用户信息
router.get('/', async (req, res) => {
try {
const { userId } = req.query;
if (!userId || typeof userId !== 'string') {
return res.status(400).json({
success: false,
error: 'userId is required'
});
}
const user = await userManager.getUserById(userId);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.json({
success: true,
data: user
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 创建用户
router.post('/', async (req, res) => {
try {
const user = await userManager.createUser(req.body);
res.json({
success: true,
data: user
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 更新用户信息
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const user = await userManager.updateUser(id, req.body);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.json({
success: true,
data: user
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
export default router;

View File

@@ -0,0 +1,99 @@
import { Router } from 'express';
import { weightRecordManager } from '../storage/database';
const router = Router();
// 获取体重记录列表
router.get('/', async (req, res) => {
try {
const { userId } = req.query;
if (!userId || typeof userId !== 'string') {
return res.status(400).json({
success: false,
error: 'userId is required'
});
}
const { startDate, endDate } = req.query;
const records = await weightRecordManager.getWeightRecords({
userId,
startDate: startDate ? new Date(startDate as string) : undefined,
endDate: endDate ? new Date(endDate as string) : undefined,
});
res.json({
success: true,
data: records
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 创建体重记录
router.post('/', async (req, res) => {
try {
const record = await weightRecordManager.createWeightRecord(req.body);
res.json({
success: true,
data: record
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 删除体重记录
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const success = await weightRecordManager.deleteWeightRecord(id);
res.json({
success,
message: success ? 'Record deleted successfully' : 'Record not found'
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 获取体重统计
router.get('/stats', async (req, res) => {
try {
const { userId } = req.query;
if (!userId || typeof userId !== 'string') {
return res.status(400).json({
success: false,
error: 'userId is required'
});
}
const stats = await weightRecordManager.getWeightStats(userId);
res.json({
success: true,
data: stats
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message
});
}
});
export default router;

View File

@@ -0,0 +1,84 @@
import { eq, and, SQL, desc, gte, lte } from "drizzle-orm";
import { getDb } from "coze-coding-dev-sdk";
import { foodRecords, insertFoodRecordSchema } from "./shared/schema";
import type { FoodRecord, InsertFoodRecord } from "./shared/schema";
export class FoodRecordManager {
async createFoodRecord(data: InsertFoodRecord): Promise<FoodRecord> {
const db = await getDb();
const validated = insertFoodRecordSchema.parse(data);
const [record] = await db.insert(foodRecords).values(validated).returning();
return record;
}
async getFoodRecords(options: {
userId: string;
skip?: number;
limit?: number;
mealType?: string;
startDate?: Date;
endDate?: Date;
}): Promise<FoodRecord[]> {
const { userId, skip = 0, limit = 100, mealType, startDate, endDate } = options;
const db = await getDb();
const conditions: SQL[] = [];
conditions.push(eq(foodRecords.userId, userId));
if (mealType) {
conditions.push(eq(foodRecords.mealType, mealType));
}
if (startDate) {
conditions.push(gte(foodRecords.recordedAt, startDate));
}
if (endDate) {
conditions.push(lte(foodRecords.recordedAt, endDate));
}
let query = db.select().from(foodRecords).$dynamic();
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
return query.orderBy(desc(foodRecords.recordedAt)).limit(limit).offset(skip);
}
async getFoodRecordById(id: string): Promise<FoodRecord | null> {
const db = await getDb();
const [record] = await db.select().from(foodRecords).where(eq(foodRecords.id, id));
return record || null;
}
async deleteFoodRecord(id: string): Promise<boolean> {
const db = await getDb();
const result = await db.delete(foodRecords).where(eq(foodRecords.id, id));
return (result.rowCount ?? 0) > 0;
}
async getTotalCaloriesByDate(userId: string, date: Date): Promise<number> {
const db = await getDb();
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
const records = await db
.select()
.from(foodRecords)
.where(
and(
eq(foodRecords.userId, userId),
gte(foodRecords.recordedAt, startOfDay),
lte(foodRecords.recordedAt, endOfDay)
)
);
return records.reduce((sum, record) => sum + record.calories, 0);
}
}
export const foodRecordManager = new FoodRecordManager();

View File

@@ -0,0 +1,7 @@
// Export instances
export { userManager } from "./userManager";
export { foodRecordManager } from "./foodRecordManager";
export { weightRecordManager } from "./weightRecordManager";
// Export types and schemas
export * from "./shared/schema";

View File

@@ -0,0 +1,3 @@
import { relations } from "drizzle-orm/relations";
import { } from "./schema";

View File

@@ -0,0 +1,130 @@
import { pgTable, text, varchar, timestamp, boolean, integer, decimal, jsonb, index } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { createSchemaFactory } from "drizzle-zod";
import { z } from "zod";
// 用户表
export const users = pgTable(
"users",
{
id: varchar("id", { length: 36 })
.primaryKey()
.default(sql`gen_random_uuid()`),
name: varchar("name", { length: 128 }).notNull(),
gender: varchar("gender", { length: 10 }), // 'male' | 'female' | 'other'
age: integer("age"),
height: integer("height"), // 身高(厘米)
targetWeight: decimal("target_weight", { precision: 5, scale: 2 }), // 目标体重(千克)
currentWeight: decimal("current_weight", { precision: 5, scale: 2 }), // 当前体重(千克)
metadata: jsonb("metadata"),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }),
},
(table) => ({
nameIdx: index("users_name_idx").on(table.name),
})
);
// 食物记录表
export const foodRecords = pgTable(
"food_records",
{
id: varchar("id", { length: 36 })
.primaryKey()
.default(sql`gen_random_uuid()`),
userId: varchar("user_id", { length: 36 }).notNull(),
foodName: varchar("food_name", { length: 256 }).notNull(),
calories: integer("calories").notNull(), // 热量(千卡)
weight: decimal("weight", { precision: 5, scale: 2 }), // 重量(克)
imageUrl: text("image_url"), // 食物图片 URL
mealType: varchar("meal_type", { length: 20 }).notNull(), // 'breakfast' | 'lunch' | 'dinner' | 'snack'
recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull(), // 记录时间
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => ({
userIdIdx: index("food_records_user_id_idx").on(table.userId),
recordedAtIdx: index("food_records_recorded_at_idx").on(table.recordedAt),
})
);
// 体重记录表
export const weightRecords = pgTable(
"weight_records",
{
id: varchar("id", { length: 36 })
.primaryKey()
.default(sql`gen_random_uuid()`),
userId: varchar("user_id", { length: 36 }).notNull(),
weight: decimal("weight", { precision: 5, scale: 2 }).notNull(), // 体重(千克)
note: text("note"), // 备注
recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull(), // 记录时间
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => ({
userIdIdx: index("weight_records_user_id_idx").on(table.userId),
recordedAtIdx: index("weight_records_recorded_at_idx").on(table.recordedAt),
})
);
// 使用 createSchemaFactory 配置 date coercion处理前端 string → Date 转换)
const { createInsertSchema: createCoercedInsertSchema } = createSchemaFactory({
coerce: { date: true },
});
// Users schemas
export const insertUserSchema = createCoercedInsertSchema(users).pick({
name: true,
gender: true,
age: true,
height: true,
targetWeight: true,
currentWeight: true,
metadata: true,
});
export const updateUserSchema = createCoercedInsertSchema(users)
.pick({
name: true,
gender: true,
age: true,
height: true,
targetWeight: true,
currentWeight: true,
})
.partial();
// Food records schemas
export const insertFoodRecordSchema = createCoercedInsertSchema(foodRecords).pick({
userId: true,
foodName: true,
calories: true,
weight: true,
imageUrl: true,
mealType: true,
recordedAt: true,
});
// Weight records schemas
export const insertWeightRecordSchema = createCoercedInsertSchema(weightRecords).pick({
userId: true,
weight: true,
note: true,
recordedAt: true,
});
// TypeScript types
export type User = typeof users.$inferSelect;
export type InsertUser = z.infer<typeof insertUserSchema>;
export type UpdateUser = z.infer<typeof updateUserSchema>;
export type FoodRecord = typeof foodRecords.$inferSelect;
export type InsertFoodRecord = z.infer<typeof insertFoodRecordSchema>;
export type WeightRecord = typeof weightRecords.$inferSelect;
export type InsertWeightRecord = z.infer<typeof insertWeightRecordSchema>;

View File

@@ -0,0 +1,47 @@
import { eq, and, SQL } from "drizzle-orm";
import { getDb } from "coze-coding-dev-sdk";
import { users, insertUserSchema, updateUserSchema } from "./shared/schema";
import type { User, InsertUser, UpdateUser } from "./shared/schema";
export class UserManager {
async createUser(data: InsertUser): Promise<User> {
const db = await getDb();
const validated = insertUserSchema.parse(data);
const [user] = await db.insert(users).values(validated).returning();
return user;
}
async getUsers(options: {
skip?: number;
limit?: number;
} = {}): Promise<User[]> {
const { skip = 0, limit = 100 } = options;
const db = await getDb();
return db.select().from(users).limit(limit).offset(skip);
}
async getUserById(id: string): Promise<User | null> {
const db = await getDb();
const [user] = await db.select().from(users).where(eq(users.id, id));
return user || null;
}
async updateUser(id: string, data: UpdateUser): Promise<User | null> {
const db = await getDb();
const validated = updateUserSchema.parse(data);
const [user] = await db
.update(users)
.set({ ...validated, updatedAt: new Date() })
.where(eq(users.id, id))
.returning();
return user || null;
}
async deleteUser(id: string): Promise<boolean> {
const db = await getDb();
const result = await db.delete(users).where(eq(users.id, id));
return (result.rowCount ?? 0) > 0;
}
}
export const userManager = new UserManager();

View File

@@ -0,0 +1,97 @@
import { eq, and, SQL, desc, gte, lte } from "drizzle-orm";
import { getDb } from "coze-coding-dev-sdk";
import { weightRecords, insertWeightRecordSchema } from "./shared/schema";
import type { WeightRecord, InsertWeightRecord } from "./shared/schema";
export class WeightRecordManager {
async createWeightRecord(data: InsertWeightRecord): Promise<WeightRecord> {
const db = await getDb();
const validated = insertWeightRecordSchema.parse(data);
const [record] = await db.insert(weightRecords).values(validated).returning();
return record;
}
async getWeightRecords(options: {
userId: string;
skip?: number;
limit?: number;
startDate?: Date;
endDate?: Date;
}): Promise<WeightRecord[]> {
const { userId, skip = 0, limit = 100, startDate, endDate } = options;
const db = await getDb();
const conditions: SQL[] = [];
conditions.push(eq(weightRecords.userId, userId));
if (startDate) {
conditions.push(gte(weightRecords.recordedAt, startDate));
}
if (endDate) {
conditions.push(lte(weightRecords.recordedAt, endDate));
}
let query = db.select().from(weightRecords).$dynamic();
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
return query.orderBy(desc(weightRecords.recordedAt)).limit(limit).offset(skip);
}
async getWeightRecordById(id: string): Promise<WeightRecord | null> {
const db = await getDb();
const [record] = await db.select().from(weightRecords).where(eq(weightRecords.id, id));
return record || null;
}
async deleteWeightRecord(id: string): Promise<boolean> {
const db = await getDb();
const result = await db.delete(weightRecords).where(eq(weightRecords.id, id));
return (result.rowCount ?? 0) > 0;
}
async getLatestWeight(userId: string): Promise<number | null> {
const db = await getDb();
const [record] = await db
.select()
.from(weightRecords)
.where(eq(weightRecords.userId, userId))
.orderBy(desc(weightRecords.recordedAt))
.limit(1);
return record ? parseFloat(record.weight) : null;
}
async getWeightStats(userId: string): Promise<{
currentWeight: number | null;
startWeight: number | null;
targetWeight: number | null;
}> {
const db = await getDb();
const records = await db
.select()
.from(weightRecords)
.where(eq(weightRecords.userId, userId))
.orderBy(weightRecords.recordedAt)
.limit(1);
const currentWeight = await this.getLatestWeight(userId);
const startWeight = records.length > 0 ? parseFloat(records[0].weight) : null;
// 获取用户目标体重
// 这里暂时返回 null后续可以关联用户表
const targetWeight = null;
return {
currentWeight,
startWeight,
targetWeight,
};
}
}
export const weightRecordManager = new WeightRecordManager();

24
server/tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://www.schemastore.org/tsconfig",
"compilerOptions": {
"lib": [
"es2024",
"ESNext.Array",
"ESNext.Collection",
"ESNext.Iterator",
"ESNext.Promise"
],
"module": "preserve",
"target": "es2024",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
},
"include": ["src"]
}