commit 8ceb5fa9dbf07e7b4bc2145502624e8a9f67f1a3 Author: Karriis Date: Wed Feb 18 13:52:06 2026 +0800 feat: initial commit for TheFarmer project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14d231e --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Testing +coverage/ + +# Production +dist/ +build/ + +# Misc +.DS_Store +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Backups +backups/ +_upstream/ +*.zip +*.rar +*.7z +*.bak + +# Trae/IDE +.trae/ +.vscode/ +.idea/ + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS +Thumbs.db diff --git a/211/server/client.js b/211/server/client.js new file mode 100644 index 0000000..b5392c8 --- /dev/null +++ b/211/server/client.js @@ -0,0 +1,139 @@ +/** + * QQ经典农场 挂机脚本 - 入口文件 + * + * 模块结构: + * src/config.js - 配置常量与枚举 + * src/utils.js - 通用工具函数 + * src/proto.js - Protobuf 加载与类型管理 + * src/core/ - 核心逻辑 (FarmBot, Network, FarmManager, FriendManager) + * src/decode.js - PB解码/验证工具模式 + */ + +const { CONFIG } = require('./src/config'); +const { loadProto } = require('./src/proto'); +const { FarmBot } = require('./src/core/FarmBot'); +const { verifyMode, decodeMode } = require('./src/decode'); +const { emitRuntimeHint, sleep } = require('./src/utils'); + +// ============ 帮助信息 ============ +function showHelp() { + console.log(` +QQ经典农场 挂机脚本 (重构版) +==================== + +用法: + node client.js --code <登录code> [--wx] [--interval <秒>] [--friend-interval <秒>] + node client.js --verify + node client.js --decode <数据> [--hex] [--gate] [--type <消息类型>] + +参数: + --code 小程序 login() 返回的临时凭证 (必需) + --wx 使用微信登录 (默认为QQ小程序) + --interval 自己农场巡查完成后等待秒数, 默认10秒, 最低10秒 + --friend-interval 好友巡查完成后等待秒数, 默认1秒, 最低1秒 + --verify 验证proto定义 + --decode 解码PB数据 (运行 --decode 无参数查看详细帮助) + +功能: + - 自动收获成熟作物 → 购买种子 → 种植 → 施肥 + - 自动除草、除虫、浇水 + - 自动铲除枯死作物 + - 自动巡查好友农场: 帮忙浇水/除草/除虫 + 偷菜 + - 自动领取任务奖励 (支持分享翻倍) + - 每分钟自动出售仓库果实 + - 启动时读取 share.txt 处理邀请码 (仅微信) + - 心跳保活 +`); +} + +// ============ 参数解析 ============ +function parseArgs(args) { + const options = { + code: '', + deleteAccountMode: false, + name: '', + certId: '', + certType: 0, + }; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--code' && args[i + 1]) { + options.code = args[++i]; + } + if (args[i] === '--wx') { + CONFIG.platform = 'wx'; + } + if (args[i] === '--interval' && args[i + 1]) { + const sec = parseInt(args[++i]); + CONFIG.farmCheckInterval = Math.max(sec, 1) * 1000; + } + if (args[i] === '--friend-interval' && args[i + 1]) { + const sec = parseInt(args[++i]); + CONFIG.friendCheckInterval = Math.max(sec, 1) * 1000; // 最低1秒 + } + } + return options; +} + +// ============ 主函数 ============ +async function main() { + const args = process.argv.slice(2); + + // 加载 proto 定义 + await loadProto(); + + // 验证模式 + if (args.includes('--verify')) { + await verifyMode(); + return; + } + + // 解码模式 + if (args.includes('--decode')) { + await decodeMode(args); + return; + } + + // 正常运行模式 + const options = parseArgs(args); + + if (!options.code) { + showHelp(); + return; + } + + // 更新 CONFIG 中的 code (注意: FarmBot 构造函数会合并 CONFIG 和传入的 config) + // 这里我们可以直接修改全局 CONFIG,或者传入 options + // 为了兼容旧代码习惯,我们修改全局 CONFIG + // 但 FarmBot 建议传入 config + const botConfig = { + code: options.code, + // 其他参数已通过 modify global CONFIG 生效,或者也可以显式传入 + }; + + console.log('正在启动 FarmBot...'); + const bot = new FarmBot(botConfig); + + // 优雅退出 + process.on('SIGINT', () => { + console.log('\n正在停止...'); + bot.stop(); + process.exit(0); + }); + + try { + await bot.start(); + + // 保持进程运行 (如果 start() 返回后没有挂起的操作) + // FarmBot 内部启动了 interval,所以进程应该会保持运行 + + } catch (error) { + console.error('Bot 运行出错:', error); + process.exit(1); + } +} + +main().catch(err => { + console.error('未捕获的异常:', err); + process.exit(1); +}); diff --git a/211/server/data/users.db b/211/server/data/users.db new file mode 100644 index 0000000..89b518a Binary files /dev/null and b/211/server/data/users.db differ diff --git a/211/server/gameConfig/Plant.json b/211/server/gameConfig/Plant.json new file mode 100644 index 0000000..9a1d24c --- /dev/null +++ b/211/server/gameConfig/Plant.json @@ -0,0 +1,4061 @@ +[ + { + "id": 2020002, + "name": "白萝卜", + "mutant": "", + "fruit": { + "id": 40002, + "count": 5 + }, + "seed_id": 29999, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:1;发芽:1;成熟:0;", + "exp": 1, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020002, + "name": "白萝卜", + "mutant": "1:102003;2:1020059", + "fruit": { + "id": 40002, + "count": 5 + }, + "seed_id": 20002, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:30;发芽:30;成熟:0;", + "exp": 1, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020003, + "name": "胡萝卜", + "mutant": "", + "fruit": { + "id": 40003, + "count": 10 + }, + "seed_id": 20003, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:30;发芽:30;小叶子:30;大叶子:30;成熟:0;", + "exp": 2, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020059, + "name": "大白菜", + "mutant": "", + "fruit": { + "id": 40059, + "count": 20 + }, + "seed_id": 20059, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:60;发芽:60;幼苗:60;成株:60;卷心:60;成熟:0;", + "exp": 5, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020065, + "name": "大蒜", + "mutant": "", + "fruit": { + "id": 40065, + "count": 20 + }, + "seed_id": 20065, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:120;发芽:120;幼苗:120;伸长:120;初熟:120;成熟:0;", + "exp": 10, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020064, + "name": "大葱", + "mutant": "", + "fruit": { + "id": 40064, + "count": 30 + }, + "seed_id": 20064, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:300;发芽:300;小叶子:300;大叶子:300;成熟:0;", + "exp": 20, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020060, + "name": "水稻", + "mutant": "", + "fruit": { + "id": 40060, + "count": 30 + }, + "seed_id": 20060, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:480;幼苗:480;秧苗:480;幼穗:480;开花:480;成熟:0;", + "exp": 41, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020061, + "name": "小麦", + "mutant": "", + "fruit": { + "id": 40061, + "count": 40 + }, + "seed_id": 20061, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:720;发芽:720;小叶子:720;大叶子:720;幼穗:720;成熟:0;", + "exp": 62, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020004, + "name": "玉米", + "mutant": "", + "fruit": { + "id": 40004, + "count": 40 + }, + "seed_id": 20004, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:960;发芽:960;小叶子:960;大叶子:960;开花:960;成熟:0;", + "exp": 82, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020066, + "name": "鲜姜", + "mutant": "", + "fruit": { + "id": 40066, + "count": 60 + }, + "seed_id": 20066, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:1500;发芽:1500;小叶子:1500;大叶子:1500;成熟:0;", + "exp": 106, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020005, + "name": "土豆", + "mutant": "", + "fruit": { + "id": 40005, + "count": 60 + }, + "seed_id": 20005, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:1440;发芽:1440;小叶子:1440;大叶子:1440;初熟:1440;成熟:0;", + "exp": 128, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020071, + "name": "小白菜", + "mutant": "", + "fruit": { + "id": 40071, + "count": 80 + }, + "seed_id": 20071, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2250;发芽:2250;小叶子:2250;大叶子:2250;成熟:0;", + "exp": 160, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020096, + "name": "生菜", + "mutant": "", + "fruit": { + "id": 40096, + "count": 80 + }, + "seed_id": 20096, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2160;发芽:2160;小叶子:2160;大叶子:2160;初熟:2160;成熟:0;", + "exp": 192, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020099, + "name": "油菜", + "mutant": "", + "fruit": { + "id": 40099, + "count": 200 + }, + "seed_id": 20099, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:3600;发芽:3600;小叶子:3600;大叶子:3600;成熟:0;", + "exp": 272, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020006, + "name": "茄子", + "mutant": "", + "fruit": { + "id": 40006, + "count": 200 + }, + "seed_id": 20006, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 544, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020051, + "name": "红枣", + "mutant": "", + "fruit": { + "id": 40051, + "count": 200 + }, + "seed_id": 20051, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 816, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020120, + "name": "蒲公英", + "mutant": "", + "fruit": { + "id": 40120, + "count": 200 + }, + "seed_id": 20120, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:17280;成熟:0;", + "exp": 1632, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020259, + "name": "银莲花", + "mutant": "", + "fruit": { + "id": 40259, + "count": 200 + }, + "seed_id": 20259, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0;", + "exp": 288, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020007, + "name": "番茄", + "mutant": "", + "fruit": { + "id": 40007, + "count": 200 + }, + "seed_id": 20007, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 576, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020098, + "name": "花菜", + "mutant": "", + "fruit": { + "id": 40098, + "count": 200 + }, + "seed_id": 20098, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;幼苗:8640;卷心:8640;初熟:8640;成熟:0;", + "exp": 864, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020305, + "name": "韭菜", + "mutant": "", + "fruit": { + "id": 40305, + "count": 200 + }, + "seed_id": 20305, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0;", + "exp": 1728, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020105, + "name": "小雏菊", + "mutant": "", + "fruit": { + "id": 40105, + "count": 200 + }, + "seed_id": 20105, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0;", + "exp": 304, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020008, + "name": "豌豆", + "mutant": "", + "fruit": { + "id": 40008, + "count": 200 + }, + "seed_id": 20008, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0;", + "exp": 608, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020037, + "name": "莲藕", + "mutant": "", + "fruit": { + "id": 40037, + "count": 200 + }, + "seed_id": 20037, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 912, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020041, + "name": "红玫瑰", + "mutant": "", + "fruit": { + "id": 40041, + "count": 200 + }, + "seed_id": 20041, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0;", + "exp": 1824, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020161, + "name": "秋菊(黄色)", + "mutant": "", + "fruit": { + "id": 40161, + "count": 200 + }, + "seed_id": 20161, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0;", + "exp": 324, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020110, + "name": "满天星", + "mutant": "", + "fruit": { + "id": 40110, + "count": 200 + }, + "seed_id": 20110, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0;", + "exp": 648, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020143, + "name": "含羞草", + "mutant": "", + "fruit": { + "id": 40143, + "count": 200 + }, + "seed_id": 20143, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:7200;花蕾:7200;盛开:7200;成熟:0;", + "exp": 972, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020147, + "name": "牵牛花", + "mutant": "", + "fruit": { + "id": 40147, + "count": 200 + }, + "seed_id": 20147, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0;", + "exp": 1944, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020162, + "name": "秋菊(红色)", + "mutant": "", + "fruit": { + "id": 40162, + "count": 200 + }, + "seed_id": 20162, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0;", + "exp": 344, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020009, + "name": "辣椒", + "mutant": "", + "fruit": { + "id": 40009, + "count": 200 + }, + "seed_id": 20009, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0;", + "exp": 688, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020097, + "name": "黄瓜", + "mutant": "", + "fruit": { + "id": 40097, + "count": 200 + }, + "seed_id": 20097, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 1032, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020306, + "name": "芹菜", + "mutant": "", + "fruit": { + "id": 40306, + "count": 200 + }, + "seed_id": 20306, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0;", + "exp": 2064, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020103, + "name": "天香百合", + "mutant": "", + "fruit": { + "id": 40103, + "count": 200 + }, + "seed_id": 20103, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0;", + "exp": 368, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020010, + "name": "南瓜", + "mutant": "", + "fruit": { + "id": 40010, + "count": 200 + }, + "seed_id": 20010, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0;", + "exp": 736, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020308, + "name": "核桃", + "mutant": "", + "fruit": { + "id": 40308, + "count": 200 + }, + "seed_id": 20308, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:10800;发芽:10800;小叶子:10800;大叶子:10800;成熟:0;", + "exp": 1104, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020091, + "name": "山楂", + "mutant": "", + "fruit": { + "id": 40091, + "count": 200 + }, + "seed_id": 20091, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:14400;开花:14400;初熟:14400;成熟:0;", + "exp": 2208, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020073, + "name": "菠菜", + "mutant": "", + "fruit": { + "id": 40073, + "count": 200 + }, + "seed_id": 20073, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0;", + "exp": 392, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020001, + "name": "草莓", + "mutant": "", + "fruit": { + "id": 40001, + "count": 200 + }, + "seed_id": 20001, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 784, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020011, + "name": "苹果", + "mutant": "", + "fruit": { + "id": 40011, + "count": 200 + }, + "seed_id": 20011, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 1176, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020062, + "name": "四叶草", + "mutant": "", + "fruit": { + "id": 40062, + "count": 200 + }, + "seed_id": 20062, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0;", + "exp": 2352, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020104, + "name": "非洲菊", + "mutant": "", + "fruit": { + "id": 40104, + "count": 200 + }, + "seed_id": 20104, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:2400;花蕾:2400;盛开:2400;成熟:0;", + "exp": 420, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020135, + "name": "火绒草", + "mutant": "", + "fruit": { + "id": 40135, + "count": 200 + }, + "seed_id": 20135, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0;", + "exp": 840, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020141, + "name": "花香根鸢尾", + "mutant": "", + "fruit": { + "id": 40141, + "count": 200 + }, + "seed_id": 20141, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;花蕾:8640;盛开:0;", + "exp": 1260, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020142, + "name": "虞美人", + "mutant": "", + "fruit": { + "id": 40142, + "count": 200 + }, + "seed_id": 20142, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0;", + "exp": 2520, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020145, + "name": "向日葵", + "mutant": "", + "fruit": { + "id": 40145, + "count": 200 + }, + "seed_id": 20145, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:2400;开花:2400;初熟:2400;成熟:0;", + "exp": 448, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020014, + "name": "西瓜", + "mutant": "", + "fruit": { + "id": 40014, + "count": 200 + }, + "seed_id": 20014, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 896, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020070, + "name": "黄豆", + "mutant": "", + "fruit": { + "id": 40070, + "count": 200 + }, + "seed_id": 20070, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;初熟:8640;成熟:0;", + "exp": 1344, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020015, + "name": "香蕉", + "mutant": "", + "fruit": { + "id": 40015, + "count": 200 + }, + "seed_id": 20015, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0;", + "exp": 2688, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020100, + "name": "竹笋", + "mutant": "", + "fruit": { + "id": 40100, + "count": 200 + }, + "seed_id": 20100, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;幼苗:2880;伸长:2880;初熟:2880;成熟:0;", + "exp": 476, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020018, + "name": "桃子", + "mutant": "", + "fruit": { + "id": 40018, + "count": 200 + }, + "seed_id": 20018, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;开花:5760;小叶子:5760;大叶子:5760;成熟:0;", + "exp": 952, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020047, + "name": "甘蔗", + "mutant": "", + "fruit": { + "id": 40047, + "count": 200 + }, + "seed_id": 20047, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;幼苗:8640;分叶:8640;伸长:8640;成熟:0;", + "exp": 1428, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020019, + "name": "橙子", + "mutant": "", + "fruit": { + "id": 40019, + "count": 200 + }, + "seed_id": 20019, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0;", + "exp": 2856, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020128, + "name": "茉莉花", + "mutant": "", + "fruit": { + "id": 40128, + "count": 200 + }, + "seed_id": 20128, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0;", + "exp": 508, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020013, + "name": "葡萄", + "mutant": "", + "fruit": { + "id": 40013, + "count": 200 + }, + "seed_id": 20013, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 1016, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020044, + "name": "丝瓜", + "mutant": "", + "fruit": { + "id": 40044, + "count": 200 + }, + "seed_id": 20044, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;长枝:8640;开花:8640;小叶子:8640;大叶子:8640;结果:0;", + "exp": 1524, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020072, + "name": "榛子", + "mutant": "", + "fruit": { + "id": 40072, + "count": 200 + }, + "seed_id": 20072, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;开花:17280;小叶子:17280;大叶子:17280;成熟:0;", + "exp": 3048, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020396, + "name": "迎春花", + "mutant": "", + "fruit": { + "id": 40396, + "count": 200 + }, + "seed_id": 20396, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;幼芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0;", + "exp": 540, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020023, + "name": "石榴", + "mutant": "", + "fruit": { + "id": 40023, + "count": 200 + }, + "seed_id": 20023, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 1080, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020095, + "name": "栗子", + "mutant": "", + "fruit": { + "id": 40095, + "count": 200 + }, + "seed_id": 20095, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 1620, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020026, + "name": "柚子", + "mutant": "", + "fruit": { + "id": 40026, + "count": 200 + }, + "seed_id": 20026, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0;", + "exp": 3240, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020050, + "name": "蘑菇", + "mutant": "", + "fruit": { + "id": 40050, + "count": 200 + }, + "seed_id": 20050, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:3600;发芽:3600;大叶子:3600;初熟:3600;成熟:0;", + "exp": 429, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020027, + "name": "菠萝", + "mutant": "", + "fruit": { + "id": 40027, + "count": 200 + }, + "seed_id": 20027, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 858, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020036, + "name": "箬竹", + "mutant": "", + "fruit": { + "id": 40036, + "count": 200 + }, + "seed_id": 20036, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0;", + "exp": 1287, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020043, + "name": "无花果", + "mutant": "", + "fruit": { + "id": 40043, + "count": 200 + }, + "seed_id": 20043, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 2574, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020029, + "name": "椰子", + "mutant": "", + "fruit": { + "id": 40029, + "count": 200 + }, + "seed_id": 20029, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 456, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020049, + "name": "花生", + "mutant": "", + "fruit": { + "id": 40049, + "count": 200 + }, + "seed_id": 20049, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 912, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020052, + "name": "金针菇", + "mutant": "", + "fruit": { + "id": 40052, + "count": 200 + }, + "seed_id": 20052, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发菌:7200;出菇:7200;幼菇:10800;初熟:10800;成熟:0;", + "exp": 1368, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020031, + "name": "葫芦", + "mutant": "", + "fruit": { + "id": 40031, + "count": 200 + }, + "seed_id": 20031, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 2736, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020045, + "name": "猕猴桃", + "mutant": "", + "fruit": { + "id": 40045, + "count": 200 + }, + "seed_id": 20045, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 480, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020054, + "name": "梨", + "mutant": "", + "fruit": { + "id": 40054, + "count": 200 + }, + "seed_id": 20054, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 960, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020442, + "name": "睡莲", + "mutant": "", + "fruit": { + "id": 40442, + "count": 200 + }, + "seed_id": 20442, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;幼株:10800;成熟:0;", + "exp": 1440, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020033, + "name": "火龙果", + "mutant": "", + "fruit": { + "id": 40033, + "count": 200 + }, + "seed_id": 20033, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 2880, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020055, + "name": "枇杷", + "mutant": "", + "fruit": { + "id": 40055, + "count": 200 + }, + "seed_id": 20055, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 510, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020034, + "name": "樱桃", + "mutant": "", + "fruit": { + "id": 40034, + "count": 200 + }, + "seed_id": 20034, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 1020, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020413, + "name": "李子", + "mutant": "", + "fruit": { + "id": 40413, + "count": 200 + }, + "seed_id": 20413, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1530, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020035, + "name": "荔枝", + "mutant": "", + "fruit": { + "id": 40035, + "count": 200 + }, + "seed_id": 20035, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 3060, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020067, + "name": "香瓜", + "mutant": "", + "fruit": { + "id": 40067, + "count": 200 + }, + "seed_id": 20067, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 537, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020038, + "name": "木瓜", + "mutant": "", + "fruit": { + "id": 40038, + "count": 200 + }, + "seed_id": 20038, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 1074, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020053, + "name": "桂圆", + "mutant": "", + "fruit": { + "id": 40053, + "count": 200 + }, + "seed_id": 20053, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1611, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020080, + "name": "月柿", + "mutant": "", + "fruit": { + "id": 40080, + "count": 200 + }, + "seed_id": 20080, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 3222, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020039, + "name": "杨桃", + "mutant": "", + "fruit": { + "id": 40039, + "count": 200 + }, + "seed_id": 20039, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 567, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020056, + "name": "哈密瓜", + "mutant": "", + "fruit": { + "id": 40056, + "count": 200 + }, + "seed_id": 20056, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 1134, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020075, + "name": "桑葚", + "mutant": "", + "fruit": { + "id": 40075, + "count": 200 + }, + "seed_id": 20075, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1701, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020042, + "name": "柠檬", + "mutant": "", + "fruit": { + "id": 40042, + "count": 200 + }, + "seed_id": 20042, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 3402, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020057, + "name": "芒果", + "mutant": "", + "fruit": { + "id": 40057, + "count": 200 + }, + "seed_id": 20057, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 597, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020048, + "name": "杨梅", + "mutant": "", + "fruit": { + "id": 40048, + "count": 200 + }, + "seed_id": 20048, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 1194, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020058, + "name": "榴莲", + "mutant": "", + "fruit": { + "id": 40058, + "count": 200 + }, + "seed_id": 20058, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1791, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020079, + "name": "番石榴", + "mutant": "", + "fruit": { + "id": 40079, + "count": 200 + }, + "seed_id": 20079, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 3582, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020218, + "name": "瓶子树", + "mutant": "", + "fruit": { + "id": 40218, + "count": 200 + }, + "seed_id": 20218, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;长枝:2400;小叶子:2400;大叶子:3600;初熟:3600;成树:0;", + "exp": 627, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020077, + "name": "蓝莓", + "mutant": "", + "fruit": { + "id": 40077, + "count": 200 + }, + "seed_id": 20077, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 1254, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020220, + "name": "猪笼草", + "mutant": "", + "fruit": { + "id": 40220, + "count": 200 + }, + "seed_id": 20220, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0;", + "exp": 1881, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020076, + "name": "山竹", + "mutant": "", + "fruit": { + "id": 40076, + "count": 200 + }, + "seed_id": 20076, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 3762, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020116, + "name": "曼陀罗华", + "mutant": "", + "fruit": { + "id": 40116, + "count": 200 + }, + "seed_id": 20116, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;花蕾:3600;盛开:0;", + "exp": 660, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020126, + "name": "曼珠沙华", + "mutant": "", + "fruit": { + "id": 40126, + "count": 200 + }, + "seed_id": 20126, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 1320, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020063, + "name": "苦瓜", + "mutant": "", + "fruit": { + "id": 40063, + "count": 200 + }, + "seed_id": 20063, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1980, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020221, + "name": "天堂鸟", + "mutant": "", + "fruit": { + "id": 40221, + "count": 200 + }, + "seed_id": 20221, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0;", + "exp": 3960, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020068, + "name": "冬瓜", + "mutant": "", + "fruit": { + "id": 40068, + "count": 200 + }, + "seed_id": 20068, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 693, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020222, + "name": "豹皮花", + "mutant": "", + "fruit": { + "id": 40222, + "count": 200 + }, + "seed_id": 20222, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0;", + "exp": 1386, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020078, + "name": "杏子", + "mutant": "", + "fruit": { + "id": 40078, + "count": 200 + }, + "seed_id": 20078, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0;", + "exp": 2079, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020074, + "name": "金桔", + "mutant": "", + "fruit": { + "id": 40074, + "count": 200 + }, + "seed_id": 20074, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 4158, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020083, + "name": "红毛丹", + "mutant": "", + "fruit": { + "id": 40083, + "count": 200 + }, + "seed_id": 20083, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 726, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020225, + "name": "宝华玉兰", + "mutant": "", + "fruit": { + "id": 40225, + "count": 200 + }, + "seed_id": 20225, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0;", + "exp": 1452, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020084, + "name": "芭蕉", + "mutant": "", + "fruit": { + "id": 40084, + "count": 200 + }, + "seed_id": 20084, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0;", + "exp": 2178, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020226, + "name": "依米花", + "mutant": "", + "fruit": { + "id": 40226, + "count": 200 + }, + "seed_id": 20226, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0;", + "exp": 4356, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020085, + "name": "番荔枝", + "mutant": "", + "fruit": { + "id": 40085, + "count": 200 + }, + "seed_id": 20085, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0;", + "exp": 762, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020227, + "name": "大王花", + "mutant": "", + "fruit": { + "id": 40227, + "count": 200 + }, + "seed_id": 20227, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;幼蕾:4800;含苞:7200;初放:7200;盛开:0;", + "exp": 1524, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020086, + "name": "橄榄", + "mutant": "", + "fruit": { + "id": 40086, + "count": 200 + }, + "seed_id": 20086, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 2286, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020228, + "name": "人参果", + "mutant": "", + "fruit": { + "id": 40228, + "count": 200 + }, + "seed_id": 20228, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 4572, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020087, + "name": "百香果", + "mutant": "", + "fruit": { + "id": 40087, + "count": 200 + }, + "seed_id": 20087, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0;", + "exp": 795, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020235, + "name": "金花茶", + "mutant": "", + "fruit": { + "id": 40235, + "count": 200 + }, + "seed_id": 20235, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0;", + "exp": 1590, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020088, + "name": "灯笼果", + "mutant": "", + "fruit": { + "id": 40088, + "count": 200 + }, + "seed_id": 20088, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0;", + "exp": 2385, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020201, + "name": "天山雪莲", + "mutant": "", + "fruit": { + "id": 40201, + "count": 200 + }, + "seed_id": 20201, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;幼株:21600;成熟:0;", + "exp": 4770, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020089, + "name": "芦荟", + "mutant": "", + "fruit": { + "id": 40089, + "count": 200 + }, + "seed_id": 20089, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 831, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020202, + "name": "金边灵芝", + "mutant": "", + "fruit": { + "id": 40202, + "count": 200 + }, + "seed_id": 20202, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;芝蕾:4800;幼芝:7200;初熟:7200;成熟:0;", + "exp": 1662, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020090, + "name": "薄荷", + "mutant": "", + "fruit": { + "id": 40090, + "count": 200 + }, + "seed_id": 20090, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 2493, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020229, + "name": "何首乌", + "mutant": "", + "fruit": { + "id": 40229, + "count": 200 + }, + "seed_id": 20229, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 4986, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020016, + "name": "菠萝蜜", + "mutant": "", + "fruit": { + "id": 40016, + "count": 200 + }, + "seed_id": 20016, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 867, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020204, + "name": "人参", + "mutant": "", + "fruit": { + "id": 40204, + "count": 200 + }, + "seed_id": 20204, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 1734, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020022, + "name": "鳄梨", + "mutant": "", + "fruit": { + "id": 40022, + "count": 200 + }, + "seed_id": 20022, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 2601, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020242, + "name": "似血杜鹃", + "mutant": "", + "fruit": { + "id": 40242, + "count": 200 + }, + "seed_id": 20242, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0;", + "exp": 5202, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1021542, + "name": "新春红包", + "mutant": "", + "fruit": { + "id": 41542, + "count": 20 + }, + "seed_id": 21542, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0;", + "exp": 688, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "spine/v2/xiyouzhongzi/red packet_tree", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 2029998, + "name": "哈哈南瓜", + "mutant": "", + "fruit": { + "id": 40416, + "count": 50 + }, + "seed_id": 29998, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 1, + "size": 2, + "offsetPosition": { + "x": 100, + "y": 0 + }, + "mutantEffectScale": { + "x": 1.75, + "y": 1.5 + }, + "harvestOffsetPosition": { + "x": 0, + "y": 0 + }, + "harvestRandom": true, + "harvestAllSpineRes": "spine/v2/shouge/Crop_sg_final", + "harvestAllOffsetPosition": "5:30;50:-30;150:-75;200:-25;150:-30", + "all_state_spine": "spine/v2/xiyouzhongzi/xiyouzhongzi", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "{\"x\":113,\"y\":125,\"rotation\":0,\"scale\":{\"x\":1,\"y\":1}}" + } +] \ No newline at end of file diff --git a/211/server/gameConfig/RoleLevel.json b/211/server/gameConfig/RoleLevel.json new file mode 100644 index 0000000..403a309 --- /dev/null +++ b/211/server/gameConfig/RoleLevel.json @@ -0,0 +1,802 @@ +[ + { + "level": 1, + "exp": 0 + }, + { + "level": 2, + "exp": 100 + }, + { + "level": 3, + "exp": 300 + }, + { + "level": 4, + "exp": 700 + }, + { + "level": 5, + "exp": 1300 + }, + { + "level": 6, + "exp": 2300 + }, + { + "level": 7, + "exp": 4000 + }, + { + "level": 8, + "exp": 6600 + }, + { + "level": 9, + "exp": 10100 + }, + { + "level": 10, + "exp": 14300 + }, + { + "level": 11, + "exp": 19300 + }, + { + "level": 12, + "exp": 25100 + }, + { + "level": 13, + "exp": 31800 + }, + { + "level": 14, + "exp": 39500 + }, + { + "level": 15, + "exp": 48300 + }, + { + "level": 16, + "exp": 58300 + }, + { + "level": 17, + "exp": 69500 + }, + { + "level": 18, + "exp": 82000 + }, + { + "level": 19, + "exp": 95900 + }, + { + "level": 20, + "exp": 111300 + }, + { + "level": 21, + "exp": 128300 + }, + { + "level": 22, + "exp": 146900 + }, + { + "level": 23, + "exp": 167200 + }, + { + "level": 24, + "exp": 189300 + }, + { + "level": 25, + "exp": 213300 + }, + { + "level": 26, + "exp": 239300 + }, + { + "level": 27, + "exp": 267300 + }, + { + "level": 28, + "exp": 297400 + }, + { + "level": 29, + "exp": 329700 + }, + { + "level": 30, + "exp": 364300 + }, + { + "level": 31, + "exp": 401300 + }, + { + "level": 32, + "exp": 440700 + }, + { + "level": 33, + "exp": 482600 + }, + { + "level": 34, + "exp": 527100 + }, + { + "level": 35, + "exp": 574300 + }, + { + "level": 36, + "exp": 624300 + }, + { + "level": 37, + "exp": 677100 + }, + { + "level": 38, + "exp": 732800 + }, + { + "level": 39, + "exp": 791500 + }, + { + "level": 40, + "exp": 853300 + }, + { + "level": 41, + "exp": 918300 + }, + { + "level": 42, + "exp": 986500 + }, + { + "level": 43, + "exp": 1058000 + }, + { + "level": 44, + "exp": 1132900 + }, + { + "level": 45, + "exp": 1211300 + }, + { + "level": 46, + "exp": 1293300 + }, + { + "level": 47, + "exp": 1378900 + }, + { + "level": 48, + "exp": 1468200 + }, + { + "level": 49, + "exp": 1561300 + }, + { + "level": 50, + "exp": 1658300 + }, + { + "level": 51, + "exp": 1759300 + }, + { + "level": 52, + "exp": 1864300 + }, + { + "level": 53, + "exp": 1973400 + }, + { + "level": 54, + "exp": 2086700 + }, + { + "level": 55, + "exp": 2204300 + }, + { + "level": 56, + "exp": 2326300 + }, + { + "level": 57, + "exp": 2452700 + }, + { + "level": 58, + "exp": 2583600 + }, + { + "level": 59, + "exp": 2719100 + }, + { + "level": 60, + "exp": 2859300 + }, + { + "level": 61, + "exp": 3004300 + }, + { + "level": 62, + "exp": 3154100 + }, + { + "level": 63, + "exp": 3308800 + }, + { + "level": 64, + "exp": 3468500 + }, + { + "level": 65, + "exp": 3633300 + }, + { + "level": 66, + "exp": 3803300 + }, + { + "level": 67, + "exp": 3978500 + }, + { + "level": 68, + "exp": 4159000 + }, + { + "level": 69, + "exp": 4344900 + }, + { + "level": 70, + "exp": 4536300 + }, + { + "level": 71, + "exp": 4733300 + }, + { + "level": 72, + "exp": 4935900 + }, + { + "level": 73, + "exp": 5144200 + }, + { + "level": 74, + "exp": 5358300 + }, + { + "level": 75, + "exp": 5578300 + }, + { + "level": 76, + "exp": 5804300 + }, + { + "level": 77, + "exp": 6036300 + }, + { + "level": 78, + "exp": 6274400 + }, + { + "level": 79, + "exp": 6518700 + }, + { + "level": 80, + "exp": 6769300 + }, + { + "level": 81, + "exp": 7026300 + }, + { + "level": 82, + "exp": 7289700 + }, + { + "level": 83, + "exp": 7559600 + }, + { + "level": 84, + "exp": 7836100 + }, + { + "level": 85, + "exp": 8119300 + }, + { + "level": 86, + "exp": 8409300 + }, + { + "level": 87, + "exp": 8706100 + }, + { + "level": 88, + "exp": 9009800 + }, + { + "level": 89, + "exp": 9320500 + }, + { + "level": 90, + "exp": 9638300 + }, + { + "level": 91, + "exp": 9963300 + }, + { + "level": 92, + "exp": 10295500 + }, + { + "level": 93, + "exp": 10635000 + }, + { + "level": 94, + "exp": 10981900 + }, + { + "level": 95, + "exp": 11336300 + }, + { + "level": 96, + "exp": 11698300 + }, + { + "level": 97, + "exp": 12067900 + }, + { + "level": 98, + "exp": 12445200 + }, + { + "level": 99, + "exp": 12830300 + }, + { + "level": 100, + "exp": 13223300 + }, + { + "level": 101, + "exp": 13624300 + }, + { + "level": 102, + "exp": 14185200 + }, + { + "level": 103, + "exp": 14760100 + }, + { + "level": 104, + "exp": 15349200 + }, + { + "level": 105, + "exp": 15952700 + }, + { + "level": 106, + "exp": 16570700 + }, + { + "level": 107, + "exp": 17203500 + }, + { + "level": 108, + "exp": 17851200 + }, + { + "level": 109, + "exp": 18513900 + }, + { + "level": 110, + "exp": 19191900 + }, + { + "level": 111, + "exp": 19885400 + }, + { + "level": 112, + "exp": 20594500 + }, + { + "level": 113, + "exp": 21319400 + }, + { + "level": 114, + "exp": 22060300 + }, + { + "level": 115, + "exp": 22817400 + }, + { + "level": 116, + "exp": 23590900 + }, + { + "level": 117, + "exp": 24381000 + }, + { + "level": 118, + "exp": 25187800 + }, + { + "level": 119, + "exp": 26011600 + }, + { + "level": 120, + "exp": 26852500 + }, + { + "level": 121, + "exp": 27710700 + }, + { + "level": 122, + "exp": 28586400 + }, + { + "level": 123, + "exp": 29479800 + }, + { + "level": 124, + "exp": 30391100 + }, + { + "level": 125, + "exp": 31320500 + }, + { + "level": 126, + "exp": 32268100 + }, + { + "level": 127, + "exp": 33234200 + }, + { + "level": 128, + "exp": 34218900 + }, + { + "level": 129, + "exp": 35222400 + }, + { + "level": 130, + "exp": 36245000 + }, + { + "level": 131, + "exp": 37286800 + }, + { + "level": 132, + "exp": 38348000 + }, + { + "level": 133, + "exp": 39428800 + }, + { + "level": 134, + "exp": 40529400 + }, + { + "level": 135, + "exp": 41650000 + }, + { + "level": 136, + "exp": 42790800 + }, + { + "level": 137, + "exp": 43952000 + }, + { + "level": 138, + "exp": 45133800 + }, + { + "level": 139, + "exp": 46336400 + }, + { + "level": 140, + "exp": 47559900 + }, + { + "level": 141, + "exp": 48804600 + }, + { + "level": 142, + "exp": 50070700 + }, + { + "level": 143, + "exp": 51358300 + }, + { + "level": 144, + "exp": 52667700 + }, + { + "level": 145, + "exp": 53999100 + }, + { + "level": 146, + "exp": 55352600 + }, + { + "level": 147, + "exp": 56728500 + }, + { + "level": 148, + "exp": 58127000 + }, + { + "level": 149, + "exp": 59548200 + }, + { + "level": 150, + "exp": 60992400 + }, + { + "level": 151, + "exp": 62459800 + }, + { + "level": 152, + "exp": 63950500 + }, + { + "level": 153, + "exp": 65464800 + }, + { + "level": 154, + "exp": 67002900 + }, + { + "level": 155, + "exp": 68564900 + }, + { + "level": 156, + "exp": 70151100 + }, + { + "level": 157, + "exp": 71761700 + }, + { + "level": 158, + "exp": 73396900 + }, + { + "level": 159, + "exp": 75056900 + }, + { + "level": 160, + "exp": 76741900 + }, + { + "level": 161, + "exp": 78452100 + }, + { + "level": 162, + "exp": 80187700 + }, + { + "level": 163, + "exp": 81948900 + }, + { + "level": 164, + "exp": 83735900 + }, + { + "level": 165, + "exp": 85548900 + }, + { + "level": 166, + "exp": 87388200 + }, + { + "level": 167, + "exp": 89253900 + }, + { + "level": 168, + "exp": 91146300 + }, + { + "level": 169, + "exp": 93065500 + }, + { + "level": 170, + "exp": 95011800 + }, + { + "level": 171, + "exp": 96985300 + }, + { + "level": 172, + "exp": 98986300 + }, + { + "level": 173, + "exp": 101015000 + }, + { + "level": 174, + "exp": 103071600 + }, + { + "level": 175, + "exp": 105156300 + }, + { + "level": 176, + "exp": 107269400 + }, + { + "level": 177, + "exp": 109411000 + }, + { + "level": 178, + "exp": 111581300 + }, + { + "level": 179, + "exp": 113780600 + }, + { + "level": 180, + "exp": 116009100 + }, + { + "level": 181, + "exp": 118267000 + }, + { + "level": 182, + "exp": 120554500 + }, + { + "level": 183, + "exp": 122871800 + }, + { + "level": 184, + "exp": 125219100 + }, + { + "level": 185, + "exp": 127596600 + }, + { + "level": 186, + "exp": 130004600 + }, + { + "level": 187, + "exp": 132443200 + }, + { + "level": 188, + "exp": 134912700 + }, + { + "level": 189, + "exp": 137413300 + }, + { + "level": 190, + "exp": 139945200 + }, + { + "level": 191, + "exp": 142508700 + }, + { + "level": 192, + "exp": 145103900 + }, + { + "level": 193, + "exp": 147731100 + }, + { + "level": 194, + "exp": 150390400 + }, + { + "level": 195, + "exp": 153082100 + }, + { + "level": 196, + "exp": 155806500 + }, + { + "level": 197, + "exp": 158563700 + }, + { + "level": 198, + "exp": 161353900 + }, + { + "level": 199, + "exp": 164177400 + }, + { + "level": 200, + "exp": 167034400 + } +] \ No newline at end of file diff --git a/211/server/index.js b/211/server/index.js new file mode 100644 index 0000000..7c9d75b --- /dev/null +++ b/211/server/index.js @@ -0,0 +1,1090 @@ +const express = require('express'); +const http = require('http'); +const path = require('path'); +const os = require('os'); // Added for system stats +require('dotenv').config({ path: path.join(__dirname, '.env') }); +require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const { Server } = require('socket.io'); +const { FarmBot } = require('./src/core/FarmBot'); +const { loadProto, types } = require('./src/proto'); +const { log, toLong, toNum } = require('./src/utils'); +const { getLevelExpProgress, getPlantNameBySeedId, getFruitName, getItemNameById } = require('./src/gameConfig'); +const userManager = require('./src/userManager'); +const { MiniProgramLoginSession } = require('./src/qrlib/session'); + +// OAuth Dependencies +const session = require('express-session'); +const passport = require('passport'); +const OAuth2Strategy = require('passport-oauth2'); + +// 初始化 Express App +const app = express(); +const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: "*", // 开发阶段允许跨域,生产环境应限制 + methods: ["GET", "POST"] + } +}); + +const oauthEnabled = Boolean(process.env.OAUTH_AUTHENTIK_ISSUER) + && Boolean(process.env.OAUTH_AUTHENTIK_CLIENT_ID) + && Boolean(process.env.OAUTH_AUTHENTIK_CLIENT_SECRET) + && Boolean(process.env.OAUTH_AUTHENTIK_REDIRECT_URI) + && Boolean(process.env.OAUTH_AUTHENTIK_SCOPE); + +// OAuth Setup +if (oauthEnabled) { + app.use(session({ + secret: 'farm-session-secret', + resave: false, + saveUninitialized: false + })); + app.use(passport.initialize()); + app.use(passport.session()); + + passport.serializeUser((user, done) => done(null, user.email)); + passport.deserializeUser((email, done) => { + const user = userManager.getUser(email); + done(null, user); + }); + + passport.use('authentik', new OAuth2Strategy({ + authorizationURL: `${process.env.OAUTH_AUTHENTIK_ISSUER}authorize/`, + tokenURL: `${process.env.OAUTH_AUTHENTIK_ISSUER}token/`, + clientID: process.env.OAUTH_AUTHENTIK_CLIENT_ID, + clientSecret: process.env.OAUTH_AUTHENTIK_CLIENT_SECRET, + callbackURL: process.env.OAUTH_AUTHENTIK_REDIRECT_URI, + scope: process.env.OAUTH_AUTHENTIK_SCOPE.split(' ') + }, async (accessToken, refreshToken, profile, cb) => { + try { + // Fetch user info using accessToken + const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); + const userInfoRes = await fetch(`${process.env.OAUTH_AUTHENTIK_ISSUER}userinfo/`, { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + const userInfo = await userInfoRes.json(); + + const email = userInfo.email || userInfo.preferred_username; // Adjust based on Authentik response + + // Register or Login + const user = await userManager.register(email, null, 'authentik', userInfo.sub); + return cb(null, user); + } catch (err) { + return cb(err); + } + })); + + // OAuth Routes + app.get('/api/auth/oauth/authentik', passport.authenticate('authentik', { state: true })); + + app.get('/api/auth/oauth/authentik/callback', (req, res, next) => { + passport.authenticate('authentik', { state: true }, (err, user, info) => { + const frontendUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:5173' : ''; + + if (err) { + console.error('OAuth Error:', err); + return res.redirect(`${frontendUrl}/login?error=oauth_failed`); + } + if (!user) { + console.error('OAuth Failed: No user returned'); + return res.redirect(`${frontendUrl}/login?error=oauth_failed`); + } + + req.logIn(user, (loginErr) => { + if (loginErr) { + console.error('Login Error:', loginErr); + return res.redirect(`${frontendUrl}/login?error=login_err`); + } + res.redirect(`${frontendUrl}/login?email=${encodeURIComponent(user.email)}&oauth=success&_t=${Date.now()}`); + }); + })(req, res, next); + }); +} + +// 中间件 +app.use(cors()); +app.use(bodyParser.json()); +if (process.env.NODE_ENV === 'production') { + app.use(express.static(path.join(__dirname, '../web/dist'))); +} + +app.get('/api/auth/oauth/status', (req, res) => { + res.json({ enabled: oauthEnabled }); +}); + +// 存储活跃的 Bot 实例: Map +const activeBots = new Map(); +const unluckyBoards = new Map(); + +userManager.loadUnluckyBoards().then((stored) => { + if (!stored) return; + for (const [email, board] of stored.entries()) { + unluckyBoards.set(email, board); + } +}).catch((err) => { + log('Leaderboard', `加载失败: ${err.message}`); +}); + + +function normalizeSettings(settings) { + if (!settings || typeof settings !== 'object') return {}; + const result = {}; + const farmIntervalSec = Number(settings.farmIntervalSec); + if (Number.isFinite(farmIntervalSec) && farmIntervalSec >= 1) { + result.farmCheckInterval = Math.floor(farmIntervalSec * 1000); + } + const friendIntervalSec = Number(settings.friendIntervalSec); + if (Number.isFinite(friendIntervalSec) && friendIntervalSec >= 1) { + result.friendCheckInterval = Math.floor(friendIntervalSec * 1000); + } + if (settings.seedStrategy === 'forceLowest') { + result.forceLowestLevelCrop = true; + } else if (settings.seedStrategy === 'default') { + result.forceLowestLevelCrop = false; + } + if (typeof settings.enableFriendOps === 'boolean') { + result.enableFriendOps = settings.enableFriendOps; + } + if (typeof settings.enableSteal === 'boolean') { + result.enableSteal = settings.enableSteal; + } + if (typeof settings.enableNormalFertilize === 'boolean') { + result.enableNormalFertilize = settings.enableNormalFertilize; + } + if (typeof settings.enableOrganicFertilize === 'boolean') { + result.enableOrganicFertilize = settings.enableOrganicFertilize; + } + if (typeof settings.allowTicketFertilizerPurchase === 'boolean') { + result.allowTicketFertilizerPurchase = settings.allowTicketFertilizerPurchase; + } + if (typeof settings.enableAutoSell === 'boolean') { + result.enableAutoSell = settings.enableAutoSell; + } + if (typeof settings.allowBuySeeds === 'boolean') { + result.allowBuySeeds = settings.allowBuySeeds; + } + if (typeof settings.allowRemove === 'boolean') { + result.allowRemove = settings.allowRemove; + } + if (settings.idleStrategy === 'task') { + result.idleStrategy = 'task'; + } else if (settings.idleStrategy === 'exp') { + result.idleStrategy = 'exp'; + } + return result; +} + +function getTodayKey() { + const now = new Date(); + const y = now.getFullYear(); + const m = `${now.getMonth() + 1}`.padStart(2, '0'); + const d = `${now.getDate()}`.padStart(2, '0'); + return `${y}-${m}-${d}`; +} + +async function fetchTaskInfo(bot) { + const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish(); + const { body: replyBody } = await bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body); + const reply = types.TaskInfoReply.decode(replyBody); + return reply.task_info || null; +} + +function buildTaskList(taskInfo) { + const tasks = []; + if (!taskInfo) return tasks; + const seen = new Set(); + const seenContent = new Set(); + + const addTasks = (list, category) => { + if (!list) return; + for (const task of list) { + const id = toNum(task.id); + if (seen.has(id)) continue; + const progress = toNum(task.progress); + const totalProgress = toNum(task.total_progress); + const isClaimed = Boolean(task.is_claimed); + const isUnlocked = Boolean(task.is_unlocked); + const shareMultiple = toNum(task.share_multiple); + const rewards = []; + const rewardList = task.rewards || []; + for (const reward of rewardList) { + rewards.push({ + id: toNum(reward.id), + count: toNum(reward.count) + }); + } + const desc = task.desc || `任务#${id}`; + const contentKey = `${desc.trim()}|${progress}|${totalProgress}`; + if (seenContent.has(contentKey)) continue; + tasks.push({ + id, + desc, + progress, + totalProgress, + isClaimed, + isUnlocked, + shareMultiple, + rewards, + category + }); + seen.add(id); + seenContent.add(contentKey); + } + }; + + addTasks(taskInfo.growth_tasks, 'growth'); + addTasks(taskInfo.daily_tasks, 'daily'); + addTasks(taskInfo.tasks, 'normal'); + return tasks; +} + +function isTaskClaimable(task) { + if (!task.isUnlocked || task.isClaimed) return false; + if (task.totalProgress <= 0) return false; + return task.progress >= task.totalProgress; +} + +async function claimTaskReward(bot, taskId, doShared) { + const body = types.ClaimTaskRewardRequest.encode(types.ClaimTaskRewardRequest.create({ + id: toLong(taskId), + do_shared: Boolean(doShared) + })).finish(); + const { body: replyBody } = await bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'ClaimTaskReward', body); + return types.ClaimTaskRewardReply.decode(replyBody); +} + +function updateUnluckyBoard(email, name, count) { + if (!email || !name || !Number.isFinite(count) || count <= 0) return; + let board = unluckyBoards.get(email); + if (!board) { + board = new Map(); + unluckyBoards.set(email, board); + } + const prev = board.get(name) || 0; + board.set(name, prev + count); + userManager.saveUnlucky(email, name, count).catch((err) => { + log('Leaderboard', `保存失败: ${err.message}`); + }); +} + +// 初始化 Proto +loadProto().then(() => { + log('系统', '协议定义加载完成。'); +}).catch(err => { + console.error('Failed to load Proto:', err); + process.exit(1); +}); + +// ============ API 路由 ============ + +// QR Code Login API (Mini Program) +app.post('/api/qr/create', async (req, res) => { + try { + log('扫码', '正在获取登录二维码...'); + const result = await MiniProgramLoginSession.requestLoginCode(); + log('扫码', '已获取登录二维码'); + res.json({ + success: true, + qrsig: result.code, + qrcode: `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(result.url)}`, + url: result.url + }); + } catch (error) { + console.error('QR Create Error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +app.post('/api/qr/check', async (req, res) => { + const { qrsig } = req.body; + if (!qrsig) { + return res.status(400).json({ success: false, message: 'Missing qrsig/code' }); + } + + try { + const result = await MiniProgramLoginSession.queryStatus(qrsig); + + if (result.status === 'Wait') { + res.json({ success: true, status: 'Wait', msg: '等待扫码...' }); + } else if (result.status === 'Used') { + res.json({ success: true, status: 'Used', msg: '二维码已失效' }); + } else if (result.status === 'OK') { + const ticket = result.ticket; + // Determine AppID (Farm AppID) + const appid = MiniProgramLoginSession.Presets.farm.appid; // QQ Classic Farm + const code = await MiniProgramLoginSession.getAuthCode(ticket, appid); + + res.json({ + success: true, + status: 'OK', + msg: '登录成功', + code: code // This is the Game Code we need + }); + } else { + res.json({ success: true, status: 'Error', msg: '状态查询错误' }); + } + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +// 注册 API +app.post('/api/auth/register', async (req, res) => { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: '请输入邮箱和密码' }); + } + + try { + if (userManager.getUser(email)) { + return res.status(400).json({ error: '该邮箱已被注册' }); + } + + const user = userManager.register(email, password); + log('Auth', `New user registered: ${email}`); + + res.json({ + success: true, + user: { + email: user.email, + hasCode: !!user.code + } + }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// 登录 API (仅登录) +app.post('/api/auth/login', async (req, res) => { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: '请输入邮箱和密码' }); + } + + try { + const user = userManager.login(email, password); + if (!user) { + return res.status(401).json({ error: '邮箱或密码错误' }); + } + + // 返回用户信息和 Bot 状态 + const botRunning = activeBots.has(email); + res.json({ + success: true, + user: { + email: user.email, + code: user.code, + hasCode: !!user.code + }, + botRunning + }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +app.get('/api/auth/profile', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const user = userManager.getUser(email); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json({ + user: { + email: user.email, + hasCode: !!user.code, + auth_provider: user.auth_provider || 'local' + } + }); +}); + +// 好友 API +app.get('/api/friends', async (req, res) => { + const { email } = req.query; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + + const bot = activeBots.get(email); + if (bot.config && bot.config.enableFriendOps === false) { + return res.status(403).json({ error: 'Friend operations disabled' }); + } + try { + const reply = await bot.friendManager.getAllFriends(); + + // 处理好友列表 + const friends = (reply.friends || []).map(f => ({ + uin: f.uin ? f.uin.toString() : '', + userName: f.userName, + headPic: f.headPic, + yellowLevel: f.yellowLevel, + exp: f.exp, + money: f.money + })); + + res.json({ friends }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// 仓库出售 API +app.post('/api/warehouse/sell', async (req, res) => { + const { email, items } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + + if (!items || !Array.isArray(items) || items.length === 0) { + return res.status(400).json({ error: 'No items to sell' }); + } + + const bot = activeBots.get(email); + try { + const reply = await bot.warehouseManager.sellItems(items); + const gold = bot.warehouseManager.extractGold(reply); + + // 更新本地金币缓存 + if (bot.user) { + bot.user.gold = (bot.user.gold || 0) + gold; + } + + res.json({ success: true, gold }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// 访问好友农场 API +app.post('/api/friends/visit', async (req, res) => { + const { email, friendUid } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + + const bot = activeBots.get(email); + if (bot.config && bot.config.enableFriendOps === false) { + return res.status(403).json({ error: 'Friend operations disabled' }); + } + try { + const reply = await bot.friendManager.enterFriendFarm(friendUid); + if (!reply.farm || !reply.farm.lands) { + return res.json({ lands: [] }); + } + + // 使用 FriendManager 的逻辑解析好友土地数据 + const analysis = bot.friendManager.analyzeFriendLands(lands, bot.user.gid); + + const formattedLands = lands.map(land => { + const id = Number(land.id); + if (!land.unlocked) return { id, type: 'locked' }; + + const plant = land.plant; + if (!plant || !plant.phases || plant.phases.length === 0) { + return { id, type: 'empty' }; + } + + const currentPhase = bot.farmManager.getCurrentPhase(plant.phases, false); + const phaseVal = currentPhase ? currentPhase.phase : 0; + + // 6=Mature, 7=Dead + let status = 'growing'; + if (phaseVal === 7) status = 'dead'; + else if (phaseVal === 6) status = 'mature'; + + return { + id, + type: 'planted', + status, + plantId: Number(plant.id), + plantName: getPlantNameBySeedId(Number(plant.id)) || `作物${plant.id}`, + phase: phaseVal, + needs: { + water: analysis.needWater.includes(id), + weed: analysis.needWeed.includes(id), + bug: analysis.needBug.includes(id) + }, + canSteal: analysis.stealable.includes(id) // 使用 analyzeFriendLands 的 stealable 结果 + }; + }); + + res.json({ lands: formattedLands, farmUser: reply.farm.user }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// 好友互动 API (偷菜/浇水等) +app.post('/api/friends/action', async (req, res) => { + const { email, friendUid, actionType, landIds } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + + const bot = activeBots.get(email); + if (bot.config && bot.config.enableFriendOps === false) { + return res.status(403).json({ error: 'Friend operations disabled' }); + } + if (actionType === 'steal' && bot.config && bot.config.enableSteal === false) { + return res.status(403).json({ error: 'Steal disabled' }); + } + try { + let result; + let message = ''; + + switch (actionType) { + case 'water': + result = await bot.friendManager.helpWater(friendUid, landIds); + message = '浇水成功'; + break; + case 'weed': + result = await bot.friendManager.helpWeed(friendUid, landIds); + message = '除草成功'; + break; + case 'insecticide': + result = await bot.friendManager.helpInsecticide(friendUid, landIds); + message = '除虫成功'; + break; + case 'steal': + result = await bot.friendManager.stealHarvest(friendUid, landIds); + message = '偷菜成功'; + break; + default: + throw new Error('Unknown action type'); + } + + res.json({ success: true, message, result }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// 启动 Bot API +app.post('/api/bot/start', async (req, res) => { + const { email, code, settings } = req.body; // 如果提供了 code,更新并启动 + + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + + const user = userManager.getUser(email); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + // 如果提供了新 code,更新用户数据 + let gameCode = code; + if (gameCode) { + userManager.updateCode(email, gameCode); + } else { + gameCode = user.code; + } + + if (!gameCode) { + return res.status(400).json({ error: 'Game code is required. Please provide it.' }); + } + + try { + // 如果已存在 Bot,先停止 + if (activeBots.has(email)) { + const oldBot = activeBots.get(email); + oldBot.stop(); + activeBots.delete(email); + } + + const botConfig = { code: gameCode, ...normalizeSettings(settings) }; + const bot = new FarmBot(botConfig); + + // 监听 Bot 事件并通过 Socket.IO 推送给前端 (使用 email 作为 room/key) + bot.on('started', () => { + io.emit(`bot-status-${email}`, { status: 'running' }); + }); + + bot.on('stopped', () => { + io.emit(`bot-status-${email}`, { status: 'stopped' }); + activeBots.delete(email); + }); + + bot.on('error', (err) => { + io.emit(`bot-error-${email}`, { message: err.message }); + userManager.saveBotLog(email, { tag: 'Error', msg: err.message, type: 'error', time: Date.now() }); + }); + + bot.on('log', (logData) => { + io.emit(`bot-log-${email}`, logData); + userManager.saveBotLog(email, logData); + }); + bot.on('stealRecord', (data) => { + const name = data && data.name; + const count = Number(data && data.count); + updateUnluckyBoard(email, name, count); + }); + + // 启动 Bot + await bot.start(); + activeBots.set(email, bot); + + res.json({ success: true, message: 'Bot started successfully.' }); + } catch (error) { + console.error('Bot start failed:', error); + res.status(500).json({ error: error.message }); + } +}); + +// 停止 Bot API +app.post('/api/bot/stop', (req, res) => { + const { email } = req.body; + if (activeBots.has(email)) { + const bot = activeBots.get(email); + bot.stop(); + activeBots.delete(email); + res.json({ success: true }); + } else { + res.status(404).json({ error: 'Bot is not running' }); + } +}); + +// 获取状态 API +app.get('/api/status', async (req, res) => { + const { email } = req.query; + if (!email || !activeBots.has(email)) { + // 返回 stopped 状态而不是 404,以便前端正确显示 UI + return res.json({ status: 'stopped', user: null }); + } + + const bot = activeBots.get(email); + const user = bot.user || {}; + const todayKey = getTodayKey(); + const freeMallClaimed = user.freeMallClaimDate === todayKey; + + // Calculate next level exp + const levelProgress = getLevelExpProgress(user.level || 0, user.exp || 0); + const tickets = user.tickets || 0; + const fertilizerContainer = Number(user.fertilizerContainer) || 0; + const fertilizerItems = user.fertilizerItems || {}; + const fallbackHours = fertilizerContainer >= 3600 ? fertilizerContainer / 3600 : fertilizerContainer; + const organicFertilizerContainer = Number(user.organicFertilizerContainer) || 0; + const organicFertilizerItems = user.organicFertilizerItems || {}; + const fallbackOrganicHours = organicFertilizerContainer >= 3600 ? organicFertilizerContainer / 3600 : organicFertilizerContainer; + let fertilizerHours = Number.isFinite(user.fertilizerHours) ? user.fertilizerHours : fallbackHours; + let organicFertilizerHours = Number.isFinite(user.organicFertilizerHours) ? user.organicFertilizerHours : fallbackOrganicHours; + if (bot.warehouseManager) { + try { + const bagReply = await bot.warehouseManager.getBag(); + const items = bot.warehouseManager.getBagItems(bagReply); + let normalContainer = 0; + let organicContainer = 0; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1011) normalContainer = count; + if (id === 1012) organicContainer = count; + } + const normalContainerHours = normalContainer >= 3600 ? normalContainer / 3600 : normalContainer; + const organicContainerHours = organicContainer >= 3600 ? organicContainer / 3600 : organicContainer; + const normalHours = normalContainerHours; + const organicHours = organicContainerHours; + fertilizerHours = normalHours; + user.normalFertilizerHours = normalHours; + user.organicFertilizerHours = organicHours; + organicFertilizerHours = organicHours; + } catch (error) { + log('状态', `获取背包失败: ${error.message}`); + } + } + + res.json({ + status: 'running', + user: { + nickname: user.name || 'Farmer', + level: user.level || 0, + exp: levelProgress.current || 0, + nextLevelExp: levelProgress.needed || 100, + gold: user.gold || 0, + tickets, + fertilizerHours, + normalFertilizerHours: Number.isFinite(user.normalFertilizerHours) ? user.normalFertilizerHours : fertilizerHours, + organicFertilizerHours, + avatarUrl: user.avatarUrl || '', + freeMallClaimed + } + }); +}); + +app.get('/api/announcement', (req, res) => { + const message = typeof process.env.LOGIN_ANNOUNCEMENT === 'string' ? process.env.LOGIN_ANNOUNCEMENT : ''; + res.json({ message }); +}); + +app.get('/api/logs', async (req, res) => { + const { email, limit } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const count = Number(limit); + try { + const logs = await userManager.getBotLogs(email, count); + res.json({ logs }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/debug/fertilizer', async (req, res) => { + const { email } = req.query; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + const bot = activeBots.get(email); + try { + const bagReply = await bot.warehouseManager.getBag(); + const items = bot.warehouseManager.getBagItems(bagReply); + let normalContainer = 0; + let normal1 = 0; + let normal4 = 0; + let normal8 = 0; + let normal12 = 0; + let organicContainer = 0; + let organic1 = 0; + let organic4 = 0; + let organic8 = 0; + let organic12 = 0; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1011) normalContainer = count; + if (id === 80001) normal1 = count; + if (id === 80002) normal4 = count; + if (id === 80003) normal8 = count; + if (id === 80004) normal12 = count; + if (id === 1012) organicContainer = count; + if (id === 80011) organic1 = count; + if (id === 80012) organic4 = count; + if (id === 80013) organic8 = count; + if (id === 80014) organic12 = count; + } + const normalContainerHours = normalContainer >= 3600 ? normalContainer / 3600 : normalContainer; + const organicContainerHours = organicContainer >= 3600 ? organicContainer / 3600 : organicContainer; + const normalHours = normalContainerHours + normal1 + normal4 * 4 + normal8 * 8 + normal12 * 12; + const organicHours = organicContainerHours + organic1 + organic4 * 4 + organic8 * 8 + organic12 * 12; + res.json({ + normal: { + container: normalContainer, + hour1: normal1, + hour4: normal4, + hour8: normal8, + hour12: normal12, + hours: normalHours + }, + organic: { + container: organicContainer, + hour1: organic1, + hour4: organic4, + hour8: organic8, + hour12: organic12, + hours: organicHours + } + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/leaderboard/unlucky', (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + if (activeBots.has(email)) { + const bot = activeBots.get(email); + if (bot.config && bot.config.enableFriendOps === false) { + return res.json({ items: [] }); + } + if (bot.config && bot.config.enableSteal === false) { + return res.json({ items: [] }); + } + } + const board = unluckyBoards.get(email); + if (!board) { + return res.json({ items: [] }); + } + const items = Array.from(board.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + res.json({ items }); +}); + +app.get('/api/tasks', async (req, res) => { + const { email } = req.query; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + + const bot = activeBots.get(email); + try { + const taskInfo = await fetchTaskInfo(bot); + const tasks = buildTaskList(taskInfo); + res.json({ tasks }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/tasks/claim', async (req, res) => { + const { email } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + + const bot = activeBots.get(email); + try { + const taskInfo = await fetchTaskInfo(bot); + const tasks = buildTaskList(taskInfo); + const claimable = []; + for (const task of tasks) { + if (isTaskClaimable(task)) { + claimable.push(task); + } + } + let claimedCount = 0; + const failed = []; + for (const task of claimable) { + try { + await claimTaskReward(bot, task.id, task.shareMultiple > 1); + claimedCount += 1; + } catch (err) { + failed.push({ id: task.id, message: err.message }); + } + } + + const latestTaskInfo = await fetchTaskInfo(bot); + const latestTasks = buildTaskList(latestTaskInfo); + res.json({ claimedCount, failed, tasks: latestTasks }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/tasks/claim-one', async (req, res) => { + const { email, taskId, doShared } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + const id = Number(taskId); + if (!Number.isFinite(id) || id <= 0) { + return res.status(400).json({ error: 'Invalid taskId' }); + } + + const bot = activeBots.get(email); + try { + await claimTaskReward(bot, id, Boolean(doShared)); + const latestTaskInfo = await fetchTaskInfo(bot); + const latestTasks = buildTaskList(latestTaskInfo); + res.json({ claimedCount: 1, failedCount: 0, tasks: latestTasks }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get lands API +app.get('/api/lands', async (req, res) => { + const { email } = req.query; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + + const bot = activeBots.get(email); + try { + const lands = await bot.farmManager.getFormattedLands(); + res.json({ lands }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/lands/remove', async (req, res) => { + const { email, landIds } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + if (!Array.isArray(landIds)) { + return res.status(400).json({ error: 'Invalid landIds' }); + } + const ids = landIds.map(Number).filter(id => Number.isFinite(id) && id > 0); + if (ids.length === 0) { + return res.status(400).json({ error: 'Invalid landIds' }); + } + + const bot = activeBots.get(email); + try { + await bot.farmManager.removePlant(ids); + res.json({ success: true, removed: ids.length }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/lands/unlock', async (req, res) => { + const { email, landId, doShared } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + const id = Number(landId); + if (!Number.isFinite(id) || id <= 0) { + return res.status(400).json({ error: 'Invalid landId' }); + } + + const bot = activeBots.get(email); + try { + await bot.farmManager.unlockLand(id, Boolean(doShared)); + res.json({ success: true, landId: id }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Shop API +app.get('/api/shop', async (req, res) => { + const { email } = req.query; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + const bot = activeBots.get(email); + try { + const goods = await bot.shopManager.getSeedShopList(); + res.json({ goods }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/shop/buy', async (req, res) => { + const { email, goodsId, count, price } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + const bot = activeBots.get(email); + try { + await bot.shopManager.buyGoods(goodsId, count, price); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/paymall/buy', async (req, res) => { + const { email, itemId, count } = req.body; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + const id = Number(itemId); + if (!Number.isFinite(id) || id <= 0) { + return res.status(400).json({ error: 'Invalid itemId' }); + } + const buyCount = Number(count ?? 1); + if (!Number.isFinite(buyCount) || buyCount <= 0) { + return res.status(400).json({ error: 'Invalid count' }); + } + const bot = activeBots.get(email); + try { + await bot.shopManager.purchaseMallItem(id, buyCount); + if (id === 1001) { + bot.user.freeMallClaimDate = getTodayKey(); + bot.emit('userUpdate', bot.user); + } + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Warehouse API +app.get('/api/warehouse', async (req, res) => { + const { email } = req.query; + if (!email || !activeBots.has(email)) { + return res.status(404).json({ error: 'Bot not running' }); + } + const bot = activeBots.get(email); + try { + const data = await bot.warehouseManager.getFormattedBag(); + res.json(data); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/warehouse/sell', async (req, res) => { + const { email, items } = req.body; + const bot = activeBots.get(email); + if (!bot) return res.status(404).json({ error: 'Bot not found' }); + + try { + const reply = await bot.warehouseManager.sellItems(items); + const gold = bot.warehouseManager.extractGold(reply); + res.json({ success: true, gold }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +app.post('/api/warehouse/use', async (req, res) => { + const { email, items } = req.body; + const bot = activeBots.get(email); + if (!bot) return res.status(404).json({ error: 'Bot not found' }); + + try { + const result = await bot.warehouseManager.useItems(items); + res.json(result); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// System stats API +app.get('/api/system/stats', (req, res) => { + const memory = process.memoryUsage(); + // Convert to MB + const memoryUsage = { + rss: Math.round(memory.rss / 1024 / 1024), + heapTotal: Math.round(memory.heapTotal / 1024 / 1024), + heapUsed: Math.round(memory.heapUsed / 1024 / 1024), + }; + + res.json({ + memory: memoryUsage, + uptime: Math.floor(process.uptime()), + activeBots: activeBots.size + }); +}); + +// 所有其他请求返回前端 index.html (SPA 支持) +app.get('*', (req, res) => { + if (process.env.NODE_ENV === 'production') { + res.sendFile(path.join(__dirname, '../web/dist/index.html')); + } else { + res.status(404).send('Not Found (Development Mode: Use Frontend Dev Server at port 5173)'); + } +}); + +// ============ Socket.IO ============ +io.on('connection', (socket) => { + log('Server', `Client connected: ${socket.id}`); + + socket.on('disconnect', () => { + log('Server', `Client disconnected: ${socket.id}`); + }); +}); + +// 启动服务器 +const PORT = process.env.PORT || 3000; +server.listen(PORT, () => { + log('Server', `Server running on http://localhost:${PORT}`); +}); diff --git a/211/server/package-lock.json b/211/server/package-lock.json new file mode 100644 index 0000000..122f462 --- /dev/null +++ b/211/server/package-lock.json @@ -0,0 +1,3343 @@ +{ + "name": "farm", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "farm", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.13.5", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^4.18.2", + "express-session": "^1.19.0", + "jsonwebtoken": "^9.0.3", + "long": "^5.3.2", + "mysql2": "^3.17.1", + "node-fetch": "^3.3.2", + "passport": "^0.7.0", + "passport-oauth2": "^1.8.0", + "protobufjs": "^8.0.0", + "socket.io": "^4.8.3", + "sqlite3": "^5.1.7", + "ws": "^8.19.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.2.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "license": "MIT", + "dependencies": { + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "~5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.1.tgz", + "integrity": "sha512-UzIzdVwPXPoZm+FaJ4lNsRt28HtUwt68gpLH7NP1oSjd91M5Qn1XJzbIsSRMRc5CV3pvktLNshmbaFfMYqPBhQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.3", + "named-placeholders": "^1.1.6", + "seq-queue": "^0.0.5", + "sql-escaper": "^1.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "license": "MIT" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-8.0.0.tgz", + "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.2.tgz", + "integrity": "sha512-lp+ZDVfSjHt+qAK1jXBTIXBNYnbo7gnaAGwoYTH9bE89kNkXwcu6g0WjJGRsdTKVpY1z70u3Y0IgmnBOoRybHw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/211/server/package.json b/211/server/package.json new file mode 100644 index 0000000..81c3cce --- /dev/null +++ b/211/server/package.json @@ -0,0 +1,30 @@ +{ + "name": "farm", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "axios": "^1.13.5", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^4.18.2", + "express-session": "^1.19.0", + "jsonwebtoken": "^9.0.3", + "long": "^5.3.2", + "mysql2": "^3.17.1", + "node-fetch": "^3.3.2", + "passport": "^0.7.0", + "passport-oauth2": "^1.8.0", + "protobufjs": "^8.0.0", + "socket.io": "^4.8.3", + "sqlite3": "^5.1.7", + "ws": "^8.19.0" + } +} diff --git a/211/server/proto/corepb.proto b/211/server/proto/corepb.proto new file mode 100644 index 0000000..2ec2b42 --- /dev/null +++ b/211/server/proto/corepb.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package corepb; + +// ============ 通用物品 ============ +message Item { + int64 id = 1; // 物品ID + int64 count = 2; // 数量 + int64 expire_time = 3; // 过期时间 + // field 4 reserved + // google.protobuf.Any detail = 5; // 详情 (略) + int64 uid = 6; // UID + bool is_new = 7; // 是否新获得 + repeated int64 mutant_types = 8; // 变异类型 + // ItemShow show = 100; // 展示信息 (略) +} + +// 背包(物品列表) +message ItemBag { + repeated Item items = 1; +} + +// ============ 物品变化 ============ +message ItemChg { + Item item = 1; // 物品信息 + int64 delta = 2; // 变化量 (正数增加, 负数减少) +} diff --git a/211/server/proto/friendpb.proto b/211/server/proto/friendpb.proto new file mode 100644 index 0000000..365e307 --- /dev/null +++ b/211/server/proto/friendpb.proto @@ -0,0 +1,120 @@ +syntax = "proto3"; + +package gamepb.friendpb; + +// ============ 好友农场摘要信息 ============ +message Plant { + int64 dry_time_sec = 1; // 缺水倒计时秒 + int64 weed_time_sec = 2; // 长草倒计时秒 + int64 insect_time_sec = 3; // 生虫倒计时秒 + int64 ripe_time_sec = 4; // 成熟倒计时秒 + int64 ripe_fruit_id = 5; // 成熟果实ID + int64 steal_plant_num = 6; // 可偷数量 + int64 dry_num = 7; // 缺水地块数 + int64 weed_num = 8; // 有草地块数 + int64 insect_num = 9; // 有虫地块数 +} + +// ============ 标签 ============ +message Tags { + bool is_new = 1; + bool is_follow = 2; +} + +// ============ 好友信息 ============ +message GameFriend { + int64 gid = 1; + string open_id = 2; + string name = 3; + string avatar_url = 4; + string remark = 5; + int64 level = 6; + int64 gold = 7; + Tags tags = 8; + Plant plant = 9; + int32 authorized_status = 10; + // field 11 reserved + // Illustrated illustrated = 12; // 略 + // repeated AvatarFrame equip_avatar_frames = 13; // 略 +} + +// ============ 请求/回复 ============ + +// --- 获取所有好友 --- +message GetAllRequest {} + +message GetAllReply { + repeated GameFriend game_friends = 1; + // repeated Invitation invitations = 2; // 略 + int64 application_count = 3; +} + +// --- 同步好友 (带 open_ids) --- +message SyncAllRequest { + // field 1 reserved + repeated string open_ids = 2; +} + +message SyncAllReply { + repeated GameFriend game_friends = 1; + // repeated Invitation invitations = 2; // 略 + int64 application_count = 3; +} + +// ============ 好友申请 (微信同玩) ============ + +// 申请信息 +message Application { + int64 gid = 1; + int64 time_at = 2; + string open_id = 3; + string name = 4; + string avatar_url = 5; + int64 level = 6; + // repeated AvatarFrame equip_avatar_frames = 7; // 略 +} + +// --- 获取好友申请列表 --- +message GetApplicationsRequest {} + +message GetApplicationsReply { + repeated Application applications = 1; + bool block_applications = 2; // 是否屏蔽申请 +} + +// --- 同意好友申请 --- +message AcceptFriendsRequest { + repeated int64 friend_gids = 1; +} + +message AcceptFriendsReply { + repeated GameFriend friends = 1; +} + +// --- 拒绝好友申请 --- +message RejectFriendsRequest { + repeated int64 friend_gids = 1; +} + +message RejectFriendsReply {} + +// --- 设置屏蔽申请 --- +message SetBlockApplicationsRequest { + bool block = 1; +} + +message SetBlockApplicationsReply { + bool block = 1; +} + +// ============ 服务器推送通知 ============ + +// 收到好友申请通知 +message FriendApplicationReceivedNotify { + repeated Application applications = 1; +} + +// 好友添加成功通知 (对方同意后) +message FriendAddedNotify { + repeated GameFriend friends = 1; +} diff --git a/211/server/proto/game.proto b/211/server/proto/game.proto new file mode 100644 index 0000000..a0e75fe --- /dev/null +++ b/211/server/proto/game.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +// ============ gatepb ============ +// 网关层协议 - 所有WS消息的外壳 + +package gatepb; + +// Meta.Type 枚举 +enum MessageType { + None = 0; + Request = 1; + Response = 2; + Notify = 3; +} + +// 消息元信息 +message Meta { + string service_name = 1; + string method_name = 2; + int32 message_type = 3; // MessageType enum + int64 client_seq = 4; + int64 server_seq = 5; + int64 error_code = 6; + string error_message = 7; + map metadata = 8; +} + +// 每个WS帧的结构 +message Message { + Meta meta = 1; + bytes body = 2; +} + +// 服务器推送事件 +message EventMessage { + string message_type = 1; + bytes body = 2; +} + +// 被踢下线通知 +message KickoutNotify { + int64 reason = 1; + string reason_message = 2; +} diff --git a/211/server/proto/itempb.proto b/211/server/proto/itempb.proto new file mode 100644 index 0000000..5446399 --- /dev/null +++ b/211/server/proto/itempb.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +package gamepb.itempb; + +import "corepb.proto"; + +// ============ 背包/仓库 ============ + +// 获取背包 +message BagRequest {} + +message BagReply { + corepb.ItemBag item_bag = 1; // 与 game 一致,背包数据在 item_bag 里 +} + +// ============ 出售物品 ============ + +message SellRequest { + repeated corepb.Item items = 1; // 要出售的物品列表 (id + count) +} + +message SellReply { + repeated corepb.Item sell_items = 1; // 出售的物品 + repeated corepb.Item get_items = 2; // 获得的物品(id=1001为金币, id=1002为点券) +} + +// ============ 使用物品 ============ + +message UseRequest { + int64 item_id = 1; + int64 count = 2; + repeated int64 land_ids = 3; +} + +message UseReply { + repeated corepb.Item items = 1; +} + +// ============ 批量使用物品 ============ + +message UseItem { + int64 item_id = 1; + int64 count = 2; + int64 land_count = 6; // 根据抓包,字段6为18(推测为土地数) +} + +message BatchUseRequest { + repeated UseItem items = 1; +} + +message BatchUseReply { + repeated corepb.Item items = 1; +} diff --git a/211/server/proto/notifypb.proto b/211/server/proto/notifypb.proto new file mode 100644 index 0000000..a23ceb6 --- /dev/null +++ b/211/server/proto/notifypb.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package gamepb.itempb; + +import "corepb.proto"; + +// ============ 物品变化通知 ============ +message ItemNotify { + repeated corepb.ItemChg items = 1; +} diff --git a/211/server/proto/plantpb.proto b/211/server/proto/plantpb.proto new file mode 100644 index 0000000..516fb72 --- /dev/null +++ b/211/server/proto/plantpb.proto @@ -0,0 +1,266 @@ +syntax = "proto3"; + +package gamepb.plantpb; + +// ============ 生长阶段枚举 ============ +enum PlantPhase { + PHASE_UNKNOWN = 0; + SEED = 1; // 种子 + GERMINATION = 2; // 发芽 + SMALL_LEAVES = 3; // 小叶 + LARGE_LEAVES = 4; // 大叶 + BLOOMING = 5; // 开花 + MATURE = 6; // 成熟 (可收获) + DEAD = 7; // 枯死 +} + +// ============ 土地信息 ============ +message LandInfo { + int64 id = 1; + bool unlocked = 2; + int64 level = 3; + int64 max_level = 4; + bool could_unlock = 5; + bool could_upgrade = 6; + LandUnlockCondition unlock_condition = 7; + LandUpgradeCondition upgrade_condition = 8; + Buff buff = 9; + PlantInfo plant = 10; + bool is_shared = 11; + bool can_share = 12; + int64 master_land_id = 13; + repeated int64 slave_land_ids = 14; + int64 land_size = 15; + int64 lands_level = 16; + + // 土地buff + message Buff { + int64 plant_yield_bonus = 1; + int64 planting_time_reduction = 2; + int64 plant_exp_bonus = 3; + } +} + +// 土地解锁条件 (简化) +message LandUnlockCondition { + int64 need_level = 1; + int64 need_gold = 2; + // ... 其他条件字段 +} + +// 土地升级条件 (简化) +message LandUpgradeCondition { + int64 need_level = 1; + int64 need_gold = 2; + // ... 其他条件字段 +} + +// ============ 植物信息 ============ +message PlantInfo { + int64 id = 1; // 植物/种子ID + string name = 2; // 名称 + // field 3 reserved + repeated PlantPhaseInfo phases = 4; // 生长阶段列表 + int64 season = 5; // 季节 + int64 dry_num = 6; // 缺水次数 (>0需要浇水) + // fields 7,8 reserved + int64 stole_num = 9; // 被偷次数 + int64 fruit_id = 10; // 果实ID + int64 fruit_num = 11; // 果实数量 + repeated int64 weed_owners = 12; // 放草的人 (非空=有草) + repeated int64 insect_owners = 13; // 放虫的人 (非空=有虫) + repeated int64 stealers = 14; // 偷菜的人 + int64 grow_sec = 15; // 生长秒数 + bool stealable = 16; // 是否可偷 + int64 left_inorc_fert_times = 17; // 剩余施肥次数 + int64 left_fruit_num = 18; // 剩余果实数 + int64 steal_intimacy_level = 19; // 偷菜亲密度等级 + repeated int64 mutant_config_ids = 20; // 变异配置ID + bool is_nudged = 21; // 是否被催熟 +} + +// ============ 生长阶段详情 ============ +message PlantPhaseInfo { + int32 phase = 1; // PlantPhase 枚举值 + int64 begin_time = 2; // 阶段开始时间 + int64 phase_id = 3; // 阶段ID + // fields 4,5 reserved + int64 dry_time = 6; // 变干时间 (>0 需要浇水) + int64 weeds_time = 7; // 长草时间 (>0 有杂草) + int64 insect_time = 8; // 生虫时间 (>0 有虫害) + map ferts_used = 9; // 已用肥料 + repeated MutantInfo mutants = 10; // 变异信息 +} + +// 变异信息 +message MutantInfo { + int64 mutant_time = 1; + int64 mutant_config_id = 2; + int64 weather_id = 3; +} + +// ============ 操作限制 ============ +// 每种操作的每日限制信息 +message OperationLimit { + int64 id = 1; // 操作类型ID + int64 day_times = 2; // 今日已操作次数 + int64 day_times_lt = 3; // 每日操作上限 + int64 day_share_id = 4; // 分享ID + int64 day_exp_times = 5; // 今日已获得经验次数 + int64 day_ex_times_lt = 6; // 每日可获得经验上限 + int64 day_exp_share_id = 7; // 经验分享ID +} +// 操作类型ID: +// 10001 = 帮好友浇水 +// 10002 = 帮好友除虫 +// 10003 = 帮好友除草 +// 10004 = 偷菜 +// 10005 = 放虫 +// 10006 = 放草 + +// ============ 请求/回复消息 ============ + +// --- 获取所有土地 --- +message AllLandsRequest { + int64 host_gid = 1; // 0或自己的GID=查看自己 +} + +message AllLandsReply { + repeated LandInfo lands = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 收获 --- +message HarvestRequest { + repeated int64 land_ids = 1; // 要收获的土地ID列表 + int64 host_gid = 2; // 农场主GID + bool is_all = 3; // 是否全部收获 +} + +message HarvestReply { + repeated LandInfo land = 1; + // repeated Item items = 2; // corepb.Item + // repeated Item lost_items = 3; + repeated OperationLimit operation_limits = 4; +} + +// --- 浇水 --- +message WaterLandRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message WaterLandReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 除草 --- +message WeedOutRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message WeedOutReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 除虫 --- +message InsecticideRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message InsecticideReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 播种 --- +message PlantItem { + int64 seed_id = 1; // 种子ID + repeated int64 land_ids = 2; // 要种植的土地ID列表 + bool auto_slave = 3; // 是否自动副产 +} + +message PlantRequest { + map land_and_seed = 1; // land_id -> seed_id (旧版) + repeated PlantItem items = 2; // 新版: 按种子分组种植 +} + +message PlantReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 移除植物 --- +message RemovePlantRequest { + repeated int64 land_ids = 1; +} + +message RemovePlantReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 施肥 --- +message FertilizeRequest { + repeated int64 land_ids = 1; + int64 fertilizer_id = 2; +} + +message FertilizeReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; + int64 fertilizer = 3; // 剩余肥料时间 +} + +// --- 升级土地 --- +message UpgradeLandRequest { + int64 land_id = 1; +} + +message UpgradeLandReply { + LandInfo land = 1; +} + +// --- 解锁土地 --- +message UnlockLandRequest { + int64 land_id = 1; + bool do_shared = 2; +} + +message UnlockLandReply { + LandInfo land = 1; +} + +// --- 放虫 --- +message PutInsectsRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message PutInsectsReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 放草 --- +message PutWeedsRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message PutWeedsReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// ============ 服务器推送通知 ============ + +// 土地状态变化通知 (被放虫/放草/偷菜等) +message LandsNotify { + repeated LandInfo lands = 1; // 变化的土地列表 + int64 host_gid = 2; // 农场主GID +} diff --git a/211/server/proto/shoppb.proto b/211/server/proto/shoppb.proto new file mode 100644 index 0000000..175bbb7 --- /dev/null +++ b/211/server/proto/shoppb.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; + +package gamepb.shoppb; + +import "corepb.proto"; + +// ============ 商店类型: 1=道具商店, 2=种子商店, 3=宠物商店 ============ + +// ============ 商店概览 ============ +message ShopProfile { + int64 shop_id = 1; // 商店ID + string shop_name = 2; // 商店名称 + int32 shop_type = 3; // 商店类型 +} + +// ============ 商品信息 ============ +message GoodsInfo { + int64 id = 1; // 商品ID (用于购买) + int64 bought_num = 2; // 已购买数量 + int64 price = 3; // 价格 (金币) + int64 limit_count = 4; // 限购数量 (0=不限购) + bool unlocked = 5; // 是否已解锁 + int64 item_id = 6; // 物品ID (即种子ID) + int64 item_count = 7; // 每次购买获得数量 + repeated Cond conds = 8; // 解锁条件 +} + +// ============ 解锁条件 ============ +enum CondType { + COND_TYPE_UNKNOWN = 0; + MIN_LEVEL = 1; // 最低等级要求 + UNLOCK_CARD = 2; // 需要解锁卡 +} + +message Cond { + int32 type = 1; // CondType + int64 param = 2; // 参数值 (如等级要求的等级数) +} + +// ============ 请求/回复 ============ + +// --- 获取商店列表 --- +message ShopProfilesRequest {} + +message ShopProfilesReply { + repeated ShopProfile shop_profiles = 1; +} + +// --- 获取商店商品 --- +message ShopInfoRequest { + int64 shop_id = 1; +} + +message ShopInfoReply { + repeated GoodsInfo goods_list = 1; +} + +// --- 购买商品 --- +message BuyGoodsRequest { + int64 goods_id = 1; // 商品ID (GoodsInfo.id) + int64 num = 2; // 购买数量 + int64 price = 3; // 单价 +} + +message BuyGoodsReply { + GoodsInfo goods = 1; // 更新后的商品信息 + repeated corepb.Item get_items = 2; // 获得的物品 + repeated corepb.Item cost_items = 3; // 消耗的物品 +} + +// --- 商品解锁推送 --- +message GoodsUnlockNotify { + repeated GoodsInfo goods_list = 1; +} diff --git a/211/server/proto/taskpb.proto b/211/server/proto/taskpb.proto new file mode 100644 index 0000000..e6d953b --- /dev/null +++ b/211/server/proto/taskpb.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package gamepb.taskpb; + +import "corepb.proto"; + +// ============ 任务类型 ============ +// 1 = 成长任务 (GROWTH_TASK) +// 2 = 每日任务 (DAILY_TASK) + +// ============ 单个任务 ============ +message Task { + int64 id = 1; // 任务ID + int64 progress = 2; // 当前进度 + bool is_claimed = 3; // 是否已领取 + bool is_unlocked = 4; // 是否已解锁 + repeated corepb.Item rewards = 5; // 奖励列表 + int64 total_progress = 6; // 总进度 + int64 share_multiple = 7; // 分享倍数 (>1 表示可分享翻倍) + repeated string params = 8; // 参数 + string desc = 9; // 描述 + int32 task_type = 10; // 任务类型 + int64 group = 11; // 分组 + int64 cond_type = 12; // 条件类型 + int64 is_show_text = 13; // 是否显示文本 +} + +// ============ 活跃度奖励 ============ +message Active { + int64 id = 1; + int32 status = 2; // 0=未完成, 1=已完成, 2=未完成 + repeated corepb.Item items = 3; +} + +// ============ 任务信息汇总 ============ +message TaskInfo { + repeated Task growth_tasks = 1; // 成长任务列表 + repeated Task daily_tasks = 2; // 每日任务列表 + repeated Task tasks = 3; // 其他任务 + repeated Active actives = 4; // 活跃度奖励 +} + +// ============ 请求/回复 ============ + +// --- 获取任务信息 --- +message TaskInfoRequest {} + +message TaskInfoReply { + TaskInfo task_info = 1; +} + +// --- 领取单个任务奖励 --- +message ClaimTaskRewardRequest { + int64 id = 1; // 任务ID + bool do_shared = 2; // 是否使用分享翻倍 (true=翻倍, false=普通领取) +} + +message ClaimTaskRewardReply { + repeated corepb.Item items = 1; // 获得的物品 + TaskInfo task_info = 2; // 更新后的任务信息 + repeated corepb.Item compensated_items = 3; // 补偿物品 +} + +// --- 批量领取任务奖励 --- +message BatchClaimTaskRewardRequest { + repeated int64 ids = 1; // 任务ID列表 + bool do_shared = 2; // 是否使用分享翻倍 +} + +message BatchClaimTaskRewardReply { + repeated corepb.Item items = 1; + TaskInfo task_info = 2; + repeated corepb.Item compensated_items = 3; +} + +// ============ 服务器推送 ============ + +// 任务状态变化通知 +message TaskInfoNotify { + TaskInfo task_info = 1; +} diff --git a/211/server/proto/userpb.proto b/211/server/proto/userpb.proto new file mode 100644 index 0000000..6b987aa --- /dev/null +++ b/211/server/proto/userpb.proto @@ -0,0 +1,123 @@ +syntax = "proto3"; + +package gamepb.userpb; + +// ============ 登录请求 ============ +message LoginRequest { + // 字段 1,2 保留未使用 + int64 sharer_id = 3; + string sharer_open_id = 4; + DeviceInfo device_info = 5; + int64 share_cfg_id = 6; + string scene_id = 7; + ReportData report_data = 8; +} + +// 设备信息 +message DeviceInfo { + string client_version = 1; // e.g. "1.6.0.8_20251224" + string sys_software = 2; // e.g. "Windows Unknown x64" + string sys_hardware = 3; + string telecom_oper = 4; + string network = 5; // e.g. "wifi" + int64 screen_width = 6; + int64 screen_height = 7; + float density = 8; + string cpu = 9; // e.g. "microsoft" + int64 memory = 10; + string gl_render = 11; + string gl_version = 12; + string device_id = 13; + string android_oaid = 14; + string ios_caid = 15; +} + +// 上报数据 +message ReportData { + string callback = 1; + string cd_extend_info = 2; + string click_id = 3; + string clue_token = 4; + string minigame_channel = 5; + int32 minigame_platid = 6; + string req_id = 7; + string trackid = 8; +} + +// ============ 登录回复 ============ +message LoginReply { + BasicInfo basic = 1; + // ItemBag item_bag = 2; // corepb.ItemBag, 复杂结构先跳过 + int64 time_now_millis = 3; + bool is_first_login = 4; + // GuideInfo guide_info = 5; + repeated QQGroupInfo qq_group_infos = 6; + // Illustrated illustrated = 7; + // repeated SystemUnlockItem unlocked_items = 8; + VersionInfo version_info = 9; + // MallMsg mall_msg = 10; + int64 qq_friend_recommend_authorized = 11; +} + +// 用户基本信息 +message BasicInfo { + int64 gid = 1; + string name = 2; + int64 level = 3; + int64 exp = 4; + int64 gold = 5; + string open_id = 6; + string avatar_url = 7; + string remark = 8; + string signature = 9; + int32 gender = 10; + // repeated AvatarFrame equip_avatar_frames = 11; + // map share_infos = 12; + int32 authorized_status = 13; + bool disable_nudge = 14; +} + +// QQ群信息 +message QQGroupInfo { + string qq_group_id = 1; + string qq_group_name = 2; +} + +// 版本信息 +message VersionInfo { + int32 status = 1; + string version_recommend = 2; + string version_force = 3; + string res_version = 4; +} + +// ============ 心跳 ============ +message HeartbeatRequest { + int64 gid = 1; + string client_version = 2; +} + +message HeartbeatReply { + int64 server_time = 1; + VersionInfo version_info = 2; +} + +// ============ 上报分享点击 ============ +// 用于已登录状态下处理分享链接(触发好友申请) +message ReportArkClickRequest { + int64 sharer_id = 1; + string sharer_open_id = 2; + string scene_id = 3; + int64 share_cfg_id = 4; +} + +message ReportArkClickReply { + // 通常为空响应 +} + +// ============ 服务器推送通知 ============ + +// 基本信息变化通知 (升级/金币变化等) +message BasicNotify { + BasicInfo basic = 1; +} \ No newline at end of file diff --git a/211/server/proto/visitpb.proto b/211/server/proto/visitpb.proto new file mode 100644 index 0000000..ffc074a --- /dev/null +++ b/211/server/proto/visitpb.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package gamepb.visitpb; + +import "plantpb.proto"; +import "userpb.proto"; + +// ============ 访问好友农场服务 ============ +// service VisitService { +// rpc Enter(EnterRequest) returns (EnterReply); +// rpc Leave(LeaveRequest) returns (LeaveReply); +// } + +// ============ 进入原因枚举 ============ +enum EnterReason { + ENTER_REASON_UNKNOWN = 0; + ENTER_REASON_BUBBLE = 1; + ENTER_REASON_FRIEND = 2; + ENTER_REASON_INTERACT = 3; +} + +// ============ 进入好友农场 ============ +message EnterRequest { + int64 host_gid = 1; // 好友的GID + int32 reason = 2; // EnterReason 进入原因 +} + +message EnterReply { + gamepb.userpb.BasicInfo basic = 1; // 好友基本信息 + repeated gamepb.plantpb.LandInfo lands = 2; // 好友的所有土地 + // field 3: brief_dog_info (不需要) + // field 4: nudge_info (不需要) +} + +// ============ 离开好友农场 ============ +message LeaveRequest { + int64 host_gid = 1; // 好友的GID +} + +message LeaveReply { + // 空消息 +} diff --git a/211/server/share.txt b/211/server/share.txt new file mode 100644 index 0000000..e69de29 diff --git a/211/server/src/config.js b/211/server/src/config.js new file mode 100644 index 0000000..6ba01be --- /dev/null +++ b/211/server/src/config.js @@ -0,0 +1,53 @@ +/** + * 配置常量与枚举定义 + */ + +const CONFIG = { + serverUrl: 'wss://gate-obt.nqf.qq.com/prod/ws', + clientVersion: '1.6.0.14_20251224', + platform: 'qq', // 平台: qq 或 wx (可通过 --wx 切换为微信) + os: 'iOS', + heartbeatInterval: 25000, // 心跳间隔 25秒 + farmCheckInterval: 1000, // 自己农场巡查完成后等待间隔 (可通过 --interval 修改, 最低1秒) + friendCheckInterval: 10000, // 好友巡查完成后等待间隔 (可通过 --friend-interval 修改, 最低1秒) + forceLowestLevelCrop: false, // 开启后固定种最低等级作物(通常是白萝卜),跳过经验效率分析 + enableNormalFertilize: false, + enableOrganicFertilize: false, + allowTicketFertilizerPurchase: false, + enableFriendOps: true, + enableSteal: true, + enableAutoSell: true, + allowBuySeeds: true, + allowRemove: true, + idleStrategy: 'exp', +}; + +// 运行期提示文案(做了简单编码,避免明文散落) +const RUNTIME_HINT_MASK = 23; +const RUNTIME_HINT_DATA = [ + 12295, 22759, 26137, 12294, 26427, 39022, 30457, 24343, 28295, 20826, + 36142, 65307, 20018, 31126, 20485, 21313, 12309, 35808, 20185, 20859, + 24343, 20164, 24196, 20826, 36142, 33696, 21441, 12309, +]; + +// 生长阶段枚举 +const PlantPhase = { + UNKNOWN: 0, + SEED: 1, + GERMINATION: 2, + SMALL_LEAVES: 3, + LARGE_LEAVES: 4, + BLOOMING: 5, + MATURE: 6, + DEAD: 7, +}; + +const PHASE_NAMES = ['未知', '种子', '发芽', '小叶', '大叶', '开花', '成熟', '枯死']; + +module.exports = { + CONFIG, + PlantPhase, + PHASE_NAMES, + RUNTIME_HINT_MASK, + RUNTIME_HINT_DATA, +}; diff --git a/211/server/src/core/FarmBot.js b/211/server/src/core/FarmBot.js new file mode 100644 index 0000000..5a30577 --- /dev/null +++ b/211/server/src/core/FarmBot.js @@ -0,0 +1,186 @@ +const EventEmitter = require('events'); +const { NetworkClient } = require('./Network'); +const { FarmManager } = require('./FarmManager'); +const { FriendManager } = require('./FriendManager'); +const { WarehouseManager } = require('./WarehouseManager'); +const { ShopManager } = require('./ShopManager'); +const { log, logWarn, emitRuntimeHint } = require('../utils'); +const { CONFIG } = require('../config'); + +class FarmBot extends EventEmitter { + constructor(config = {}) { + super(); + // Merge provided config with default CONFIG + // Deep copy to avoid modifying global CONFIG if passed by reference (though usually it's shallow copy) + this.config = { ...CONFIG, ...config }; + + // Initialize components + this.network = new NetworkClient(this); + this.farmManager = new FarmManager(this); + this.friendManager = new FriendManager(this); + this.warehouseManager = new WarehouseManager(this); + this.shopManager = new ShopManager(this); + this.dailyClaimTimer = null; + + // State + this.isRunning = false; + this.user = { + gid: 0, + name: 'Loading...', + level: 0, + gold: 0, + exp: 0, + tickets: 0, + fertilizer: 0, + freeMallClaimDate: '' + }; + + // Error handling + this.network.on('error', (err) => { + log('机器人', `网络错误: ${err.message}`); + this.emit('error', err); + }); + + this.network.on('disconnected', () => { + if (this.isRunning) { + log('机器人', '连接断开,正在尝试重新连接...'); + // Reconnection logic is handled in NetworkClient, but we can monitor it here + } + }); + } + + async start() { + if (this.isRunning) return; + this.isRunning = true; + + this.log('系统', `正在启动农场小助手... 平台: ${this.config.platform}`); + emitRuntimeHint(true); + + try { + // 1. Connect + this.log('系统', '正在连接服务器...'); + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('连接超时')), 10000); + this.network.connect((success) => { + clearTimeout(timeout); + if (success) resolve(); + else reject(new Error('连接失败')); + }); + }); + + // 2. Login + this.log('系统', '正在登录...'); + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('登录超时')), 10000); + this.network.sendLogin((success) => { + clearTimeout(timeout); + if (success) resolve(); + else reject(new Error('登录失败')); + }); + }); + + // 3. Start Loops + this.log('系统', '开始自动化作业...'); + this.farmManager.startLoop(); + if (this.config.enableFriendOps !== false) { + this.friendManager.startLoop(); + } + if (this.config.enableAutoSell !== false) { + this.warehouseManager.startLoop(); + } + this.startDailyClaimLoop(); + + this.emit('started'); + this.log('系统', '农场小助手启动成功!'); + + } catch (error) { + this.log('系统', `启动失败: ${error.message}`); + this.stop(); + throw error; + } + } + + stop() { + this.isRunning = false; + this.farmManager.stopLoop(); + this.friendManager.stopLoop(); + this.warehouseManager.stopLoop(); + this.stopDailyClaimLoop(); + + // Stop network + if (this.network.ws) { + this.network.ws.close(); + } + + this.emit('stopped'); + log('系统', '农场小助手已停止'); + } + + log(tag, msg) { + log(tag, msg); + this.emit('log', { + tag, + msg, + time: Date.now() + }); + } + + logWarn(tag, msg) { + logWarn(tag, msg); + this.emit('log', { + tag, + msg, + type: 'warn', + time: Date.now() + }); + } + + getTodayKey() { + const now = new Date(); + const y = now.getFullYear(); + const m = `${now.getMonth() + 1}`.padStart(2, '0'); + const d = `${now.getDate()}`.padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + async runDailyClaims() { + if (!this.isRunning) return; + await this.network.tryClaimShareReward(1); + const todayKey = this.getTodayKey(); + if (this.user.freeMallClaimDate === todayKey) return; + try { + await this.shopManager.purchaseMallItem(1001, 1); + this.user.freeMallClaimDate = todayKey; + this.emit('userUpdate', this.user); + this.log('商城', '已领取每日免费礼包'); + } catch (e) { + this.logWarn('商城', `领取失败: ${e.message}`); + } + } + + startDailyClaimLoop() { + if (this.dailyClaimTimer) return; + const scheduleNext = () => { + if (!this.isRunning) return; + const now = new Date(); + const next = new Date(now); + next.setHours(24, 0, 0, 0); + const delay = Math.max(1000, next.getTime() - now.getTime()); + this.dailyClaimTimer = setTimeout(async () => { + this.dailyClaimTimer = null; + await this.runDailyClaims(); + scheduleNext(); + }, delay); + }; + scheduleNext(); + } + + stopDailyClaimLoop() { + if (this.dailyClaimTimer) { + clearTimeout(this.dailyClaimTimer); + this.dailyClaimTimer = null; + } + } +} + +module.exports = { FarmBot }; diff --git a/211/server/src/core/FarmManager.js b/211/server/src/core/FarmManager.js new file mode 100644 index 0000000..6b5bd34 --- /dev/null +++ b/211/server/src/core/FarmManager.js @@ -0,0 +1,1089 @@ +const protobuf = require('protobufjs'); +const { PlantPhase, PHASE_NAMES } = require('../config'); +const { types } = require('../proto'); +const { toLong, toNum, getServerTimeSec, toTimeSec, sleep } = require('../utils'); +const { getPlantNameBySeedId, getPlantName, getPlantExp, formatGrowTime, getPlantGrowTime, getSeedIdByPlantName, getPlantByName } = require('../gameConfig'); +const { getPlantingRecommendation } = require('../../tools/calc-exp-yield'); + +const PLANT_COOLDOWN_MS = 5000; +const PLANT_ALREADY_COOLDOWN_MS = 30000; +const PLANT_PENDING_MS = 15000; +const FERT_PENDING_MS = 15000; + +class FarmManager { + constructor(bot) { + this.bot = bot; + + // Internal state + this.isCheckingFarm = false; + this.isFirstFarmCheck = true; + this.farmCheckTimer = null; + this.farmLoopRunning = false; + this.taskExecuting = false; + this.taskCache = null; + this.taskCacheAt = 0; + this.lastTaskActionAt = 0; + this.plantNextAllowed = new Map(); + this.pendingPlantUntil = new Map(); + this.pendingFertilizeUntil = new Map(); + + // Listen for lands changed push + this.bot.on('landsChanged', this.onLandsChangedPush.bind(this)); + this.bot.on('taskInfoNotify', (taskInfo) => { + this.taskCache = taskInfo || null; + this.taskCacheAt = Date.now(); + }); + } + + // ============ Farm API ============ + + async getAllLands() { + const body = types.AllLandsRequest.encode(types.AllLandsRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'AllLands', body); + const reply = types.AllLandsReply.decode(replyBody); + + // Emit operation limits update (for FriendManager) + if (reply.operation_limits) { + this.bot.emit('operationLimits', reply.operation_limits); + } + return reply; + } + + async harvest(landIds) { + const body = types.HarvestRequest.encode(types.HarvestRequest.create({ + land_ids: landIds, + host_gid: toLong(this.bot.user.gid), + is_all: true, + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body); + return types.HarvestReply.decode(replyBody); + } + + async waterLand(landIds) { + const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({ + land_ids: landIds, + host_gid: toLong(this.bot.user.gid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body); + return types.WaterLandReply.decode(replyBody); + } + + async weedOut(landIds) { + const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({ + land_ids: landIds, + host_gid: toLong(this.bot.user.gid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body); + return types.WeedOutReply.decode(replyBody); + } + + async insecticide(landIds) { + const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({ + land_ids: landIds, + host_gid: toLong(this.bot.user.gid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body); + return types.InsecticideReply.decode(replyBody); + } + + async fertilize(landIds, fertilizerId = 1011) { + let successCount = 0; + for (const landId of landIds) { + try { + const body = types.FertilizeRequest.encode(types.FertilizeRequest.create({ + land_ids: [toLong(landId)], + fertilizer_id: toLong(fertilizerId), + })).finish(); + await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Fertilize', body); + successCount++; + this.pendingFertilizeUntil.set(landId, Date.now() + FERT_PENDING_MS); + } catch (e) { + break; + } + if (landIds.length > 1) await sleep(50); + } + return successCount; + } + + async removePlant(landIds) { + const body = types.RemovePlantRequest.encode(types.RemovePlantRequest.create({ + land_ids: landIds.map(id => toLong(id)), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'RemovePlant', body); + return types.RemovePlantReply.decode(replyBody); + } + + async unlockLand(landId, doShared = false) { + const body = types.UnlockLandRequest.encode(types.UnlockLandRequest.create({ + land_id: toLong(landId), + do_shared: Boolean(doShared), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'UnlockLand', body); + return types.UnlockLandReply.decode(replyBody); + } + + // ============ Shop API ============ + + async getShopProfiles() { + const body = types.ShopProfilesRequest.encode(types.ShopProfilesRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopProfiles', body); + return types.ShopProfilesReply.decode(replyBody); + } + + async getShopInfo(shopId) { + const body = types.ShopInfoRequest.encode(types.ShopInfoRequest.create({ + shop_id: toLong(shopId), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopInfo', body); + return types.ShopInfoReply.decode(replyBody); + } + + async buyGoods(goodsId, num, price) { + const body = types.BuyGoodsRequest.encode(types.BuyGoodsRequest.create({ + goods_id: toLong(goodsId), + num: toLong(num), + price: toLong(price), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'BuyGoods', body); + return types.BuyGoodsReply.decode(replyBody); + } + + async buyNormalFertilizer(count) { + if (!Number.isFinite(count) || count <= 0) return 0; + const fertilizerItemId = 1011; + let propsShopId = null; + try { + const profilesReply = await this.getShopProfiles(); + const profiles = profilesReply.shop_profiles || []; + const propsShop = profiles.find(profile => toNum(profile.shop_type) === 1); + if (propsShop) { + propsShopId = toNum(propsShop.shop_id); + } + } catch (e) { + this.bot.logWarn('商店', `获取商店列表失败: ${e.message}`); + return 0; + } + + if (!propsShopId) { + this.bot.logWarn('商店', '未找到道具商店'); + return 0; + } + + let goods = null; + try { + const shopReply = await this.getShopInfo(propsShopId); + const goodsList = shopReply.goods_list || []; + goods = goodsList.find(item => toNum(item.item_id) === fertilizerItemId && item.unlocked); + } catch (e) { + this.bot.logWarn('商店', `获取道具商店商品失败: ${e.message}`); + return 0; + } + + if (!goods) { + this.bot.logWarn('商店', '普通化肥不可购买'); + return 0; + } + + const limitCount = toNum(goods.limit_count); + const boughtNum = toNum(goods.bought_num); + let buyCount = count; + if (limitCount > 0) { + const remaining = Math.max(0, limitCount - boughtNum); + buyCount = Math.min(buyCount, remaining); + } + if (buyCount <= 0) return 0; + + try { + const buyReply = await this.buyGoods(toNum(goods.id), buyCount, toNum(goods.price)); + const items = buyReply.get_items || []; + let gotCount = 0; + for (const item of items) { + if (toNum(item.id) === fertilizerItemId) { + gotCount += toNum(item.count); + } + } + return gotCount; + } catch (e) { + this.bot.logWarn('购买', `购买普通化肥失败: ${e.message}`); + return 0; + } + } + + async fetchTaskInfo() { + const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body); + const reply = types.TaskInfoReply.decode(replyBody); + return reply.task_info || null; + } + + async getTaskInfoCached(maxAgeMs = 15000) { + const now = Date.now(); + if (this.taskCache && now - this.taskCacheAt < maxAgeMs) return this.taskCache; + const taskInfo = await this.fetchTaskInfo(); + if (taskInfo) { + this.taskCache = taskInfo; + this.taskCacheAt = now; + } + return taskInfo; + } + + buildTaskList(taskInfo) { + const tasks = []; + if (!taskInfo) return tasks; + const addTasks = (list) => { + if (!list) return; + for (const task of list) { + tasks.push({ + id: toNum(task.id), + desc: task.desc || '', + progress: toNum(task.progress), + totalProgress: toNum(task.total_progress), + isClaimed: Boolean(task.is_claimed), + isUnlocked: Boolean(task.is_unlocked), + }); + } + }; + addTasks(taskInfo.growth_tasks); + addTasks(taskInfo.daily_tasks); + addTasks(taskInfo.tasks); + return tasks; + } + + parseTaskDesc(desc) { + if (!desc) return null; + let match = desc.match(/购买(\d+)个(.+?)种子/); + if (match) { + return { type: 'buySeed', count: Number(match[1]) || 0, name: match[2].trim() }; + } + match = desc.match(/种植(\d+)个(.+)/); + if (match) { + return { type: 'plantSeed', count: Number(match[1]) || 0, name: match[2].trim() }; + } + match = desc.match(/收获(\d+)个(.+)/); + if (match) { + return { type: 'harvest', count: Number(match[1]) || 0, name: match[2].trim() }; + } + return null; + } + + pickTaskByType(tasks, type) { + for (const task of tasks) { + if (!task.isUnlocked || task.isClaimed) continue; + const parsed = this.parseTaskDesc(task.desc); + if (!parsed || parsed.type !== type) continue; + const remaining = task.totalProgress > 0 ? Math.max(0, task.totalProgress - task.progress) : parsed.count; + if (remaining <= 0) continue; + return { ...parsed, remaining }; + } + return null; + } + + async findSeedGoods(seedId) { + const seedShopId = 2; + const shopReply = await this.getShopInfo(seedShopId); + const goodsList = shopReply.goods_list || []; + const goods = goodsList.find(item => toNum(item.item_id) === seedId && item.unlocked); + if (!goods) return null; + return { + seedId, + goodsId: toNum(goods.id), + price: toNum(goods.price), + limitCount: toNum(goods.limit_count), + boughtNum: toNum(goods.bought_num), + }; + } + + async executeBuySeedTask(task) { + if (!task || !task.name) return false; + if (this.bot.config.allowBuySeeds === false) { + this.bot.log('任务', '已关闭自动购买种子,跳过任务购买'); + return false; + } + const seedId = getSeedIdByPlantName(task.name); + if (!seedId) return false; + const seedGoods = await this.findSeedGoods(seedId); + if (!seedGoods) { + this.bot.logWarn('任务', `未找到种子: ${task.name}`); + return false; + } + let buyCount = task.remaining || task.count || 0; + if (buyCount <= 0) return false; + if (seedGoods.limitCount > 0) { + const remainingLimit = Math.max(0, seedGoods.limitCount - seedGoods.boughtNum); + buyCount = Math.min(buyCount, remainingLimit); + } + const affordable = Math.floor(this.bot.user.gold / seedGoods.price); + buyCount = Math.min(buyCount, affordable); + if (buyCount <= 0) return false; + try { + const buyReply = await this.buyGoods(seedGoods.goodsId, buyCount, seedGoods.price); + if (buyReply.cost_items) { + for (const item of buyReply.cost_items) { + this.bot.user.gold -= toNum(item.count); + } + } + this.bot.log('任务', `购买种子: ${task.name} x${buyCount}`); + return true; + } catch (e) { + this.bot.logWarn('任务', `购买失败: ${e.message}`); + return false; + } + } + + // ============ Planting Logic ============ + + encodePlantRequest(seedId, landIds) { + const writer = protobuf.Writer.create(); + const itemWriter = writer.uint32(18).fork(); + itemWriter.uint32(8).int64(seedId); + const idsWriter = itemWriter.uint32(18).fork(); + for (const id of landIds) { + idsWriter.int64(id); + } + idsWriter.ldelim(); + itemWriter.ldelim(); + return writer.finish(); + } + + async plantSeeds(seedId, landIds) { + let successCount = 0; + const now = Date.now(); + for (const landId of landIds) { + const nextAllowed = this.plantNextAllowed.get(landId) || 0; + if (now < nextAllowed) continue; + this.plantNextAllowed.set(landId, now + PLANT_COOLDOWN_MS); + try { + const body = this.encodePlantRequest(seedId, [landId]); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Plant', body); + types.PlantReply.decode(replyBody); + successCount++; + this.pendingPlantUntil.set(landId, now + PLANT_PENDING_MS); + } catch (e) { + const msg = String(e && e.message ? e.message : e); + if (msg.includes('1001008') || msg.includes('土地已种植')) { + this.plantNextAllowed.set(landId, now + PLANT_ALREADY_COOLDOWN_MS); + this.pendingPlantUntil.set(landId, now + PLANT_ALREADY_COOLDOWN_MS); + this.bot.log('种植', `土地#${landId} 已种植,跳过`); + } else { + this.bot.logWarn('种植', `土地#${landId} 失败: ${msg}`); + } + } + if (landIds.length > 1) await sleep(50); + } + return successCount; + } + + async findBestSeed(landsCount, useFertilizerOverride) { + const SEED_SHOP_ID = 2; + const shopReply = await this.getShopInfo(SEED_SHOP_ID); + if (!shopReply.goods_list || shopReply.goods_list.length === 0) { + this.bot.logWarn('商店', '种子商店暂无商品'); + return null; + } + + const available = []; + for (const goods of shopReply.goods_list) { + if (!goods.unlocked) continue; + + let meetsConditions = true; + let requiredLevel = 0; + const conds = goods.conds || []; + for (const cond of conds) { + if (toNum(cond.type) === 1) { + requiredLevel = toNum(cond.param); + if (this.bot.user.level < requiredLevel) { + meetsConditions = false; + break; + } + } + } + if (!meetsConditions) continue; + + const limitCount = toNum(goods.limit_count); + const boughtNum = toNum(goods.bought_num); + if (limitCount > 0 && boughtNum >= limitCount) continue; + + available.push({ + goods, + goodsId: toNum(goods.id), + seedId: toNum(goods.item_id), + price: toNum(goods.price), + requiredLevel, + }); + } + + if (available.length === 0) { + this.bot.logWarn('商店', '没有找到可购买的种子'); + return null; + } + + if (this.bot.config.forceLowestLevelCrop) { + available.sort((a, b) => a.requiredLevel - b.requiredLevel || a.price - b.price); + return available[0]; + } + + try { + const rec = getPlantingRecommendation(this.bot.user.level, landsCount == null ? 18 : landsCount, { top: 50 }); + const useFertilizer = typeof useFertilizerOverride === 'boolean' + ? useFertilizerOverride + : (this.bot.config.enableNormalFertilize || this.bot.config.enableOrganicFertilize); + let candidates = useFertilizer ? rec.candidatesNormalFert : rec.candidatesNoFert; + if (!candidates) candidates = rec.candidatesNoFert; // fallback + + let rankedSeedIds = candidates.map(x => x.seedId); + + if (this.bot.user.level > 30) { + const minLevel = Math.max(1, this.bot.user.level - 20); + const filtered = rec.candidatesNoFert.filter(x => x.requiredLevel >= minLevel); + if (filtered.length > 0) { + rankedSeedIds = filtered.map(x => x.seedId); + this.bot.log('商店', `高等级策略生效 (Lv${this.bot.user.level}),仅考虑 Lv${minLevel}+ 作物`); + } + } + for (const seedId of rankedSeedIds) { + const hit = available.find(x => x.seedId === seedId); + if (hit) return hit; + } + } catch (e) { + this.bot.logWarn('商店', `经验效率推荐失败: ${e.message}`); + } + + if(this.bot.user.level && this.bot.user.level <= 28){ + available.sort((a, b) => a.requiredLevel - b.requiredLevel); + }else{ + available.sort((a, b) => b.requiredLevel - a.requiredLevel); + } + return available[0]; + } + + async autoPlantEmptyLands(deadLandIds, emptyLandIds, unlockedLandCount, preferredSeedId = 0, preferredCount = 0) { + let landsToPlant = emptyLandIds.length > 0 ? [...emptyLandIds] : []; + + if (deadLandIds.length > 0) { + if (this.bot.config.allowRemove === false) { + this.bot.log('铲除', '已关闭自动铲除,跳过枯萎土地'); + } else { + try { + await this.removePlant(deadLandIds); + this.bot.log('铲除', `已铲除 ${deadLandIds.length} 块土地 (${deadLandIds.join(',')})`); + landsToPlant.push(...deadLandIds); + } catch (e) { + this.bot.logWarn('铲除', `批量铲除失败: ${e.message}`); + landsToPlant.push(...deadLandIds); + } + } + } + + if (landsToPlant.length === 0) return 0; + if (landsToPlant.length > 1) { + landsToPlant = Array.from(new Set(landsToPlant)); + } + if (landsToPlant.length > 0) { + const now = Date.now(); + const filtered = []; + for (const landId of landsToPlant) { + const nextAllowed = this.plantNextAllowed.get(landId) || 0; + if (now >= nextAllowed) { + filtered.push(landId); + } + } + landsToPlant = filtered; + } + if (landsToPlant.length === 0) return 0; + if (preferredCount > 0 && landsToPlant.length > preferredCount) { + landsToPlant = landsToPlant.slice(0, preferredCount); + } + + let bagItems = null; + try { + const bagReply = await this.bot.warehouseManager.getBag(); + bagItems = this.bot.warehouseManager.getBagItems(bagReply); + } catch (e) { + this.bot.logWarn('背包', `获取背包失败: ${e.message}`); + } + + const enableNormal = this.bot.config.enableNormalFertilize === true; + const enableOrganic = this.bot.config.enableOrganicFertilize === true; + let normalCount = 0; + let organicCount = 0; + let fertilizerInventoryEmpty = false; + if (bagItems) { + for (const item of bagItems) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1011) normalCount = count; + if (id === 1012) organicCount = count; + } + if ((enableNormal || enableOrganic) && normalCount <= 0 && organicCount <= 0) { + fertilizerInventoryEmpty = true; + this.bot.log('施肥', '化肥容器为空,切换为无化肥策略'); + } + } + const useFertilizer = (enableNormal || enableOrganic) && !fertilizerInventoryEmpty; + + let targetSeedGoods = null; + let targetSeedId = preferredSeedId; + let targetSeedName = preferredSeedId ? getPlantNameBySeedId(preferredSeedId) : ''; + if (!preferredSeedId) { + try { + targetSeedGoods = await this.findBestSeed(unlockedLandCount, useFertilizer); + } catch (e) { + this.bot.logWarn('商店', `查询失败: ${e.message}`); + return 0; + } + if (!targetSeedGoods) return 0; + targetSeedId = targetSeedGoods.seedId; + targetSeedName = getPlantNameBySeedId(targetSeedId); + } else { + try { + targetSeedGoods = await this.findSeedGoods(preferredSeedId); + } catch (e) { + this.bot.logWarn('商店', `查询失败: ${e.message}`); + } + } + + let selectedSeed = null; + if (bagItems && targetSeedId) { + for (const item of bagItems) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id !== targetSeedId || count <= 0) continue; + selectedSeed = { id, count, name: targetSeedName || getPlantNameBySeedId(id) }; + break; + } + } + + let actualSeedId = 0; + let seedName = ''; + if (selectedSeed) { + actualSeedId = selectedSeed.id; + seedName = selectedSeed.name; + if (landsToPlant.length > selectedSeed.count) { + landsToPlant = landsToPlant.slice(0, selectedSeed.count); + } + this.bot.log('种植', `优先使用背包种子: ${seedName} x${landsToPlant.length}`); + } else if (this.bot.config.allowBuySeeds === false) { + if (targetSeedName) { + this.bot.log('种植', `已关闭购买种子,背包无 ${targetSeedName} 种子`); + } else { + this.bot.log('种植', '已关闭购买种子,背包无可用种子'); + } + return 0; + } else { + if (!targetSeedGoods) { + try { + targetSeedGoods = await this.findSeedGoods(targetSeedId); + } catch (e) { + this.bot.logWarn('商店', `查询失败: ${e.message}`); + return 0; + } + } + if (!targetSeedGoods) return 0; + + seedName = targetSeedName || getPlantNameBySeedId(targetSeedGoods.seedId); + const growTime = getPlantGrowTime(1020000 + (targetSeedGoods.seedId - 20000)); + const growTimeStr = growTime > 0 ? ` 生长${formatGrowTime(growTime)}` : ''; + this.bot.log('商店', `最佳作物: ${seedName} (${targetSeedGoods.seedId}) 价格: ${targetSeedGoods.price}金币${growTimeStr}`); + + const needCount = landsToPlant.length; + const totalCost = targetSeedGoods.price * needCount; + if (totalCost > this.bot.user.gold) { + const canBuy = Math.floor(this.bot.user.gold / targetSeedGoods.price); + if (canBuy <= 0) return 0; + landsToPlant = landsToPlant.slice(0, canBuy); + this.bot.log('商店', `金币不足,本次仅种植 ${canBuy} 块土地`); + } + + actualSeedId = targetSeedGoods.seedId; + try { + const buyReply = await this.buyGoods(targetSeedGoods.goodsId, landsToPlant.length, targetSeedGoods.price); + if (buyReply.get_items && buyReply.get_items.length > 0) { + const gotItem = buyReply.get_items[0]; + const gotId = toNum(gotItem.id); + if (gotId > 0) actualSeedId = gotId; + } + if (buyReply.cost_items) { + for (const item of buyReply.cost_items) { + this.bot.user.gold -= toNum(item.count); + } + } + } catch (e) { + this.bot.logWarn('购买', e.message); + return 0; + } + } + + let plantedLands = []; + try { + const planted = await this.plantSeeds(actualSeedId, landsToPlant); + this.bot.log('种植', `成功种植 ${planted} 块土地`); + if (planted > 0) { + plantedLands = landsToPlant.slice(0, planted); + } + } catch (e) { + this.bot.logWarn('种植', e.message); + } + + if (plantedLands.length > 0) { + if (!enableNormal && !enableOrganic) { + return plantedLands.length; + } + if (fertilizerInventoryEmpty) { + return plantedLands.length; + } + + let items = bagItems; + if (!items) { + try { + const bagReply = await this.bot.warehouseManager.getBag(); + items = this.bot.warehouseManager.getBagItems(bagReply); + } catch (e) { + this.bot.logWarn('施肥', `检查背包失败: ${e.message}`); + } + } + + if (!items) { + if (enableNormal) normalCount = plantedLands.length; + if (enableOrganic) organicCount = plantedLands.length; + } else if (!bagItems) { + normalCount = 0; + organicCount = 0; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1011) normalCount = count; + if (id === 1012) organicCount = count; + } + } + + let remainingLands = [...plantedLands]; + if (enableOrganic && organicCount > 0) { + const organicTargets = remainingLands.slice(0, organicCount); + const fertilized = await this.fertilize(organicTargets, 1012); + if (fertilized > 0) { + this.bot.log('施肥', `有机化肥施肥 ${fertilized} 块土地`); + } + remainingLands = remainingLands.slice(organicTargets.length); + } + + if (remainingLands.length > 0 && enableNormal) { + if (normalCount < remainingLands.length && this.bot.config.allowTicketFertilizerPurchase) { + const needBuy = remainingLands.length - normalCount; + const bought = await this.buyNormalFertilizer(needBuy); + if (bought > 0) { + normalCount += bought; + this.bot.log('施肥', `自动购买普通化肥 ${bought} 个`); + } + } + if (normalCount < remainingLands.length) { + this.bot.log('施肥', `普通化肥不足 (库存: ${normalCount}, 需要: ${remainingLands.length}),仅施肥部分土地`); + remainingLands = remainingLands.slice(0, normalCount); + } + if (remainingLands.length > 0) { + const fertilized = await this.fertilize(remainingLands, 1011); + if (fertilized > 0) { + this.bot.log('施肥', `普通化肥施肥 ${fertilized} 块土地`); + } + } + } + } + + return plantedLands.length; + } + + // ============ Analysis ============ + + getCurrentPhase(phases, debug, landLabel) { + if (!phases || phases.length === 0) return null; + const nowSec = getServerTimeSec(); + + for (let i = phases.length - 1; i >= 0; i--) { + const beginTime = toTimeSec(phases[i].begin_time); + if (beginTime > 0 && beginTime <= nowSec) { + return phases[i]; + } + } + return phases[0]; + } + + analyzeLands(lands) { + const result = { + harvestable: [], needWater: [], needWeed: [], needBug: [], + growing: [], empty: [], dead: [], + harvestableInfo: [], + }; + + const nowSec = getServerTimeSec(); + + for (const land of lands) { + const id = toNum(land.id); + if (!land.unlocked) continue; + + const plant = land.plant; + if (!plant) { + const pendingUntil = this.pendingPlantUntil.get(id) || 0; + if (pendingUntil > Date.now()) { + result.growing.push(id); + } else { + result.empty.push(id); + } + continue; + } + if (!plant.phases || plant.phases.length === 0) { + const plantId = toNum(plant.id); + if (plantId > 0 || plant.name) { + result.growing.push(id); + } else { + const pendingUntil = this.pendingPlantUntil.get(id) || 0; + if (pendingUntil > Date.now()) { + result.growing.push(id); + } else { + result.empty.push(id); + } + } + continue; + } + + const currentPhase = this.getCurrentPhase(plant.phases, false, `土地#${id}`); + if (!currentPhase) { + result.empty.push(id); + continue; + } + const phaseVal = currentPhase.phase; + + if (phaseVal === PlantPhase.DEAD) { + result.dead.push(id); + continue; + } + + if (phaseVal === PlantPhase.MATURE) { + result.harvestable.push(id); + const plantId = toNum(plant.id); + result.harvestableInfo.push({ + landId: id, + plantId, + name: getPlantName(plantId), + exp: getPlantExp(plantId), + }); + continue; + } + + const dryNum = toNum(plant.dry_num); + const dryTime = toTimeSec(currentPhase.dry_time); + if (dryNum > 0 || (dryTime > 0 && dryTime <= nowSec)) { + result.needWater.push(id); + } + + const weedsTime = toTimeSec(currentPhase.weeds_time); + const hasWeeds = (plant.weed_owners && plant.weed_owners.length > 0) || (weedsTime > 0 && weedsTime <= nowSec); + if (hasWeeds) { + result.needWeed.push(id); + } + + const insectTime = toTimeSec(currentPhase.insect_time); + const hasBugs = (plant.insect_owners && plant.insect_owners.length > 0) || (insectTime > 0 && insectTime <= nowSec); + if (hasBugs) { + result.needBug.push(id); + } + + result.growing.push(id); + } + + return result; + } + + // ============ Loop ============ + + async getFormattedLands() { + try { + const landsReply = await this.getAllLands(); + if (!landsReply || !landsReply.lands) return []; + + const lands = landsReply.lands; + const analysis = this.analyzeLands(lands); + + return lands.map(land => { + const id = toNum(land.id); + if (!land.unlocked) { + const unlockCondition = land.unlock_condition || {}; + return { + id, + type: 'locked', + couldUnlock: !!land.could_unlock, + unlockCondition: { + needLevel: toNum(unlockCondition.need_level || 0), + needGold: toNum(unlockCondition.need_gold || 0), + } + }; + } + + const plant = land.plant; + if (!plant) { + const pendingUntil = this.pendingPlantUntil.get(id) || 0; + if (pendingUntil > Date.now()) { + return { + id, + type: 'planted', + status: 'growing', + plantId: 0, + plantName: '', + phase: 0, + phaseName: PHASE_NAMES[0], + fertilized: false, + needs: { water: false, weed: false, bug: false } + }; + } + return { id, type: 'empty' }; + } + if (!plant.phases || plant.phases.length === 0) { + const plantId = toNum(plant.id); + const plantName = plant.name || getPlantName(plantId); + if (plantId > 0 || plantName) { + return { + id, + type: 'planted', + status: 'growing', + plantId, + plantName, + phase: 0, + phaseName: PHASE_NAMES[0], + fertilized: false, + needs: { water: false, weed: false, bug: false } + }; + } + const pendingUntil = this.pendingPlantUntil.get(id) || 0; + if (pendingUntil > Date.now()) { + return { + id, + type: 'planted', + status: 'growing', + plantId: 0, + plantName: '', + phase: 0, + phaseName: PHASE_NAMES[0], + fertilized: false, + needs: { water: false, weed: false, bug: false } + }; + } + return { id, type: 'empty' }; + } + + const currentPhase = this.getCurrentPhase(plant.phases, false); + const phaseVal = currentPhase ? currentPhase.phase : 0; + const serverFertsUsed = currentPhase && currentPhase.ferts_used && Object.keys(currentPhase.ferts_used).length > 0; + const pendingFert = (this.pendingFertilizeUntil.get(id) || 0) > Date.now(); + const fertilized = !!(serverFertsUsed || pendingFert); + + let status = 'growing'; + if (phaseVal === PlantPhase.DEAD) status = 'dead'; + else if (phaseVal === PlantPhase.MATURE) status = 'mature'; + + const phaseName = PHASE_NAMES[phaseVal] || PHASE_NAMES[0]; + return { + id, + type: 'planted', + status, + plantId: toNum(plant.id), + plantName: getPlantName(toNum(plant.id)), + phase: phaseVal, + phaseName, + fertilized, + needs: { + water: analysis.needWater.includes(id), + weed: analysis.needWeed.includes(id), + bug: analysis.needBug.includes(id) + } + }; + }); + } catch (e) { + this.bot.logWarn('API', `Failed to get lands: ${e.message}`); + return []; + } + } + + async check() { + if (this.isCheckingFarm || !this.bot.user.gid) return; + this.isCheckingFarm = true; + + try { + const now = Date.now(); + const canUseTask = now - this.lastTaskActionAt >= 12000; + let tasks = []; + let buyTask = null; + let plantTask = null; + let harvestTask = null; + + if (canUseTask) { + try { + const taskInfo = await this.getTaskInfoCached(); + tasks = this.buildTaskList(taskInfo); + buyTask = this.pickTaskByType(tasks, 'buySeed'); + plantTask = this.pickTaskByType(tasks, 'plantSeed'); + harvestTask = this.pickTaskByType(tasks, 'harvest'); + } catch (e) { } + } + + if (canUseTask && buyTask && !this.taskExecuting) { + this.taskExecuting = true; + let bought = false; + try { + bought = await this.executeBuySeedTask(buyTask); + } finally { + this.taskExecuting = false; + } + if (bought) { + this.lastTaskActionAt = Date.now(); + return; + } + } + + const landsReply = await this.getAllLands(); + if (!landsReply.lands || landsReply.lands.length === 0) { + return; + } + + const lands = landsReply.lands; + const status = this.analyzeLands(lands); + const unlockedLandCount = lands.filter(land => land && land.unlocked).length; + this.isFirstFarmCheck = false; + + if (canUseTask && harvestTask) { + const plant = getPlantByName(harvestTask.name); + if (plant) { + const targetIds = status.harvestableInfo + .filter(info => info.plantId === plant.id) + .map(info => info.landId); + if (targetIds.length > 0) { + try { + await this.harvest(targetIds); + this.bot.log('任务', `收获 ${harvestTask.name} ${targetIds.length} 块`); + this.lastTaskActionAt = Date.now(); + return; + } catch (e) { + this.bot.logWarn('任务', `收获失败: ${e.message}`); + } + } + } + } + + const statusParts = []; + if (status.harvestable.length) statusParts.push(`待收获 ${status.harvestable.length} 块`); + if (status.needWeed.length) statusParts.push(`需除草 ${status.needWeed.length} 块`); + if (status.needBug.length) statusParts.push(`需除虫 ${status.needBug.length} 块`); + if (status.needWater.length) statusParts.push(`需浇水 ${status.needWater.length} 块`); + if (status.dead.length) statusParts.push(`枯萎 ${status.dead.length} 块`); + if (status.empty.length) statusParts.push(`空闲 ${status.empty.length} 块`); + if (status.growing.length) statusParts.push(`生长中 ${status.growing.length} 块`); + + const hasWork = status.harvestable.length || status.needWeed.length || status.needBug.length + || status.needWater.length || status.dead.length || status.empty.length; + + const actions = []; + const batchOps = []; + + if (status.needWeed.length > 0) { + batchOps.push(this.weedOut(status.needWeed).then(() => actions.push(`除草 ${status.needWeed.length} 块`)).catch(e => this.bot.logWarn('除草', e.message))); + } + if (status.needBug.length > 0) { + batchOps.push(this.insecticide(status.needBug).then(() => actions.push(`除虫 ${status.needBug.length} 块`)).catch(e => this.bot.logWarn('除虫', e.message))); + } + if (status.needWater.length > 0) { + batchOps.push(this.waterLand(status.needWater).then(() => actions.push(`浇水 ${status.needWater.length} 块`)).catch(e => this.bot.logWarn('浇水', e.message))); + } + if (batchOps.length > 0) await Promise.all(batchOps); + + let harvestedLandIds = []; + if (status.harvestable.length > 0) { + try { + await this.harvest(status.harvestable); + actions.push(`收获 ${status.harvestable.length} 块`); + harvestedLandIds = [...status.harvestable]; + } catch (e) { this.bot.logWarn('收获', e.message); } + } + + const allDeadLands = [...status.dead]; + const allEmptyLands = [...status.empty, ...harvestedLandIds]; + if (canUseTask && plantTask && this.bot.config.idleStrategy === 'task') { + const seedId = getSeedIdByPlantName(plantTask.name); + if (seedId && allEmptyLands.length > 0) { + try { + const planted = await this.autoPlantEmptyLands([], allEmptyLands, unlockedLandCount, seedId, plantTask.remaining || plantTask.count); + if (planted > 0) { + this.bot.log('任务', `种植 ${plantTask.name} ${planted} 块`); + this.lastTaskActionAt = Date.now(); + return; + } + } catch (e) { + this.bot.logWarn('任务', `种植失败: ${e.message}`); + } + } + } + if (canUseTask && plantTask && this.bot.config.idleStrategy !== 'task') { + const seedId = getSeedIdByPlantName(plantTask.name); + if (seedId && (allDeadLands.length > 0 || allEmptyLands.length > 0)) { + try { + const planted = await this.autoPlantEmptyLands(allDeadLands, allEmptyLands, unlockedLandCount, seedId, plantTask.remaining || plantTask.count); + if (planted > 0) { + this.bot.log('任务', `种植 ${plantTask.name} ${planted} 块`); + this.lastTaskActionAt = Date.now(); + return; + } + } catch (e) { + this.bot.logWarn('任务', `种植失败: ${e.message}`); + } + } + } + if (allDeadLands.length > 0 || allEmptyLands.length > 0) { + try { + await this.autoPlantEmptyLands(allDeadLands, allEmptyLands, unlockedLandCount); + actions.push(`种植 ${allDeadLands.length + allEmptyLands.length} 块`); + } catch (e) { this.bot.logWarn('种植', e.message); } + } + + if(hasWork) { + this.bot.log('农场', `土地状态: ${statusParts.join(',')}${actions.length > 0 ? '。正在执行: ' + actions.join(',') : ''}`); + } + } catch (err) { + this.bot.logWarn('巡田', `检查失败: ${err.message}`); + } finally { + this.isCheckingFarm = false; + } + } + + async loop() { + while (this.farmLoopRunning) { + await this.check(); + if (!this.farmLoopRunning) break; + await sleep(this.bot.config.farmCheckInterval); + } + } + + startLoop() { + if (this.farmLoopRunning) return; + this.farmLoopRunning = true; + this.farmCheckTimer = setTimeout(() => this.loop(), 2000); + } + + stopLoop() { + this.farmLoopRunning = false; + if (this.farmCheckTimer) { clearTimeout(this.farmCheckTimer); this.farmCheckTimer = null; } + } + + lastPushTime = 0; + onLandsChangedPush(lands) { + if (this.isCheckingFarm) return; + const now = Date.now(); + if (now - this.lastPushTime < 500) return; + + this.lastPushTime = now; + this.bot.log('农场', `收到推送: ${lands.length}块土地变化,检查中...`); + + setTimeout(async () => { + if (!this.isCheckingFarm) { + await this.check(); + } + }, 100); + } +} + +module.exports = { FarmManager }; diff --git a/211/server/src/core/FriendManager.js b/211/server/src/core/FriendManager.js new file mode 100644 index 0000000..5bf3cdf --- /dev/null +++ b/211/server/src/core/FriendManager.js @@ -0,0 +1,497 @@ +const { PlantPhase } = require('../config'); +const { types } = require('../proto'); +const { toLong, toNum, sleep, getServerTimeSec, toTimeSec } = require('../utils'); +const { getPlantName } = require('../gameConfig'); + +const OP_NAMES = { + 10001: '收获', 10002: '铲除', 10003: '放草', 10004: '放虫', + 10005: '除草', 10006: '除虫', 10007: '浇水', 10008: '偷菜', +}; + +class FriendManager { + constructor(bot) { + this.bot = bot; + + // Internal state + this.isCheckingFriends = false; + this.isFirstFriendCheck = true; + this.friendCheckTimer = null; + this.friendLoopRunning = false; + this.lastResetDate = ''; + this.operationLimits = new Map(); + + // Listen for events + this.bot.on('operationLimits', this.updateOperationLimits.bind(this)); + this.bot.on('friendApplicationReceived', this.onFriendApplicationReceived.bind(this)); + } + + // ============ Helper Methods ============ + + checkDailyReset() { + const today = new Date().toISOString().slice(0, 10); + if (this.lastResetDate !== today) { + if (this.lastResetDate !== '') { + this.bot.log('系统', '跨日重置,已清空操作限制缓存'); + } + this.operationLimits.clear(); + this.lastResetDate = today; + } + } + + updateOperationLimits(limits) { + if (!limits || limits.length === 0) return; + this.checkDailyReset(); + for (const limit of limits) { + const id = toNum(limit.id); + if (id > 0) { + this.operationLimits.set(id, { + dayTimes: toNum(limit.day_times), + dayTimesLimit: toNum(limit.day_times_lt), + dayExpTimes: toNum(limit.day_exp_times), + dayExpTimesLimit: toNum(limit.day_ex_times_lt), + }); + } + } + } + + canGetExp(opId) { + const limit = this.operationLimits.get(opId); + if (!limit) return false; + if (limit.dayExpTimesLimit <= 0) return true; + return limit.dayExpTimes < limit.dayExpTimesLimit; + } + + canOperate(opId) { + const limit = this.operationLimits.get(opId); + if (!limit) return true; + if (limit.dayTimesLimit <= 0) return true; + return limit.dayTimes < limit.dayTimesLimit; + } + + getRemainingTimes(opId) { + const limit = this.operationLimits.get(opId); + if (!limit || limit.dayTimesLimit <= 0) return 999; + return Math.max(0, limit.dayTimesLimit - limit.dayTimes); + } + + // ============ Friend API ============ + + async getAllFriends() { + const body = types.GetAllFriendsRequest.encode(types.GetAllFriendsRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'GetAll', body); + return types.GetAllFriendsReply.decode(replyBody); + } + + async getApplications() { + const body = types.GetApplicationsRequest.encode(types.GetApplicationsRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'GetApplications', body); + return types.GetApplicationsReply.decode(replyBody); + } + + async acceptFriends(gids) { + const body = types.AcceptFriendsRequest.encode(types.AcceptFriendsRequest.create({ + friend_gids: gids.map(g => toLong(g)), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'AcceptFriends', body); + return types.AcceptFriendsReply.decode(replyBody); + } + + async enterFriendFarm(friendGid) { + const body = types.VisitEnterRequest.encode(types.VisitEnterRequest.create({ + host_gid: toLong(friendGid), + reason: 2, + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.visitpb.VisitService', 'Enter', body); + return types.VisitEnterReply.decode(replyBody); + } + + async leaveFriendFarm(friendGid) { + const body = types.VisitLeaveRequest.encode(types.VisitLeaveRequest.create({ + host_gid: toLong(friendGid), + })).finish(); + try { + await this.bot.network.sendMsgAsync('gamepb.visitpb.VisitService', 'Leave', body); + } catch (e) { } + } + + async helpWater(friendGid, landIds) { + const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body); + const reply = types.WaterLandReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async helpWeed(friendGid, landIds) { + const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body); + const reply = types.WeedOutReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async helpInsecticide(friendGid, landIds) { + const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body); + const reply = types.InsecticideReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async stealHarvest(friendGid, landIds) { + const body = types.HarvestRequest.encode(types.HarvestRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + is_all: true, + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body); + const reply = types.HarvestReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async putInsects(friendGid, landIds) { + const body = types.PutInsectsRequest.encode(types.PutInsectsRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'PutInsects', body); + const reply = types.PutInsectsReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async putWeeds(friendGid, landIds) { + const body = types.PutWeedsRequest.encode(types.PutWeedsRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'PutWeeds', body); + const reply = types.PutWeedsReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + // ============ Logic ============ + + getCurrentPhase(phases) { + if (!phases || phases.length === 0) return null; + const nowSec = getServerTimeSec(); + for (let i = phases.length - 1; i >= 0; i--) { + const beginTime = toTimeSec(phases[i].begin_time); + if (beginTime > 0 && beginTime <= nowSec) { + return phases[i]; + } + } + return phases[0]; + } + + analyzeFriendLands(lands, myGid) { + const result = { + stealable: [], stealableInfo: [], + needWater: [], needWeed: [], needBug: [], + canPutWeed: [], canPutBug: [], + }; + + const nowSec = getServerTimeSec(); + + for (const land of lands) { + const id = toNum(land.id); + const plant = land.plant; + + if (!plant || !plant.phases || plant.phases.length === 0) continue; + + const currentPhase = this.getCurrentPhase(plant.phases); + if (!currentPhase) continue; + const phaseVal = currentPhase.phase; + + if (phaseVal === PlantPhase.MATURE) { + if (plant.stealable) { + result.stealable.push(id); + const plantId = toNum(plant.id); + const plantName = getPlantName(plantId) || plant.name || '未知'; + result.stealableInfo.push({ landId: id, plantId, name: plantName }); + } + continue; + } + + if (phaseVal === PlantPhase.DEAD) continue; + + const dryNum = toNum(plant.dry_num); + const dryTime = toTimeSec(currentPhase.dry_time); + if (dryNum > 0 || (dryTime > 0 && dryTime <= nowSec)) result.needWater.push(id); + + const weedsTime = toTimeSec(currentPhase.weeds_time); + const hasWeeds = (plant.weed_owners && plant.weed_owners.length > 0) || (weedsTime > 0 && weedsTime <= nowSec); + if (hasWeeds) result.needWeed.push(id); + + const insectTime = toTimeSec(currentPhase.insect_time); + const hasBugs = (plant.insect_owners && plant.insect_owners.length > 0) || (insectTime > 0 && insectTime <= nowSec); + if (hasBugs) result.needBug.push(id); + + const weedOwners = plant.weed_owners || []; + const insectOwners = plant.insect_owners || []; + const iAlreadyPutWeed = weedOwners.some(gid => toNum(gid) === myGid); + const iAlreadyPutBug = insectOwners.some(gid => toNum(gid) === myGid); + + if (weedOwners.length < 2 && !iAlreadyPutWeed) result.canPutWeed.push(id); + if (insectOwners.length < 2 && !iAlreadyPutBug) result.canPutBug.push(id); + } + return result; + } + + async visitFriend(friend, totalActions, myGid) { + const { gid, name } = friend; + + let enterReply; + try { + enterReply = await this.enterFriendFarm(gid); + } catch (e) { + this.bot.logWarn('好友', `进入 ${name} 农场失败: ${e.message}`); + return; + } + + const lands = enterReply.lands || []; + if (lands.length === 0) { + await this.leaveFriendFarm(gid); + return; + } + + const status = this.analyzeFriendLands(lands, myGid); + const actions = []; + const HELP_ONLY_WITH_EXP = true; + // Note: Logic copied from original friend.js + + if (status.needWeed.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10005); + if (shouldHelp) { + let ok = 0; + for (const landId of status.needWeed) { + try { await this.helpWeed(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`除草 ${ok} 块`); totalActions.weed += ok; } + } + } + + if (status.needBug.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10006); + if (shouldHelp) { + let ok = 0; + for (const landId of status.needBug) { + try { await this.helpInsecticide(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`除虫 ${ok} 块`); totalActions.bug += ok; } + } + } + + if (status.needWater.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10007); + if (shouldHelp) { + let ok = 0; + for (const landId of status.needWater) { + try { await this.helpWater(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`浇水 ${ok} 块`); totalActions.water += ok; } + } + } + + const canSteal = !(this.bot.config && this.bot.config.enableSteal === false); + if (canSteal && status.stealable.length > 0) { + let ok = 0; + const stolenPlants = []; + for (let i = 0; i < status.stealable.length; i++) { + const landId = status.stealable[i]; + try { + await this.stealHarvest(gid, [landId]); + ok++; + if (status.stealableInfo[i]) { + stolenPlants.push(status.stealableInfo[i].name); + } + } catch (e) { } + await sleep(100); + } + if (ok > 0) { + const plantNames = [...new Set(stolenPlants)].join('、'); + actions.push(`偷取 ${ok} 块${plantNames ? ' (' + plantNames + ')' : ''}`); + totalActions.steal += ok; + this.bot.emit('stealRecord', { name, count: ok }); + } + } + + const ENABLE_PUT_BAD_THINGS = false; // Copied from original + + if (ENABLE_PUT_BAD_THINGS && status.canPutBug.length > 0 && this.canOperate(10004)) { + let ok = 0; + const remaining = this.getRemainingTimes(10004); + const toProcess = status.canPutBug.slice(0, remaining); + for (const landId of toProcess) { + if (!this.canOperate(10004)) break; + try { await this.putInsects(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`放虫 ${ok} 块`); totalActions.putBug += ok; } + } + + if (ENABLE_PUT_BAD_THINGS && status.canPutWeed.length > 0 && this.canOperate(10003)) { + let ok = 0; + const remaining = this.getRemainingTimes(10003); + const toProcess = status.canPutWeed.slice(0, remaining); + for (const landId of toProcess) { + if (!this.canOperate(10003)) break; + try { await this.putWeeds(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`放草 ${ok} 块`); totalActions.putWeed += ok; } + } + + if (actions.length > 0) { + this.bot.log('好友', `[${name}] 执行: ${actions.join(',')}`); + } + + await this.leaveFriendFarm(gid); + } + + async check() { + if (this.isCheckingFriends || !this.bot.user.gid) return; + if (this.bot.config && this.bot.config.enableFriendOps === false) return; + this.isCheckingFriends = true; + this.checkDailyReset(); + + try { + const friendsReply = await this.getAllFriends(); + const friends = friendsReply.game_friends || []; + if (friends.length === 0) { this.bot.log('好友', '没有好友'); return; } + + const canPutBugOrWeed = this.canOperate(10004) || this.canOperate(10003); + const ENABLE_PUT_BAD_THINGS = false; + + const priorityFriends = []; + const otherFriends = []; + const visitedGids = new Set(); + + for (const f of friends) { + const gid = toNum(f.gid); + if (gid === this.bot.user.gid) continue; + if (visitedGids.has(gid)) continue; + const name = f.remark || f.name || `GID:${gid}`; + const p = f.plant; + + const stealNum = p ? toNum(p.steal_plant_num) : 0; + const dryNum = p ? toNum(p.dry_num) : 0; + const weedNum = p ? toNum(p.weed_num) : 0; + const insectNum = p ? toNum(p.insect_num) : 0; + + if (stealNum > 0 || dryNum > 0 || weedNum > 0 || insectNum > 0) { + priorityFriends.push({ gid, name }); + visitedGids.add(gid); + } else if (ENABLE_PUT_BAD_THINGS && canPutBugOrWeed) { + otherFriends.push({ gid, name }); + visitedGids.add(gid); + } + } + + const friendsToVisit = [...priorityFriends, ...otherFriends]; + if (friendsToVisit.length === 0) return; + + let totalActions = { steal: 0, water: 0, weed: 0, bug: 0, putBug: 0, putWeed: 0 }; + for (const friend of friendsToVisit) { + try { + await this.visitFriend(friend, totalActions, this.bot.user.gid); + } catch (e) { } + await sleep(500); + } + + const summary = []; + if (totalActions.steal > 0) summary.push(`偷取 ${totalActions.steal} 块`); + if (totalActions.weed > 0) summary.push(`除草 ${totalActions.weed} 块`); + if (totalActions.bug > 0) summary.push(`除虫 ${totalActions.bug} 块`); + if (totalActions.water > 0) summary.push(`浇水 ${totalActions.water} 块`); + if (totalActions.putBug > 0) summary.push(`放虫 ${totalActions.putBug} 块`); + if (totalActions.putWeed > 0) summary.push(`放草 ${totalActions.putWeed} 块`); + + if (summary.length > 0) { + this.bot.log('好友', `已巡查 ${friendsToVisit.length} 位好友。总计: ${summary.join(',')}`); + } + this.isFirstFriendCheck = false; + } catch (err) { + this.bot.logWarn('好友', `巡查失败: ${err.message}`); + } finally { + this.isCheckingFriends = false; + } + } + + async loop() { + while (this.friendLoopRunning) { + await this.check(); + if (!this.friendLoopRunning) break; + await sleep(this.bot.config.friendCheckInterval); + } + } + + startLoop() { + if (this.friendLoopRunning) return; + if (this.bot.config && this.bot.config.enableFriendOps === false) return; + this.friendLoopRunning = true; + + this.friendCheckTimer = setTimeout(() => this.loop(), 5000); + setTimeout(() => this.checkAndAcceptApplications(), 3000); + } + + stopLoop() { + this.friendLoopRunning = false; + if (this.friendCheckTimer) { clearTimeout(this.friendCheckTimer); this.friendCheckTimer = null; } + } + + // ============ Applications ============ + + onFriendApplicationReceived(applications) { + if (this.bot.config && this.bot.config.enableFriendOps === false) return; + const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', '); + this.bot.log('申请', `收到 ${applications.length} 个好友申请: ${names}`); + const gids = applications.map(a => toNum(a.gid)); + this.acceptFriendsWithRetry(gids); + } + + async checkAndAcceptApplications() { + if (this.bot.config && this.bot.config.enableFriendOps === false) return; + try { + const reply = await this.getApplications(); + const applications = reply.applications || []; + if (applications.length === 0) return; + + const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', '); + this.bot.log('申请', `发现 ${applications.length} 个待处理申请: ${names}`); + + const gids = applications.map(a => toNum(a.gid)); + await this.acceptFriendsWithRetry(gids); + } catch (e) { } + } + + async acceptFriendsWithRetry(gids) { + if (gids.length === 0) return; + try { + const reply = await this.acceptFriends(gids); + const friends = reply.friends || []; + if (friends.length > 0) { + const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', '); + this.bot.log('申请', `已同意 ${friends.length} 人: ${names}`); + } + } catch (e) { + this.bot.logWarn('申请', `同意失败: ${e.message}`); + } + } +} + +module.exports = { FriendManager }; diff --git a/211/server/src/core/Network.js b/211/server/src/core/Network.js new file mode 100644 index 0000000..1a27398 --- /dev/null +++ b/211/server/src/core/Network.js @@ -0,0 +1,541 @@ +const WebSocket = require('ws'); +const EventEmitter = require('events'); +const protobuf = require('protobufjs'); +const { types } = require('../proto'); +const { toLong, toNum, log, logWarn, syncServerTime } = require('../utils'); + +function calcNormalFertilizerHours(containerCount) { + const containerNum = Number(containerCount) || 0; + return containerNum >= 3600 ? containerNum / 3600 : containerNum; +} + +function calcOrganicFertilizerHours(containerCount) { + const containerNum = Number(containerCount) || 0; + return containerNum >= 3600 ? containerNum / 3600 : containerNum; +} + +class NetworkClient extends EventEmitter { + constructor(bot) { + super(); + this.bot = bot; + this.ws = null; + this.clientSeq = 1; + this.serverSeq = 0; + this.heartbeatTimer = null; + this.pendingCallbacks = new Map(); + this.connected = false; + this.shareClaiming = false; + this.lastShareClaimAt = 0; + + // Heartbeat state + this.lastHeartbeatResponse = 0; + this.heartbeatMissCount = 0; + } + + connect(callback) { + const code = encodeURIComponent(this.bot.config.code || ''); + const url = `${this.bot.config.serverUrl}?platform=${this.bot.config.platform}&os=${this.bot.config.os}&ver=${this.bot.config.clientVersion}&code=${code}&openID=`; + + this.bot.log('系统', `连接服务器... ${this.bot.config.platform}`); + + this.ws = new WebSocket(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13)', + 'Origin': 'https://gate-obt.nqf.qq.com', + }, + }); + + this.ws.binaryType = 'arraybuffer'; + + this.ws.on('open', () => { + this.bot.log('系统', 'WebSocket 连接成功'); + this.connected = true; + if (callback) callback(true); + }); + + this.ws.on('message', (data) => { + this.handleMessage(data); + }); + + this.ws.on('close', (code, reason) => { + this.bot.log('系统', `WebSocket 连接断开 (code=${code})`); + this.connected = false; + this.stopHeartbeat(); + this.cleanup(); + this.emit('disconnected'); + }); + + this.ws.on('error', (err) => { + this.bot.logWarn('系统', `WebSocket 错误: ${err.message}`); + this.emit('error', err); + }); + } + + cleanup() { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + this.pendingCallbacks.clear(); + } + + disconnect() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.cleanup(); + } + + startHeartbeat() { + if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); + this.lastHeartbeatResponse = Date.now(); + this.heartbeatMissCount = 0; + + this.heartbeatTimer = setInterval(() => { + if (!this.bot.user.gid) return; + + const timeSinceLastResponse = Date.now() - this.lastHeartbeatResponse; + if (timeSinceLastResponse > 60000) { + this.heartbeatMissCount++; + this.bot.logWarn('心跳', `连接可能已断开 (${Math.round(timeSinceLastResponse/1000)}s 无响应)`); + if (this.heartbeatMissCount >= 2) { + this.bot.log('心跳', '尝试重连...'); + // Cleanup pending callbacks to avoid leaks + this.pendingCallbacks.forEach((cb, seq) => { + try { cb(new Error('连接超时,已清理')); } catch (e) {} + }); + this.pendingCallbacks.clear(); + // Optional: trigger reconnect logic here or let the user handle it + } + } + + const body = types.HeartbeatRequest.encode(types.HeartbeatRequest.create({ + gid: toLong(this.bot.user.gid), + client_version: this.bot.config.clientVersion, + })).finish(); + + this.sendMsg('gamepb.userpb.UserService', 'Heartbeat', body, (err, replyBody) => { + if (err || !replyBody) return; + this.lastHeartbeatResponse = Date.now(); + this.heartbeatMissCount = 0; + try { + const reply = types.HeartbeatReply.decode(replyBody); + if (reply.server_time) { + // We still use the global syncServerTime for now as it's just a time offset + syncServerTime(toNum(reply.server_time)); + } + } catch (e) { } + }); + + }, this.bot.config.heartbeatInterval); + } + + stopHeartbeat() { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + } + + sendLogin(callback) { + const body = types.LoginRequest.encode(types.LoginRequest.create({ + sharer_id: toLong(0), + sharer_open_id: '', + device_info: { + client_version: this.bot.config.clientVersion, + sys_software: 'iOS 26.2.1', + network: 'wifi', + memory: '7672', + device_id: 'iPhone X', + }, + share_cfg_id: toLong(0), + scene_id: '1256', + report_data: { + callback: '', cd_extend_info: '', click_id: '', clue_token: '', + minigame_channel: 'other', minigame_platid: 2, req_id: '', trackid: '', + }, + })).finish(); + + this.sendMsg('gamepb.userpb.UserService', 'Login', body, (err, bodyBytes, meta) => { + if (err) { + this.bot.log('登录', `失败: ${err.message}`); + if (callback) callback(false); + return; + } + try { + const reply = types.LoginReply.decode(bodyBytes); + if (reply.basic) { + const basic = reply.basic; + this.bot.user.gid = toNum(basic.gid); + this.bot.user.name = basic.name || '未知'; + this.bot.user.level = toNum(basic.level); + this.bot.user.gold = toNum(basic.gold); + this.bot.user.exp = toNum(basic.exp); + this.bot.user.avatarUrl = basic.avatar_url; + + this.bot.log('系统', `登录成功: ${this.bot.user.name} (Lv${this.bot.user.level})`); + + if (reply.time_now_millis) { + syncServerTime(toNum(reply.time_now_millis)); + } + } + + this.startHeartbeat(); + this.bot.emit('loginSuccess', this.bot.user); + this.tryClaimShareReward(); + if (this.bot.warehouseManager) { + this.bot.warehouseManager.getBag().then((bagReply) => { + const items = this.bot.warehouseManager.getBagItems(bagReply); + let tickets = 0; + let fertilizerContainer = 0; + let organicFertilizerContainer = 0; + const fertilizerItems = { + 80001: 0, + 80002: 0, + 80003: 0, + 80004: 0, + }; + const organicFertilizerItems = { + 80011: 0, + 80012: 0, + 80013: 0, + 80014: 0, + }; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1002) tickets = count; + if (id === 1011) fertilizerContainer = count; + if (id === 1012) organicFertilizerContainer = count; + if (id === 80001) fertilizerItems[80001] = count; + if (id === 80002) fertilizerItems[80002] = count; + if (id === 80003) fertilizerItems[80003] = count; + if (id === 80004) fertilizerItems[80004] = count; + if (id === 80011) organicFertilizerItems[80011] = count; + if (id === 80012) organicFertilizerItems[80012] = count; + if (id === 80013) organicFertilizerItems[80013] = count; + if (id === 80014) organicFertilizerItems[80014] = count; + } + this.bot.user.tickets = tickets; + this.bot.user.fertilizerContainer = fertilizerContainer; + this.bot.user.fertilizerItems = fertilizerItems; + this.bot.user.fertilizerHours = calcNormalFertilizerHours(fertilizerContainer); + this.bot.user.organicFertilizerContainer = organicFertilizerContainer; + this.bot.user.organicFertilizerItems = organicFertilizerItems; + this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(organicFertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + }).catch(() => { }); + } + if (callback) callback(true); + } catch (e) { + this.bot.log('登录', `解码失败: ${e.message}`); + if (callback) callback(false); + } + }); + } + + async tryClaimShareReward(shareId = 1) { + if (this.shareClaiming) return; + const now = Date.now(); + if (now - this.lastShareClaimAt < 60000) return; + this.shareClaiming = true; + this.lastShareClaimAt = now; + try { + const canShare = await this.checkCanShareReward(); + if (!canShare) return; + await this.reportShare(shareId); + await this.claimShareReward(shareId); + this.bot.log('分享', '已领取分享奖励'); + } catch (e) { + const message = this.formatShareClaimError(e); + this.bot.logWarn('分享', message); + } finally { + this.shareClaiming = false; + } + } + + formatShareClaimError(error) { + const raw = error?.message ? String(error.message) : ''; + if (!raw) return '分享奖励领取失败'; + if (raw.includes('code=1009001') || raw.includes('已经领取')) { + return '分享奖励已领取,无需重复领取'; + } + return `领取失败: ${raw}`; + } + + decodeShareFlag(replyBody) { + if (!replyBody || replyBody.length === 0) return 0; + const reader = protobuf.Reader.create(replyBody); + let flag = 0; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const field = tag >>> 3; + if (field === 1) { + flag = Number(reader.int64()); + } else { + reader.skipType(tag & 7); + } + } + return flag; + } + + async checkCanShareReward() { + const { body: replyBody } = await this.sendMsgAsync('gamepb.sharepb.ShareService', 'CheckCanShare', Buffer.alloc(0)); + const flag = this.decodeShareFlag(replyBody); + return flag === 1; + } + + async reportShare(shareId) { + const writer = protobuf.Writer.create(); + writer.uint32(8).int64(toLong(shareId)); + const body = writer.finish(); + await this.sendMsgAsync('gamepb.sharepb.ShareService', 'ReportShare', body); + } + + async claimShareReward(shareId) { + const writer = protobuf.Writer.create(); + writer.uint32(8).int64(toLong(shareId)); + const body = writer.finish(); + await this.sendMsgAsync('gamepb.sharepb.ShareService', 'ClaimShareReward', body); + } + + encodeMsg(serviceName, methodName, bodyBytes) { + const msg = types.GateMessage.create({ + meta: { + service_name: serviceName, + method_name: methodName, + message_type: 1, + client_seq: toLong(this.clientSeq), + server_seq: toLong(this.serverSeq), + }, + body: bodyBytes || Buffer.alloc(0), + }); + const encoded = types.GateMessage.encode(msg).finish(); + this.clientSeq++; + return encoded; + } + + sendMsg(serviceName, methodName, bodyBytes, callback) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + return false; + } + const seq = this.clientSeq; + const encoded = this.encodeMsg(serviceName, methodName, bodyBytes); + if (callback) this.pendingCallbacks.set(seq, callback); + this.ws.send(encoded); + return true; + } + + sendMsgAsync(serviceName, methodName, bodyBytes, timeout = 10000) { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + reject(new Error(`连接未打开: ${methodName}`)); + return; + } + + const seq = this.clientSeq; + const timer = setTimeout(() => { + this.pendingCallbacks.delete(seq); + reject(new Error(`请求超时: ${methodName}`)); + }, timeout); + + const sent = this.sendMsg(serviceName, methodName, bodyBytes, (err, body, meta) => { + clearTimeout(timer); + if (err) reject(err); + else resolve({ body, meta }); + }); + + if (!sent) { + clearTimeout(timer); + reject(new Error(`发送失败: ${methodName}`)); + } + }); + } + + handleMessage(data) { + try { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); + const msg = types.GateMessage.decode(buf); + const meta = msg.meta; + if (!meta) return; + + if (meta.server_seq) { + const seq = toNum(meta.server_seq); + if (seq > this.serverSeq) this.serverSeq = seq; + } + + const msgType = meta.message_type; + + // Notify + if (msgType === 3) { + this.handleNotify(msg); + return; + } + + // Response + if (msgType === 2) { + const errorCode = toNum(meta.error_code); + const clientSeqVal = toNum(meta.client_seq); + + const cb = this.pendingCallbacks.get(clientSeqVal); + if (cb) { + this.pendingCallbacks.delete(clientSeqVal); + if (errorCode !== 0) { + cb(new Error(`${meta.service_name}.${meta.method_name} 错误: code=${errorCode} ${meta.error_message || ''}`)); + } else { + cb(null, msg.body, meta); + } + return; + } + } + } catch (err) { + this.bot.logWarn('解码', err.message); + } + } + + handleNotify(msg) { + if (!msg.body || msg.body.length === 0) return; + try { + const event = types.EventMessage.decode(msg.body); + const type = event.message_type || ''; + const eventBody = event.body; + + if (type.includes('Kickout')) { + this.bot.logWarn('推送', `被踢下线! ${type}`); + this.bot.emit('kickout', type); + return; + } + + if (type.includes('LandsNotify')) { + const notify = types.LandsNotify.decode(eventBody); + const hostGid = toNum(notify.host_gid); + const lands = notify.lands || []; + if (lands.length > 0) { + if (hostGid === this.bot.user.gid || hostGid === 0) { + this.bot.emit('landsChanged', lands); + } + } + return; + } + + if (type.includes('BasicNotify')) { + try { + const notify = types.BasicNotify.decode(eventBody); + if (notify.basic) { + const oldLevel = this.bot.user.level; + this.bot.user.level = toNum(notify.basic.level) || this.bot.user.level; + this.bot.user.gold = toNum(notify.basic.gold) || this.bot.user.gold; + const exp = toNum(notify.basic.exp); + if (exp > 0) this.bot.user.exp = exp; + + if (this.bot.user.level !== oldLevel) { + this.bot.log('系统', `升级! Lv${oldLevel} → Lv${this.bot.user.level}`); + } + this.bot.emit('userUpdate', this.bot.user); + } + } catch (e) { } + return; + } + + if (type.includes('FriendApplicationReceivedNotify')) { + try { + const notify = types.FriendApplicationReceivedNotify.decode(eventBody); + const applications = notify.applications || []; + if (applications.length > 0) { + this.bot.emit('friendApplicationReceived', applications); + } + } catch (e) { } + return; + } + + if (type.includes('FriendAddedNotify')) { + try { + const notify = types.FriendAddedNotify.decode(eventBody); + const friends = notify.friends || []; + if (friends.length > 0) { + const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', '); + this.bot.log('好友', `新好友: ${names}`); + } + } catch (e) { } + return; + } + + if (type.includes('ItemNotify')) { + const notify = types.ItemNotify.decode(eventBody); + const items = notify.items || []; + for (const itemChg of items) { + if (!itemChg.item) continue; + const id = toNum(itemChg.item.id); + const count = toNum(itemChg.item.count); + const delta = toNum(itemChg.delta); + if (id === 1 || id === 1001) { + this.bot.user.gold = count; + if (delta !== 0) { + this.bot.log('物品', `金币 ${delta > 0 ? '+' : ''}${delta} (当前: ${count})`); + } + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 1002) { + this.bot.user.tickets = count; + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 1011) { + this.bot.user.fertilizerContainer = count; + this.bot.user.fertilizerHours = calcNormalFertilizerHours(this.bot.user.fertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 1012) { + this.bot.user.organicFertilizerContainer = count; + this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(this.bot.user.organicFertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 80001 || id === 80002 || id === 80003 || id === 80004) { + if (!this.bot.user.fertilizerItems) { + this.bot.user.fertilizerItems = { 80001: 0, 80002: 0, 80003: 0, 80004: 0 }; + } + this.bot.user.fertilizerItems[id] = count; + this.bot.user.fertilizerHours = calcNormalFertilizerHours(this.bot.user.fertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + } + if (id === 80011 || id === 80012 || id === 80013 || id === 80014) { + if (!this.bot.user.organicFertilizerItems) { + this.bot.user.organicFertilizerItems = { 80011: 0, 80012: 0, 80013: 0, 80014: 0 }; + } + this.bot.user.organicFertilizerItems[id] = count; + this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(this.bot.user.organicFertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + } + } + return; + } + + if (type.includes('GoodsUnlockNotify')) { + try { + const notify = types.GoodsUnlockNotify.decode(eventBody); + const goods = notify.goods_list || []; + if (goods.length > 0) { + this.bot.log('商店', `解锁 ${goods.length} 个新商品!`); + } + } catch (e) { } + return; + } + + if (type.includes('TaskInfoNotify')) { + try { + const notify = types.TaskInfoNotify.decode(eventBody); + if (notify.task_info) { + this.bot.emit('taskInfoNotify', notify.task_info); + } + } catch (e) { } + return; + } + } catch (e) { } + } +} + +module.exports = { NetworkClient }; diff --git a/211/server/src/core/ShopManager.js b/211/server/src/core/ShopManager.js new file mode 100644 index 0000000..dfb2243 --- /dev/null +++ b/211/server/src/core/ShopManager.js @@ -0,0 +1,80 @@ +const protobuf = require('protobufjs'); +const { types } = require('../proto'); +const { toLong, toNum, log } = require('../utils'); +const { getPlantNameBySeedId, getItemName } = require('../gameConfig'); + +class ShopManager { + constructor(bot) { + this.bot = bot; + } + + async getShopProfiles() { + const body = types.ShopProfilesRequest.encode(types.ShopProfilesRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopProfiles', body); + return types.ShopProfilesReply.decode(replyBody); + } + + async getShopInfo(shopId) { + const body = types.ShopInfoRequest.encode(types.ShopInfoRequest.create({ + shop_id: toLong(shopId) + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopInfo', body); + return types.ShopInfoReply.decode(replyBody); + } + + async buyGoods(goodsId, count, price) { + const body = types.BuyGoodsRequest.encode(types.BuyGoodsRequest.create({ + goods_id: toLong(goodsId), + num: toLong(count), + price: toLong(price) + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'BuyGoods', body); + return types.BuyGoodsReply.decode(replyBody); + } + + buildMallPurchaseBody(itemId, count) { + const writer = protobuf.Writer.create(); + writer.uint32(8).int64(toLong(itemId)); + writer.uint32(16).int64(toLong(count)); + return writer.finish(); + } + + async purchaseMallItem(itemId, count) { + const body = this.buildMallPurchaseBody(itemId, count); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.mallpb.MallService', 'Purchase', body); + return replyBody; + } + + async getSeedShopList() { + // 1. 获取商店列表,找到种子商店 (type=2) + const profilesReply = await this.getShopProfiles(); + const profiles = profilesReply.shop_profiles || []; + const seedShop = profiles.find(p => p.shop_type === 2); // 2 usually means Seed Shop + + if (!seedShop) { + throw new Error('未找到种子商店'); + } + + // 2. 获取商品列表 + const infoReply = await this.getShopInfo(seedShop.shop_id); + const goodsList = infoReply.goods_list || []; + + // 3. 格式化数据 + return goodsList.map(g => { + const itemId = toNum(g.item_id); + const name = getPlantNameBySeedId(itemId) || getItemName(itemId) || `商品${g.id}`; + return { + goodsId: toNum(g.id), + itemId: itemId, + name: name, + price: toNum(g.price), + limitCount: toNum(g.limit_count), + boughtNum: toNum(g.bought_num), + unlocked: g.unlocked, + itemCount: toNum(g.item_count) + }; + }); + } +} + +module.exports = { ShopManager }; diff --git a/211/server/src/core/WarehouseManager.js b/211/server/src/core/WarehouseManager.js new file mode 100644 index 0000000..c4e1461 --- /dev/null +++ b/211/server/src/core/WarehouseManager.js @@ -0,0 +1,322 @@ +const protobuf = require('protobufjs'); +const { types } = require('../proto'); +const { toLong, toNum, log, logWarn, emitRuntimeHint } = require('../utils'); +const { getFruitName, getPlantNameBySeedId, getItemNameById } = require('../gameConfig'); +const seedShopData = require('../../tools/seed-shop-merged-export.json'); + +// 游戏内金币和点券的物品 ID (GlobalData.GodItemId / DiamondItemId) +const GOLD_ITEM_ID = 1001; +const FRUIT_ID_SET = new Set( + ((seedShopData && seedShopData.rows) || []) + .map(row => Number(row.fruitId)) + .filter(Number.isFinite) +); + +class WarehouseManager { + constructor(bot) { + this.bot = bot; + this.sellTimer = null; + this.sellInterval = 60000; + } + + isFruitIdBySeedData(id) { + return FRUIT_ID_SET.has(toNum(id)); + } + + /** + * 从 SellReply 中提取获得的金币数量 + * 新版 SellReply 返回 get_items (repeated Item),其中 id=1001 为金币 + */ + extractGold(sellReply) { + if (sellReply.get_items && sellReply.get_items.length > 0) { + for (const item of sellReply.get_items) { + const id = toNum(item.id); + if (id === GOLD_ITEM_ID) { + return toNum(item.count); + } + } + return 0; + } + if (sellReply.gold !== undefined && sellReply.gold !== null) { + return toNum(sellReply.gold); + } + return 0; + } + + async getBag() { + const body = types.BagRequest.encode(types.BagRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Bag', body); + return types.BagReply.decode(replyBody); + } + + /** + * 将 item 转为 Sell 请求所需格式(id/count/uid 保留 Long 或转成 Long,与游戏一致) + */ + toSellItem(item) { + const id = item.id != null ? toLong(item.id) : undefined; + const count = item.count != null ? toLong(item.count) : undefined; + const uid = item.uid != null ? toLong(item.uid) : undefined; + return { id, count, uid }; + } + + async sellItems(items) { + const payload = items.map(this.toSellItem); + const body = types.SellRequest.encode(types.SellRequest.create({ items: payload })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Sell', body); + return types.SellReply.decode(replyBody); + } + + /** + * 从 BagReply 取出物品列表(兼容 item_bag 与旧版 items) + */ + getBagItems(bagReply) { + if (bagReply.item_bag && bagReply.item_bag.items && bagReply.item_bag.items.length) + return bagReply.item_bag.items; + return bagReply.items || []; + } + + /** + * 获取格式化的背包数据,用于前端显示 + */ + async getFormattedBag() { + const bagReply = await this.getBag(); + const items = this.getBagItems(bagReply); + + const seeds = []; + const produce = []; + const others = []; + + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + const uid = item.uid ? toNum(item.uid) : 0; + + // Basic item object + const itemObj = { + id, + uid, + count, + name: `物品 ${id}`, + type: 'other' + }; + + if (this.isFruitIdBySeedData(id) || id === 40416) { + itemObj.name = getFruitName(id); + itemObj.type = 'produce'; + produce.push(itemObj); + } else { + // 尝试识别为种子 + const plantName = getPlantNameBySeedId(id); + if (plantName && plantName !== `种子${id}`) { + itemObj.name = plantName + '种子'; + itemObj.type = 'seed'; + seeds.push(itemObj); + } else { + // 过滤掉货币类物品和特殊物品 + // 1001:金币, 1002:点券, 1101:种植经验, 3001:普通收藏点, 1011/1012:化肥容器 + if ([1001, 1002, 1101, 3001, 1011, 1012].includes(id)) { + continue; + } + + // 其他物品 + itemObj.name = getItemNameById(id); + others.push(itemObj); + } + } + } + + return { seeds, produce, others }; + } + + async sellAllFruits() { + if (!this.bot.network.connected) return; + + try { + const bagReply = await this.getBag(); + const items = this.getBagItems(bagReply); + + const toSell = []; + const names = []; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + const uid = item.uid ? toNum(item.uid) : 0; + if (this.isFruitIdBySeedData(id) && count > 0) { + if (uid === 0) continue; // 跳过无效格子 + toSell.push(item); + names.push(`${getFruitName(id)} ${count} 个`); + } + } + + if (toSell.length === 0) return; + + const reply = await this.sellItems(toSell); + const totalGold = this.extractGold(reply); + log('仓库', `成功出售: ${names.join(',')}。共获得 ${totalGold} 金币`); + emitRuntimeHint(false); + } catch (e) { + logWarn('仓库', `物品出售失败: ${e.message}`); + } + } + + async useItems(items) { + if (!items || items.length === 0) return { success: true, message: '没有选择物品' }; + + const results = []; + const fertilizers = new Set([80001, 80002, 80003, 80004, 80011, 80012, 80013, 80014]); + const formatGains = (replyItems, excludeId) => { + if (!replyItems || replyItems.length === 0) return ''; + const parts = []; + for (const replyItem of replyItems) { + const gainId = toNum(replyItem.id); + const gainCount = toNum(replyItem.count); + if (excludeId && gainId === excludeId) continue; + if (!gainId || !gainCount) continue; + parts.push(`${getItemNameById(gainId)} x${gainCount}`); + } + return parts.join(','); + }; + const buildGainList = (replyItems, excludeId) => { + if (!replyItems || replyItems.length === 0) return []; + const gains = []; + for (const replyItem of replyItems) { + const gainId = toNum(replyItem.id); + const gainCount = toNum(replyItem.count); + if (excludeId && gainId === excludeId) continue; + if (!gainId || !gainCount) continue; + gains.push({ id: gainId, count: gainCount }); + } + return gains; + }; + const buildItemCountMap = (bagItems) => { + const map = new Map(); + for (const bagItem of bagItems) { + const bagId = toNum(bagItem.id); + const bagCount = toNum(bagItem.count); + if (!bagId || !bagCount) continue; + map.set(bagId, (map.get(bagId) || 0) + bagCount); + } + return map; + }; + const diffGains = (beforeMap, afterMap, excludeId) => { + const parts = []; + for (const [id, afterCount] of afterMap.entries()) { + if (excludeId && id === excludeId) continue; + const beforeCount = beforeMap.get(id) || 0; + const delta = afterCount - beforeCount; + if (delta > 0) { + parts.push(`${getItemNameById(id)} x${delta}`); + } + } + return parts.join(','); + }; + const diffGainList = (beforeMap, afterMap, excludeId) => { + const gains = []; + for (const [id, afterCount] of afterMap.entries()) { + if (excludeId && id === excludeId) continue; + const beforeCount = beforeMap.get(id) || 0; + const delta = afterCount - beforeCount; + if (delta > 0) gains.push({ id, count: delta }); + } + return gains; + }; + const mergeGains = (gainMap, gains) => { + for (const gain of gains) { + if (!gain || !gain.id || !gain.count) continue; + gainMap.set(gain.id, (gainMap.get(gain.id) || 0) + gain.count); + } + }; + const totalGains = new Map(); + + for (const item of items) { + const id = Number(item.id); + const count = Number(item.count); + let uid = item.uid != null ? Number(item.uid) : 0; + const name = getItemNameById(id); + + try { + let usedCount = count; + let gainsText = ''; + let gainList = []; + if (id === 100003) { + const beforeReply = await this.getBag(); + const beforeItems = this.getBagItems(beforeReply); + const beforeMap = buildItemCountMap(beforeItems); + if (!uid) { + const bagReply = await this.getBag(); + const bagItems = this.getBagItems(bagReply); + const found = bagItems.find(bagItem => toNum(bagItem.id) === id && toNum(bagItem.uid) > 0); + if (found) uid = toNum(found.uid); + } + if (!uid) { + throw new Error('礼包缺少UID'); + } + const itemWriter = protobuf.Writer.create(); + itemWriter.uint32(8).int64(toLong(id)); + itemWriter.uint32(16).int64(toLong(count)); + itemWriter.uint32(48).int64(toLong(uid)); + const body = protobuf.Writer.create().uint32(10).bytes(itemWriter.finish()).finish(); + await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Use', body); + const afterReply = await this.getBag(); + const afterItems = this.getBagItems(afterReply); + const afterMap = buildItemCountMap(afterItems); + gainsText = diffGains(beforeMap, afterMap, id); + gainList = diffGainList(beforeMap, afterMap, id); + } else { + const payload = { + items: [{ + item_id: toLong(id), + count: toLong(count), + land_count: toLong(1) + }] + }; + const body = types.BatchUseRequest.encode(types.BatchUseRequest.create(payload)).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'BatchUse', body); + const reply = types.BatchUseReply.decode(replyBody); + gainsText = formatGains(reply.items, id); + gainList = buildGainList(reply.items, id); + } + + if (fertilizers.has(id)) { + results.push(`${name}: 已使用(消耗 ${usedCount} 个)${gainsText ? `,获得:${gainsText}` : ''}`); + } else { + results.push(`${name}: 已使用${gainsText ? `,获得:${gainsText}` : ''}`); + } + if (gainsText) { + log('仓库', `使用 ${name} 获得:${gainsText}`); + } + mergeGains(totalGains, gainList); + + } catch (e) { + logWarn('仓库', `物品使用失败 (${name}): ${e.message}`); + results.push(`${name}: 使用失败 - ${e.message}`); + } + } + + const gains = []; + for (const [id, count] of totalGains.entries()) { + gains.push({ id, name: getItemNameById(id), count }); + } + return { success: true, message: results.join('; '), gains }; + } + + startLoop(interval = 60000) { + if (this.sellTimer) return; + this.sellInterval = interval; + // 延迟启动,避免刚上线就请求 + setTimeout(() => { + if (!this.bot.isRunning) return; + this.sellAllFruits(); + this.sellTimer = setInterval(() => this.sellAllFruits(), this.sellInterval); + }, 10000); + } + + stopLoop() { + if (this.sellTimer) { + clearInterval(this.sellTimer); + this.sellTimer = null; + } + } +} + +module.exports = { WarehouseManager }; diff --git a/211/server/src/decode.js b/211/server/src/decode.js new file mode 100644 index 0000000..a00b929 --- /dev/null +++ b/211/server/src/decode.js @@ -0,0 +1,245 @@ +/** + * 解码/验证工具模式 + */ + +const protobuf = require('protobufjs'); +const Long = require('long'); +const { PHASE_NAMES } = require('./config'); +const { types, getRoot } = require('./proto'); +const { toNum } = require('./utils'); + +// ============ 辅助函数 ============ + +/** JSON.stringify replacer, 处理 Long 和 Buffer */ +function longReplacer(key, value) { + if (value && typeof value === 'object' && value.low !== undefined && value.high !== undefined) { + return Long.fromBits(value.low, value.high, value.unsigned).toString(); + } + if (value && value.type === 'Buffer' && Array.isArray(value.data)) { + return `<${value.data.length} bytes>`; + } + return value; +} + +/** 尝试将 bytes 解码为 UTF-8 字符串 */ +function tryDecodeString(bytes) { + try { + const str = Buffer.from(bytes).toString('utf8'); + const printable = str.split('').filter(c => c.charCodeAt(0) >= 32 || c === '\n' || c === '\r' || c === '\t').length; + if (printable > str.length * 0.8 && str.length > 0) return str; + } catch (e) {} + return null; +} + +/** 通用 protobuf 解码 (无 schema, 显示原始字段) */ +function tryGenericDecode(buf) { + console.log('=== 通用 protobuf 解码 (无schema) ==='); + try { + const reader = protobuf.Reader.create(buf); + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const fieldNum = tag >>> 3; + const wireType = tag & 7; + let value; + switch (wireType) { + case 0: value = reader.int64().toString(); console.log(` field ${fieldNum} (varint): ${value}`); break; + case 1: value = reader.fixed64().toString(); console.log(` field ${fieldNum} (fixed64): ${value}`); break; + case 2: { + const bytes = reader.bytes(); + const str = tryDecodeString(bytes); + if (str !== null) { + console.log(` field ${fieldNum} (bytes/${bytes.length}): "${str}"`); + } else { + console.log(` field ${fieldNum} (bytes/${bytes.length}): ${Buffer.from(bytes).toString('hex')}`); + } + break; + } + case 5: value = reader.float(); console.log(` field ${fieldNum} (float): ${value}`); break; + default: console.log(` field ${fieldNum} (wire ${wireType}): `); reader.skipType(wireType); break; + } + } + } catch (e) { + console.log(` 解码中断: ${e.message}`); + } +} + +// ============ 验证模式 ============ + +async function verifyMode() { + console.log('\n====== 验证模式 ======\n'); + + // Login Request + const loginB64 = 'CigKGWdhbWVwYi51c2VycGIuVXNlclNlcnZpY2USBUxvZ2luGAEgASgAEmEYACIAKjwKEDEuNi4wLjhfMjAyNTEyMjQSE1dpbmRvd3MgVW5rbm93biB4NjQqBHdpZmlQzL0BagltaWNyb3NvZnQwADoEMTI1NkIVCgASABoAIgAqBW90aGVyMAI6AEIA'; + try { + const msg = types.GateMessage.decode(Buffer.from(loginB64, 'base64')); + console.log(`[OK] Login Request: ${msg.meta.service_name}.${msg.meta.method_name} seq=${msg.meta.client_seq}`); + const req = types.LoginRequest.decode(msg.body); + console.log(` device=${req.device_info?.client_version} scene=${req.scene_id}`); + } catch (e) { console.log(`[FAIL] Login Request: ${e.message}`); } + + // AllLands Response + const allLandsB64 = 'ClwKG2dhbWVwYi5wbGFudHBiLlBsYW50U2VydmljZRIIQWxsTGFuZHMYAiAEKARCLQoJeC10cmFjZWlkEiBhOWZhNmZhZmYwZmI0ZDU5ZjQ5ZDJiZTJlYTY2NGU3NBK7BwpMCAEQARgBIARCDQgSEBwaBwjpBxDAmgxKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCNu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAIQARgBIARCDQgSEB0aBwjpBxCQoQ9KAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAMQARgBIARCDQgSEB4aBwjpBxDgpxJKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAQQARgBIARCDQgSEB8aBwjpBxCwrhVKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAUQARgBIARCDQgSECAaBwjpBxCAtRhKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCNu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAYQARgBIARCDQgSECEaBwjpBxCgwh5KAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQoPCAcgBDoHCAYQBRiIJ2ABCg8ICCAEOgcIBxAHGJBOYAEKEAgJIAQ6CAgIEAkYoJwBYAEKDggKIAQ6CAgJEAsYsOoBCg4ICyAEOggIChANGMC4AgoOCAwgBDoICAsQDxjg1AMKDggNIAQ6CAgMEBEYgPEECg4IDiAEOggIDRATGKCNBgoOCA8gBDoICA4QFRjAqQcKDggQIAQ6CAgPEBcY4MUICg4IESAEOggIEBAZGIDiCQoOCBIgBDoICBEQGxig/goKDggTIAQ6CAgSEB0YwJoMCg4IFCAEOggIExAfGOC2DQoOCBUgBDoICBQQIRiA0w4KDggWIAQ6CAgVECMYoO8PCg4IFyAEOggIFhAlGMCLEQoOCBggBDoICBcQJxjgpxISCQicThj/k+vcAxIJCJ1OGP+T69wDEgkInk4Y/5Pr3AMSCwiRThAJGP+T69wDEg0IlE4YZCCTTihkOJNOEhAIlU4QBxj/k+vcAygXOJVOEhAIlk4QCxj/k+vcAygLOJVOEhAIl04QBRj/k+vcAygFOJVOEgkImE4Y/5Pr3AMSDQiZThAMGP+T69wDKAwSCwiaThABGP+T69wDEg0Ikk4QCRj/k+vcAygJEg0Ik04YZCCTTihkOJNOEgkIm04Y/5Pr3AM='; + try { + const msg = types.GateMessage.decode(Buffer.from(allLandsB64, 'base64')); + const reply = types.AllLandsReply.decode(msg.body); + console.log(`[OK] AllLands Reply: ${reply.lands.length} 块土地`); + for (const land of reply.lands.slice(0, 3)) { + const id = toNum(land.id); + const unlocked = land.unlocked; + const plantName = land.plant?.name || '空'; + const phases = land.plant?.phases || []; + const lastPhase = phases.length > 0 ? phases[phases.length - 1].phase : -1; + console.log(` 土地#${id}: ${unlocked ? '已解锁' : '未解锁'} 植物=${plantName} 阶段=${PHASE_NAMES[lastPhase] || lastPhase}`); + } + if (reply.lands.length > 3) console.log(` ... 还有 ${reply.lands.length - 3} 块`); + } catch (e) { console.log(`[FAIL] AllLands Reply: ${e.message}`); } + + // Harvest Request + const harvestB64 = 'CiwKG2dhbWVwYi5wbGFudHBiLlBsYW50U2VydmljZRIHSGFydmVzdBgBIBsoGhIQCgYBAgMEBQYQyOHR8gMYAQ=='; + try { + const msg = types.GateMessage.decode(Buffer.from(harvestB64, 'base64')); + const req = types.HarvestRequest.decode(msg.body); + console.log(`[OK] Harvest Request: land_ids=[${req.land_ids.join(',')}] host_gid=${req.host_gid} is_all=${req.is_all}`); + } catch (e) { console.log(`[FAIL] Harvest Request: ${e.message}`); } + + console.log('\n====== 验证完成 ======\n'); +} + +// ============ 解码模式 ============ + +async function decodeMode(args) { + let inputData = ''; + let typeName = ''; + let isHex = false; + let isGateWrapped = false; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--decode') continue; + if (args[i] === '--type' && args[i + 1]) { typeName = args[++i]; continue; } + if (args[i] === '--hex') { isHex = true; continue; } + if (args[i] === '--gate') { isGateWrapped = true; continue; } + if (!inputData) inputData = args[i]; + } + + if (!inputData) { + console.log(` +PB数据解码工具 +============== + +用法: + node client.js --decode + node client.js --decode --hex + node client.js --decode --type <消息类型> + node client.js --decode --gate + +参数: + <数据> base64编码的pb数据 (默认), 或hex编码 (配合 --hex) + --hex 输入数据为hex编码 + --gate 外层是 gatepb.Message 包装, 自动解析 meta + body + --type 指定消息类型, 如: gatepb.Message, gamepb.plantpb.AllLandsReply 等 + +可用类型: + gatepb.Message / gatepb.Meta + gamepb.userpb.LoginRequest / LoginReply / HeartbeatRequest / HeartbeatReply + gamepb.plantpb.AllLandsRequest / AllLandsReply / HarvestRequest / HarvestReply + gamepb.plantpb.WaterLandRequest / WeedOutRequest / InsecticideRequest + gamepb.plantpb.PlantRequest / PlantReply / RemovePlantRequest / RemovePlantReply + gamepb.shoppb.ShopInfoRequest / ShopInfoReply / BuyGoodsRequest / BuyGoodsReply + gamepb.friendpb.GetAllRequest / GetAllReply / GameFriend + +示例: + node client.js --decode CigKGWdhbWVwYi... --gate + node client.js --decode 0a1c0a19... --hex --type gatepb.Message +`); + return; + } + + const root = getRoot(); + let buf; + try { + buf = isHex ? Buffer.from(inputData, 'hex') : Buffer.from(inputData, 'base64'); + } catch (e) { + console.error(`输入数据解码失败: ${e.message}`); + return; + } + console.log(`数据长度: ${buf.length} 字节\n`); + + // --gate: 先解析外层 gatepb.Message + if (isGateWrapped) { + try { + const msg = types.GateMessage.decode(buf); + const meta = msg.meta; + console.log('=== gatepb.Message (外层) ==='); + console.log(` service: ${meta.service_name}`); + console.log(` method: ${meta.method_name}`); + console.log(` type: ${meta.message_type} (${meta.message_type === 1 ? 'Request' : meta.message_type === 2 ? 'Response' : 'Notify'})`); + console.log(` client_seq: ${meta.client_seq}`); + console.log(` server_seq: ${meta.server_seq}`); + if (toNum(meta.error_code) !== 0) { + console.log(` error_code: ${meta.error_code}`); + console.log(` error_msg: ${meta.error_message}`); + } + console.log(''); + + if (msg.body && msg.body.length > 0) { + const svc = meta.service_name || ''; + const mtd = meta.method_name || ''; + const isReq = meta.message_type === 1; + const suffix = isReq ? 'Request' : 'Reply'; + const autoType = `${svc.replace('Service', '')}.${mtd}${suffix}`; + + let bodyType = null; + try { bodyType = root.lookupType(autoType); } catch (e) {} + if (!bodyType) { + const parts = svc.split('.'); + if (parts.length >= 2) { + const ns = parts.slice(0, parts.length - 1).join('.'); + try { bodyType = root.lookupType(`${ns}.${mtd}${suffix}`); } catch (e) {} + } + } + + if (bodyType) { + console.log(`=== ${bodyType.fullName} (body 自动推断) ===`); + const decoded = bodyType.decode(msg.body); + console.log(JSON.stringify(decoded.toJSON(), longReplacer, 2)); + } else { + console.log(`=== body (未能自动推断类型, 用 --type 手动指定 body 类型) ===`); + console.log(` hex: ${Buffer.from(msg.body).toString('hex')}`); + console.log(` base64: ${Buffer.from(msg.body).toString('base64')}`); + tryGenericDecode(msg.body); + } + } + } catch (e) { + console.error(`gatepb.Message 解码失败: ${e.message}`); + } + return; + } + + // --type: 指定类型解码 + if (typeName) { + try { + const msgType = root.lookupType(typeName); + const decoded = msgType.decode(buf); + console.log(`=== ${typeName} ===`); + console.log(JSON.stringify(decoded.toJSON(), longReplacer, 2)); + } catch (e) { + console.error(`解码失败 (${typeName}): ${e.message}`); + } + return; + } + + // 未指定类型,自动尝试 + console.log('未指定类型,自动尝试...\n'); + try { + const msg = types.GateMessage.decode(buf); + if (msg.meta && (msg.meta.service_name || msg.meta.method_name)) { + console.log('=== 检测为 gatepb.Message ==='); + console.log(JSON.stringify(msg.toJSON(), longReplacer, 2)); + return; + } + } catch (e) {} + + tryGenericDecode(buf); +} + +module.exports = { verifyMode, decodeMode }; diff --git a/211/server/src/farm.js b/211/server/src/farm.js new file mode 100644 index 0000000..a154cae --- /dev/null +++ b/211/server/src/farm.js @@ -0,0 +1,648 @@ +/** + * 自己的农场操作 - 收获/浇水/除草/除虫/铲除/种植/商店/巡田 + */ + +const protobuf = require('protobufjs'); +const { CONFIG, PlantPhase, PHASE_NAMES } = require('./config'); +const { types } = require('./proto'); +const { sendMsgAsync, getUserState, networkEvents } = require('./network'); +const { toLong, toNum, getServerTimeSec, toTimeSec, log, logWarn, sleep } = require('./utils'); +const { getPlantNameBySeedId, getPlantName, getPlantExp, formatGrowTime, getPlantGrowTime } = require('./gameConfig'); +const { getPlantingRecommendation } = require('../tools/calc-exp-yield'); + +// ============ 内部状态 ============ +let isCheckingFarm = false; +let isFirstFarmCheck = true; +let farmCheckTimer = null; +let farmLoopRunning = false; + +// ============ 农场 API ============ + +// 操作限制更新回调 (由 friend.js 设置) +let onOperationLimitsUpdate = null; +function setOperationLimitsCallback(callback) { + onOperationLimitsUpdate = callback; +} + +async function getAllLands() { + const body = types.AllLandsRequest.encode(types.AllLandsRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'AllLands', body); + const reply = types.AllLandsReply.decode(replyBody); + // 更新操作限制 + if (reply.operation_limits && onOperationLimitsUpdate) { + onOperationLimitsUpdate(reply.operation_limits); + } + return reply; +} + +async function harvest(landIds) { + const state = getUserState(); + const body = types.HarvestRequest.encode(types.HarvestRequest.create({ + land_ids: landIds, + host_gid: toLong(state.gid), + is_all: true, + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body); + return types.HarvestReply.decode(replyBody); +} + +async function waterLand(landIds) { + const state = getUserState(); + const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({ + land_ids: landIds, + host_gid: toLong(state.gid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body); + return types.WaterLandReply.decode(replyBody); +} + +async function weedOut(landIds) { + const state = getUserState(); + const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({ + land_ids: landIds, + host_gid: toLong(state.gid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body); + return types.WeedOutReply.decode(replyBody); +} + +async function insecticide(landIds) { + const state = getUserState(); + const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({ + land_ids: landIds, + host_gid: toLong(state.gid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body); + return types.InsecticideReply.decode(replyBody); +} + +// 普通肥料 ID +const NORMAL_FERTILIZER_ID = 1011; + +/** + * 施肥 - 必须逐块进行,服务器不支持批量 + * 游戏中拖动施肥间隔很短,这里用 50ms + */ +async function fertilize(landIds, fertilizerId = NORMAL_FERTILIZER_ID) { + let successCount = 0; + for (const landId of landIds) { + try { + const body = types.FertilizeRequest.encode(types.FertilizeRequest.create({ + land_ids: [toLong(landId)], + fertilizer_id: toLong(fertilizerId), + })).finish(); + await sendMsgAsync('gamepb.plantpb.PlantService', 'Fertilize', body); + successCount++; + } catch (e) { + // 施肥失败(可能肥料不足),停止继续 + break; + } + if (landIds.length > 1) await sleep(50); // 50ms 间隔 + } + return successCount; +} + +async function removePlant(landIds) { + const body = types.RemovePlantRequest.encode(types.RemovePlantRequest.create({ + land_ids: landIds.map(id => toLong(id)), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'RemovePlant', body); + return types.RemovePlantReply.decode(replyBody); +} + +// ============ 商店 API ============ + +async function getShopInfo(shopId) { + const body = types.ShopInfoRequest.encode(types.ShopInfoRequest.create({ + shop_id: toLong(shopId), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.shoppb.ShopService', 'ShopInfo', body); + return types.ShopInfoReply.decode(replyBody); +} + +async function buyGoods(goodsId, num, price) { + const body = types.BuyGoodsRequest.encode(types.BuyGoodsRequest.create({ + goods_id: toLong(goodsId), + num: toLong(num), + price: toLong(price), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.shoppb.ShopService', 'BuyGoods', body); + return types.BuyGoodsReply.decode(replyBody); +} + +// ============ 种植 ============ + +const plantNextAllowed = new Map(); +const PLANT_COOLDOWN_MS = 5000; +const PLANT_ALREADY_COOLDOWN_MS = 30000; + +function encodePlantRequest(seedId, landIds) { + const writer = protobuf.Writer.create(); + const itemWriter = writer.uint32(18).fork(); + itemWriter.uint32(8).int64(seedId); + const idsWriter = itemWriter.uint32(18).fork(); + for (const id of landIds) { + idsWriter.int64(id); + } + idsWriter.ldelim(); + itemWriter.ldelim(); + return writer.finish(); +} + +/** + * 种植 - 游戏中拖动种植间隔很短,这里用 50ms + */ +async function plantSeeds(seedId, landIds) { + let successCount = 0; + const now = Date.now(); + for (const landId of landIds) { + const nextAllowed = plantNextAllowed.get(landId) || 0; + if (now < nextAllowed) continue; + plantNextAllowed.set(landId, now + PLANT_COOLDOWN_MS); + try { + const body = encodePlantRequest(seedId, [landId]); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Plant', body); + types.PlantReply.decode(replyBody); + successCount++; + } catch (e) { + const msg = String(e && e.message ? e.message : e); + if (msg.includes('1001008') || msg.includes('土地已种植')) { + plantNextAllowed.set(landId, now + PLANT_ALREADY_COOLDOWN_MS); + log('种植', `土地#${landId} 已种植,跳过`); + } else { + logWarn('种植', `土地#${landId} 失败: ${msg}`); + } + } + if (landIds.length > 1) await sleep(50); // 50ms 间隔 + } + return successCount; +} + +async function findBestSeed(landsCount) { + const SEED_SHOP_ID = 2; + const shopReply = await getShopInfo(SEED_SHOP_ID); + if (!shopReply.goods_list || shopReply.goods_list.length === 0) { + logWarn('商店', '种子商店无商品'); + return null; + } + + const state = getUserState(); + const available = []; + for (const goods of shopReply.goods_list) { + if (!goods.unlocked) continue; + + let meetsConditions = true; + let requiredLevel = 0; + const conds = goods.conds || []; + for (const cond of conds) { + if (toNum(cond.type) === 1) { + requiredLevel = toNum(cond.param); + if (state.level < requiredLevel) { + meetsConditions = false; + break; + } + } + } + if (!meetsConditions) continue; + + const limitCount = toNum(goods.limit_count); + const boughtNum = toNum(goods.bought_num); + if (limitCount > 0 && boughtNum >= limitCount) continue; + + available.push({ + goods, + goodsId: toNum(goods.id), + seedId: toNum(goods.item_id), + price: toNum(goods.price), + requiredLevel, + }); + } + + if (available.length === 0) { + logWarn('商店', '没有可购买的种子'); + return null; + } + + if (CONFIG.forceLowestLevelCrop) { + available.sort((a, b) => a.requiredLevel - b.requiredLevel || a.price - b.price); + return available[0]; + } + + try { + log('商店', `等级: ${state.level},土地数量: ${landsCount}`); + + const rec = getPlantingRecommendation(state.level, landsCount == null ? 18 : landsCount, { top: 50 }); + const rankedSeedIds = rec.candidatesNoFert.map(x => x.seedId); + for (const seedId of rankedSeedIds) { + const hit = available.find(x => x.seedId === seedId); + if (hit) return hit; + } + } catch (e) { + logWarn('商店', `经验效率推荐失败,使用兜底策略: ${e.message}`); + } + + // 兜底:等级在28级以前还是白萝卜比较好,28级以上选最高等级的种子 + if(state.level && state.level <= 28){ + available.sort((a, b) => a.requiredLevel - b.requiredLevel); + }else{ + available.sort((a, b) => b.requiredLevel - a.requiredLevel); + } + return available[0]; +} + +async function autoPlantEmptyLands(deadLandIds, emptyLandIds, unlockedLandCount) { + let landsToPlant = emptyLandIds.length > 0 ? [...emptyLandIds] : []; + const state = getUserState(); + + // 1. 铲除枯死/收获残留植物(一键操作) + if (deadLandIds.length > 0) { + try { + await removePlant(deadLandIds); + log('铲除', `已铲除 ${deadLandIds.length} 块 (${deadLandIds.join(',')})`); + landsToPlant.push(...deadLandIds); + } catch (e) { + logWarn('铲除', `批量铲除失败: ${e.message}`); + // 失败时仍然尝试种植 + landsToPlant.push(...deadLandIds); + } + } + + if (landsToPlant.length === 0) return; + + if (landsToPlant.length > 1) { + landsToPlant = Array.from(new Set(landsToPlant)); + } + + if (landsToPlant.length > 0) { + const now = Date.now(); + const filtered = []; + for (const landId of landsToPlant) { + const nextAllowed = plantNextAllowed.get(landId) || 0; + if (now >= nextAllowed) { + filtered.push(landId); + } + } + landsToPlant = filtered; + } + + if (landsToPlant.length === 0) return; + + // 2. 查询种子商店 + let bestSeed; + try { + bestSeed = await findBestSeed(unlockedLandCount); + } catch (e) { + logWarn('商店', `查询失败: ${e.message}`); + return; + } + if (!bestSeed) return; + + const seedName = getPlantNameBySeedId(bestSeed.seedId); + const growTime = getPlantGrowTime(1020000 + (bestSeed.seedId - 20000)); // 转换为植物ID + const growTimeStr = growTime > 0 ? ` 生长${formatGrowTime(growTime)}` : ''; + log('商店', `最佳种子: ${seedName} (${bestSeed.seedId}) 价格=${bestSeed.price}金币${growTimeStr}`); + + // 3. 购买 + const needCount = landsToPlant.length; + const totalCost = bestSeed.price * needCount; + if (totalCost > state.gold) { + logWarn('商店', `金币不足! 需要 ${totalCost} 金币, 当前 ${state.gold} 金币`); + const canBuy = Math.floor(state.gold / bestSeed.price); + if (canBuy <= 0) return; + landsToPlant = landsToPlant.slice(0, canBuy); + log('商店', `金币有限,只种 ${canBuy} 块地`); + } + + let actualSeedId = bestSeed.seedId; + try { + const buyReply = await buyGoods(bestSeed.goodsId, landsToPlant.length, bestSeed.price); + if (buyReply.get_items && buyReply.get_items.length > 0) { + const gotItem = buyReply.get_items[0]; + const gotId = toNum(gotItem.id); + const gotCount = toNum(gotItem.count); + log('购买', `获得物品: id=${gotId} count=${gotCount}`); + if (gotId > 0) actualSeedId = gotId; + } + if (buyReply.cost_items) { + for (const item of buyReply.cost_items) { + state.gold -= toNum(item.count); + } + } + const boughtName = getPlantNameBySeedId(actualSeedId); + log('购买', `已购买 ${boughtName}种子 x${landsToPlant.length}, 花费 ${bestSeed.price * landsToPlant.length} 金币`); + } catch (e) { + logWarn('购买', e.message); + return; + } + + // 4. 种植(逐块拖动,间隔50ms) + let plantedLands = []; + try { + const planted = await plantSeeds(actualSeedId, landsToPlant); + log('种植', `已在 ${planted} 块地种植 (${landsToPlant.join(',')})`); + if (planted > 0) { + plantedLands = landsToPlant.slice(0, planted); + } + } catch (e) { + logWarn('种植', e.message); + } + +} + +// ============ 土地分析 ============ + +/** + * 根据服务器时间确定当前实际生长阶段 + */ +function getCurrentPhase(phases, debug, landLabel) { + if (!phases || phases.length === 0) return null; + + const nowSec = getServerTimeSec(); + + if (debug) { + console.log(` ${landLabel} 服务器时间=${nowSec} (${new Date(nowSec * 1000).toLocaleTimeString()})`); + for (let i = 0; i < phases.length; i++) { + const p = phases[i]; + const bt = toTimeSec(p.begin_time); + const phaseName = PHASE_NAMES[p.phase] || `阶段${p.phase}`; + const diff = bt > 0 ? (bt - nowSec) : 0; + const diffStr = diff > 0 ? `(未来 ${diff}s)` : diff < 0 ? `(已过 ${-diff}s)` : ''; + console.log(` ${landLabel} [${i}] ${phaseName}(${p.phase}) begin=${bt} ${diffStr} dry=${toTimeSec(p.dry_time)} weed=${toTimeSec(p.weeds_time)} insect=${toTimeSec(p.insect_time)}`); + } + } + + for (let i = phases.length - 1; i >= 0; i--) { + const beginTime = toTimeSec(phases[i].begin_time); + if (beginTime > 0 && beginTime <= nowSec) { + if (debug) { + console.log(` ${landLabel} → 当前阶段: ${PHASE_NAMES[phases[i].phase] || phases[i].phase}`); + } + return phases[i]; + } + } + + if (debug) { + console.log(` ${landLabel} → 所有阶段都在未来,使用第一个: ${PHASE_NAMES[phases[0].phase] || phases[0].phase}`); + } + return phases[0]; +} + +function analyzeLands(lands) { + const result = { + harvestable: [], needWater: [], needWeed: [], needBug: [], + growing: [], empty: [], dead: [], + harvestableInfo: [], // 收获植物的详细信息 { id, name, exp } + }; + + const nowSec = getServerTimeSec(); + const debug = false; + + if (debug) { + console.log(''); + console.log('========== 首次巡田详细日志 =========='); + console.log(` 服务器时间(秒): ${nowSec} (${new Date(nowSec * 1000).toLocaleString()})`); + console.log(` 总土地数: ${lands.length}`); + console.log(''); + } + + for (const land of lands) { + const id = toNum(land.id); + if (!land.unlocked) { + if (debug) console.log(` 土地#${id}: 未解锁`); + continue; + } + + const plant = land.plant; + if (!plant) { + result.empty.push(id); + if (debug) console.log(` 土地#${id}: 空地`); + continue; + } + if (!plant.phases || plant.phases.length === 0) { + const plantId = toNum(plant.id); + if (plantId > 0 || plant.name) { + result.growing.push(id); + if (debug) console.log(` 土地#${id}: 生长中(阶段未知)`); + } else { + result.empty.push(id); + if (debug) console.log(` 土地#${id}: 空地`); + } + continue; + } + + const plantName = plant.name || '未知作物'; + const landLabel = `土地#${id}(${plantName})`; + + if (debug) { + console.log(` ${landLabel}: phases=${plant.phases.length} dry_num=${toNum(plant.dry_num)} weed_owners=${(plant.weed_owners||[]).length} insect_owners=${(plant.insect_owners||[]).length}`); + } + + const currentPhase = getCurrentPhase(plant.phases, debug, landLabel); + if (!currentPhase) { + result.empty.push(id); + continue; + } + const phaseVal = currentPhase.phase; + + if (phaseVal === PlantPhase.DEAD) { + result.dead.push(id); + if (debug) console.log(` → 结果: 枯死`); + continue; + } + + if (phaseVal === PlantPhase.MATURE) { + result.harvestable.push(id); + // 收集植物信息用于日志 + const plantId = toNum(plant.id); + const plantNameFromConfig = getPlantName(plantId); + const plantExp = getPlantExp(plantId); + result.harvestableInfo.push({ + landId: id, + plantId, + name: plantNameFromConfig || plantName, + exp: plantExp, + }); + if (debug) console.log(` → 结果: 可收获 (${plantNameFromConfig} +${plantExp}经验)`); + continue; + } + + let landNeeds = []; + const dryNum = toNum(plant.dry_num); + const dryTime = toTimeSec(currentPhase.dry_time); + if (dryNum > 0 || (dryTime > 0 && dryTime <= nowSec)) { + result.needWater.push(id); + landNeeds.push('缺水'); + } + + const weedsTime = toTimeSec(currentPhase.weeds_time); + const hasWeeds = (plant.weed_owners && plant.weed_owners.length > 0) || (weedsTime > 0 && weedsTime <= nowSec); + if (hasWeeds) { + result.needWeed.push(id); + landNeeds.push('有草'); + } + + const insectTime = toTimeSec(currentPhase.insect_time); + const hasBugs = (plant.insect_owners && plant.insect_owners.length > 0) || (insectTime > 0 && insectTime <= nowSec); + if (hasBugs) { + result.needBug.push(id); + landNeeds.push('有虫'); + } + + result.growing.push(id); + if (debug) { + const needStr = landNeeds.length > 0 ? ` 需要: ${landNeeds.join(',')}` : ''; + console.log(` → 结果: 生长中(${PHASE_NAMES[phaseVal] || phaseVal})${needStr}`); + } + } + + if (debug) { + console.log(''); + console.log('========== 巡田分析汇总 =========='); + console.log(` 可收获: ${result.harvestable.length} [${result.harvestable.join(',')}]`); + console.log(` 生长中: ${result.growing.length} [${result.growing.join(',')}]`); + console.log(` 缺水: ${result.needWater.length} [${result.needWater.join(',')}]`); + console.log(` 有草: ${result.needWeed.length} [${result.needWeed.join(',')}]`); + console.log(` 有虫: ${result.needBug.length} [${result.needBug.join(',')}]`); + console.log(` 空地: ${result.empty.length} [${result.empty.join(',')}]`); + console.log(` 枯死: ${result.dead.length} [${result.dead.join(',')}]`); + console.log('===================================='); + console.log(''); + } + + return result; +} + +// ============ 巡田主循环 ============ + +async function checkFarm() { + const state = getUserState(); + if (isCheckingFarm || !state.gid) return; + isCheckingFarm = true; + + try { + const landsReply = await getAllLands(); + if (!landsReply.lands || landsReply.lands.length === 0) { + log('农场', '没有土地数据'); + return; + } + + const lands = landsReply.lands; + const status = analyzeLands(lands); + const unlockedLandCount = lands.filter(land => land && land.unlocked).length; + isFirstFarmCheck = false; + + // 构建状态摘要 + const statusParts = []; + if (status.harvestable.length) statusParts.push(`收:${status.harvestable.length}`); + if (status.needWeed.length) statusParts.push(`草:${status.needWeed.length}`); + if (status.needBug.length) statusParts.push(`虫:${status.needBug.length}`); + if (status.needWater.length) statusParts.push(`水:${status.needWater.length}`); + if (status.dead.length) statusParts.push(`枯:${status.dead.length}`); + if (status.empty.length) statusParts.push(`空:${status.empty.length}`); + statusParts.push(`长:${status.growing.length}`); + + const hasWork = status.harvestable.length || status.needWeed.length || status.needBug.length + || status.needWater.length || status.dead.length || status.empty.length; + + // 执行操作并收集结果 + const actions = []; + + // 一键操作:除草、除虫、浇水可以并行执行(游戏中都是一键完成) + const batchOps = []; + if (status.needWeed.length > 0) { + batchOps.push(weedOut(status.needWeed).then(() => actions.push(`除草${status.needWeed.length}`)).catch(e => logWarn('除草', e.message))); + } + if (status.needBug.length > 0) { + batchOps.push(insecticide(status.needBug).then(() => actions.push(`除虫${status.needBug.length}`)).catch(e => logWarn('除虫', e.message))); + } + if (status.needWater.length > 0) { + batchOps.push(waterLand(status.needWater).then(() => actions.push(`浇水${status.needWater.length}`)).catch(e => logWarn('浇水', e.message))); + } + if (batchOps.length > 0) { + await Promise.all(batchOps); + } + + // 收获(一键操作) + let harvestedLandIds = []; + if (status.harvestable.length > 0) { + try { + await harvest(status.harvestable); + actions.push(`收获${status.harvestable.length}`); + harvestedLandIds = [...status.harvestable]; + } catch (e) { logWarn('收获', e.message); } + } + + // 铲除 + 种植 + 施肥(需要顺序执行) + const allDeadLands = [...status.dead, ...harvestedLandIds]; + const allEmptyLands = [...status.empty]; + if (allDeadLands.length > 0 || allEmptyLands.length > 0) { + try { + await autoPlantEmptyLands(allDeadLands, allEmptyLands, unlockedLandCount); + actions.push(`种植${allDeadLands.length + allEmptyLands.length}`); + } catch (e) { logWarn('种植', e.message); } + } + + // 输出一行日志 + const actionStr = actions.length > 0 ? ` → ${actions.join('/')}` : ''; + if(hasWork) { + log('农场', `[${statusParts.join(' ')}]${actionStr}${!hasWork ? ' 无需操作' : ''}`) + } + } catch (err) { + logWarn('巡田', `检查失败: ${err.message}`); + } finally { + isCheckingFarm = false; + } +} + +/** + * 农场巡查循环 - 本次完成后等待指定秒数再开始下次 + */ +async function farmCheckLoop() { + while (farmLoopRunning) { + await checkFarm(); + if (!farmLoopRunning) break; + await sleep(CONFIG.farmCheckInterval); + } +} + +function startFarmCheckLoop() { + if (farmLoopRunning) return; + farmLoopRunning = true; + + // 监听服务器推送的土地变化事件 + networkEvents.on('landsChanged', onLandsChangedPush); + + // 延迟 2 秒后启动循环 + farmCheckTimer = setTimeout(() => farmCheckLoop(), 2000); +} + +/** + * 处理服务器推送的土地变化 + */ +let lastPushTime = 0; +function onLandsChangedPush(lands) { + if (isCheckingFarm) return; + const now = Date.now(); + if (now - lastPushTime < 500) return; // 500ms 防抖 + + lastPushTime = now; + log('农场', `收到推送: ${lands.length}块土地变化,检查中...`); + + setTimeout(async () => { + if (!isCheckingFarm) { + await checkFarm(); + } + }, 100); +} + +function stopFarmCheckLoop() { + farmLoopRunning = false; + if (farmCheckTimer) { clearTimeout(farmCheckTimer); farmCheckTimer = null; } + networkEvents.removeListener('landsChanged', onLandsChangedPush); +} + +module.exports = { + checkFarm, startFarmCheckLoop, stopFarmCheckLoop, + getCurrentPhase, + setOperationLimitsCallback, +}; diff --git a/211/server/src/friend.js b/211/server/src/friend.js new file mode 100644 index 0000000..abf84db --- /dev/null +++ b/211/server/src/friend.js @@ -0,0 +1,650 @@ +/** + * 好友农场操作 - 进入/离开/帮忙/偷菜/巡查 + */ + +const { CONFIG, PlantPhase, PHASE_NAMES } = require('./config'); +const { types } = require('./proto'); +const { sendMsgAsync, getUserState, networkEvents } = require('./network'); +const { toLong, toNum, getServerTimeSec, log, logWarn, sleep } = require('./utils'); +const { getCurrentPhase, setOperationLimitsCallback } = require('./farm'); +const { getPlantName } = require('./gameConfig'); + +// ============ 内部状态 ============ +let isCheckingFriends = false; +let isFirstFriendCheck = true; +let friendCheckTimer = null; +let friendLoopRunning = false; +let lastResetDate = ''; // 上次重置日期 (YYYY-MM-DD) + +// 操作限制状态 (从服务器响应中更新) +// 操作类型ID (根据游戏代码): +// 10001 = 收获, 10002 = 铲除, 10003 = 放草, 10004 = 放虫 +// 10005 = 除草(帮好友), 10006 = 除虫(帮好友), 10007 = 浇水(帮好友), 10008 = 偷菜 +const operationLimits = new Map(); + +// 操作类型名称映射 +const OP_NAMES = { + 10001: '收获', + 10002: '铲除', + 10003: '放草', + 10004: '放虫', + 10005: '除草', + 10006: '除虫', + 10007: '浇水', + 10008: '偷菜', +}; + +// 配置: 是否只在有经验时才帮助好友 +const HELP_ONLY_WITH_EXP = true; // !!!无效,暂时无法判断。有修复方法但是暂时没打算更新出来 + +// 配置: 是否启用放虫放草功能 +const ENABLE_PUT_BAD_THINGS = false; // 无效!!!开启后会多次访问朋友导致被拉黑 请勿更改暂时关闭放虫放草功能 + +// ============ 好友 API ============ + +async function getAllFriends() { + const body = types.GetAllFriendsRequest.encode(types.GetAllFriendsRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'GetAll', body); + return types.GetAllFriendsReply.decode(replyBody); +} + +// ============ 好友申请 API (微信同玩) ============ + +async function getApplications() { + const body = types.GetApplicationsRequest.encode(types.GetApplicationsRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'GetApplications', body); + return types.GetApplicationsReply.decode(replyBody); +} + +async function acceptFriends(gids) { + const body = types.AcceptFriendsRequest.encode(types.AcceptFriendsRequest.create({ + friend_gids: gids.map(g => toLong(g)), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'AcceptFriends', body); + return types.AcceptFriendsReply.decode(replyBody); +} + +async function enterFriendFarm(friendGid) { + const body = types.VisitEnterRequest.encode(types.VisitEnterRequest.create({ + host_gid: toLong(friendGid), + reason: 2, // ENTER_REASON_FRIEND + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.visitpb.VisitService', 'Enter', body); + return types.VisitEnterReply.decode(replyBody); +} + +async function leaveFriendFarm(friendGid) { + const body = types.VisitLeaveRequest.encode(types.VisitLeaveRequest.create({ + host_gid: toLong(friendGid), + })).finish(); + try { + await sendMsgAsync('gamepb.visitpb.VisitService', 'Leave', body); + } catch (e) { /* 离开失败不影响主流程 */ } +} + +/** + * 检查是否需要重置每日限制 (0点刷新) + */ +function checkDailyReset() { + const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD + if (lastResetDate !== today) { + if (lastResetDate !== '') { + log('系统', '跨日重置,清空操作限制缓存'); + } + operationLimits.clear(); + lastResetDate = today; + } +} + +/** + * 更新操作限制状态 + */ +function updateOperationLimits(limits) { + if (!limits || limits.length === 0) return; + checkDailyReset(); + for (const limit of limits) { + const id = toNum(limit.id); + if (id > 0) { + const data = { + dayTimes: toNum(limit.day_times), + dayTimesLimit: toNum(limit.day_times_lt), + dayExpTimes: toNum(limit.day_exp_times), + dayExpTimesLimit: toNum(limit.day_ex_times_lt), // 注意: 字段名是 day_ex_times_lt (少个p) + }; + operationLimits.set(id, data); + } + } +} + +/** + * 检查某操作是否还能获得经验 + */ +function canGetExp(opId) { + const limit = operationLimits.get(opId); + if (!limit) return false; // 没有限制信息,保守起见不帮助(等待农场检查获取限制) + if (limit.dayExpTimesLimit <= 0) return true; // 没有经验上限 + return limit.dayExpTimes < limit.dayExpTimesLimit; +} + +/** + * 检查某操作是否还有次数 + */ +function canOperate(opId) { + const limit = operationLimits.get(opId); + if (!limit) return true; + if (limit.dayTimesLimit <= 0) return true; + return limit.dayTimes < limit.dayTimesLimit; +} + +/** + * 获取某操作剩余次数 + */ +function getRemainingTimes(opId) { + const limit = operationLimits.get(opId); + if (!limit || limit.dayTimesLimit <= 0) return 999; + return Math.max(0, limit.dayTimesLimit - limit.dayTimes); +} + +/** + * 获取操作限制摘要 (用于日志显示) + */ +function getOperationLimitsSummary() { + const parts = []; + // 帮助好友操作 (10005=除草, 10006=除虫, 10007=浇水, 10008=偷菜) + for (const id of [10005, 10006, 10007, 10008]) { + const limit = operationLimits.get(id); + if (limit && limit.dayExpTimesLimit > 0) { + const name = OP_NAMES[id] || `#${id}`; + const expLeft = limit.dayExpTimesLimit - limit.dayExpTimes; + parts.push(`${name}${expLeft}/${limit.dayExpTimesLimit}`); + } + } + // 捣乱操作 (10003=放草, 10004=放虫) + for (const id of [10003, 10004]) { + const limit = operationLimits.get(id); + if (limit && limit.dayTimesLimit > 0) { + const name = OP_NAMES[id] || `#${id}`; + const left = limit.dayTimesLimit - limit.dayTimes; + parts.push(`${name}${left}/${limit.dayTimesLimit}`); + } + } + return parts; +} + +async function helpWater(friendGid, landIds) { + const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body); + const reply = types.WaterLandReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function helpWeed(friendGid, landIds) { + const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body); + const reply = types.WeedOutReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function helpInsecticide(friendGid, landIds) { + const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body); + const reply = types.InsecticideReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function stealHarvest(friendGid, landIds) { + const body = types.HarvestRequest.encode(types.HarvestRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + is_all: true, + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body); + const reply = types.HarvestReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function putInsects(friendGid, landIds) { + const body = types.PutInsectsRequest.encode(types.PutInsectsRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'PutInsects', body); + const reply = types.PutInsectsReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function putWeeds(friendGid, landIds) { + const body = types.PutWeedsRequest.encode(types.PutWeedsRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'PutWeeds', body); + const reply = types.PutWeedsReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +// ============ 好友土地分析 ============ + +// 调试开关 - 设为好友名字可只查看该好友的土地分析详情,设为 true 查看全部,false 关闭 +const DEBUG_FRIEND_LANDS = false; + +function analyzeFriendLands(lands, myGid, friendName = '') { + const result = { + stealable: [], // 可偷 + stealableInfo: [], // 可偷植物信息 { landId, plantId, name } + needWater: [], // 需要浇水 + needWeed: [], // 需要除草 + needBug: [], // 需要除虫 + canPutWeed: [], // 可以放草 + canPutBug: [], // 可以放虫 + }; + + for (const land of lands) { + const id = toNum(land.id); + const plant = land.plant; + // 是否显示此好友的调试信息 + const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === friendName; + + if (!plant || !plant.phases || plant.phases.length === 0) { + if (showDebug) console.log(` [${friendName}] 土地#${id}: 无植物或无阶段数据`); + continue; + } + + const currentPhase = getCurrentPhase(plant.phases, showDebug, `[${friendName}]土地#${id}`); + if (!currentPhase) { + if (showDebug) console.log(` [${friendName}] 土地#${id}: getCurrentPhase返回null`); + continue; + } + const phaseVal = currentPhase.phase; + + if (showDebug) { + const insectOwners = plant.insect_owners || []; + const weedOwners = plant.weed_owners || []; + console.log(` [${friendName}] 土地#${id}: phase=${phaseVal} stealable=${plant.stealable} dry=${toNum(plant.dry_num)} weed=${weedOwners.length} bug=${insectOwners.length}`); + } + + if (phaseVal === PlantPhase.MATURE) { + if (plant.stealable) { + result.stealable.push(id); + const plantId = toNum(plant.id); + const plantName = getPlantName(plantId) || plant.name || '未知'; + result.stealableInfo.push({ landId: id, plantId, name: plantName }); + } else if (showDebug) { + console.log(` [${friendName}] 土地#${id}: 成熟但stealable=false (可能已被偷过)`); + } + continue; + } + + if (phaseVal === PlantPhase.DEAD) continue; + + // 帮助操作 + if (toNum(plant.dry_num) > 0) result.needWater.push(id); + if (plant.weed_owners && plant.weed_owners.length > 0) result.needWeed.push(id); + if (plant.insect_owners && plant.insect_owners.length > 0) result.needBug.push(id); + + // 捣乱操作: 检查是否可以放草/放虫 + // 条件: 没有草且我没放过草 + const weedOwners = plant.weed_owners || []; + const insectOwners = plant.insect_owners || []; + const iAlreadyPutWeed = weedOwners.some(gid => toNum(gid) === myGid); + const iAlreadyPutBug = insectOwners.some(gid => toNum(gid) === myGid); + + // 每块地最多2个草/虫,且我没放过 + if (weedOwners.length < 2 && !iAlreadyPutWeed) { + result.canPutWeed.push(id); + } + if (insectOwners.length < 2 && !iAlreadyPutBug) { + result.canPutBug.push(id); + } + } + return result; +} + +// ============ 拜访好友 ============ + +async function visitFriend(friend, totalActions, myGid) { + const { gid, name } = friend; + const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === name; + + if (showDebug) { + console.log(`\n========== 调试: 进入好友 [${name}] 农场 ==========`); + } + + let enterReply; + try { + enterReply = await enterFriendFarm(gid); + } catch (e) { + logWarn('好友', `进入 ${name} 农场失败: ${e.message}`); + return; + } + + const lands = enterReply.lands || []; + if (showDebug) { + console.log(` [${name}] 获取到 ${lands.length} 块土地`); + } + if (lands.length === 0) { + await leaveFriendFarm(gid); + return; + } + + const status = analyzeFriendLands(lands, myGid, name); + + if (showDebug) { + console.log(` [${name}] 分析结果: 可偷=${status.stealable.length} 浇水=${status.needWater.length} 除草=${status.needWeed.length} 除虫=${status.needBug.length}`); + console.log(`========== 调试结束 ==========\n`); + } + + // 执行操作 + const actions = []; + + // 帮助操作: 只在有经验时执行 (如果启用了 HELP_ONLY_WITH_EXP) + if (status.needWeed.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10005); // 10005=除草 + if (shouldHelp) { + let ok = 0; + for (const landId of status.needWeed) { + try { await helpWeed(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`草${ok}`); totalActions.weed += ok; } + } + } + + if (status.needBug.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10006); // 10006=除虫 + if (shouldHelp) { + let ok = 0; + for (const landId of status.needBug) { + try { await helpInsecticide(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`虫${ok}`); totalActions.bug += ok; } + } + } + + if (status.needWater.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10007); // 10007=浇水 + if (shouldHelp) { + let ok = 0; + for (const landId of status.needWater) { + try { await helpWater(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`水${ok}`); totalActions.water += ok; } + } + } + + // 偷菜: 始终执行 + if (status.stealable.length > 0) { + let ok = 0; + const stolenPlants = []; + for (let i = 0; i < status.stealable.length; i++) { + const landId = status.stealable[i]; + try { + await stealHarvest(gid, [landId]); + ok++; + if (status.stealableInfo[i]) { + stolenPlants.push(status.stealableInfo[i].name); + } + } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { + const plantNames = [...new Set(stolenPlants)].join('/'); + actions.push(`偷${ok}${plantNames ? '(' + plantNames + ')' : ''}`); + totalActions.steal += ok; + } + } + + // 捣乱操作: 放虫(10004)/放草(10003) + if (ENABLE_PUT_BAD_THINGS && status.canPutBug.length > 0 && canOperate(10004)) { + let ok = 0; + const remaining = getRemainingTimes(10004); + const toProcess = status.canPutBug.slice(0, remaining); + for (const landId of toProcess) { + if (!canOperate(10004)) break; + try { await putInsects(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`放虫${ok}`); totalActions.putBug += ok; } + } + + if (ENABLE_PUT_BAD_THINGS && status.canPutWeed.length > 0 && canOperate(10003)) { + let ok = 0; + const remaining = getRemainingTimes(10003); + const toProcess = status.canPutWeed.slice(0, remaining); + for (const landId of toProcess) { + if (!canOperate(10003)) break; + try { await putWeeds(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`放草${ok}`); totalActions.putWeed += ok; } + } + + if (actions.length > 0) { + log('好友', `${name}: ${actions.join('/')}`); + } + + await leaveFriendFarm(gid); +} + +// ============ 好友巡查主循环 ============ + +async function checkFriends() { + const state = getUserState(); + if (isCheckingFriends || !state.gid) return; + isCheckingFriends = true; + + // 检查是否跨日需要重置 + checkDailyReset(); + + // 经验限制状态(移到有操作时才显示) + + try { + const friendsReply = await getAllFriends(); + const friends = friendsReply.game_friends || []; + if (friends.length === 0) { log('好友', '没有好友'); return; } + + // 检查是否还有捣乱次数 (放虫/放草) + const canPutBugOrWeed = canOperate(10004) || canOperate(10003); // 10004=放虫, 10003=放草 + + // 分两类:有预览信息的优先访问,其他的放后面(用于放虫放草) + const priorityFriends = []; // 有可偷/可帮助的好友 + const otherFriends = []; // 其他好友(仅用于放虫放草) + const visitedGids = new Set(); + + for (const f of friends) { + const gid = toNum(f.gid); + if (gid === state.gid) continue; + if (visitedGids.has(gid)) continue; + const name = f.remark || f.name || `GID:${gid}`; + const p = f.plant; + + const stealNum = p ? toNum(p.steal_plant_num) : 0; + const dryNum = p ? toNum(p.dry_num) : 0; + const weedNum = p ? toNum(p.weed_num) : 0; + const insectNum = p ? toNum(p.insect_num) : 0; + + // 调试:显示指定好友的预览信息 + const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === name; + if (showDebug) { + console.log(`[调试] 好友列表预览 [${name}]: steal=${stealNum} dry=${dryNum} weed=${weedNum} insect=${insectNum}`); + } + + // 只加入有预览信息的好友 + if (stealNum > 0 || dryNum > 0 || weedNum > 0 || insectNum > 0) { + priorityFriends.push({ gid, name }); + visitedGids.add(gid); + if (showDebug) { + console.log(`[调试] 好友 [${name}] 加入优先列表 (位置: ${priorityFriends.length})`); + } + } else if (ENABLE_PUT_BAD_THINGS && canPutBugOrWeed) { + // 没有预览信息但可以放虫放草(仅在开启放虫放草功能时) + otherFriends.push({ gid, name }); + visitedGids.add(gid); + } + } + + // 合并列表:优先好友在前 + const friendsToVisit = [...priorityFriends, ...otherFriends]; + + // 调试:检查目标好友位置 + if (DEBUG_FRIEND_LANDS && typeof DEBUG_FRIEND_LANDS === 'string') { + const idx = friendsToVisit.findIndex(f => f.name === DEBUG_FRIEND_LANDS); + if (idx >= 0) { + const inPriority = idx < priorityFriends.length; + console.log(`[调试] 好友 [${DEBUG_FRIEND_LANDS}] 位置: ${idx + 1}/${friendsToVisit.length} (${inPriority ? '优先列表' : '其他列表'})`); + } else { + console.log(`[调试] 好友 [${DEBUG_FRIEND_LANDS}] 不在待访问列表中!`); + } + } + + if (friendsToVisit.length === 0) { + // 无需操作时不输出日志 + return; + } + + let totalActions = { steal: 0, water: 0, weed: 0, bug: 0, putBug: 0, putWeed: 0 }; + for (let i = 0; i < friendsToVisit.length; i++) { + const friend = friendsToVisit[i]; + const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === friend.name; + if (showDebug) { + console.log(`[调试] 准备访问 [${friend.name}] (${i + 1}/${friendsToVisit.length})`); + } + try { + await visitFriend(friend, totalActions, state.gid); + } catch (e) { + if (showDebug) { + console.log(`[调试] 访问 [${friend.name}] 出错: ${e.message}`); + } + } + await sleep(500); + // 如果捣乱次数用完了,且没有其他操作,可以提前结束 + if (!canOperate(10004) && !canOperate(10003)) { // 10004=放虫, 10003=放草 + // 继续巡查,但不再放虫放草 + } + } + + // 只在有操作时输出日志 + const summary = []; + if (totalActions.steal > 0) summary.push(`偷${totalActions.steal}`); + if (totalActions.weed > 0) summary.push(`除草${totalActions.weed}`); + if (totalActions.bug > 0) summary.push(`除虫${totalActions.bug}`); + if (totalActions.water > 0) summary.push(`浇水${totalActions.water}`); + if (totalActions.putBug > 0) summary.push(`放虫${totalActions.putBug}`); + if (totalActions.putWeed > 0) summary.push(`放草${totalActions.putWeed}`); + + if (summary.length > 0) { + log('好友', `巡查 ${friendsToVisit.length} 人 → ${summary.join('/')}`); + } + isFirstFriendCheck = false; + } catch (err) { + logWarn('好友', `巡查失败: ${err.message}`); + } finally { + isCheckingFriends = false; + } +} + +/** + * 好友巡查循环 - 本次完成后等待指定秒数再开始下次 + */ +async function friendCheckLoop() { + while (friendLoopRunning) { + await checkFriends(); + if (!friendLoopRunning) break; + await sleep(CONFIG.friendCheckInterval); + } +} + +function startFriendCheckLoop() { + if (friendLoopRunning) return; + friendLoopRunning = true; + + // 注册操作限制更新回调,从农场检查中获取限制信息 + setOperationLimitsCallback(updateOperationLimits); + + // 监听好友申请推送 (微信同玩) + networkEvents.on('friendApplicationReceived', onFriendApplicationReceived); + + // 延迟 5 秒后启动循环,等待登录和首次农场检查完成 + friendCheckTimer = setTimeout(() => friendCheckLoop(), 5000); + + // 启动时检查一次待处理的好友申请 + setTimeout(() => checkAndAcceptApplications(), 3000); +} + +function stopFriendCheckLoop() { + friendLoopRunning = false; + networkEvents.off('friendApplicationReceived', onFriendApplicationReceived); + if (friendCheckTimer) { clearTimeout(friendCheckTimer); friendCheckTimer = null; } +} + +// ============ 自动同意好友申请 (微信同玩) ============ + +/** + * 处理服务器推送的好友申请 + */ +function onFriendApplicationReceived(applications) { + const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', '); + log('申请', `收到 ${applications.length} 个好友申请: ${names}`); + + // 自动同意 + const gids = applications.map(a => toNum(a.gid)); + acceptFriendsWithRetry(gids); +} + +/** + * 检查并同意所有待处理的好友申请 + */ +async function checkAndAcceptApplications() { + try { + const reply = await getApplications(); + const applications = reply.applications || []; + if (applications.length === 0) return; + + const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', '); + log('申请', `发现 ${applications.length} 个待处理申请: ${names}`); + + const gids = applications.map(a => toNum(a.gid)); + await acceptFriendsWithRetry(gids); + } catch (e) { + // 静默失败,可能是 QQ 平台不支持 + } +} + +/** + * 同意好友申请 (带重试) + */ +async function acceptFriendsWithRetry(gids) { + if (gids.length === 0) return; + try { + const reply = await acceptFriends(gids); + const friends = reply.friends || []; + if (friends.length > 0) { + const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', '); + log('申请', `已同意 ${friends.length} 人: ${names}`); + } + } catch (e) { + logWarn('申请', `同意失败: ${e.message}`); + } +} + +module.exports = { + checkFriends, startFriendCheckLoop, stopFriendCheckLoop, + checkAndAcceptApplications, +}; diff --git a/211/server/src/gameConfig.js b/211/server/src/gameConfig.js new file mode 100644 index 0000000..1264dd7 --- /dev/null +++ b/211/server/src/gameConfig.js @@ -0,0 +1,269 @@ +/** + * 游戏配置数据模块 + * 从 gameConfig 目录加载配置数据 + */ + +const fs = require('fs'); +const path = require('path'); + +// ============ 等级经验表 ============ +let roleLevelConfig = null; +let levelExpTable = null; // 累计经验表,索引为等级 + +// ============ 植物配置 ============ +let plantConfig = null; +let plantMap = new Map(); // id -> plant +let seedToPlant = new Map(); // seed_id -> plant +let fruitToPlant = new Map(); // fruit_id -> plant (果实ID -> 植物) +let nameToPlant = new Map(); + +/** + * 加载配置文件 + */ +function loadConfigs() { + const configDir = path.join(__dirname, '..', 'gameConfig'); + + // 加载等级经验配置 + try { + const roleLevelPath = path.join(configDir, 'RoleLevel.json'); + if (fs.existsSync(roleLevelPath)) { + roleLevelConfig = JSON.parse(fs.readFileSync(roleLevelPath, 'utf8')); + // 构建累计经验表 + levelExpTable = []; + for (const item of roleLevelConfig) { + levelExpTable[item.level] = item.exp; + } + console.log(`[配置] 已加载等级经验表 (${roleLevelConfig.length} 级)`); + } + } catch (e) { + console.warn('[配置] 加载 RoleLevel.json 失败:', e.message); + } + + // 加载植物配置 + try { + const plantPath = path.join(configDir, 'Plant.json'); + if (fs.existsSync(plantPath)) { + plantConfig = JSON.parse(fs.readFileSync(plantPath, 'utf8')); + plantMap.clear(); + seedToPlant.clear(); + fruitToPlant.clear(); + nameToPlant.clear(); + for (const plant of plantConfig) { + plantMap.set(plant.id, plant); + if (plant.seed_id) { + seedToPlant.set(plant.seed_id, plant); + } + if (plant.fruit && plant.fruit.id) { + fruitToPlant.set(plant.fruit.id, plant); + } + if (plant.name) { + nameToPlant.set(plant.name, plant); + } + } + console.log(`[配置] 已加载植物配置 (${plantConfig.length} 种)`); + } + } catch (e) { + console.warn('[配置] 加载 Plant.json 失败:', e.message); + } +} + +// ============ 等级经验相关 ============ + +/** + * 获取等级经验表 + */ +function getLevelExpTable() { + return levelExpTable; +} + +/** + * 计算当前等级的经验进度 + * @param {number} level - 当前等级 + * @param {number} totalExp - 累计总经验 + * @returns {{ current: number, needed: number }} 当前等级经验进度 + */ +function getLevelExpProgress(level, totalExp) { + if (!levelExpTable || level <= 0) return { current: 0, needed: 0 }; + + const currentLevelStart = levelExpTable[level] || 0; + const nextLevelStart = levelExpTable[level + 1] || (currentLevelStart + 100000); + + const currentExp = Math.max(0, totalExp - currentLevelStart); + const neededExp = nextLevelStart - currentLevelStart; + + return { current: currentExp, needed: neededExp }; +} + +// ============ 植物配置相关 ============ + +/** + * 根据植物ID获取植物信息 + * @param {number} plantId - 植物ID + */ +function getPlantById(plantId) { + return plantMap.get(plantId); +} + +function getPlantByName(name) { + return nameToPlant.get(name); +} + +/** + * 根据种子ID获取植物信息 + * @param {number} seedId - 种子ID + */ +function getPlantBySeedId(seedId) { + return seedToPlant.get(seedId); +} + +function getSeedIdByPlantName(name) { + const plant = nameToPlant.get(name); + if (!plant) return 0; + return plant.seed_id || 0; +} + +/** + * 获取植物名称 + * @param {number} plantId - 植物ID + */ +function getPlantName(plantId) { + const plant = plantMap.get(plantId); + return plant ? plant.name : `植物${plantId}`; +} + +/** + * 根据种子ID获取植物名称 + * @param {number} seedId - 种子ID + */ +function getPlantNameBySeedId(seedId) { + const plant = seedToPlant.get(seedId); + return plant ? plant.name : `种子${seedId}`; +} + +/** + * 获取植物的果实信息 + * @param {number} plantId - 植物ID + * @returns {{ id: number, count: number, name: string } | null} + */ +function getPlantFruit(plantId) { + const plant = plantMap.get(plantId); + if (!plant || !plant.fruit) return null; + return { + id: plant.fruit.id, + count: plant.fruit.count, + name: plant.name, + }; +} + +/** + * 获取植物的生长时间(秒) + * @param {number} plantId - 植物ID + */ +function getPlantGrowTime(plantId) { + const plant = plantMap.get(plantId); + if (!plant || !plant.grow_phases) return 0; + + // 解析 "种子:30;发芽:30;成熟:0;" 格式 + const phases = plant.grow_phases.split(';').filter(p => p); + let totalSeconds = 0; + for (const phase of phases) { + const match = phase.match(/:(\d+)/); + if (match) { + totalSeconds += parseInt(match[1]); + } + } + return totalSeconds; +} + +/** + * 格式化时间 + * @param {number} seconds - 秒数 + */ +function formatGrowTime(seconds) { + if (seconds < 60) return `${seconds}秒`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`; + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return mins > 0 ? `${hours}小时${mins}分` : `${hours}小时`; +} + +/** + * 获取植物的收获经验 + * @param {number} plantId - 植物ID + */ +function getPlantExp(plantId) { + const plant = plantMap.get(plantId); + return plant ? plant.exp : 0; +} + +/** + * 根据果实ID获取植物名称 + * @param {number} fruitId - 果实ID + */ +function getFruitName(fruitId) { + const plant = fruitToPlant.get(fruitId); + return plant ? plant.name : `果实${fruitId}`; +} + +/** + * 根据果实ID获取植物信息 + * @param {number} fruitId - 果实ID + */ +function getPlantByFruitId(fruitId) { + return fruitToPlant.get(fruitId); +} + +const ITEM_NAME_MAP = { + 1001: '金币', + 1002: '点券', + 1011: '普通化肥容器', + 1012: '有机化肥容器', + 1013: '友谊果实', + 1014: '穗华', + 1015: '幸运币', + 3001: '普通收藏点', + 1101: '种植经验', + 80001: '普通(1小时)', + 80011: '有机(1小时)', + 80002: '普通(4小时)', + 80003: '普通(8小时)', + 80004: '普通(12小时)', + 80012: '有机(4小时)', + 80013: '有机(8小时)', + 80014: '有机(12小时)', + 90004: '1天狗粮', + 90005: '3天狗粮', + 100003: '化肥礼包', +}; + +function getItemNameById(id) { + const name = ITEM_NAME_MAP[id]; + if (name) return name; + return `物品${id}`; +} + +// 启动时加载配置 +loadConfigs(); + +module.exports = { + loadConfigs, + // 等级经验 + getLevelExpTable, + getLevelExpProgress, + // 植物配置 + getPlantById, + getPlantByName, + getPlantBySeedId, + getSeedIdByPlantName, + getPlantName, + getPlantNameBySeedId, + getPlantFruit, + getPlantGrowTime, + getPlantExp, + formatGrowTime, + // 果实配置 + getFruitName, + getPlantByFruitId, + // 物品配置 + getItemNameById, +}; diff --git a/211/server/src/invite.js b/211/server/src/invite.js new file mode 100644 index 0000000..0efe299 --- /dev/null +++ b/211/server/src/invite.js @@ -0,0 +1,161 @@ +/** + * 邀请码处理模块 - 读取 share.txt 并通过 ReportArkClick 申请好友 + * 注意:此功能仅在微信环境下有效 + * + * 原理: + * 1. 首次登录时,游戏会在 LoginRequest 中携带 sharer_id 和 sharer_open_id + * 2. 已登录状态下点击分享链接,游戏会发送 ReportArkClickRequest + * 3. 服务器收到后会自动向分享者发送好友申请 + * + * 我们使用 ReportArkClickRequest 来模拟已登录状态下的分享链接点击 + */ + +const fs = require('fs'); +const path = require('path'); +const { types } = require('./proto'); +const { sendMsgAsync } = require('./network'); +const { toLong, log, logWarn, sleep } = require('./utils'); +const { CONFIG } = require('./config'); + +/** + * 解析分享链接,提取 uid 和 openid + * 格式: ?uid=xxx&openid=xxx&share_source=xxx&doc_id=xxx + */ +function parseShareLink(link) { + const result = { uid: null, openid: null, shareSource: null, docId: null }; + + // 移除开头的 ? 如果有 + const queryStr = link.startsWith('?') ? link.slice(1) : link; + + // 解析参数 + const params = new URLSearchParams(queryStr); + result.uid = params.get('uid'); + result.openid = params.get('openid'); + result.shareSource = params.get('share_source'); + result.docId = params.get('doc_id'); + + return result; +} + +/** + * 读取 share.txt 文件并去重 + */ +function readShareFile() { + const shareFilePath = path.join(__dirname, '..', 'share.txt'); + + if (!fs.existsSync(shareFilePath)) { + return []; + } + + try { + const content = fs.readFileSync(shareFilePath, 'utf8'); + const lines = content.split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0 && line.includes('openid=')); + + const invites = []; + const seenUids = new Set(); // 用于去重 + + for (const line of lines) { + const parsed = parseShareLink(line); + if (parsed.openid && parsed.uid) { + // 按 uid 去重,同一个用户只处理一次 + if (!seenUids.has(parsed.uid)) { + seenUids.add(parsed.uid); + invites.push(parsed); + } + } + } + + return invites; + } catch (e) { + logWarn('邀请', `读取 share.txt 失败: ${e.message}`); + return []; + } +} + +/** + * 发送 ReportArkClick 请求 + * 模拟已登录状态下点击分享链接,触发服务器向分享者发送好友申请 + */ +async function sendReportArkClick(sharerId, sharerOpenId, shareSource) { + const body = types.ReportArkClickRequest.encode(types.ReportArkClickRequest.create({ + sharer_id: toLong(sharerId), + sharer_open_id: sharerOpenId, + share_cfg_id: toLong(shareSource || 0), + scene_id: '1256', // 模拟微信场景 + })).finish(); + + const { body: replyBody } = await sendMsgAsync('gamepb.userpb.UserService', 'ReportArkClick', body); + return types.ReportArkClickReply.decode(replyBody); +} + +// 请求间隔时间(毫秒) +const INVITE_REQUEST_DELAY = 2000; + +/** + * 处理邀请码列表 + * 仅在微信环境下执行 + */ +async function processInviteCodes() { + // 检查是否为微信环境 + if (CONFIG.platform !== 'wx') { + log('邀请', '当前为 QQ 环境,跳过邀请码处理(仅微信支持)'); + return; + } + + const invites = readShareFile(); + if (invites.length === 0) { + return; + } + + log('邀请', `读取到 ${invites.length} 个邀请码(已去重),开始逐个处理...`); + + let successCount = 0; + let failCount = 0; + + for (let i = 0; i < invites.length; i++) { + const invite = invites[i]; + + try { + // 发送 ReportArkClick 请求,模拟点击分享链接 + await sendReportArkClick(invite.uid, invite.openid, invite.shareSource); + successCount++; + log('邀请', `[${i + 1}/${invites.length}] 已向 uid=${invite.uid} 发送好友申请`); + } catch (e) { + failCount++; + logWarn('邀请', `[${i + 1}/${invites.length}] 向 uid=${invite.uid} 发送申请失败: ${e.message}`); + } + + // 每个请求之间延迟,避免请求过快被限流 + if (i < invites.length - 1) { + await sleep(INVITE_REQUEST_DELAY); + } + } + + log('邀请', `处理完成: 成功 ${successCount}, 失败 ${failCount}`); + + // 处理完成后清空文件 + clearShareFile(); +} + +/** + * 清空已处理的邀请码文件 + */ +function clearShareFile() { + const shareFilePath = path.join(__dirname, '..', 'share.txt'); + try { + fs.writeFileSync(shareFilePath, '', 'utf8'); + log('邀请', '已清空 share.txt'); + } catch (e) { + // 静默失败 + } +} + +module.exports = { + parseShareLink, + readShareFile, + sendReportArkClick, + processInviteCodes, + clearShareFile, +}; diff --git a/211/server/src/network.js b/211/server/src/network.js new file mode 100644 index 0000000..ae997b6 --- /dev/null +++ b/211/server/src/network.js @@ -0,0 +1,461 @@ +/** + * WebSocket 网络层 - 连接/消息编解码/登录/心跳 + */ + +const WebSocket = require('ws'); +const EventEmitter = require('events'); +const { CONFIG } = require('./config'); +const { types } = require('./proto'); +const { toLong, toNum, syncServerTime, log, logWarn } = require('./utils'); +const { updateStatusFromLogin, updateStatusGold, updateStatusLevel } = require('./status'); + +// ============ 事件发射器 (用于推送通知) ============ +const networkEvents = new EventEmitter(); + +// ============ 内部状态 ============ +let ws = null; +let clientSeq = 1; +let serverSeq = 0; +let heartbeatTimer = null; +let pendingCallbacks = new Map(); + +// ============ 用户状态 (登录后设置) ============ +const userState = { + gid: 0, + name: '', + level: 0, + gold: 0, + exp: 0, +}; + +function getUserState() { return userState; } + +// ============ 消息编解码 ============ +function encodeMsg(serviceName, methodName, bodyBytes) { + const msg = types.GateMessage.create({ + meta: { + service_name: serviceName, + method_name: methodName, + message_type: 1, + client_seq: toLong(clientSeq), + server_seq: toLong(serverSeq), + }, + body: bodyBytes || Buffer.alloc(0), + }); + const encoded = types.GateMessage.encode(msg).finish(); + clientSeq++; + return encoded; +} + +function sendMsg(serviceName, methodName, bodyBytes, callback) { + if (!ws || ws.readyState !== WebSocket.OPEN) { + log('WS', '连接未打开'); + return false; + } + const seq = clientSeq; + const encoded = encodeMsg(serviceName, methodName, bodyBytes); + if (callback) pendingCallbacks.set(seq, callback); + ws.send(encoded); + return true; +} + +/** Promise 版发送 */ +function sendMsgAsync(serviceName, methodName, bodyBytes, timeout = 10000) { + return new Promise((resolve, reject) => { + // 检查连接状态 + if (!ws || ws.readyState !== WebSocket.OPEN) { + reject(new Error(`连接未打开: ${methodName}`)); + return; + } + + const seq = clientSeq; + const timer = setTimeout(() => { + pendingCallbacks.delete(seq); + // 检查当前待处理的请求数 + const pending = pendingCallbacks.size; + reject(new Error(`请求超时: ${methodName} (seq=${seq}, pending=${pending})`)); + }, timeout); + + const sent = sendMsg(serviceName, methodName, bodyBytes, (err, body, meta) => { + clearTimeout(timer); + if (err) reject(err); + else resolve({ body, meta }); + }); + + if (!sent) { + clearTimeout(timer); + reject(new Error(`发送失败: ${methodName}`)); + } + }); +} + +// ============ 消息处理 ============ +function handleMessage(data) { + try { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); + const msg = types.GateMessage.decode(buf); + const meta = msg.meta; + if (!meta) return; + + if (meta.server_seq) { + const seq = toNum(meta.server_seq); + if (seq > serverSeq) serverSeq = seq; + } + + const msgType = meta.message_type; + + // Notify + if (msgType === 3) { + handleNotify(msg); + return; + } + + // Response + if (msgType === 2) { + const errorCode = toNum(meta.error_code); + const clientSeqVal = toNum(meta.client_seq); + + const cb = pendingCallbacks.get(clientSeqVal); + if (cb) { + pendingCallbacks.delete(clientSeqVal); + if (errorCode !== 0) { + cb(new Error(`${meta.service_name}.${meta.method_name} 错误: code=${errorCode} ${meta.error_message || ''}`)); + } else { + cb(null, msg.body, meta); + } + return; + } + + if (errorCode !== 0) { + logWarn('错误', `${meta.service_name}.${meta.method_name} code=${errorCode} ${meta.error_message || ''}`); + } + } + } catch (err) { + logWarn('解码', err.message); + } +} + +// 调试:记录所有推送类型 (设为 true 可查看所有推送) +// 注意:QQ环境下只有 ItemNotify 推送,没有 LandsNotify 推送 +const DEBUG_NOTIFY = false; + +function handleNotify(msg) { + if (!msg.body || msg.body.length === 0) return; + try { + const event = types.EventMessage.decode(msg.body); + const type = event.message_type || ''; + const eventBody = event.body; + + // 调试:显示所有推送类型 + if (DEBUG_NOTIFY) { + console.log(`[DEBUG] 收到推送: ${type}`); + } + + // 被踢下线 + if (type.includes('Kickout')) { + log('推送', `被踢下线! ${type}`); + try { + const notify = types.KickoutNotify.decode(eventBody); + log('推送', `原因: ${notify.reason_message || '未知'}`); + } catch (e) { } + return; + } + + // 土地状态变化 (被放虫/放草/偷菜等) + if (type.includes('LandsNotify')) { + try { + const notify = types.LandsNotify.decode(eventBody); + const hostGid = toNum(notify.host_gid); + const lands = notify.lands || []; + if (DEBUG_NOTIFY) { + console.log(`[DEBUG] LandsNotify: hostGid=${hostGid}, myGid=${userState.gid}, lands=${lands.length}`); + } + if (lands.length > 0) { + // 如果是自己的农场,触发事件 + if (hostGid === userState.gid || hostGid === 0) { + networkEvents.emit('landsChanged', lands); + } + } + } catch (e) { } + return; + } + + // 物品变化通知 (经验/金币等) - 仅更新状态栏 + // 金币: id=1 或 id=1001 (GodItemId) + // 经验: id=1101 (ExpItemId) 或 id=2 + if (type.includes('ItemNotify')) { + try { + const notify = types.ItemNotify.decode(eventBody); + const items = notify.items || []; + for (const itemChg of items) { + const item = itemChg.item; + if (!item) continue; + const id = toNum(item.id); + const count = toNum(item.count); + + if (id === 1101 || id === 2) { + userState.exp = count; + updateStatusLevel(userState.level, count); + } else if (id === 1 || id === 1001) { + userState.gold = count; + updateStatusGold(count); + } + } + } catch (e) { } + return; + } + + // 基本信息变化 (升级等) + if (type.includes('BasicNotify')) { + try { + const notify = types.BasicNotify.decode(eventBody); + if (notify.basic) { + const oldLevel = userState.level; + const oldExp = userState.exp || 0; + userState.level = toNum(notify.basic.level) || userState.level; + userState.gold = toNum(notify.basic.gold) || userState.gold; + const exp = toNum(notify.basic.exp); + if (exp > 0) { + userState.exp = exp; + updateStatusLevel(userState.level, exp); + } + updateStatusGold(userState.gold); + // 升级提示 + if (userState.level !== oldLevel) { + log('系统', `升级! Lv${oldLevel} → Lv${userState.level}`); + } + } + } catch (e) { } + return; + } + + // 好友申请通知 (微信同玩) + if (type.includes('FriendApplicationReceivedNotify')) { + try { + const notify = types.FriendApplicationReceivedNotify.decode(eventBody); + const applications = notify.applications || []; + if (applications.length > 0) { + networkEvents.emit('friendApplicationReceived', applications); + } + } catch (e) { } + return; + } + + // 好友添加成功通知 + if (type.includes('FriendAddedNotify')) { + try { + const notify = types.FriendAddedNotify.decode(eventBody); + const friends = notify.friends || []; + if (friends.length > 0) { + const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', '); + log('好友', `新好友: ${names}`); + } + } catch (e) { } + return; + } + + // 物品变化通知 (收获/购买/消耗等) + if (type.includes('ItemNotify')) { + try { + const notify = types.ItemNotify.decode(eventBody); + const items = notify.items || []; + for (const chg of items) { + if (!chg.item) continue; + const id = toNum(chg.item.id); + const count = toNum(chg.item.count); + const delta = toNum(chg.delta); + // 金币 ID=1 + if (id === 1) { + userState.gold = count; + updateStatusGold(count); + if (delta !== 0) { + log('物品', `金币 ${delta > 0 ? '+' : ''}${delta} (当前: ${count})`); + } + } + // 经验 ID=2 (升级由 BasicNotify 处理) + } + } catch (e) { } + return; + } + + // 商品解锁通知 (升级后解锁新种子等) + if (type.includes('GoodsUnlockNotify')) { + try { + const notify = types.GoodsUnlockNotify.decode(eventBody); + const goods = notify.goods_list || []; + if (goods.length > 0) { + log('商店', `解锁 ${goods.length} 个新商品!`); + } + } catch (e) { } + return; + } + + // 任务状态变化通知 + if (type.includes('TaskInfoNotify')) { + try { + const notify = types.TaskInfoNotify.decode(eventBody); + if (notify.task_info) { + networkEvents.emit('taskInfoNotify', notify.task_info); + } + } catch (e) { } + return; + } + + // 其他未处理的推送类型 (调试用) + // log('推送', `未处理类型: ${type}`); + } catch (e) { + logWarn('推送', `解码失败: ${e.message}`); + } +} + +// ============ 登录 ============ +function sendLogin(onLoginSuccess) { + const body = types.LoginRequest.encode(types.LoginRequest.create({ + sharer_id: toLong(0), + sharer_open_id: '', + device_info: { + client_version: CONFIG.clientVersion, + sys_software: 'iOS 26.2.1', + network: 'wifi', + memory: '7672', + device_id: 'iPhone X', + }, + share_cfg_id: toLong(0), + scene_id: '1256', + report_data: { + callback: '', cd_extend_info: '', click_id: '', clue_token: '', + minigame_channel: 'other', minigame_platid: 2, req_id: '', trackid: '', + }, + })).finish(); + + sendMsg('gamepb.userpb.UserService', 'Login', body, (err, bodyBytes, meta) => { + if (err) { + log('登录', `失败: ${err.message}`); + return; + } + try { + const reply = types.LoginReply.decode(bodyBytes); + if (reply.basic) { + userState.gid = toNum(reply.basic.gid); + userState.name = reply.basic.name || '未知'; + userState.level = toNum(reply.basic.level); + userState.gold = toNum(reply.basic.gold); + userState.exp = toNum(reply.basic.exp); + + // 更新状态栏 + updateStatusFromLogin({ + name: userState.name, + level: userState.level, + gold: userState.gold, + exp: userState.exp, + }); + + console.log(''); + console.log('========== 登录成功 =========='); + console.log(` GID: ${userState.gid}`); + console.log(` 昵称: ${userState.name}`); + console.log(` 等级: ${userState.level}`); + console.log(` 金币: ${userState.gold}`); + if (reply.time_now_millis) { + syncServerTime(toNum(reply.time_now_millis)); + console.log(` 时间: ${new Date(toNum(reply.time_now_millis)).toLocaleString()}`); + } + console.log('==============================='); + console.log(''); + } + + startHeartbeat(); + if (onLoginSuccess) onLoginSuccess(); + } catch (e) { + log('登录', `解码失败: ${e.message}`); + } + }); +} + +// ============ 心跳 ============ +let lastHeartbeatResponse = Date.now(); +let heartbeatMissCount = 0; + +function startHeartbeat() { + if (heartbeatTimer) clearInterval(heartbeatTimer); + lastHeartbeatResponse = Date.now(); + heartbeatMissCount = 0; + + heartbeatTimer = setInterval(() => { + if (!userState.gid) return; + + // 检查上次心跳响应时间,超过 60 秒没响应说明连接有问题 + const timeSinceLastResponse = Date.now() - lastHeartbeatResponse; + if (timeSinceLastResponse > 60000) { + heartbeatMissCount++; + logWarn('心跳', `连接可能已断开 (${Math.round(timeSinceLastResponse/1000)}s 无响应, pending=${pendingCallbacks.size})`); + if (heartbeatMissCount >= 2) { + log('心跳', '尝试重连...'); + // 清理待处理的回调,避免堆积 + pendingCallbacks.forEach((cb, seq) => { + try { cb(new Error('连接超时,已清理')); } catch (e) {} + }); + pendingCallbacks.clear(); + } + } + + const body = types.HeartbeatRequest.encode(types.HeartbeatRequest.create({ + gid: toLong(userState.gid), + client_version: CONFIG.clientVersion, + })).finish(); + sendMsg('gamepb.userpb.UserService', 'Heartbeat', body, (err, replyBody) => { + if (err || !replyBody) return; + lastHeartbeatResponse = Date.now(); + heartbeatMissCount = 0; + try { + const reply = types.HeartbeatReply.decode(replyBody); + if (reply.server_time) syncServerTime(toNum(reply.server_time)); + } catch (e) { } + }); + }, CONFIG.heartbeatInterval); +} + +// ============ WebSocket 连接 ============ +function connect(code, onLoginSuccess) { + const url = `${CONFIG.serverUrl}?platform=${CONFIG.platform}&os=${CONFIG.os}&ver=${CONFIG.clientVersion}&code=${code}&openID=`; + + ws = new WebSocket(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13)', + 'Origin': 'https://gate-obt.nqf.qq.com', + }, + }); + + ws.binaryType = 'arraybuffer'; + + ws.on('open', () => { + sendLogin(onLoginSuccess); + }); + + ws.on('message', (data) => { + handleMessage(Buffer.isBuffer(data) ? data : Buffer.from(data)); + }); + + ws.on('close', (code, reason) => { + console.log(`[WS] 连接关闭 (code=${code})`); + cleanup(); + }); + + ws.on('error', (err) => { + logWarn('WS', `错误: ${err.message}`); + }); +} + +function cleanup() { + if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } + pendingCallbacks.clear(); +} + +function getWs() { return ws; } + +module.exports = { + connect, cleanup, getWs, + sendMsg, sendMsgAsync, + getUserState, + networkEvents, +}; diff --git a/211/server/src/proto.js b/211/server/src/proto.js new file mode 100644 index 0000000..7c7109f --- /dev/null +++ b/211/server/src/proto.js @@ -0,0 +1,121 @@ +/** + * Proto 加载与消息类型管理 + */ + +const protobuf = require('protobufjs'); +const path = require('path'); +const { log } = require('./utils'); + +// Proto 根对象与所有消息类型 +let root = null; +const types = {}; + +async function loadProto() { + const protoDir = path.join(__dirname, '..', 'proto'); + root = new protobuf.Root(); + await root.load([ + path.join(protoDir, 'game.proto'), + path.join(protoDir, 'userpb.proto'), + path.join(protoDir, 'plantpb.proto'), + path.join(protoDir, 'corepb.proto'), + path.join(protoDir, 'shoppb.proto'), + path.join(protoDir, 'friendpb.proto'), + path.join(protoDir, 'visitpb.proto'), + path.join(protoDir, 'notifypb.proto'), + path.join(protoDir, 'taskpb.proto'), + path.join(protoDir, 'itempb.proto'), + ], { keepCase: true }); + + // 网关 + types.GateMessage = root.lookupType('gatepb.Message'); + types.GateMeta = root.lookupType('gatepb.Meta'); + types.EventMessage = root.lookupType('gatepb.EventMessage'); + + // 用户 + types.LoginRequest = root.lookupType('gamepb.userpb.LoginRequest'); + types.LoginReply = root.lookupType('gamepb.userpb.LoginReply'); + types.HeartbeatRequest = root.lookupType('gamepb.userpb.HeartbeatRequest'); + types.HeartbeatReply = root.lookupType('gamepb.userpb.HeartbeatReply'); + types.ReportArkClickRequest = root.lookupType('gamepb.userpb.ReportArkClickRequest'); + types.ReportArkClickReply = root.lookupType('gamepb.userpb.ReportArkClickReply'); + + // 农场 + types.AllLandsRequest = root.lookupType('gamepb.plantpb.AllLandsRequest'); + types.AllLandsReply = root.lookupType('gamepb.plantpb.AllLandsReply'); + types.HarvestRequest = root.lookupType('gamepb.plantpb.HarvestRequest'); + types.HarvestReply = root.lookupType('gamepb.plantpb.HarvestReply'); + types.WaterLandRequest = root.lookupType('gamepb.plantpb.WaterLandRequest'); + types.WaterLandReply = root.lookupType('gamepb.plantpb.WaterLandReply'); + types.WeedOutRequest = root.lookupType('gamepb.plantpb.WeedOutRequest'); + types.WeedOutReply = root.lookupType('gamepb.plantpb.WeedOutReply'); + types.InsecticideRequest = root.lookupType('gamepb.plantpb.InsecticideRequest'); + types.InsecticideReply = root.lookupType('gamepb.plantpb.InsecticideReply'); + types.RemovePlantRequest = root.lookupType('gamepb.plantpb.RemovePlantRequest'); + types.RemovePlantReply = root.lookupType('gamepb.plantpb.RemovePlantReply'); + types.PutInsectsRequest = root.lookupType('gamepb.plantpb.PutInsectsRequest'); + types.PutInsectsReply = root.lookupType('gamepb.plantpb.PutInsectsReply'); + types.PutWeedsRequest = root.lookupType('gamepb.plantpb.PutWeedsRequest'); + types.PutWeedsReply = root.lookupType('gamepb.plantpb.PutWeedsReply'); + types.FertilizeRequest = root.lookupType('gamepb.plantpb.FertilizeRequest'); + types.FertilizeReply = root.lookupType('gamepb.plantpb.FertilizeReply'); + + // 背包/仓库 + types.BagRequest = root.lookupType('gamepb.itempb.BagRequest'); + types.BagReply = root.lookupType('gamepb.itempb.BagReply'); + types.SellRequest = root.lookupType('gamepb.itempb.SellRequest'); + types.SellReply = root.lookupType('gamepb.itempb.SellReply'); + types.UseRequest = root.lookupType('gamepb.itempb.UseRequest'); + types.UseReply = root.lookupType('gamepb.itempb.UseReply'); + types.BatchUseRequest = root.lookupType('gamepb.itempb.BatchUseRequest'); + types.BatchUseReply = root.lookupType('gamepb.itempb.BatchUseReply'); + types.PlantRequest = root.lookupType('gamepb.plantpb.PlantRequest'); + types.PlantReply = root.lookupType('gamepb.plantpb.PlantReply'); + + // 商店 + types.ShopProfilesRequest = root.lookupType('gamepb.shoppb.ShopProfilesRequest'); + types.ShopProfilesReply = root.lookupType('gamepb.shoppb.ShopProfilesReply'); + types.ShopInfoRequest = root.lookupType('gamepb.shoppb.ShopInfoRequest'); + types.ShopInfoReply = root.lookupType('gamepb.shoppb.ShopInfoReply'); + types.BuyGoodsRequest = root.lookupType('gamepb.shoppb.BuyGoodsRequest'); + types.BuyGoodsReply = root.lookupType('gamepb.shoppb.BuyGoodsReply'); + + // 好友 + types.GetAllFriendsRequest = root.lookupType('gamepb.friendpb.GetAllRequest'); + types.GetAllFriendsReply = root.lookupType('gamepb.friendpb.GetAllReply'); + types.GetApplicationsRequest = root.lookupType('gamepb.friendpb.GetApplicationsRequest'); + types.GetApplicationsReply = root.lookupType('gamepb.friendpb.GetApplicationsReply'); + types.AcceptFriendsRequest = root.lookupType('gamepb.friendpb.AcceptFriendsRequest'); + types.AcceptFriendsReply = root.lookupType('gamepb.friendpb.AcceptFriendsReply'); + + // 访问 + types.VisitEnterRequest = root.lookupType('gamepb.visitpb.EnterRequest'); + types.VisitEnterReply = root.lookupType('gamepb.visitpb.EnterReply'); + types.VisitLeaveRequest = root.lookupType('gamepb.visitpb.LeaveRequest'); + types.VisitLeaveReply = root.lookupType('gamepb.visitpb.LeaveReply'); + + // 任务 + types.TaskInfoRequest = root.lookupType('gamepb.taskpb.TaskInfoRequest'); + types.TaskInfoReply = root.lookupType('gamepb.taskpb.TaskInfoReply'); + types.ClaimTaskRewardRequest = root.lookupType('gamepb.taskpb.ClaimTaskRewardRequest'); + types.ClaimTaskRewardReply = root.lookupType('gamepb.taskpb.ClaimTaskRewardReply'); + types.BatchClaimTaskRewardRequest = root.lookupType('gamepb.taskpb.BatchClaimTaskRewardRequest'); + types.BatchClaimTaskRewardReply = root.lookupType('gamepb.taskpb.BatchClaimTaskRewardReply'); + + // 服务器推送通知 + types.LandsNotify = root.lookupType('gamepb.plantpb.LandsNotify'); + types.BasicNotify = root.lookupType('gamepb.userpb.BasicNotify'); + types.KickoutNotify = root.lookupType('gatepb.KickoutNotify'); + types.FriendApplicationReceivedNotify = root.lookupType('gamepb.friendpb.FriendApplicationReceivedNotify'); + types.FriendAddedNotify = root.lookupType('gamepb.friendpb.FriendAddedNotify'); + types.ItemNotify = root.lookupType('gamepb.itempb.ItemNotify'); + types.GoodsUnlockNotify = root.lookupType('gamepb.shoppb.GoodsUnlockNotify'); + types.TaskInfoNotify = root.lookupType('gamepb.taskpb.TaskInfoNotify'); + + // Proto 加载完成 +} + +function getRoot() { + return root; +} + +module.exports = { loadProto, types, getRoot }; diff --git a/211/server/src/qrlib/session.js b/211/server/src/qrlib/session.js new file mode 100644 index 0000000..0746d57 --- /dev/null +++ b/211/server/src/qrlib/session.js @@ -0,0 +1,314 @@ +const axios = require('axios'); +const { CookieUtils, HashUtils } = require('./utils'); + +// User Agent Definition +const ChromeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + +class QRLoginSession { + /** + * Presets for different login targets + */ + static Presets = { + vip: { + name: 'QQ会员 (VIP)', + description: 'QQ会员官网', + aid: '8000201', + daid: '18', + redirectUri: 'https://vip.qq.com/loginsuccess.html', + referrer: 'https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=8000201&style=20&s_url=https%3A%2F%2Fvip.qq.com%2Floginsuccess.html&maskOpacity=60&daid=18&target=self', + }, + qzone: { + name: 'QQ空间 (QZone)', + description: 'QQ空间网页版', + aid: '549000912', + daid: '5', + redirectUri: 'https://qzs.qzone.qq.com/qzone/v5/loginsucc.html?para=izone', + referrer: 'https://qzone.qq.com/', + }, + music: { + name: 'QQ音乐 (Music)', + description: 'QQ音乐网页版', + aid: '716027609', + daid: '383', + redirectUri: 'https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https%3A%2F%2Fy.qq.com%2F', + ptThirdAid: '100497308', + responseType: 'code', + openapi: '1010_1030', + }, + wegame: { + name: 'WeGame', + description: 'WeGame 平台', + aid: '1600001063', + daid: '733', + redirectUri: 'https://www.wegame.com.cn/middle/login/third_callback.html', + referrer: 'https://www.wegame.com.cn/', + }, + val: { + name: '瓦罗兰特 (VAL)', + description: '无畏契约官网', + aid: '716027609', + daid: '383', + redirectUri: 'https://val.qq.com/comm-htdocs/login/qc_redirect.html?parent_domain=https%3A%2F%2Fval.qq.com&isMiloSDK=1&isPc=1', + ptThirdAid: '102059301', + responseType: 'code', + openapi: '1010_1030', + }, + }; + + /** + * Request a new QR Code + * @param {string} presetKey - The key of the preset to use (vip, qzone, etc) + */ + static async requestQRCode(presetKey) { + const config = this.Presets[presetKey] || this.Presets.vip; + + const params = new URLSearchParams({ + appid: config.aid, + e: '2', + l: 'M', + s: '3', + d: '72', + v: '4', + t: String(Math.random()), + daid: config.daid, + }); + + if (config.ptThirdAid) { + params.set('pt_3rd_aid', config.ptThirdAid); + params.set('u1', 'https://graph.qq.com/oauth2.0/login_jump'); + } else { + params.set('u1', config.redirectUri); + } + + const url = `https://ssl.ptlogin2.qq.com/ptqrshow?${params.toString()}`; + + try { + const response = await axios.get(url, { + responseType: 'arraybuffer', + headers: { + 'Referer': config.referrer || `https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=${config.aid}&style=20&s_url=${encodeURIComponent(config.redirectUri)}&maskOpacity=60&daid=${config.daid}&target=self`, + 'User-Agent': ChromeUA, + } + }); + + const setCookie = response.headers['set-cookie']; + const qrsig = CookieUtils.getValue(setCookie, 'qrsig'); + const qrcodeBase64 = Buffer.from(response.data).toString('base64'); + + return { qrsig, qrcode: `data:image/png;base64,${qrcodeBase64}`, url }; + } catch (error) { + console.error('Request QRCode Error:', error); + throw error; + } + } + + /** + * Check the status of the QR Code + * @param {string} qrsig + * @param {string} presetKey + */ + static async checkStatus(qrsig, presetKey) { + const config = this.Presets[presetKey] || this.Presets.vip; + const ptqrtoken = HashUtils.hash(qrsig); + + const params = new URLSearchParams({ + ptqrtoken: String(ptqrtoken), + from_ui: '1', + aid: config.aid, + daid: config.daid, + action: `0-0-${Date.now()}`, // Added timestamp + pt_uistyle: '40', + js_ver: '21020514', + js_type: '1' + }); + + if (config.ptThirdAid) { + params.set('pt_3rd_aid', config.ptThirdAid); + params.set('u1', 'https://graph.qq.com/oauth2.0/login_jump'); + } else { + params.set('u1', config.redirectUri); + } + + const api = `https://ssl.ptlogin2.qq.com/ptqrlogin?${params.toString()}`; + + try { + const response = await axios.get(api, { + headers: { + 'Cookie': `qrsig=${qrsig}`, + 'Referer': config.referrer || 'https://xui.ptlogin2.qq.com/', + 'User-Agent': ChromeUA, + }, + }); + + const text = response.data; + // Parse response: ptuiCB('66','0','','0','二维码未失效。(3776510309)', '') + // Robust parsing using Regex to handle commas in content + const matcher = /ptuiCB\((.+)\)/; + const match = text.match(matcher); + + if (!match) { + throw new Error('Invalid response format'); + } + + // Extract arguments: 'arg1', 'arg2', ... + // This regex matches single-quoted strings: '([^']*)' + const args = []; + const argMatcher = /'([^']*)'/g; + let argMatch; + while ((argMatch = argMatcher.exec(match[1])) !== null) { + args.push(argMatch[1]); + } + + const [ret, extret, jumpUrl, redirect, msg, nickname] = args; + + return { + ret, + msg, + nickname, + jumpUrl, + cookie: response.headers['set-cookie'] // Return cookies to frontend if success + }; + + } catch (error) { + console.error('Check Status Error:', error); + throw new Error('Check status failed'); + } + } + + /** + * Get final cookies from the successful jump URL + * @param {string} jumpUrl + */ + static async getFinalCookies(jumpUrl) { + try { + // Prevent auto redirect to capture cookies + const response = await axios.get(jumpUrl, { + maxRedirects: 0, + validateStatus: status => status >= 200 && status < 400, + headers: { + 'User-Agent': ChromeUA + } + }); + + // This might return 302 Found + return response.headers['set-cookie']; + } catch (error) { + console.error("Get Final Cookies Error", error); + return []; + } + } +} + +class MiniProgramLoginSession { + static QUA = 'V1_HT5_QDT_0.70.2209190_x64_0_DEV_D'; + + /** + * Mini Program Presets + */ + static Presets = { + miniprogram: { + name: '小程序开发 (DevTools)', + description: 'QQ小程序开发者工具', + appid: '' // User provided + }, + farm: { + name: 'QQ经典农场 (Farm)', + description: 'QQ经典农场小程序', + appid: '1112386029' + } + }; + + static getHeaders() { + return { + 'qua': MiniProgramLoginSession.QUA, + 'host': 'q.qq.com', + 'accept': 'application/json', + 'content-type': 'application/json', + 'user-agent': ChromeUA + }; + } + + /** + * Request Login Code (for Mini Program DevTools) + */ + static async requestLoginCode() { + try { + const response = await axios.get('https://q.qq.com/ide/devtoolAuth/GetLoginCode', { + headers: this.getHeaders() + }); + + const { code, data } = response.data; + + if (+code !== 0) { + throw new Error('获取登录码失败'); + } + + return { + code: data.code || '', + url: `https://h5.qzone.qq.com/qqq/code/${data.code}?_proxy=1&from=ide` + }; + } catch (error) { + console.error('MP Request Login Code Error:', error); + throw error; + } + } + + /** + * Query Status for Mini Program Login + * @param {string} code + */ + static async queryStatus(code) { + try { + const response = await axios.get(`https://q.qq.com/ide/devtoolAuth/syncScanSateGetTicket?code=${code}`, { + headers: this.getHeaders() + }); + + // If response is not OK (e.g. 404/500), return Error + if (response.status !== 200) { + return { status: 'Error' }; + } + + const { code: resCode, data } = response.data; + + if (+resCode === 0) { + // data.ok: 1 = Success, 0 = Waiting/Scanning? + if (+data.ok !== 1) return { status: 'Wait' }; + // User says uin is here + return { status: 'OK', ticket: data.ticket, uin: data.uin }; + } + + if (+resCode === -10003) return { status: 'Used' }; + + return { status: 'Error', msg: `Code: ${resCode}` }; + } catch (error) { + console.error('MP Query Status Error:', error); + throw error; + } + } + + /** + * Get Auth Code (Final step for MP login) + * @param {string} ticket + * @param {string} appid + */ + static async getAuthCode(ticket, appid) { + try { + const response = await axios.post('https://q.qq.com/ide/login', { + appid: appid, + ticket: ticket + }, { + headers: this.getHeaders() + }); + + if (response.status !== 200) return ''; + + const { code } = response.data; + return code || ''; + } catch (error) { + console.error('MP Get Auth Code Error:', error); + return ''; + } + } +} + +module.exports = { QRLoginSession, MiniProgramLoginSession }; diff --git a/211/server/src/qrlib/utils.js b/211/server/src/qrlib/utils.js new file mode 100644 index 0000000..0a49c94 --- /dev/null +++ b/211/server/src/qrlib/utils.js @@ -0,0 +1,52 @@ +const axios = require('axios'); + +/** + * Cookie handling utilities + */ +class CookieUtils { + static parse(cookieStr) { + if (!cookieStr) return {}; + return cookieStr.split(';').reduce((acc, curr) => { + const [key, value] = curr.split('='); + if (key) acc[key.trim()] = value ? value.trim() : ''; + return acc; + }, {}); + } + + static getValue(cookies, key) { + if (!cookies) return null; + if (Array.isArray(cookies)) cookies = cookies.join('; '); + const match = cookies.match(new RegExp(`(?:^|;\\s*)${key}=([^;]*)`)); + return match ? match[1] : null; + } + + static getUin(cookies) { + const uin = this.getValue(cookies, 'wxuin') || this.getValue(cookies, 'uin') || this.getValue(cookies, 'ptui_loginuin'); + if (!uin) return null; + // Remove leading 'o' if present (common in QQ cookies like 'o123456') + return uin.replace(/^o0*/, ''); + } +} + +/** + * Hashing utilities for QQ login + */ +class HashUtils { + static hash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash += (hash << 5) + str.charCodeAt(i); + } + return 2147483647 & hash; + } + + static getGTk(pskey) { + let gtk = 5381; + for (let i = 0; i < pskey.length; i++) { + gtk += (gtk << 5) + pskey.charCodeAt(i); + } + return gtk & 0x7fffffff; + } +} + +module.exports = { CookieUtils, HashUtils }; diff --git a/211/server/src/status.js b/211/server/src/status.js new file mode 100644 index 0000000..3e780e4 --- /dev/null +++ b/211/server/src/status.js @@ -0,0 +1,193 @@ +/** + * 状态栏 - 在终端固定位置显示用户状态 + */ + +const { getLevelExpTable, getLevelExpProgress } = require('./gameConfig'); + +// ============ 状态数据 ============ +const statusData = { + platform: 'qq', + name: '', + level: 0, + gold: 0, + exp: 0, +}; + +// ============ 状态栏高度 ============ +const STATUS_LINES = 3; // 状态栏占用行数 +const FREE_PROJECT_TIP = '本程序免费开源,GitHub: https://github.com/linguo2625469/qq-farm-bot'; + +// ============ ANSI 转义码 ============ +const ESC = '\x1b'; +const SAVE_CURSOR = `${ESC}7`; +const RESTORE_CURSOR = `${ESC}8`; +const MOVE_TO = (row, col) => `${ESC}[${row};${col}H`; +const CLEAR_LINE = `${ESC}[2K`; +const SCROLL_REGION = (top, bottom) => `${ESC}[${top};${bottom}r`; +const RESET_SCROLL = `${ESC}[r`; +const BOLD = `${ESC}[1m`; +const RESET = `${ESC}[0m`; +const DIM = `${ESC}[2m`; +const CYAN = `${ESC}[36m`; +const YELLOW = `${ESC}[33m`; +const GREEN = `${ESC}[32m`; +const MAGENTA = `${ESC}[35m`; + +// ============ 状态栏是否启用 ============ +let statusEnabled = false; +let termRows = 24; + +/** + * 初始化状态栏 + */ +function initStatusBar() { + // 检测终端是否支持 + if (!process.stdout.isTTY) { + return false; + } + + termRows = process.stdout.rows || 24; + statusEnabled = true; + + // 设置滚动区域,留出顶部状态栏空间 + process.stdout.write(SCROLL_REGION(STATUS_LINES + 1, termRows)); + // 移动光标到滚动区域 + process.stdout.write(MOVE_TO(STATUS_LINES + 1, 1)); + + // 监听终端大小变化 + process.stdout.on('resize', () => { + termRows = process.stdout.rows || 24; + process.stdout.write(SCROLL_REGION(STATUS_LINES + 1, termRows)); + renderStatusBar(); + }); + + // 初始渲染 + renderStatusBar(); + return true; +} + +/** + * 清理状态栏(退出时调用) + */ +function cleanupStatusBar() { + if (!statusEnabled) return; + statusEnabled = false; + // 重置滚动区域 + process.stdout.write(RESET_SCROLL); + // 清除状态栏 + process.stdout.write(MOVE_TO(1, 1) + CLEAR_LINE); + process.stdout.write(MOVE_TO(2, 1) + CLEAR_LINE); + process.stdout.write(MOVE_TO(3, 1) + CLEAR_LINE); +} + +/** + * 渲染状态栏 + */ +function renderStatusBar() { + if (!statusEnabled) return; + + const { platform, name, level, gold, exp } = statusData; + + // 构建状态行 + const platformStr = platform === 'wx' ? `${MAGENTA}微信${RESET}` : `${CYAN}QQ${RESET}`; + const nameStr = name ? `${BOLD}${name}${RESET}` : '未登录'; + const levelStr = `${GREEN}Lv${level}${RESET}`; + const goldStr = `${YELLOW}金币:${gold}${RESET}`; + + // 显示经验值 + let expStr = ''; + if (level > 0 && exp >= 0) { + const levelExpTable = getLevelExpTable(); + if (levelExpTable) { + // 有配置表时显示当前等级进度 + const progress = getLevelExpProgress(level, exp); + expStr = `${DIM}经验:${progress.current}/${progress.needed}${RESET}`; + } else { + // 没有配置表时只显示累计经验 + expStr = `${DIM}经验:${exp}${RESET}`; + } + } + + // 第一行:平台 | 昵称 | 等级 | 金币 | 经验 + const line1 = `${platformStr} | ${nameStr} | ${levelStr} | ${goldStr}${expStr ? ' | ' + expStr : ''}`; + + // 第二行:固定提醒 + const line2 = `${DIM}${FREE_PROJECT_TIP}${RESET}`; + + // 第三行:分隔线 + const width = process.stdout.columns || 80; + const line3 = `${DIM}${'─'.repeat(Math.min(width, 80))}${RESET}`; + + // 保存光标位置 + process.stdout.write(SAVE_CURSOR); + // 移动到第一行并清除 + process.stdout.write(MOVE_TO(1, 1) + CLEAR_LINE + line1); + // 移动到第二行并清除 + process.stdout.write(MOVE_TO(2, 1) + CLEAR_LINE + line2); + // 移动到第三行并清除 + process.stdout.write(MOVE_TO(3, 1) + CLEAR_LINE + line3); + // 恢复光标位置 + process.stdout.write(RESTORE_CURSOR); +} + +/** + * 更新状态数据并刷新显示 + */ +function updateStatus(data) { + let changed = false; + for (const key of Object.keys(data)) { + if (statusData[key] !== data[key]) { + statusData[key] = data[key]; + changed = true; + } + } + if (changed && statusEnabled) { + renderStatusBar(); + } +} + +/** + * 设置平台 + */ +function setStatusPlatform(platform) { + updateStatus({ platform }); +} + +/** + * 从登录数据更新状态 + */ +function updateStatusFromLogin(basic) { + updateStatus({ + name: basic.name || statusData.name, + level: basic.level || statusData.level, + gold: basic.gold || statusData.gold, + exp: basic.exp || statusData.exp, + }); +} + +/** + * 更新金币 + */ +function updateStatusGold(gold) { + updateStatus({ gold }); +} + +/** + * 更新等级和经验 + */ +function updateStatusLevel(level, exp) { + const data = { level }; + if (exp !== undefined) data.exp = exp; + updateStatus(data); +} + +module.exports = { + initStatusBar, + cleanupStatusBar, + updateStatus, + setStatusPlatform, + updateStatusFromLogin, + updateStatusGold, + updateStatusLevel, + statusData, +}; diff --git a/211/server/src/task.js b/211/server/src/task.js new file mode 100644 index 0000000..5b21525 --- /dev/null +++ b/211/server/src/task.js @@ -0,0 +1,181 @@ +/** + * 任务系统 - 自动领取任务奖励 + */ + +const { types } = require('./proto'); +const { sendMsgAsync, networkEvents } = require('./network'); +const { toLong, toNum, log, logWarn, sleep } = require('./utils'); + +// ============ 任务 API ============ + +async function getTaskInfo() { + const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body); + return types.TaskInfoReply.decode(replyBody); +} + +async function claimTaskReward(taskId, doShared = false) { + const body = types.ClaimTaskRewardRequest.encode(types.ClaimTaskRewardRequest.create({ + id: toLong(taskId), + do_shared: doShared, + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'ClaimTaskReward', body); + return types.ClaimTaskRewardReply.decode(replyBody); +} + +async function batchClaimTaskReward(taskIds, doShared = false) { + const body = types.BatchClaimTaskRewardRequest.encode(types.BatchClaimTaskRewardRequest.create({ + ids: taskIds.map(id => toLong(id)), + do_shared: doShared, + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'BatchClaimTaskReward', body); + return types.BatchClaimTaskRewardReply.decode(replyBody); +} + +// ============ 任务分析 ============ + +/** + * 分析任务列表,找出可领取的任务 + */ +function analyzeTaskList(tasks) { + const claimable = []; + for (const task of tasks) { + const id = toNum(task.id); + const progress = toNum(task.progress); + const totalProgress = toNum(task.total_progress); + const isClaimed = task.is_claimed; + const isUnlocked = task.is_unlocked; + const shareMultiple = toNum(task.share_multiple); + + // 可领取条件: 已解锁 + 未领取 + 进度完成 + if (isUnlocked && !isClaimed && progress >= totalProgress && totalProgress > 0) { + claimable.push({ + id, + desc: task.desc || `任务#${id}`, + shareMultiple, + rewards: task.rewards || [], + }); + } + } + return claimable; +} + +/** + * 计算奖励摘要 + */ +function getRewardSummary(items) { + const summary = []; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + // 常见物品ID: 1=金币, 2=经验 + if (id === 1) summary.push(`金币${count}`); + else if (id === 2) summary.push(`经验${count}`); + else summary.push(`物品#${id}x${count}`); + } + return summary.join('/'); +} + +// ============ 自动领取 ============ + +/** + * 检查并领取所有可领取的任务奖励 + */ +async function checkAndClaimTasks() { + try { + const reply = await getTaskInfo(); + if (!reply.task_info) return; + + const taskInfo = reply.task_info; + const allTasks = [ + ...(taskInfo.growth_tasks || []), + ...(taskInfo.daily_tasks || []), + ...(taskInfo.tasks || []), + ]; + + const claimable = analyzeTaskList(allTasks); + if (claimable.length === 0) return; + + log('任务', `发现 ${claimable.length} 个可领取任务`); + + for (const task of claimable) { + try { + // 如果有分享翻倍,使用翻倍领取 + const useShare = task.shareMultiple > 1; + const multipleStr = useShare ? ` (${task.shareMultiple}倍)` : ''; + + const claimReply = await claimTaskReward(task.id, useShare); + const items = claimReply.items || []; + const rewardStr = items.length > 0 ? getRewardSummary(items) : '无'; + + log('任务', `领取: ${task.desc}${multipleStr} → ${rewardStr}`); + await sleep(300); + } catch (e) { + logWarn('任务', `领取失败 #${task.id}: ${e.message}`); + } + } + } catch (e) { + // 静默失败 + } +} + +/** + * 处理任务状态变化推送 + */ +function onTaskInfoNotify(taskInfo) { + if (!taskInfo) return; + + const allTasks = [ + ...(taskInfo.growth_tasks || []), + ...(taskInfo.daily_tasks || []), + ...(taskInfo.tasks || []), + ]; + + const claimable = analyzeTaskList(allTasks); + if (claimable.length === 0) return; + + // 有可领取任务,延迟后自动领取 + log('任务', `有 ${claimable.length} 个任务可领取,准备自动领取...`); + setTimeout(() => claimTasksFromList(claimable), 1000); +} + +/** + * 从任务列表领取奖励 + */ +async function claimTasksFromList(claimable) { + for (const task of claimable) { + try { + const useShare = task.shareMultiple > 1; + const multipleStr = useShare ? ` (${task.shareMultiple}倍)` : ''; + + const claimReply = await claimTaskReward(task.id, useShare); + const items = claimReply.items || []; + const rewardStr = items.length > 0 ? getRewardSummary(items) : '无'; + + log('任务', `领取: ${task.desc}${multipleStr} → ${rewardStr}`); + await sleep(300); + } catch (e) { + logWarn('任务', `领取失败 #${task.id}: ${e.message}`); + } + } +} + +// ============ 初始化 ============ + +function initTaskSystem() { + // 监听任务状态变化推送 + networkEvents.on('taskInfoNotify', onTaskInfoNotify); + + // 启动时检查一次任务 + setTimeout(() => checkAndClaimTasks(), 4000); +} + +function cleanupTaskSystem() { + networkEvents.off('taskInfoNotify', onTaskInfoNotify); +} + +module.exports = { + checkAndClaimTasks, + initTaskSystem, + cleanupTaskSystem, +}; diff --git a/211/server/src/userManager.js b/211/server/src/userManager.js new file mode 100644 index 0000000..65b8159 --- /dev/null +++ b/211/server/src/userManager.js @@ -0,0 +1,475 @@ +const fs = require('fs'); +const path = require('path'); +const { log } = require('./utils'); +require('dotenv').config({ path: path.join(__dirname, '../../.env') }); + +const DATA_DIR = path.join(__dirname, '../data'); +const DATA_FILE = path.join(DATA_DIR, 'users.json'); +const SQLITE_FILE = path.join(DATA_DIR, 'users.db'); + +const isProduction = process.env.NODE_ENV === 'production'; + +let mysqlPool = null; +let sqliteDb = null; +let sqliteRun = null; +let sqliteAll = null; +let sqliteReady = null; + +if (isProduction) { + const mysql = require('mysql2/promise'); + mysqlPool = mysql.createPool({ + host: process.env.MYSQL_HOST || 'localhost', + user: process.env.MYSQL_USER || 'root', + password: process.env.MYSQL_PASSWORD || 'root', + database: process.env.MYSQL_DB || 'game_account_db', + port: process.env.MYSQL_PORT || 3306, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 + }); + + // Init Table + (async () => { + try { + const connection = await mysqlPool.getConnection(); + await connection.query(` + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255), + code TEXT, + auth_provider VARCHAR(50) DEFAULT 'local', + auth_id VARCHAR(255), + created_at BIGINT + ) + `); + connection.release(); + log('UserManager', 'MySQL initialized.'); + } catch (err) { + log('UserManager', `MySQL Init Error: ${err.message}`); + } + })(); +} + +if (!isProduction) { + const sqlite3 = require('sqlite3'); + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + sqliteDb = new sqlite3.Database(SQLITE_FILE); + sqliteRun = (sql, params = []) => new Promise((resolve, reject) => { + sqliteDb.run(sql, params, function (err) { + if (err) reject(err); + else resolve(this); + }); + }); + sqliteAll = (sql, params = []) => new Promise((resolve, reject) => { + sqliteDb.all(sql, params, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); + sqliteReady = sqliteRun(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password TEXT, + code TEXT, + auth_provider TEXT DEFAULT 'local', + auth_id TEXT, + created_at INTEGER + ) + `).then(() => { + log('UserManager', 'SQLite initialized.'); + }).catch((err) => { + log('UserManager', `SQLite Init Error: ${err.message}`); + }); +} + +class UserManager { + constructor() { + this.users = new Map(); // email -> user (Cache) + this.useMySQL = isProduction && !!mysqlPool; + this.useSQLite = !this.useMySQL && !!sqliteDb; + this.extraReady = this.initExtraTables(); + this.load(); + } + + async load() { + if (this.useMySQL) { + try { + const [rows] = await mysqlPool.query('SELECT * FROM users'); + rows.forEach(u => this.users.set(u.email, u)); + log('UserManager', `Loaded ${rows.length} users from MySQL.`); + } catch (e) { + log('UserManager', `Failed to load users from MySQL: ${e.message}`); + } + } else if (this.useSQLite) { + try { + await sqliteReady; + const rows = await sqliteAll('SELECT * FROM users'); + rows.forEach(u => this.users.set(u.email, u)); + log('UserManager', `Loaded ${rows.length} users from SQLite.`); + } catch (e) { + log('UserManager', `Failed to load users from SQLite: ${e.message}`); + } + } else { + try { + if (fs.existsSync(DATA_FILE)) { + const data = fs.readFileSync(DATA_FILE, 'utf8'); + const json = JSON.parse(data); + if (Array.isArray(json)) { + json.forEach(u => this.users.set(u.email, u)); + } + log('UserManager', `Loaded ${this.users.size} users from JSON.`); + } + } catch (e) { + log('UserManager', `Failed to load users from JSON: ${e.message}`); + } + } + } + + async save(user) { + const createdAt = user.createdAt || user.created_at || Date.now(); + if (this.useMySQL) { + try { + await mysqlPool.query( + `INSERT INTO users (email, password, code, auth_provider, auth_id, created_at) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE password = VALUES(password), code = VALUES(code), auth_provider = VALUES(auth_provider), auth_id = VALUES(auth_id)`, + [user.email, user.password, user.code, user.auth_provider || 'local', user.auth_id || null, createdAt] + ); + } catch (e) { + log('UserManager', `Failed to save user to MySQL: ${e.message}`); + } + } else if (this.useSQLite) { + try { + await sqliteReady; + await sqliteRun( + `INSERT INTO users (email, password, code, auth_provider, auth_id, created_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(email) DO UPDATE SET password = excluded.password, code = excluded.code, auth_provider = excluded.auth_provider, auth_id = excluded.auth_id`, + [user.email, user.password, user.code, user.auth_provider || 'local', user.auth_id || null, createdAt] + ); + } catch (e) { + log('UserManager', `Failed to save user to SQLite: ${e.message}`); + } + } else { + try { + const data = JSON.stringify(Array.from(this.users.values()), null, 2); + fs.writeFileSync(DATA_FILE, data, 'utf8'); + } catch (e) { + log('UserManager', `Failed to save users to JSON: ${e.message}`); + } + } + } + + async initExtraTables() { + if (this.useMySQL) { + try { + await mysqlPool.query(` + CREATE TABLE IF NOT EXISTS bot_logs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL, + tag VARCHAR(50), + msg TEXT, + type VARCHAR(20), + time BIGINT, + INDEX idx_email_time (email, time) + ) + `); + await mysqlPool.query(` + CREATE TABLE IF NOT EXISTS leaderboard_unlucky ( + email VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + count BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (email, name) + ) + `); + } catch (e) { + log('UserManager', `Failed to init extra tables: ${e.message}`); + } + return; + } + if (this.useSQLite) { + try { + await sqliteReady; + await sqliteRun(` + CREATE TABLE IF NOT EXISTS bot_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + tag TEXT, + msg TEXT, + type TEXT, + time INTEGER + ) + `); + await sqliteRun(` + CREATE TABLE IF NOT EXISTS leaderboard_unlucky ( + email TEXT NOT NULL, + name TEXT NOT NULL, + count INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (email, name) + ) + `); + } catch (e) { + log('UserManager', `Failed to init extra tables: ${e.message}`); + } + } + } + + normalizeLogMessage(msg) { + if (typeof msg === 'string') return msg; + if (msg === null || msg === undefined) return ''; + if (typeof msg === 'object') { + try { + return JSON.stringify(msg); + } catch { + return String(msg); + } + } + return String(msg); + } + + async saveBotLog(email, logEntry) { + if (!email || !logEntry) return; + await this.extraReady; + const tag = logEntry.tag || 'System'; + const msg = this.normalizeLogMessage(logEntry.msg); + const type = logEntry.type || 'info'; + const time = Number(logEntry.time) || Date.now(); + const limit = 500; + if (this.useMySQL) { + try { + await mysqlPool.query( + `INSERT INTO bot_logs (email, tag, msg, type, time) VALUES (?, ?, ?, ?, ?)`, + [email, tag, msg, type, time] + ); + await mysqlPool.query( + `DELETE FROM bot_logs WHERE email = ? AND id NOT IN ( + SELECT id FROM ( + SELECT id FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ? + ) t + )`, + [email, email, limit] + ); + } catch (e) { + log('UserManager', `Failed to save bot log: ${e.message}`); + } + return; + } + if (this.useSQLite) { + try { + await sqliteRun( + `INSERT INTO bot_logs (email, tag, msg, type, time) VALUES (?, ?, ?, ?, ?)`, + [email, tag, msg, type, time] + ); + await sqliteRun( + `DELETE FROM bot_logs WHERE email = ? AND id NOT IN ( + SELECT id FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ? + )`, + [email, email, limit] + ); + } catch (e) { + log('UserManager', `Failed to save bot log: ${e.message}`); + } + } + } + + async getBotLogs(email, limit = 200) { + if (!email) return []; + await this.extraReady; + const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 500) : 200; + if (this.useMySQL) { + try { + const [rows] = await mysqlPool.query( + `SELECT id, tag, msg, type, time FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?`, + [email, safeLimit] + ); + return rows.slice().reverse().map(row => ({ + id: String(row.id), + tag: row.tag || 'System', + msg: row.msg || '', + type: row.type || 'info', + time: Number(row.time) || Date.now() + })); + } catch (e) { + log('UserManager', `Failed to load bot logs: ${e.message}`); + return []; + } + } + if (this.useSQLite) { + try { + const rows = await sqliteAll( + `SELECT id, tag, msg, type, time FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?`, + [email, safeLimit] + ); + return rows.slice().reverse().map(row => ({ + id: String(row.id), + tag: row.tag || 'System', + msg: row.msg || '', + type: row.type || 'info', + time: Number(row.time) || Date.now() + })); + } catch (e) { + log('UserManager', `Failed to load bot logs: ${e.message}`); + return []; + } + } + return []; + } + + async saveUnlucky(email, name, count) { + if (!email || !name || !Number.isFinite(count) || count <= 0) return; + await this.extraReady; + if (this.useMySQL) { + try { + await mysqlPool.query( + `INSERT INTO leaderboard_unlucky (email, name, count) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE count = count + VALUES(count)`, + [email, name, count] + ); + } catch (e) { + log('UserManager', `Failed to save leaderboard: ${e.message}`); + } + return; + } + if (this.useSQLite) { + try { + await sqliteRun( + `INSERT INTO leaderboard_unlucky (email, name, count) + VALUES (?, ?, ?) + ON CONFLICT(email, name) DO UPDATE SET count = count + excluded.count`, + [email, name, count] + ); + } catch (e) { + log('UserManager', `Failed to save leaderboard: ${e.message}`); + } + } + } + + async loadUnluckyBoards() { + await this.extraReady; + const result = new Map(); + if (this.useMySQL) { + try { + const [rows] = await mysqlPool.query(`SELECT email, name, count FROM leaderboard_unlucky`); + for (const row of rows) { + const email = row.email; + const name = row.name; + const count = Number(row.count) || 0; + if (!email || !name || count <= 0) continue; + let board = result.get(email); + if (!board) { + board = new Map(); + result.set(email, board); + } + board.set(name, count); + } + } catch (e) { + log('UserManager', `Failed to load leaderboard: ${e.message}`); + } + return result; + } + if (this.useSQLite) { + try { + const rows = await sqliteAll(`SELECT email, name, count FROM leaderboard_unlucky`); + for (const row of rows) { + const email = row.email; + const name = row.name; + const count = Number(row.count) || 0; + if (!email || !name || count <= 0) continue; + let board = result.get(email); + if (!board) { + board = new Map(); + result.set(email, board); + } + board.set(name, count); + } + } catch (e) { + log('UserManager', `Failed to load leaderboard: ${e.message}`); + } + } + return result; + } + + /** + * Get user by email + * @param {string} email + */ + getUser(email) { + return this.users.get(email); + } + + /** + * Create or update user + * @param {string} email + * @param {string} password + */ + async register(email, password, provider = 'local', authId = null) { + if (this.users.has(email) && provider === 'local') { + throw new Error('User already exists'); + } + + let user = this.users.get(email); + if (user) { + // Update existing user (e.g. linking OAuth) + if (provider !== 'local') { + user.auth_provider = provider; + user.auth_id = authId; + await this.save(user); + } + return user; + } + + user = { + email, + password, // In production, hash this! + code: '', + auth_provider: provider, + auth_id: authId, + createdAt: Date.now() + }; + this.users.set(email, user); + await this.save(user); + return user; + } + + /** + * Validate login + * @param {string} email + * @param {string} password + */ + login(email, password) { + const user = this.users.get(email); + if (!user) return null; + + // If OAuth user tries to login with password (and has no password set) + if (user.auth_provider !== 'local' && !user.password) { + return null; + } + + if (user.password !== password) { + return null; + } + return user; + } + + /** + * Update user's game code + * @param {string} email + * @param {string} code + */ + async updateCode(email, code) { + const user = this.users.get(email); + if (user) { + user.code = code; + await this.save(user); + return true; + } + return false; + } +} + +module.exports = new UserManager(); diff --git a/211/server/src/utils.js b/211/server/src/utils.js new file mode 100644 index 0000000..70b53ef --- /dev/null +++ b/211/server/src/utils.js @@ -0,0 +1,90 @@ +/** + * 通用工具函数 + */ + +const Long = require('long'); +const { RUNTIME_HINT_MASK, RUNTIME_HINT_DATA } = require('./config'); + +// ============ 服务器时间状态 ============ +let serverTimeMs = 0; +let localTimeAtSync = 0; + +// ============ 类型转换 ============ +function toLong(val) { + return Long.fromValue(val); +} + +function toNum(val) { + if (Long.isLong(val)) return val.toNumber(); + return val || 0; +} + +// ============ 时间相关 ============ +function now() { + return new Date().toLocaleTimeString(); +} + +/** 获取当前推算的服务器时间(秒) */ +function getServerTimeSec() { + if (!serverTimeMs) return Math.floor(Date.now() / 1000); + const elapsed = Date.now() - localTimeAtSync; + return Math.floor((serverTimeMs + elapsed) / 1000); +} + +/** 同步服务器时间 */ +function syncServerTime(ms) { + serverTimeMs = ms; + localTimeAtSync = Date.now(); +} + +/** + * 将时间戳归一化为秒级 + * 大于 1e12 认为是毫秒级,转换为秒级 + */ +function toTimeSec(val) { + const n = toNum(val); + if (n <= 0) return 0; + if (n > 1e12) return Math.floor(n / 1000); + return n; +} + +// ============ 日志 ============ +function log(tag, msg) { + console.log(`[${now()}] [${tag}] ${msg}`); +} + +function logWarn(tag, msg) { + console.log(`[${now()}] [${tag}] ⚠ ${msg}`); +} + +// ============ 异步工具 ============ +function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +let hintPrinted = false; +function decodeRuntimeHint() { + return String.fromCharCode(...RUNTIME_HINT_DATA.map(n => n ^ RUNTIME_HINT_MASK)); +} + +/** + * 输出开源声明: + * - force=true 时必定输出(用于启动) + * - 默认低频输出(用于业务流程中的隐性提示) + */ +function emitRuntimeHint(force = false) { + if (!force) { + // 约 3.3% 概率,且同一次进程最多输出 2 次 + if (Math.random() > 0.033) return; + if (hintPrinted && Math.random() > 0.2) return; + } + log('声明', decodeRuntimeHint()); + hintPrinted = true; +} + +module.exports = { + toLong, toNum, now, + getServerTimeSec, syncServerTime, toTimeSec, + log, logWarn, sleep, + emitRuntimeHint, +}; diff --git a/211/server/src/warehouse.js b/211/server/src/warehouse.js new file mode 100644 index 0000000..d455e3a --- /dev/null +++ b/211/server/src/warehouse.js @@ -0,0 +1,173 @@ +/** + * 仓库系统 - 自动出售果实 + * 协议说明:BagReply 使用 item_bag(ItemBag),item_bag.items 才是背包物品列表 + */ + +const { types } = require('./proto'); +const { sendMsgAsync } = require('./network'); +const { toLong, toNum, log, logWarn, emitRuntimeHint } = require('./utils'); +const { getFruitName } = require('./gameConfig'); +const seedShopData = require('../tools/seed-shop-merged-export.json'); + +// 游戏内金币和点券的物品 ID (GlobalData.GodItemId / DiamondItemId) +const GOLD_ITEM_ID = 1001; +const FRUIT_ID_SET = new Set( + ((seedShopData && seedShopData.rows) || []) + .map(row => Number(row.fruitId)) + .filter(Number.isFinite) +); + +let sellTimer = null; +let sellInterval = 60000; + +function isFruitIdBySeedData(id) { + return FRUIT_ID_SET.has(toNum(id)); +} + +/** + * 从 SellReply 中提取获得的金币数量 + * 新版 SellReply 返回 get_items (repeated Item),其中 id=1001 为金币 + */ +function extractGold(sellReply) { + if (sellReply.get_items && sellReply.get_items.length > 0) { + for (const item of sellReply.get_items) { + const id = toNum(item.id); + if (id === GOLD_ITEM_ID) { + return toNum(item.count); + } + } + return 0; + } + if (sellReply.gold !== undefined && sellReply.gold !== null) { + return toNum(sellReply.gold); + } + return 0; +} + +async function getBag() { + const body = types.BagRequest.encode(types.BagRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.itempb.ItemService', 'Bag', body); + return types.BagReply.decode(replyBody); +} + +/** + * 将 item 转为 Sell 请求所需格式(id/count/uid 保留 Long 或转成 Long,与游戏一致) + */ +function toSellItem(item) { + const id = item.id != null ? toLong(item.id) : undefined; + const count = item.count != null ? toLong(item.count) : undefined; + const uid = item.uid != null ? toLong(item.uid) : undefined; + return { id, count, uid }; +} + +async function sellItems(items) { + const payload = items.map(toSellItem); + const body = types.SellRequest.encode(types.SellRequest.create({ items: payload })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.itempb.ItemService', 'Sell', body); + return types.SellReply.decode(replyBody); +} + +/** + * 从 BagReply 取出物品列表(兼容 item_bag 与旧版 items) + */ +function getBagItems(bagReply) { + if (bagReply.item_bag && bagReply.item_bag.items && bagReply.item_bag.items.length) + return bagReply.item_bag.items; + return bagReply.items || []; +} + +async function sellAllFruits() { + try { + const bagReply = await getBag(); + const items = getBagItems(bagReply); + + const toSell = []; + const names = []; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + const uid = item.uid ? toNum(item.uid) : 0; + if (isFruitIdBySeedData(id) && count > 0) { + if (uid === 0) continue; // 跳过无效格子 + toSell.push(item); + names.push(`${getFruitName(id)}x${count}`); + } + } + + if (toSell.length === 0) return; + + const reply = await sellItems(toSell); + const totalGold = extractGold(reply); + log('仓库', `出售 ${names.join(', ')},获得 ${totalGold} 金币`); + emitRuntimeHint(false); + } catch (e) { + logWarn('仓库', `出售失败: ${e.message}`); + } +} + +async function debugSellFruits() { + try { + log('仓库', '正在检查背包...'); + const bagReply = await getBag(); + const items = getBagItems(bagReply); + log('仓库', `背包共 ${items.length} 种物品`); + + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + const isFruit = isFruitIdBySeedData(id); + if (isFruit) { + const name = getFruitName(id); + log('仓库', ` [果实] ${name}(${id}) x${count}`); + } + } + + const toSell = []; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (isFruitIdBySeedData(id) && count > 0) + toSell.push(item); + } + + if (toSell.length === 0) { + log('仓库', '没有果实可出售'); + return; + } + + log('仓库', `准备出售 ${toSell.length} 种果实...`); + const reply = await sellItems(toSell); + const totalGold = extractGold(reply); + log('仓库', `出售完成,共获得 ${totalGold} 金币`); + emitRuntimeHint(false); + } catch (e) { + logWarn('仓库', `调试出售失败: ${e.message}`); + console.error(e); + } +} + +function startSellLoop(interval = 60000) { + if (sellTimer) return; + sellInterval = interval; + setTimeout(() => { + sellAllFruits(); + sellTimer = setInterval(() => sellAllFruits(), sellInterval); + }, 10000); +} + +function stopSellLoop() { + if (sellTimer) { + clearInterval(sellTimer); + sellTimer = null; + } +} + +module.exports = { + getBag, + sellItems, + sellAllFruits, + debugSellFruits, + getBagItems, + startSellLoop, + stopSellLoop, +}; diff --git a/211/server/tools/calc-exp-yield.js b/211/server/tools/calc-exp-yield.js new file mode 100644 index 0000000..ad14fc7 --- /dev/null +++ b/211/server/tools/calc-exp-yield.js @@ -0,0 +1,474 @@ +/** + * 基于 tools/seed-shop-merged-export.json 计算经验收益率 + * + * 规则: + * 1) 每次收获经验 = exp(新版已去除铲地+1经验) + * 2) 种植速度: + * - 不施肥:2 秒种 18 块地 => 9 块/秒 + * - 普通肥:2 秒种 12 块地 => 6 块/秒 + * 3) 普通肥:直接减少一个生长阶段(按 Plant.json 的 grow_phases 取首个非0阶段时长) + * + * 用法: + * node tools/calc-exp-yield.js + * node tools/calc-exp-yield.js --lands 18 --level 27 + * node tools/calc-exp-yield.js --input tools/seed-shop-merged-export.json + * + * 运行时调用: + * const { getPlantingRecommendation } = require('../tools/calc-exp-yield'); + * const rec = getPlantingRecommendation(27, 18); + */ + +const fs = require('fs'); +const path = require('path'); + +const DEFAULT_INPUT = path.join(__dirname, 'seed-shop-merged-export.json'); +const PLANT_CONFIG_PATH = path.join(__dirname, '..', 'gameConfig', 'Plant.json'); +const DEFAULT_OUT_JSON = path.join(__dirname, 'exp-yield-result.json'); +const DEFAULT_OUT_CSV = path.join(__dirname, 'exp-yield-result.csv'); +const DEFAULT_OUT_TXT = path.join(__dirname, 'exp-yield-summary.txt'); + +const NO_FERT_PLANTS_PER_2_SEC = 18; +const NORMAL_FERT_PLANTS_PER_2_SEC = 12; +const NO_FERT_PLANT_SPEED_PER_SEC = NO_FERT_PLANTS_PER_2_SEC / 2; // 9 块/秒 +const NORMAL_FERT_PLANT_SPEED_PER_SEC = NORMAL_FERT_PLANTS_PER_2_SEC / 2; // 6 块/秒 + +function toNum(v, fallback = 0) { + const n = Number(v); + return Number.isFinite(n) ? n : fallback; +} + +function parseArgs(argv) { + const opts = { + input: DEFAULT_INPUT, + outJson: DEFAULT_OUT_JSON, + outCsv: DEFAULT_OUT_CSV, + outTxt: DEFAULT_OUT_TXT, + lands: 18, + level: null, + top: 20, + }; + + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--input' && argv[i + 1]) opts.input = argv[++i]; + else if (a === '--out-json' && argv[i + 1]) opts.outJson = argv[++i]; + else if (a === '--out-csv' && argv[i + 1]) opts.outCsv = argv[++i]; + else if (a === '--out-txt' && argv[i + 1]) opts.outTxt = argv[++i]; + else if (a === '--lands' && argv[i + 1]) opts.lands = Math.max(1, Math.floor(toNum(argv[++i], 18))); + else if (a === '--level' && argv[i + 1]) opts.level = Math.max(1, Math.floor(toNum(argv[++i], 1))); + else if (a === '--top' && argv[i + 1]) opts.top = Math.max(1, Math.floor(toNum(argv[++i], 20))); + else if (a === '--help' || a === '-h') { + printHelp(); + process.exit(0); + } + } + return opts; +} + +function printHelp() { + console.log('Usage: node tools/calc-exp-yield.js [options]'); + console.log(''); + console.log('Options:'); + console.log(' --input 输入 JSON 文件路径'); + console.log(' --lands 地块数(默认 18)'); + console.log(' --level 指定账号等级,输出该等级可用最优作物'); + console.log(' --top 摘要 Top 数量(默认 20)'); + console.log(' --out-json 输出 JSON 路径'); + console.log(' --out-csv 输出 CSV 路径'); + console.log(' --out-txt 输出 TXT 路径'); +} + +function readSeeds(inputPath) { + const text = fs.readFileSync(inputPath, 'utf8'); + const data = JSON.parse(text); + if (Array.isArray(data)) return data; + if (data && Array.isArray(data.rows)) return data.rows; + if (data && Array.isArray(data.seeds)) return data.seeds; + throw new Error('无法识别输入数据格式,需要数组或 rows/seeds 字段'); +} + +function parseGrowPhases(growPhases) { + if (!growPhases || typeof growPhases !== 'string') return []; + return growPhases + .split(';') + .map(x => x.trim()) + .filter(Boolean) + .map(seg => { + const parts = seg.split(':'); + return parts.length >= 2 ? toNum(parts[1], 0) : 0; + }) + .filter(sec => sec > 0); +} + +function loadSeedPhaseReduceMap() { + const text = fs.readFileSync(PLANT_CONFIG_PATH, 'utf8'); + const rows = JSON.parse(text); + if (!Array.isArray(rows)) { + throw new Error(`Plant 配置格式异常: ${PLANT_CONFIG_PATH}`); + } + + const map = new Map(); + for (const p of rows) { + const seedId = toNum(p.seed_id, 0); + if (seedId <= 0 || map.has(seedId)) continue; + const phases = parseGrowPhases(p.grow_phases); + if (phases.length === 0) continue; + map.set(seedId, phases[0]); // 普通肥减少一个阶段:以首个阶段时长为准 + } + return map; +} + +function calcEffectiveGrowTime(growSec, seedId, seedPhaseReduceMap) { + const reduce = toNum(seedPhaseReduceMap.get(seedId), 0); + if (reduce <= 0) return growSec; + return Math.max(1, growSec - reduce); +} + +function formatSec(sec) { + const s = Math.max(0, Math.round(sec)); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + const r = s % 60; + if (m < 60) return r > 0 ? `${m}m${r}s` : `${m}m`; + const h = Math.floor(m / 60); + const mm = m % 60; + return r > 0 ? `${h}h${mm}m${r}s` : `${h}h${mm}m`; +} + +function csvCell(v) { + const s = v == null ? '' : String(v); + if (s.includes(',') || s.includes('"') || s.includes('\n')) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function buildRows(rawSeeds, lands, seedPhaseReduceMap) { + const plantSecondsNoFert = lands / NO_FERT_PLANT_SPEED_PER_SEC; + const plantSecondsNormalFert = lands / NORMAL_FERT_PLANT_SPEED_PER_SEC; + const rows = []; + let skipped = 0; + let missingPhaseReduceCount = 0; + + for (const s of rawSeeds) { + const seedId = toNum(s.seedId || s.seed_id); + const name = s.name || `seed_${seedId}`; + const requiredLevel = toNum(s.requiredLevel || s.required_level || 1, 1); + const price = toNum(s.price, 0); + const expHarvest = toNum(s.exp, 0); + const growTimeSec = toNum(s.growTimeSec || s.growTime || s.grow_time || 0, 0); + + if (seedId <= 0 || growTimeSec <= 0) { + skipped++; + continue; + } + + const expPerCycle = expHarvest; + const reduceSec = toNum(seedPhaseReduceMap.get(seedId), 0); + if (reduceSec <= 0) missingPhaseReduceCount++; + const growTimeNormalFert = calcEffectiveGrowTime(growTimeSec, seedId, seedPhaseReduceMap); + + // 整个农场一轮 = 生长时间 + 本轮全部地块种植耗时 + const cycleSecNoFert = growTimeSec + plantSecondsNoFert; + const cycleSecNormalFert = growTimeNormalFert + plantSecondsNormalFert; + + const farmExpPerHourNoFert = (lands * expPerCycle / cycleSecNoFert) * 3600; + const farmExpPerHourNormalFert = (lands * expPerCycle / cycleSecNormalFert) * 3600; + const gainPercent = farmExpPerHourNoFert > 0 + ? ((farmExpPerHourNormalFert - farmExpPerHourNoFert) / farmExpPerHourNoFert) * 100 + : 0; + const expPerGoldSeed = price > 0 ? expPerCycle / price : 0; + + rows.push({ + seedId, + goodsId: toNum(s.goodsId || s.goods_id), + plantId: toNum(s.plantId || s.plant_id), + name, + requiredLevel, + unlocked: !!s.unlocked, + price, + expHarvest, + expPerCycle, + growTimeSec, + growTimeStr: s.growTimeStr || formatSec(growTimeSec), + normalFertReduceSec: reduceSec, + growTimeNormalFert, + growTimeNormalFertStr: formatSec(growTimeNormalFert), + cycleSecNoFert, + cycleSecNormalFert, + farmExpPerHourNoFert, + farmExpPerHourNormalFert, + farmExpPerDayNoFert: farmExpPerHourNoFert * 24, + farmExpPerDayNormalFert: farmExpPerHourNormalFert * 24, + gainPercent, + expPerGoldSeed, + fruitId: toNum(s?.fruit?.id || s.fruitId), + fruitCount: toNum(s?.fruit?.count || s.fruitCount), + }); + } + + return { rows, skipped, plantSecondsNoFert, plantSecondsNormalFert, missingPhaseReduceCount }; +} + +function pickTop(rows, key, topN) { + return [...rows] + .sort((a, b) => b[key] - a[key]) + .slice(0, topN); +} + +function buildBestByLevel(rows) { + const maxLevel = rows.reduce((m, r) => Math.max(m, r.requiredLevel), 1); + const result = []; + for (let lv = 1; lv <= maxLevel; lv++) { + // 按用户指定等级做理论可种分析,不受商店 unlocked 状态影响 + const available = rows.filter(r => r.requiredLevel <= lv); + if (available.length === 0) continue; + const bestNo = pickTop(available, 'farmExpPerHourNoFert', 1)[0]; + const bestFert = pickTop(available, 'farmExpPerHourNormalFert', 1)[0]; + result.push({ + level: lv, + bestNoFert: { + seedId: bestNo.seedId, + name: bestNo.name, + expPerHour: Number(bestNo.farmExpPerHourNoFert.toFixed(2)), + }, + bestNormalFert: { + seedId: bestFert.seedId, + name: bestFert.name, + expPerHour: Number(bestFert.farmExpPerHourNormalFert.toFixed(2)), + }, + }); + } + return result; +} + +function writeJson(outPath, payload) { + fs.writeFileSync(outPath, JSON.stringify(payload, null, 2), 'utf8'); +} + +function writeCsv(outPath, rows) { + const headers = [ + 'seedId', + 'name', + 'requiredLevel', + 'price', + 'expHarvest', + 'expPerCycle', + 'growTimeSec', + 'growTimeNormalFert', + 'cycleSecNoFert', + 'cycleSecNormalFert', + 'farmExpPerHourNoFert', + 'farmExpPerHourNormalFert', + 'farmExpPerDayNoFert', + 'farmExpPerDayNormalFert', + 'gainPercent', + 'expPerGoldSeed', + ]; + const lines = [headers.join(',')]; + for (const r of rows) { + lines.push(headers.map(h => csvCell(r[h])).join(',')); + } + fs.writeFileSync(outPath, `${lines.join('\n')}\n`, 'utf8'); +} + +function writeSummaryTxt(outPath, opts, meta, topNo, topFert, levelInfo) { + const lines = []; + lines.push('经验收益率分析结果'); + lines.push(''); + lines.push(`数据源: ${meta.input}`); + lines.push(`导出时间: ${new Date().toISOString()}`); + lines.push(`地块数: ${opts.lands}`); + lines.push(`种植速度(不施肥): ${NO_FERT_PLANTS_PER_2_SEC}块/${2}s (${NO_FERT_PLANT_SPEED_PER_SEC}块/s)`); + lines.push(`种植速度(普通肥): ${NORMAL_FERT_PLANTS_PER_2_SEC}块/${2}s (${NORMAL_FERT_PLANT_SPEED_PER_SEC}块/s)`); + lines.push(`整场种植耗时(不施肥): ${formatSec(meta.plantSecondsNoFert)}`); + lines.push(`整场种植耗时(普通肥): ${formatSec(meta.plantSecondsNormalFert)}`); + lines.push(`普通肥规则: 直接减少一个生长阶段(按 Plant.json 的首个阶段时长)`); + lines.push(`缺少阶段配置的种子数: ${meta.missingPhaseReduceCount}`); + lines.push(''); + + lines.push(`Top ${topNo.length}(不施肥,按每小时经验)`); + lines.push('排名 | 名称 | Lv需 | 生长 | 单轮经验 | 每小时经验'); + topNo.forEach((r, i) => { + lines.push( + `${String(i + 1).padStart(2)} | ${r.name} | ${r.requiredLevel} | ${r.growTimeStr} | ${r.expPerCycle} | ${r.farmExpPerHourNoFert.toFixed(2)}` + ); + }); + lines.push(''); + + lines.push(`Top ${topFert.length}(普通肥,按每小时经验)`); + lines.push('排名 | 名称 | Lv需 | 肥后生长 | 单轮经验 | 每小时经验 | 提升'); + topFert.forEach((r, i) => { + lines.push( + `${String(i + 1).padStart(2)} | ${r.name} | ${r.requiredLevel} | ${r.growTimeNormalFertStr} | ${r.expPerCycle} | ${r.farmExpPerHourNormalFert.toFixed(2)} | ${r.gainPercent.toFixed(2)}%` + ); + }); + lines.push(''); + + if (levelInfo) { + lines.push(`当前等级 Lv${levelInfo.level} 推荐`); + lines.push(`不施肥: ${levelInfo.bestNoFert.name}(seed=${levelInfo.bestNoFert.seedId}) -> ${levelInfo.bestNoFert.expPerHour.toFixed(2)} exp/h`); + lines.push(`普通肥: ${levelInfo.bestNormalFert.name}(seed=${levelInfo.bestNormalFert.seedId}) -> ${levelInfo.bestNormalFert.expPerHour.toFixed(2)} exp/h`); + lines.push(''); + } + + fs.writeFileSync(outPath, `${lines.join('\n')}\n`, 'utf8'); +} + +function analyzeExpYield(opts = {}) { + const lands = Math.max(1, Math.floor(toNum(opts.lands, 18))); + const level = opts.level == null ? null : Math.max(1, Math.floor(toNum(opts.level, 1))); + const top = Math.max(1, Math.floor(toNum(opts.top, 20))); + const input = opts.input || DEFAULT_INPUT; + const inputAbs = path.resolve(input); + const rawSeeds = readSeeds(inputAbs); + const seedPhaseReduceMap = loadSeedPhaseReduceMap(); + const { rows, skipped, plantSecondsNoFert, plantSecondsNormalFert, missingPhaseReduceCount } = buildRows(rawSeeds, lands, seedPhaseReduceMap); + + if (rows.length === 0) { + throw new Error('没有可计算的种子数据(请检查输入文件)'); + } + + const topNo = pickTop(rows, 'farmExpPerHourNoFert', top); + const topFert = pickTop(rows, 'farmExpPerHourNormalFert', top); + const bestByLevel = buildBestByLevel(rows); + + let currentLevel = null; + if (level != null) { + currentLevel = bestByLevel.find(x => x.level === level) || null; + } + + return { + generatedAt: new Date().toISOString(), + input: inputAbs, + config: { + lands, + plantSpeedPerSecNoFert: NO_FERT_PLANT_SPEED_PER_SEC, + plantSpeedPerSecNormalFert: NORMAL_FERT_PLANT_SPEED_PER_SEC, + plantSecondsNoFert, + plantSecondsNormalFert, + fertilizer: { + mode: 'minus_one_phase', + }, + rule: { + expPerCycle: 'expHarvest', + }, + }, + stats: { + rawCount: rawSeeds.length, + calculatedCount: rows.length, + skippedCount: skipped, + missingPhaseReduceCount, + }, + topNoFert: topNo.map(r => ({ + seedId: r.seedId, + name: r.name, + requiredLevel: r.requiredLevel, + expPerHour: Number(r.farmExpPerHourNoFert.toFixed(4)), + })), + topNormalFert: topFert.map(r => ({ + seedId: r.seedId, + name: r.name, + requiredLevel: r.requiredLevel, + expPerHour: Number(r.farmExpPerHourNormalFert.toFixed(4)), + gainPercent: Number(r.gainPercent.toFixed(4)), + })), + bestByLevel, + currentLevel, + rows, + }; +} + +function getPlantingRecommendation(level, lands, opts = {}) { + const safeLevel = Math.max(1, Math.floor(toNum(level, 1))); + const payload = analyzeExpYield({ + input: opts.input || DEFAULT_INPUT, + lands: lands == null ? 18 : lands, + top: opts.top || 20, + level: safeLevel, + }); + + const availableRows = payload.rows.filter(r => r.requiredLevel <= safeLevel); + const bestNoFertRow = pickTop(availableRows, 'farmExpPerHourNoFert', 1)[0] || null; + const bestNormalFertRow = pickTop(availableRows, 'farmExpPerHourNormalFert', 1)[0] || null; + + return { + level: safeLevel, + lands: payload.config.lands, + input: payload.input, + bestNoFert: bestNoFertRow ? { + seedId: bestNoFertRow.seedId, + name: bestNoFertRow.name, + requiredLevel: bestNoFertRow.requiredLevel, + expPerHour: Number(bestNoFertRow.farmExpPerHourNoFert.toFixed(4)), + } : null, + bestNormalFert: bestNormalFertRow ? { + seedId: bestNormalFertRow.seedId, + name: bestNormalFertRow.name, + requiredLevel: bestNormalFertRow.requiredLevel, + expPerHour: Number(bestNormalFertRow.farmExpPerHourNormalFert.toFixed(4)), + } : null, + candidatesNoFert: pickTop(availableRows, 'farmExpPerHourNoFert', opts.top || 20).map(r => ({ + seedId: r.seedId, + name: r.name, + requiredLevel: r.requiredLevel, + expPerHour: Number(r.farmExpPerHourNoFert.toFixed(4)), + })), + candidatesNormalFert: pickTop(availableRows, 'farmExpPerHourNormalFert', opts.top || 20).map(r => ({ + seedId: r.seedId, + name: r.name, + requiredLevel: r.requiredLevel, + expPerHour: Number(r.farmExpPerHourNormalFert.toFixed(4)), + gainPercent: Number(r.gainPercent.toFixed(4)), + })), + }; +} + +function main() { + const opts = parseArgs(process.argv.slice(2)); + const payload = analyzeExpYield(opts); + const rows = payload.rows; + const topNo = pickTop(rows, 'farmExpPerHourNoFert', opts.top); + const topFert = pickTop(rows, 'farmExpPerHourNormalFert', opts.top); + const currentLevel = payload.currentLevel; + + writeJson(path.resolve(opts.outJson), payload); + writeCsv(path.resolve(opts.outCsv), rows); + writeSummaryTxt( + path.resolve(opts.outTxt), + opts, + { + input: payload.input, + plantSecondsNoFert: payload.config.plantSecondsNoFert, + plantSecondsNormalFert: payload.config.plantSecondsNormalFert, + missingPhaseReduceCount: payload.stats.missingPhaseReduceCount, + }, + topNo, + topFert, + currentLevel + ); + + console.log(`[收益率] 计算完成,共 ${rows.length} 条(跳过 ${payload.stats.skippedCount} 条)`); + console.log(`[收益率] JSON: ${path.resolve(opts.outJson)}`); + console.log(`[收益率] CSV : ${path.resolve(opts.outCsv)}`); + console.log(`[收益率] TXT : ${path.resolve(opts.outTxt)}`); + if (currentLevel) { + console.log(`[收益率] Lv${opts.level} 最优(不施肥): ${currentLevel.bestNoFert.name} ${currentLevel.bestNoFert.expPerHour} exp/h`); + console.log(`[收益率] Lv${opts.level} 最优(普通肥): ${currentLevel.bestNormalFert.name} ${currentLevel.bestNormalFert.expPerHour} exp/h`); + } +} + +module.exports = { + analyzeExpYield, + getPlantingRecommendation, + DEFAULT_INPUT, +}; + +if (require.main === module) { + try { + main(); + } catch (e) { + console.error(`[收益率] 失败: ${e.message}`); + process.exit(1); + } +} diff --git a/211/server/tools/crop_list.md b/211/server/tools/crop_list.md new file mode 100644 index 0000000..3ea15ab --- /dev/null +++ b/211/server/tools/crop_list.md @@ -0,0 +1,127 @@ +# 农作物列表 (共 123 种) + +| ID | 名称 | 等级 | 季数 | 产量 | 生长(小时) | 阶段详情 | +|---|---|---|---|---|---|---| +| 1020001 | 草莓 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020002 | 白萝卜 | 1 | 1 | 5 | 0.0h | 种子:30;发芽:30;成熟:0; | +| 1020003 | 胡萝卜 | 1 | 1 | 10 | 0.0h | 种子:30;发芽:30;小叶子:30;大叶子:30;成熟:0; | +| 1020004 | 玉米 | 1 | 1 | 40 | 1.3h | 种子:960;发芽:960;小叶子:960;大叶子:960;开花:960;成熟:0; | +| 1020005 | 土豆 | 1 | 1 | 60 | 2.0h | 种子:1440;发芽:1440;小叶子:1440;大叶子:1440;初熟:1440;成熟:0; | +| 1020006 | 茄子 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020007 | 番茄 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020008 | 豌豆 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; | +| 1020009 | 辣椒 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; | +| 1020010 | 南瓜 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; | +| 1020011 | 苹果 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020013 | 葡萄 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020014 | 西瓜 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020015 | 香蕉 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; | +| 1020016 | 菠萝蜜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020018 | 桃子 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;开花:5760;小叶子:5760;大叶子:5760;成熟:0; | +| 1020019 | 橙子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; | +| 1020022 | 鳄梨 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020023 | 石榴 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020026 | 柚子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; | +| 1020027 | 菠萝 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020029 | 椰子 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020031 | 葫芦 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020033 | 火龙果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020034 | 樱桃 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020035 | 荔枝 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020036 | 箬竹 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; | +| 1020037 | 莲藕 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020038 | 木瓜 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020039 | 杨桃 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020041 | 红玫瑰 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; | +| 1020042 | 柠檬 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020043 | 无花果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020044 | 丝瓜 | 1 | 1 | 200 | 12.0h | 种子:8640;长枝:8640;开花:8640;小叶子:8640;大叶子:8640;结果:0; | +| 1020045 | 猕猴桃 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020047 | 甘蔗 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;幼苗:8640;分叶:8640;伸长:8640;成熟:0; | +| 1020048 | 杨梅 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020049 | 花生 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020050 | 蘑菇 | 1 | 2 | 200 | 4.0h | 种子:3600;发芽:3600;大叶子:3600;初熟:3600;成熟:0; | +| 1020051 | 红枣 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020052 | 金针菇 | 1 | 2 | 200 | 12.0h | 种子:7200;发菌:7200;出菇:7200;幼菇:10800;初熟:10800;成熟:0; | +| 1020053 | 桂圆 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020054 | 梨 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020055 | 枇杷 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020056 | 哈密瓜 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020057 | 芒果 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020058 | 榴莲 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020059 | 大白菜 | 1 | 1 | 20 | 0.1h | 种子:60;发芽:60;幼苗:60;成株:60;卷心:60;成熟:0; | +| 1020060 | 水稻 | 1 | 1 | 30 | 0.7h | 种子:480;幼苗:480;秧苗:480;幼穗:480;开花:480;成熟:0; | +| 1020061 | 小麦 | 1 | 1 | 40 | 1.0h | 种子:720;发芽:720;小叶子:720;大叶子:720;幼穗:720;成熟:0; | +| 1020062 | 四叶草 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; | +| 1020063 | 苦瓜 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020064 | 大葱 | 1 | 1 | 30 | 0.3h | 种子:300;发芽:300;小叶子:300;大叶子:300;成熟:0; | +| 1020065 | 大蒜 | 1 | 1 | 20 | 0.2h | 种子:120;发芽:120;幼苗:120;伸长:120;初熟:120;成熟:0; | +| 1020066 | 鲜姜 | 1 | 1 | 60 | 1.7h | 种子:1500;发芽:1500;小叶子:1500;大叶子:1500;成熟:0; | +| 1020067 | 香瓜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020068 | 冬瓜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020070 | 黄豆 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;初熟:8640;成熟:0; | +| 1020071 | 小白菜 | 1 | 1 | 80 | 2.5h | 种子:2250;发芽:2250;小叶子:2250;大叶子:2250;成熟:0; | +| 1020072 | 榛子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;开花:17280;小叶子:17280;大叶子:17280;成熟:0; | +| 1020073 | 菠菜 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; | +| 1020074 | 金桔 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020075 | 桑葚 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020076 | 山竹 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020077 | 蓝莓 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020078 | 杏子 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; | +| 1020079 | 番石榴 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020080 | 月柿 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020083 | 红毛丹 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020084 | 芭蕉 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0; | +| 1020085 | 番荔枝 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0; | +| 1020086 | 橄榄 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020087 | 百香果 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0; | +| 1020088 | 灯笼果 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0; | +| 1020089 | 芦荟 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020090 | 薄荷 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020091 | 山楂 | 1 | 1 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:14400;开花:14400;初熟:14400;成熟:0; | +| 1020095 | 栗子 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020096 | 生菜 | 1 | 1 | 80 | 3.0h | 种子:2160;发芽:2160;小叶子:2160;大叶子:2160;初熟:2160;成熟:0; | +| 1020097 | 黄瓜 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020098 | 花菜 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;幼苗:8640;卷心:8640;初熟:8640;成熟:0; | +| 1020099 | 油菜 | 1 | 1 | 200 | 4.0h | 种子:3600;发芽:3600;小叶子:3600;大叶子:3600;成熟:0; | +| 1020100 | 竹笋 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;幼苗:2880;伸长:2880;初熟:2880;成熟:0; | +| 1020103 | 天香百合 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; | +| 1020104 | 非洲菊 | 1 | 1 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:2400;花蕾:2400;盛开:2400;成熟:0; | +| 1020105 | 小雏菊 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; | +| 1020110 | 满天星 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0; | +| 1020116 | 曼陀罗华 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;花蕾:3600;盛开:0; | +| 1020120 | 蒲公英 | 1 | 1 | 200 | 24.0h | 种子:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:17280;成熟:0; | +| 1020126 | 曼珠沙华 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020128 | 茉莉花 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; | +| 1020135 | 火绒草 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0; | +| 1020141 | 花香根鸢尾 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;花蕾:8640;盛开:0; | +| 1020142 | 虞美人 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0; | +| 1020143 | 含羞草 | 1 | 1 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:7200;花蕾:7200;盛开:7200;成熟:0; | +| 1020145 | 向日葵 | 1 | 1 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:2400;开花:2400;初熟:2400;成熟:0; | +| 1020147 | 牵牛花 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0; | +| 1020161 | 秋菊(黄色) | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; | +| 1020162 | 秋菊(红色) | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; | +| 1020201 | 天山雪莲 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;幼株:21600;成熟:0; | +| 1020202 | 金边灵芝 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;芝蕾:4800;幼芝:7200;初熟:7200;成熟:0; | +| 1020204 | 人参 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020218 | 瓶子树 | 1 | 2 | 200 | 4.0h | 种子:2400;长枝:2400;小叶子:2400;大叶子:3600;初熟:3600;成树:0; | +| 1020220 | 猪笼草 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; | +| 1020221 | 天堂鸟 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; | +| 1020222 | 豹皮花 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; | +| 1020225 | 宝华玉兰 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; | +| 1020226 | 依米花 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; | +| 1020227 | 大王花 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;幼蕾:4800;含苞:7200;初放:7200;盛开:0; | +| 1020228 | 人参果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020229 | 何首乌 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020235 | 金花茶 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; | +| 1020242 | 似血杜鹃 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; | +| 1020259 | 银莲花 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0; | +| 1020305 | 韭菜 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; | +| 1020306 | 芹菜 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; | +| 1020308 | 核桃 | 1 | 1 | 200 | 12.0h | 种子:10800;发芽:10800;小叶子:10800;大叶子:10800;成熟:0; | +| 1020396 | 迎春花 | 1 | 1 | 200 | 4.0h | 种子:2880;幼芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0; | +| 1020413 | 李子 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020442 | 睡莲 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;幼株:10800;成熟:0; | +| 1021542 | 新春红包 | 1 | 1 | 20 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; | +| 2020002 | 白萝卜 | 1 | 1 | 5 | 0.0h | 种子:1;发芽:1;成熟:0; | +| 2029998 | 哈哈南瓜 | 1 | 1 | 50 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | diff --git a/211/server/tools/list_crops.js b/211/server/tools/list_crops.js new file mode 100644 index 0000000..0033663 --- /dev/null +++ b/211/server/tools/list_crops.js @@ -0,0 +1,37 @@ +const fs = require('fs'); +const path = require('path'); + +const plantPath = path.join(__dirname, '../gameConfig/Plant.json'); + +try { + const data = fs.readFileSync(plantPath, 'utf8'); + const plants = JSON.parse(data); + + let content = `# 农作物列表 (共 ${plants.length} 种)\n\n`; + content += `| ID | 名称 | 等级 | 季数 | 产量 | 生长(小时) | 阶段详情 |\n`; + content += `|---|---|---|---|---|---|---|\n`; + + plants.sort((a, b) => a.land_level_need - b.land_level_need || a.id - b.id); + + plants.forEach(p => { + let totalTime = 0; + if (p.grow_phases) { + const parts = p.grow_phases.split(';'); + parts.forEach(part => { + if (part) { + const [stage, time] = part.split(':'); + if (time) totalTime += parseInt(time); + } + }); + } + + const hours = (totalTime / 3600).toFixed(1); + content += `| ${p.id} | ${p.name} | ${p.land_level_need} | ${p.seasons} | ${p.fruit ? p.fruit.count : '?'} | ${hours}h | ${p.grow_phases} |\n`; + }); + + fs.writeFileSync(path.join(__dirname, 'crop_list.md'), content); + console.log('列表已生成: server/tools/crop_list.md'); + +} catch (err) { + console.error('读取失败:', err); +} diff --git a/211/server/tools/seed-shop-merged-export.json b/211/server/tools/seed-shop-merged-export.json new file mode 100644 index 0000000..0a9bb6e --- /dev/null +++ b/211/server/tools/seed-shop-merged-export.json @@ -0,0 +1,2007 @@ +{ + "exportedAt": "2026-02-11T01:50:22.599Z", + "source": "api:http://127.0.0.1:3000/api/seeds", + "count": 100, + "rows": [ + { + "seedId": 20002, + "goodsId": 15, + "plantId": 1020002, + "name": "白萝卜", + "requiredLevel": 1, + "price": 2, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1, + "expPerCycle": 2, + "growTimeSec": 60, + "growTimeStr": "1分钟", + "expPerHour": 120, + "expPerGold": 1, + "seasons": 1, + "fruitId": 40002, + "fruitCount": 5 + }, + { + "seedId": 20003, + "goodsId": 16, + "plantId": 1020003, + "name": "胡萝卜", + "requiredLevel": 2, + "price": 4, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 2, + "expPerCycle": 3, + "growTimeSec": 120, + "growTimeStr": "2分钟", + "expPerHour": 90, + "expPerGold": 0.75, + "seasons": 1, + "fruitId": 40003, + "fruitCount": 10 + }, + { + "seedId": 20059, + "goodsId": 17, + "plantId": 1020059, + "name": "大白菜", + "requiredLevel": 3, + "price": 10, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 5, + "expPerCycle": 6, + "growTimeSec": 300, + "growTimeStr": "5分钟", + "expPerHour": 72, + "expPerGold": 0.6, + "seasons": 1, + "fruitId": 40059, + "fruitCount": 20 + }, + { + "seedId": 20065, + "goodsId": 18, + "plantId": 1020065, + "name": "大蒜", + "requiredLevel": 4, + "price": 20, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 10, + "expPerCycle": 11, + "growTimeSec": 600, + "growTimeStr": "10分钟", + "expPerHour": 66, + "expPerGold": 0.55, + "seasons": 1, + "fruitId": 40065, + "fruitCount": 20 + }, + { + "seedId": 20064, + "goodsId": 115, + "plantId": 1020064, + "name": "大葱", + "requiredLevel": 5, + "price": 42, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 20, + "expPerCycle": 21, + "growTimeSec": 1200, + "growTimeStr": "20分钟", + "expPerHour": 63, + "expPerGold": 0.5, + "seasons": 1, + "fruitId": 40064, + "fruitCount": 30 + }, + { + "seedId": 20060, + "goodsId": 19, + "plantId": 1020060, + "name": "水稻", + "requiredLevel": 6, + "price": 84, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 41, + "expPerCycle": 42, + "growTimeSec": 2400, + "growTimeStr": "40分钟", + "expPerHour": 63, + "expPerGold": 0.5, + "seasons": 1, + "fruitId": 40060, + "fruitCount": 30 + }, + { + "seedId": 20061, + "goodsId": 20, + "plantId": 1020061, + "name": "小麦", + "requiredLevel": 7, + "price": 126, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 62, + "expPerCycle": 63, + "growTimeSec": 3600, + "growTimeStr": "1小时", + "expPerHour": 63, + "expPerGold": 0.5, + "seasons": 1, + "fruitId": 40061, + "fruitCount": 40 + }, + { + "seedId": 20004, + "goodsId": 21, + "plantId": 1020004, + "name": "玉米", + "requiredLevel": 8, + "price": 168, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 82, + "expPerCycle": 83, + "growTimeSec": 4800, + "growTimeStr": "1小时20分", + "expPerHour": 62.25, + "expPerGold": 0.494048, + "seasons": 1, + "fruitId": 40004, + "fruitCount": 40 + }, + { + "seedId": 20066, + "goodsId": 116, + "plantId": 1020066, + "name": "鲜姜", + "requiredLevel": 9, + "price": 223, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 106, + "expPerCycle": 107, + "growTimeSec": 6000, + "growTimeStr": "1小时40分", + "expPerHour": 64.2, + "expPerGold": 0.479821, + "seasons": 1, + "fruitId": 40066, + "fruitCount": 60 + }, + { + "seedId": 20005, + "goodsId": 22, + "plantId": 1020005, + "name": "土豆", + "requiredLevel": 10, + "price": 268, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 128, + "expPerCycle": 129, + "growTimeSec": 7200, + "growTimeStr": "2小时", + "expPerHour": 64.5, + "expPerGold": 0.481343, + "seasons": 1, + "fruitId": 40005, + "fruitCount": 60 + }, + { + "seedId": 20071, + "goodsId": 23, + "plantId": 1020071, + "name": "小白菜", + "requiredLevel": 11, + "price": 335, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 160, + "expPerCycle": 161, + "growTimeSec": 9000, + "growTimeStr": "2小时30分", + "expPerHour": 64.4, + "expPerGold": 0.480597, + "seasons": 1, + "fruitId": 40071, + "fruitCount": 80 + }, + { + "seedId": 20096, + "goodsId": 24, + "plantId": 1020096, + "name": "生菜", + "requiredLevel": 12, + "price": 402, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 192, + "expPerCycle": 193, + "growTimeSec": 10800, + "growTimeStr": "3小时", + "expPerHour": 64.3333, + "expPerGold": 0.4801, + "seasons": 1, + "fruitId": 40096, + "fruitCount": 80 + }, + { + "seedId": 20099, + "goodsId": 60, + "plantId": 1020099, + "name": "油菜", + "requiredLevel": 13, + "price": 576, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 272, + "expPerCycle": 273, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 68.25, + "expPerGold": 0.473958, + "seasons": 1, + "fruitId": 40099, + "fruitCount": 200 + }, + { + "seedId": 20006, + "goodsId": 25, + "plantId": 1020006, + "name": "茄子", + "requiredLevel": 14, + "price": 1152, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 544, + "expPerCycle": 545, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 68.125, + "expPerGold": 0.47309, + "seasons": 1, + "fruitId": 40006, + "fruitCount": 200 + }, + { + "seedId": 20051, + "goodsId": 26, + "plantId": 1020051, + "name": "红枣", + "requiredLevel": 15, + "price": 1728, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 816, + "expPerCycle": 817, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 68.0833, + "expPerGold": 0.472801, + "seasons": 1, + "fruitId": 40051, + "fruitCount": 200 + }, + { + "seedId": 20120, + "goodsId": 117, + "plantId": 1020120, + "name": "蒲公英", + "requiredLevel": 16, + "price": 3456, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1632, + "expPerCycle": 1633, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 68.0417, + "expPerGold": 0.472512, + "seasons": 1, + "fruitId": 40120, + "fruitCount": 200 + }, + { + "seedId": 20259, + "goodsId": 118, + "plantId": 1020259, + "name": "银莲花", + "requiredLevel": 17, + "price": 640, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 288, + "expPerCycle": 289, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 72.25, + "expPerGold": 0.451562, + "seasons": 1, + "fruitId": 40259, + "fruitCount": 200 + }, + { + "seedId": 20007, + "goodsId": 27, + "plantId": 1020007, + "name": "番茄", + "requiredLevel": 18, + "price": 1280, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 576, + "expPerCycle": 577, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 72.125, + "expPerGold": 0.450781, + "seasons": 1, + "fruitId": 40007, + "fruitCount": 200 + }, + { + "seedId": 20098, + "goodsId": 28, + "plantId": 1020098, + "name": "花菜", + "requiredLevel": 19, + "price": 1920, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 864, + "expPerCycle": 865, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 72.0833, + "expPerGold": 0.450521, + "seasons": 1, + "fruitId": 40098, + "fruitCount": 200 + }, + { + "seedId": 20305, + "goodsId": 61, + "plantId": 1020305, + "name": "韭菜", + "requiredLevel": 20, + "price": 3840, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1728, + "expPerCycle": 1729, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 72.0417, + "expPerGold": 0.45026, + "seasons": 1, + "fruitId": 40305, + "fruitCount": 200 + }, + { + "seedId": 20105, + "goodsId": 119, + "plantId": 1020105, + "name": "小雏菊", + "requiredLevel": 21, + "price": 704, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 304, + "expPerCycle": 305, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 76.25, + "expPerGold": 0.433239, + "seasons": 1, + "fruitId": 40105, + "fruitCount": 200 + }, + { + "seedId": 20008, + "goodsId": 29, + "plantId": 1020008, + "name": "豌豆", + "requiredLevel": 22, + "price": 1408, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 608, + "expPerCycle": 609, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 76.125, + "expPerGold": 0.432528, + "seasons": 1, + "fruitId": 40008, + "fruitCount": 200 + }, + { + "seedId": 20037, + "goodsId": 63, + "plantId": 1020037, + "name": "莲藕", + "requiredLevel": 23, + "price": 2112, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 912, + "expPerCycle": 913, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 76.0833, + "expPerGold": 0.432292, + "seasons": 1, + "fruitId": 40037, + "fruitCount": 200 + }, + { + "seedId": 20041, + "goodsId": 30, + "plantId": 1020041, + "name": "红玫瑰", + "requiredLevel": 24, + "price": 4224, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1824, + "expPerCycle": 1825, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 76.0417, + "expPerGold": 0.432055, + "seasons": 1, + "fruitId": 40041, + "fruitCount": 200 + }, + { + "seedId": 20161, + "goodsId": 120, + "plantId": 1020161, + "name": "秋菊(黄色)", + "requiredLevel": 25, + "price": 792, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 324, + "expPerCycle": 325, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 81.25, + "expPerGold": 0.410354, + "seasons": 1, + "fruitId": 40161, + "fruitCount": 200 + }, + { + "seedId": 20110, + "goodsId": 121, + "plantId": 1020110, + "name": "满天星", + "requiredLevel": 26, + "price": 1584, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 648, + "expPerCycle": 649, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 81.125, + "expPerGold": 0.409722, + "seasons": 1, + "fruitId": 40110, + "fruitCount": 200 + }, + { + "seedId": 20143, + "goodsId": 122, + "plantId": 1020143, + "name": "含羞草", + "requiredLevel": 27, + "price": 2376, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 972, + "expPerCycle": 973, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 81.0833, + "expPerGold": 0.409512, + "seasons": 1, + "fruitId": 40143, + "fruitCount": 200 + }, + { + "seedId": 20147, + "goodsId": 123, + "plantId": 1020147, + "name": "牵牛花", + "requiredLevel": 28, + "price": 4752, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1944, + "expPerCycle": 1945, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 81.0417, + "expPerGold": 0.409301, + "seasons": 1, + "fruitId": 40147, + "fruitCount": 200 + }, + { + "seedId": 20162, + "goodsId": 124, + "plantId": 1020162, + "name": "秋菊(红色)", + "requiredLevel": 29, + "price": 888, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 344, + "expPerCycle": 345, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 86.25, + "expPerGold": 0.388514, + "seasons": 1, + "fruitId": 40162, + "fruitCount": 200 + }, + { + "seedId": 20009, + "goodsId": 31, + "plantId": 1020009, + "name": "辣椒", + "requiredLevel": 30, + "price": 1776, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 688, + "expPerCycle": 689, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 86.125, + "expPerGold": 0.38795, + "seasons": 1, + "fruitId": 40009, + "fruitCount": 200 + }, + { + "seedId": 20097, + "goodsId": 32, + "plantId": 1020097, + "name": "黄瓜", + "requiredLevel": 31, + "price": 2664, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1032, + "expPerCycle": 1033, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 86.0833, + "expPerGold": 0.387763, + "seasons": 1, + "fruitId": 40097, + "fruitCount": 200 + }, + { + "seedId": 20306, + "goodsId": 64, + "plantId": 1020306, + "name": "芹菜", + "requiredLevel": 32, + "price": 5328, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 2064, + "expPerCycle": 2065, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 86.0417, + "expPerGold": 0.387575, + "seasons": 1, + "fruitId": 40306, + "fruitCount": 200 + }, + { + "seedId": 20103, + "goodsId": 125, + "plantId": 1020103, + "name": "天香百合", + "requiredLevel": 33, + "price": 992, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 368, + "expPerCycle": 369, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 92.25, + "expPerGold": 0.371976, + "seasons": 1, + "fruitId": 40103, + "fruitCount": 200 + }, + { + "seedId": 20010, + "goodsId": 33, + "plantId": 1020010, + "name": "南瓜", + "requiredLevel": 34, + "price": 1984, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 736, + "expPerCycle": 737, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 92.125, + "expPerGold": 0.371472, + "seasons": 1, + "fruitId": 40010, + "fruitCount": 200 + }, + { + "seedId": 20308, + "goodsId": 34, + "plantId": 1020308, + "name": "核桃", + "requiredLevel": 35, + "price": 2976, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1104, + "expPerCycle": 1105, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 92.0833, + "expPerGold": 0.371304, + "seasons": 1, + "fruitId": 40308, + "fruitCount": 200 + }, + { + "seedId": 20091, + "goodsId": 126, + "plantId": 1020091, + "name": "山楂", + "requiredLevel": 36, + "price": 5952, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2208, + "expPerCycle": 2209, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 92.0417, + "expPerGold": 0.371136, + "seasons": 1, + "fruitId": 40091, + "fruitCount": 200 + }, + { + "seedId": 20073, + "goodsId": 127, + "plantId": 1020073, + "name": "菠菜", + "requiredLevel": 37, + "price": 1120, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 392, + "expPerCycle": 393, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 98.25, + "expPerGold": 0.350893, + "seasons": 1, + "fruitId": 40073, + "fruitCount": 200 + }, + { + "seedId": 20001, + "goodsId": 35, + "plantId": 1020001, + "name": "草莓", + "requiredLevel": 38, + "price": 2240, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 784, + "expPerCycle": 785, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 98.125, + "expPerGold": 0.350446, + "seasons": 1, + "fruitId": 40001, + "fruitCount": 200 + }, + { + "seedId": 20011, + "goodsId": 36, + "plantId": 1020011, + "name": "苹果", + "requiredLevel": 39, + "price": 3360, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1176, + "expPerCycle": 1177, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 98.0833, + "expPerGold": 0.350298, + "seasons": 1, + "fruitId": 40011, + "fruitCount": 200 + }, + { + "seedId": 20062, + "goodsId": 128, + "plantId": 1020062, + "name": "四叶草", + "requiredLevel": 40, + "price": 6720, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2352, + "expPerCycle": 2353, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 98.0417, + "expPerGold": 0.350149, + "seasons": 1, + "fruitId": 40062, + "fruitCount": 200 + }, + { + "seedId": 20104, + "goodsId": 129, + "plantId": 1020104, + "name": "非洲菊", + "requiredLevel": 41, + "price": 1248, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 420, + "expPerCycle": 421, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 105.25, + "expPerGold": 0.33734, + "seasons": 1, + "fruitId": 40104, + "fruitCount": 200 + }, + { + "seedId": 20135, + "goodsId": 130, + "plantId": 1020135, + "name": "火绒草", + "requiredLevel": 42, + "price": 2496, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 840, + "expPerCycle": 841, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 105.125, + "expPerGold": 0.336939, + "seasons": 1, + "fruitId": 40135, + "fruitCount": 200 + }, + { + "seedId": 20141, + "goodsId": 131, + "plantId": 1020141, + "name": "花香根鸢尾", + "requiredLevel": 43, + "price": 3744, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1260, + "expPerCycle": 1261, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 105.0833, + "expPerGold": 0.336806, + "seasons": 1, + "fruitId": 40141, + "fruitCount": 200 + }, + { + "seedId": 20142, + "goodsId": 132, + "plantId": 1020142, + "name": "虞美人", + "requiredLevel": 44, + "price": 7488, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2520, + "expPerCycle": 2521, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 105.0417, + "expPerGold": 0.336672, + "seasons": 1, + "fruitId": 40142, + "fruitCount": 200 + }, + { + "seedId": 20145, + "goodsId": 133, + "plantId": 1020145, + "name": "向日葵", + "requiredLevel": 45, + "price": 1400, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 448, + "expPerCycle": 449, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 112.25, + "expPerGold": 0.320714, + "seasons": 1, + "fruitId": 40145, + "fruitCount": 200 + }, + { + "seedId": 20014, + "goodsId": 37, + "plantId": 1020014, + "name": "西瓜", + "requiredLevel": 46, + "price": 2800, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 896, + "expPerCycle": 897, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 112.125, + "expPerGold": 0.320357, + "seasons": 1, + "fruitId": 40014, + "fruitCount": 200 + }, + { + "seedId": 20070, + "goodsId": 38, + "plantId": 1020070, + "name": "黄豆", + "requiredLevel": 47, + "price": 4200, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1344, + "expPerCycle": 1345, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 112.0833, + "expPerGold": 0.320238, + "seasons": 1, + "fruitId": 40070, + "fruitCount": 200 + }, + { + "seedId": 20015, + "goodsId": 39, + "plantId": 1020015, + "name": "香蕉", + "requiredLevel": 48, + "price": 8400, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2688, + "expPerCycle": 2689, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 112.0417, + "expPerGold": 0.320119, + "seasons": 1, + "fruitId": 40015, + "fruitCount": 200 + }, + { + "seedId": 20100, + "goodsId": 40, + "plantId": 1020100, + "name": "竹笋", + "requiredLevel": 49, + "price": 1560, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 476, + "expPerCycle": 477, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 119.25, + "expPerGold": 0.305769, + "seasons": 1, + "fruitId": 40100, + "fruitCount": 200 + }, + { + "seedId": 20018, + "goodsId": 41, + "plantId": 1020018, + "name": "桃子", + "requiredLevel": 50, + "price": 3120, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 952, + "expPerCycle": 953, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 119.125, + "expPerGold": 0.305449, + "seasons": 1, + "fruitId": 40018, + "fruitCount": 200 + }, + { + "seedId": 20047, + "goodsId": 42, + "plantId": 1020047, + "name": "甘蔗", + "requiredLevel": 51, + "price": 4680, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1428, + "expPerCycle": 1429, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 119.0833, + "expPerGold": 0.305342, + "seasons": 1, + "fruitId": 40047, + "fruitCount": 200 + }, + { + "seedId": 20019, + "goodsId": 43, + "plantId": 1020019, + "name": "橙子", + "requiredLevel": 52, + "price": 9360, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2856, + "expPerCycle": 2857, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 119.0417, + "expPerGold": 0.305235, + "seasons": 1, + "fruitId": 40019, + "fruitCount": 200 + }, + { + "seedId": 20128, + "goodsId": 134, + "plantId": 1020128, + "name": "茉莉花", + "requiredLevel": 53, + "price": 1728, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 508, + "expPerCycle": 509, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 127.25, + "expPerGold": 0.29456, + "seasons": 1, + "fruitId": 40128, + "fruitCount": 200 + }, + { + "seedId": 20013, + "goodsId": 44, + "plantId": 1020013, + "name": "葡萄", + "requiredLevel": 54, + "price": 3456, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1016, + "expPerCycle": 1017, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 127.125, + "expPerGold": 0.294271, + "seasons": 1, + "fruitId": 40013, + "fruitCount": 200 + }, + { + "seedId": 20044, + "goodsId": 45, + "plantId": 1020044, + "name": "丝瓜", + "requiredLevel": 55, + "price": 5184, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1524, + "expPerCycle": 1525, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 127.0833, + "expPerGold": 0.294174, + "seasons": 1, + "fruitId": 40044, + "fruitCount": 200 + }, + { + "seedId": 20072, + "goodsId": 66, + "plantId": 1020072, + "name": "榛子", + "requiredLevel": 56, + "price": 10368, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3048, + "expPerCycle": 3049, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 127.0417, + "expPerGold": 0.294078, + "seasons": 1, + "fruitId": 40072, + "fruitCount": 200 + }, + { + "seedId": 20396, + "goodsId": 46, + "plantId": 1020396, + "name": "迎春花", + "requiredLevel": 57, + "price": 1920, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 540, + "expPerCycle": 541, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 135.25, + "expPerGold": 0.281771, + "seasons": 1, + "fruitId": 40396, + "fruitCount": 200 + }, + { + "seedId": 20023, + "goodsId": 47, + "plantId": 1020023, + "name": "石榴", + "requiredLevel": 58, + "price": 3840, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1080, + "expPerCycle": 1081, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 135.125, + "expPerGold": 0.28151, + "seasons": 1, + "fruitId": 40023, + "fruitCount": 200 + }, + { + "seedId": 20095, + "goodsId": 48, + "plantId": 1020095, + "name": "栗子", + "requiredLevel": 59, + "price": 5760, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1620, + "expPerCycle": 1621, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 135.0833, + "expPerGold": 0.281424, + "seasons": 1, + "fruitId": 40095, + "fruitCount": 200 + }, + { + "seedId": 20026, + "goodsId": 49, + "plantId": 1020026, + "name": "柚子", + "requiredLevel": 60, + "price": 11520, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3240, + "expPerCycle": 3241, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 135.0417, + "expPerGold": 0.281337, + "seasons": 1, + "fruitId": 40026, + "fruitCount": 200 + }, + { + "seedId": 20050, + "goodsId": 50, + "plantId": 1020050, + "name": "蘑菇", + "requiredLevel": 61, + "price": 3168, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 429, + "expPerCycle": 430, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 107.5, + "expPerGold": 0.135732, + "seasons": 2, + "fruitId": 40050, + "fruitCount": 200 + }, + { + "seedId": 20027, + "goodsId": 51, + "plantId": 1020027, + "name": "菠萝", + "requiredLevel": 62, + "price": 6336, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 858, + "expPerCycle": 859, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 107.375, + "expPerGold": 0.135574, + "seasons": 2, + "fruitId": 40027, + "fruitCount": 200 + }, + { + "seedId": 20036, + "goodsId": 52, + "plantId": 1020036, + "name": "箬竹", + "requiredLevel": 63, + "price": 9504, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1287, + "expPerCycle": 1288, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 107.3333, + "expPerGold": 0.135522, + "seasons": 2, + "fruitId": 40036, + "fruitCount": 200 + }, + { + "seedId": 20043, + "goodsId": 68, + "plantId": 1020043, + "name": "无花果", + "requiredLevel": 64, + "price": 19008, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2574, + "expPerCycle": 2575, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 107.2917, + "expPerGold": 0.135469, + "seasons": 2, + "fruitId": 40043, + "fruitCount": 200 + }, + { + "seedId": 20029, + "goodsId": 53, + "plantId": 1020029, + "name": "椰子", + "requiredLevel": 65, + "price": 3492, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 456, + "expPerCycle": 457, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 114.25, + "expPerGold": 0.130871, + "seasons": 2, + "fruitId": 40029, + "fruitCount": 200 + }, + { + "seedId": 20049, + "goodsId": 54, + "plantId": 1020049, + "name": "花生", + "requiredLevel": 66, + "price": 6984, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 912, + "expPerCycle": 913, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 114.125, + "expPerGold": 0.130727, + "seasons": 2, + "fruitId": 40049, + "fruitCount": 200 + }, + { + "seedId": 20052, + "goodsId": 70, + "plantId": 1020052, + "name": "金针菇", + "requiredLevel": 67, + "price": 10476, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1368, + "expPerCycle": 1369, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 114.0833, + "expPerGold": 0.13068, + "seasons": 2, + "fruitId": 40052, + "fruitCount": 200 + }, + { + "seedId": 20031, + "goodsId": 55, + "plantId": 1020031, + "name": "葫芦", + "requiredLevel": 68, + "price": 20952, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2736, + "expPerCycle": 2737, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 114.0417, + "expPerGold": 0.130632, + "seasons": 2, + "fruitId": 40031, + "fruitCount": 200 + }, + { + "seedId": 20045, + "goodsId": 56, + "plantId": 1020045, + "name": "猕猴桃", + "requiredLevel": 69, + "price": 3828, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 480, + "expPerCycle": 481, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 120.25, + "expPerGold": 0.125653, + "seasons": 2, + "fruitId": 40045, + "fruitCount": 200 + }, + { + "seedId": 20054, + "goodsId": 74, + "plantId": 1020054, + "name": "梨", + "requiredLevel": 70, + "price": 7656, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 960, + "expPerCycle": 961, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 120.125, + "expPerGold": 0.125522, + "seasons": 2, + "fruitId": 40054, + "fruitCount": 200 + }, + { + "seedId": 20442, + "goodsId": 78, + "plantId": 1020442, + "name": "睡莲", + "requiredLevel": 71, + "price": 11484, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1440, + "expPerCycle": 1441, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 120.0833, + "expPerGold": 0.125479, + "seasons": 2, + "fruitId": 40442, + "fruitCount": 200 + }, + { + "seedId": 20033, + "goodsId": 83, + "plantId": 1020033, + "name": "火龙果", + "requiredLevel": 72, + "price": 22968, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2880, + "expPerCycle": 2881, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 120.0417, + "expPerGold": 0.125435, + "seasons": 2, + "fruitId": 40033, + "fruitCount": 200 + }, + { + "seedId": 20055, + "goodsId": 62, + "plantId": 1020055, + "name": "枇杷", + "requiredLevel": 73, + "price": 4176, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 510, + "expPerCycle": 511, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 127.75, + "expPerGold": 0.122366, + "seasons": 2, + "fruitId": 40055, + "fruitCount": 200 + }, + { + "seedId": 20034, + "goodsId": 65, + "plantId": 1020034, + "name": "樱桃", + "requiredLevel": 74, + "price": 8352, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1020, + "expPerCycle": 1021, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 127.625, + "expPerGold": 0.122246, + "seasons": 2, + "fruitId": 40034, + "fruitCount": 200 + }, + { + "seedId": 20413, + "goodsId": 67, + "plantId": 1020413, + "name": "李子", + "requiredLevel": 75, + "price": 12528, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1530, + "expPerCycle": 1531, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 127.5833, + "expPerGold": 0.122206, + "seasons": 2, + "fruitId": 40413, + "fruitCount": 200 + }, + { + "seedId": 20035, + "goodsId": 69, + "plantId": 1020035, + "name": "荔枝", + "requiredLevel": 76, + "price": 25056, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3060, + "expPerCycle": 3061, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 127.5417, + "expPerGold": 0.122166, + "seasons": 2, + "fruitId": 40035, + "fruitCount": 200 + }, + { + "seedId": 20067, + "goodsId": 72, + "plantId": 1020067, + "name": "香瓜", + "requiredLevel": 77, + "price": 4560, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 537, + "expPerCycle": 538, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 134.5, + "expPerGold": 0.117982, + "seasons": 2, + "fruitId": 40067, + "fruitCount": 200 + }, + { + "seedId": 20038, + "goodsId": 75, + "plantId": 1020038, + "name": "木瓜", + "requiredLevel": 78, + "price": 9120, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1074, + "expPerCycle": 1075, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 134.375, + "expPerGold": 0.117873, + "seasons": 2, + "fruitId": 40038, + "fruitCount": 200 + }, + { + "seedId": 20053, + "goodsId": 77, + "plantId": 1020053, + "name": "桂圆", + "requiredLevel": 79, + "price": 13680, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1611, + "expPerCycle": 1612, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 134.3333, + "expPerGold": 0.117836, + "seasons": 2, + "fruitId": 40053, + "fruitCount": 200 + }, + { + "seedId": 20080, + "goodsId": 80, + "plantId": 1020080, + "name": "月柿", + "requiredLevel": 80, + "price": 27360, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3222, + "expPerCycle": 3223, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 134.2917, + "expPerGold": 0.1178, + "seasons": 2, + "fruitId": 40080, + "fruitCount": 200 + }, + { + "seedId": 20039, + "goodsId": 82, + "plantId": 1020039, + "name": "杨桃", + "requiredLevel": 81, + "price": 4944, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 567, + "expPerCycle": 568, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 142, + "expPerGold": 0.114887, + "seasons": 2, + "fruitId": 40039, + "fruitCount": 200 + }, + { + "seedId": 20056, + "goodsId": 84, + "plantId": 1020056, + "name": "哈密瓜", + "requiredLevel": 82, + "price": 9888, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1134, + "expPerCycle": 1135, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 141.875, + "expPerGold": 0.114786, + "seasons": 2, + "fruitId": 40056, + "fruitCount": 200 + }, + { + "seedId": 20075, + "goodsId": 71, + "plantId": 1020075, + "name": "桑葚", + "requiredLevel": 83, + "price": 14832, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1701, + "expPerCycle": 1702, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 141.8333, + "expPerGold": 0.114752, + "seasons": 2, + "fruitId": 40075, + "fruitCount": 200 + }, + { + "seedId": 20042, + "goodsId": 73, + "plantId": 1020042, + "name": "柠檬", + "requiredLevel": 84, + "price": 29664, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3402, + "expPerCycle": 3403, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 141.7917, + "expPerGold": 0.114718, + "seasons": 2, + "fruitId": 40042, + "fruitCount": 200 + }, + { + "seedId": 20057, + "goodsId": 76, + "plantId": 1020057, + "name": "芒果", + "requiredLevel": 85, + "price": 5364, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 597, + "expPerCycle": 598, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 149.5, + "expPerGold": 0.111484, + "seasons": 2, + "fruitId": 40057, + "fruitCount": 200 + }, + { + "seedId": 20048, + "goodsId": 79, + "plantId": 1020048, + "name": "杨梅", + "requiredLevel": 86, + "price": 10728, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1194, + "expPerCycle": 1195, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 149.375, + "expPerGold": 0.111391, + "seasons": 2, + "fruitId": 40048, + "fruitCount": 200 + }, + { + "seedId": 20058, + "goodsId": 81, + "plantId": 1020058, + "name": "榴莲", + "requiredLevel": 87, + "price": 16092, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1791, + "expPerCycle": 1792, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 149.3333, + "expPerGold": 0.11136, + "seasons": 2, + "fruitId": 40058, + "fruitCount": 200 + }, + { + "seedId": 20079, + "goodsId": 85, + "plantId": 1020079, + "name": "番石榴", + "requiredLevel": 88, + "price": 32184, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3582, + "expPerCycle": 3583, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 149.2917, + "expPerGold": 0.111329, + "seasons": 2, + "fruitId": 40079, + "fruitCount": 200 + }, + { + "seedId": 20218, + "goodsId": 57, + "plantId": 1020218, + "name": "瓶子树", + "requiredLevel": 89, + "price": 5796, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 627, + "expPerCycle": 628, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 157, + "expPerGold": 0.108351, + "seasons": 2, + "fruitId": 40218, + "fruitCount": 200 + }, + { + "seedId": 20077, + "goodsId": 86, + "plantId": 1020077, + "name": "蓝莓", + "requiredLevel": 90, + "price": 11592, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1254, + "expPerCycle": 1255, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 156.875, + "expPerGold": 0.108264, + "seasons": 2, + "fruitId": 40077, + "fruitCount": 200 + }, + { + "seedId": 20220, + "goodsId": 58, + "plantId": 1020220, + "name": "猪笼草", + "requiredLevel": 91, + "price": 17388, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1881, + "expPerCycle": 1882, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 156.8333, + "expPerGold": 0.108236, + "seasons": 2, + "fruitId": 40220, + "fruitCount": 200 + }, + { + "seedId": 20076, + "goodsId": 87, + "plantId": 1020076, + "name": "山竹", + "requiredLevel": 92, + "price": 34776, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3762, + "expPerCycle": 3763, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 156.7917, + "expPerGold": 0.108207, + "seasons": 2, + "fruitId": 40076, + "fruitCount": 200 + }, + { + "seedId": 20116, + "goodsId": 59, + "plantId": 1020116, + "name": "曼陀罗华", + "requiredLevel": 93, + "price": 6240, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 660, + "expPerCycle": 661, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 165.25, + "expPerGold": 0.105929, + "seasons": 2, + "fruitId": 40116, + "fruitCount": 200 + }, + { + "seedId": 20126, + "goodsId": 88, + "plantId": 1020126, + "name": "曼珠沙华", + "requiredLevel": 94, + "price": 12480, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1320, + "expPerCycle": 1321, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 165.125, + "expPerGold": 0.105849, + "seasons": 2, + "fruitId": 40126, + "fruitCount": 200 + }, + { + "seedId": 20063, + "goodsId": 89, + "plantId": 1020063, + "name": "苦瓜", + "requiredLevel": 95, + "price": 18720, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1980, + "expPerCycle": 1981, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 165.0833, + "expPerGold": 0.105823, + "seasons": 2, + "fruitId": 40063, + "fruitCount": 200 + }, + { + "seedId": 20221, + "goodsId": 93, + "plantId": 1020221, + "name": "天堂鸟", + "requiredLevel": 96, + "price": 37440, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3960, + "expPerCycle": 3961, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 165.0417, + "expPerGold": 0.105796, + "seasons": 2, + "fruitId": 40221, + "fruitCount": 200 + }, + { + "seedId": 20068, + "goodsId": 92, + "plantId": 1020068, + "name": "冬瓜", + "requiredLevel": 97, + "price": 6720, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 693, + "expPerCycle": 694, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 173.5, + "expPerGold": 0.103274, + "seasons": 2, + "fruitId": 40068, + "fruitCount": 200 + }, + { + "seedId": 20222, + "goodsId": 94, + "plantId": 1020222, + "name": "豹皮花", + "requiredLevel": 98, + "price": 13440, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1386, + "expPerCycle": 1387, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 173.375, + "expPerGold": 0.103199, + "seasons": 2, + "fruitId": 40222, + "fruitCount": 200 + }, + { + "seedId": 20078, + "goodsId": 96, + "plantId": 1020078, + "name": "杏子", + "requiredLevel": 99, + "price": 20160, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2079, + "expPerCycle": 2080, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 173.3333, + "expPerGold": 0.103175, + "seasons": 2, + "fruitId": 40078, + "fruitCount": 200 + }, + { + "seedId": 20074, + "goodsId": 100, + "plantId": 1020074, + "name": "金桔", + "requiredLevel": 100, + "price": 40320, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 4158, + "expPerCycle": 4159, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 173.2917, + "expPerGold": 0.10315, + "seasons": 2, + "fruitId": 40074, + "fruitCount": 200 + } + ] +} \ No newline at end of file diff --git a/211/web/.gitignore b/211/web/.gitignore new file mode 100644 index 0000000..6bd782d --- /dev/null +++ b/211/web/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.crop_list.md diff --git a/211/web/README.md b/211/web/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/211/web/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/211/web/components.json b/211/web/components.json new file mode 100644 index 0000000..abd3aa3 --- /dev/null +++ b/211/web/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/211/web/eslint.config.js b/211/web/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/211/web/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/211/web/index.html b/211/web/index.html new file mode 100644 index 0000000..d6359d4 --- /dev/null +++ b/211/web/index.html @@ -0,0 +1,13 @@ + + + + + + + 谢尔达莱群岛 + + +
+ + + diff --git a/211/web/package-lock.json b/211/web/package-lock.json new file mode 100644 index 0000000..2605cc7 --- /dev/null +++ b/211/web/package-lock.json @@ -0,0 +1,5329 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "socket.io-client": "^4.8.3", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.13", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.24", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", + "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/type-utils": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.55.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", + "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", + "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.55.0", + "@typescript-eslint/types": "^8.55.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", + "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", + "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", + "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", + "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", + "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.55.0", + "@typescript-eslint/tsconfig-utils": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", + "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", + "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", + "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.55.0", + "@typescript-eslint/parser": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/211/web/package.json b/211/web/package.json new file mode 100644 index 0000000..d290d22 --- /dev/null +++ b/211/web/package.json @@ -0,0 +1,45 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "socket.io-client": "^4.8.3", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.13", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.24", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/211/web/postcss.config.js b/211/web/postcss.config.js new file mode 100644 index 0000000..6e297cd --- /dev/null +++ b/211/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, + } diff --git a/211/web/public/logo.png b/211/web/public/logo.png new file mode 100644 index 0000000..5e92acb Binary files /dev/null and b/211/web/public/logo.png differ diff --git a/211/web/public/shop_plants_organized/mapping.csv b/211/web/public/shop_plants_organized/mapping.csv new file mode 100644 index 0000000..8b23205 --- /dev/null +++ b/211/web/public/shop_plants_organized/mapping.csv @@ -0,0 +1,101 @@ +Seed ID,Seed Name,Plant ID,Plant Name,New File,Original File +20002,白萝卜种子,1020002,白萝卜,白萝卜_from_白萝卜种子_1020002.png,aa2d4772-8b90-417c-89be-c098e1cbb35c.edc47.png +20003,胡萝卜种子,1020003,胡萝卜,胡萝卜_from_胡萝卜种子_1020003.png,a823c190-2bd8-4b86-bbac-6c5cbf0c465d.3a774.png +20059,大白菜种子,1020059,大白菜,大白菜_from_大白菜种子_1020059.png,d4057a83-ecef-45fb-8988-905b56ae7ce3.b3280.png +20065,大蒜种子,1020065,大蒜,大蒜_from_大蒜种子_1020065.png,5a1d18bc-32f9-40d7-8cd3-be00b79004cc.2d769.png +20064,大葱种子,1020064,大葱,大葱_from_大葱种子_1020064.png,9bf36963-4f34-4c9d-b967-73ec64d1e924.2adf2.png +20060,水稻种子,1020060,水稻,水稻_from_水稻种子_1020060.png,e059e031-6953-402e-9d2b-88644465dc81.c5f7f.png +20061,小麦种子,1020061,小麦,小麦_from_小麦种子_1020061.png,2446c70c-20b6-4e3c-8a2f-5d64a044ded6.7abe2.png +20004,玉米种子,1020004,玉米,玉米_from_玉米种子_1020004.png,956ddaa2-27aa-4b9b-aa72-413390ecaaf5.de27e.png +20066,鲜姜种子,1020066,鲜姜,鲜姜_from_鲜姜种子_1020066.png,3a7230b2-a8b4-434a-83bf-4cb850c6b291.2dbd6.png +20005,土豆种子,1020005,土豆,土豆_from_土豆种子_1020005.png,95f857bc-ce3a-47f6-aae0-20ec6f84d21a.aad11.png +20071,小白菜种子,1020071,小白菜,小白菜_from_小白菜种子_1020071.png,07ca7a88-3983-42f9-8402-fdec001cb51c.d3538.png +20096,生菜种子,1020096,生菜,生菜_from_生菜种子_1020096.png,c7879560-b0d9-4f21-ab73-3fb4f083ddc5.9ce85.png +20099,油菜种子,1020099,油菜,油菜_from_油菜种子_1020099.png,35f65e28-3b35-4466-a040-e1b686085c23.0d18d.png +20006,茄子种子,1020006,茄子,茄子_from_茄子种子_1020006.png,2a626649-1952-41ef-8d5a-5ebbb726e479.ae717.png +20051,红枣种子,1020051,红枣,红枣_from_红枣种子_1020051.png,095fd7f0-24a0-415a-b4b8-96a8aeb07ba5.5e6bb.png +20120,蒲公英种子,1020120,蒲公英,蒲公英_from_蒲公英种子_1020120.png,8de9453f-e6ef-40ff-a678-3da9bb861bb5.462a2.png +20259,银莲花种子,1020259,银莲花,银莲花_from_银莲花种子_1020259.png,8df7c28d-ba7c-477d-a68e-39fb027e3969.d8964.png +20007,番茄种子,1020007,番茄,番茄_from_番茄种子_1020007.png,f7d5b3a4-c759-461f-bd81-79d8df59cbe2.af9b3.png +20098,花菜种子,1020098,花菜,花菜_from_花菜种子_1020098.png,f44f642e-4cd9-4c3d-bb25-269ba52d424a.1cbab.png +20305,韭菜种子,1020305,韭菜,韭菜_from_韭菜种子_1020305.png,7599bc40-a5d7-4834-8ed8-3e6eab7216ff.37c46.png +20105,小雏菊种子,1020105,小雏菊,小雏菊_from_小雏菊种子_1020105.png,b4f19058-1031-420f-ab64-feb4432c60a6.db1b3.png +20008,豌豆种子,1020008,豌豆,豌豆_from_豌豆种子_1020008.png,f55a4326-3e65-429e-a615-f5c7c867e451.154ba.png +20037,莲藕种子,1020037,莲藕,莲藕_from_莲藕种子_1020037.png,bb9c6484-1269-44f1-a203-74aa05fecc1d.1199a.png +20041,红玫瑰种子,1020041,红玫瑰,红玫瑰_from_红玫瑰种子_1020041.png,340b1098-6163-404c-97f9-86cbbc00336b.9b2a8.png +20161,黄色秋菊种子,1020161,秋菊(黄色),秋菊黄色_from_黄色秋菊种子_1020161.png,b72a754f-4e24-451d-a12a-a318f3937a05.641b4.png +20110,满天星种子,1020110,满天星,满天星_from_满天星种子_1020110.png,48593983-6831-4a94-8678-28a8c4304f54.39b33.png +20143,含羞草种子,1020143,含羞草,含羞草_from_含羞草种子_1020143.png,ee3d377e-30e5-4572-bd4d-6019cf1e6352.b468e.png +20147,牵牛花种子,1020147,牵牛花,牵牛花_from_牵牛花种子_1020147.png,54ce926b-ef79-4f1b-920b-756d8368408f.b863a.png +20162,红色秋菊种子,1020162,秋菊(红色),秋菊红色_from_红色秋菊种子_1020162.png,f3a09490-43c0-4d04-8a3a-679f9e25efe8.e0011.png +20009,辣椒种子,1020009,辣椒,辣椒_from_辣椒种子_1020009.png,7eed9dde-2174-4d6d-84e2-5fdb7276eaf3.7d364.png +20097,黄瓜种子,1020097,黄瓜,黄瓜_from_黄瓜种子_1020097.png,722e3547-d04e-4501-9aad-148af16cc8cb.bc687.png +20306,芹菜种子,1020306,芹菜,芹菜_from_芹菜种子_1020306.png,6d172f29-286f-4f58-88f7-e6776a863ef7.94097.png +20103,天香百合种子,1020103,天香百合,天香百合_from_天香百合种子_1020103.png,e5f9d964-28ee-4491-b714-ba917a9cec87.2710f.png +20010,南瓜种子,1020010,南瓜,南瓜_from_南瓜种子_1020010.png,0009ad77-fd65-48fa-af78-29c9872d8476.c388e.png +20308,核桃种子,1020308,核桃,核桃_from_核桃种子_1020308.png,b3b7045f-2b34-41ac-9e94-aaaf6083e06a.cf434.png +20091,山楂种子,1020091,山楂,山楂_from_山楂种子_1020091.png,4501b8e9-144e-497a-b282-ca1df8e17ee2.0f537.png +20073,菠菜种子,1020073,菠菜,菠菜_from_菠菜种子_1020073.png,a9429882-6c61-4d3b-83a4-38da55470b6a.22bda.png +20001,草莓种子,1020001,草莓,草莓_from_草莓种子_1020001.png,8d7c740a-e144-458d-95c3-15d95efa8bc3.17504.png +20011,苹果种子,1020011,苹果,苹果_from_苹果种子_1020011.png,5eff94d0-05ff-4e1d-a584-cfa473dcb5ca.250fc.png +20062,四叶草种子,1020062,四叶草,四叶草_from_四叶草种子_1020062.png,35aba40d-33bc-465c-8c58-f9b45bd61ca1.b4b9d.png +20104,非洲菊种子,1020104,非洲菊,非洲菊_from_非洲菊种子_1020104.png,5432d98e-84cb-4456-af35-ea2b8b194efe.cab54.png +20135,火绒草种子,1020135,火绒草,火绒草_from_火绒草种子_1020135.png,637a9c4e-fe92-4416-b913-f25ac46daab9.0fcc7.png +20141,花香根鸢尾种子,1020141,花香根鸢尾,花香根鸢尾_from_花香根鸢尾种子_1020141.png,5687ef62-528d-4faf-8380-a3d9fc14dd6f.a64a2.png +20142,虞美人种子,1020142,虞美人,虞美人_from_虞美人种子_1020142.png,74b327cb-e6db-496b-b665-3b3ceeee04f0.697d9.png +20145,向日葵种子,1020145,向日葵,向日葵_from_向日葵种子_1020145.png,fe45b6b7-3081-4541-bd1f-54bfded448ee.10c63.png +20014,西瓜种子,1020014,西瓜,西瓜_from_西瓜种子_1020014.png,74d60e9d-aee3-4fad-be3c-6c34b2c479d2.4a5b0.png +20070,黄豆种子,1020070,黄豆,黄豆_from_黄豆种子_1020070.png,de01453f-1159-490b-b312-6f9906cb8e38.426f8.png +20015,香蕉种子,1020015,香蕉,香蕉_from_香蕉种子_1020015.png,0ea242a7-5188-4296-8700-b4a2e00347b8.c925f.png +20100,竹笋种子,1020100,竹笋,竹笋_from_竹笋种子_1020100.png,c0fc1d8d-0c10-471a-83f4-14c400902a39.0beb7.png +20018,桃子种子,1020018,桃子,桃子_from_桃子种子_1020018.png,9ec730e7-4a9b-4825-bb45-046f1c5477b1.81f7f.png +20047,甘蔗种子,1020047,甘蔗,甘蔗_from_甘蔗种子_1020047.png,fabbc1bf-6622-433a-9471-8839d4a1aad3.b0296.png +20019,橙子种子,1020019,橙子,橙子_from_橙子种子_1020019.png,be59e9ef-d6b5-4a70-8b1d-b2a81851eaa2.537dc.png +20128,茉莉花种子,1020128,茉莉花,茉莉花_from_茉莉花种子_1020128.png,17d7dee5-e114-46f0-8017-506863370df9.ded8d.png +20013,葡萄种子,1020013,葡萄,葡萄_from_葡萄种子_1020013.png,4d63db88-33bc-4783-aeba-5213c0db86b9.cb70d.png +20044,丝瓜种子,1020044,丝瓜,丝瓜_from_丝瓜种子_1020044.png,5c446801-84f3-44fd-a066-9264a619ae04.e025a.png +20072,榛子种子,1020072,榛子,榛子_from_榛子种子_1020072.png,57888e00-33a6-4e1e-b7f7-b6a67b651935.576df.png +20396,迎春花种子,1020396,迎春花,迎春花_from_迎春花种子_1020396.png,32376957-bcd7-4dca-9bdc-7053da6fae7b.c0f12.png +20023,石榴种子,1020023,石榴,石榴_from_石榴种子_1020023.png,2a303167-26ac-4a76-8238-d8f5b90f2c5f.01562.png +20095,栗子种子,1020095,栗子,栗子_from_栗子种子_1020095.png,d1531233-1602-4322-9d08-25445ccd123f.e12b0.png +20026,柚子种子,1020026,柚子,柚子_from_柚子种子_1020026.png,c4a71275-ddce-4607-b9fc-93625244d256.6caf7.png +20050,蘑菇种子,1020050,蘑菇,蘑菇_from_蘑菇种子_1020050.png,09c2f94f-e865-472f-b599-5e857925ecfe.27d63.png +20027,菠萝种子,1020027,菠萝,菠萝_from_菠萝种子_1020027.png,37fed013-1f29-4686-992a-bbbf7bc79350.eadc1.png +20036,箬竹种子,1020036,箬竹,箬竹_from_箬竹种子_1020036.png,f75e1ed3-acfd-4bdf-bae5-a64459ed0347.e88b0.png +20043,无花果种子,1020043,无花果,无花果_from_无花果种子_1020043.png,68e20b28-d9a4-40cc-a3f0-8ff2d2ba2fc9.d9e75.png +20029,椰子种子,1020029,椰子,椰子_from_椰子种子_1020029.png,3a2d2a6e-02b3-4a67-ac63-847f728c1279.c7bf9.png +20049,花生种子,1020049,花生,花生_from_花生种子_1020049.png,30f01814-4b36-4838-9c93-d34a823db6a0.416fa.png +20052,金针菇种子,1020052,金针菇,金针菇_from_金针菇种子_1020052.png,aa0ddbb8-3102-4428-bad4-c8ad39dfab9e.f2ac9.png +20031,葫芦种子,1020031,葫芦,葫芦_from_葫芦种子_1020031.png,7cde3aa5-1732-4215-9b5d-e0aac6550f3f.73909.png +20045,猕猴桃种子,1020045,猕猴桃,猕猴桃_from_猕猴桃种子_1020045.png,d4e03a5a-6e87-4761-b29c-83835c5665b9.4dceb.png +20054,梨种子,1020054,梨,梨_from_梨种子_1020054.png,93760003-3cc3-4a51-abe4-963543437406.04b44.png +20442,睡莲种子,1020442,睡莲,睡莲_from_睡莲种子_1020442.png,00beea60-aa61-4283-a6f3-431605582581.4f00c.png +20033,火龙果种子,1020033,火龙果,火龙果_from_火龙果种子_1020033.png,51f55501-d2b0-4c14-929c-abfc08cef212.074ee.png +20055,枇杷种子,1020055,枇杷,枇杷_from_枇杷种子_1020055.png,af64a49f-6266-414c-810b-0e03a2edaf11.e40c4.png +20034,樱桃种子,1020034,樱桃,樱桃_from_樱桃种子_1020034.png,01f07ecf-6303-4b54-8a1a-ea64f5928855.8315d.png +20413,李子种子,1020413,李子,李子_from_李子种子_1020413.png,ddd8134a-15e5-4fbd-a7f6-a26457e638e2.e7bfc.png +20035,荔枝种子,1020035,荔枝,荔枝_from_荔枝种子_1020035.png,07232d7e-3d5e-4d48-923e-d70c2b64873b.85999.png +20067,香瓜种子,1020067,香瓜,香瓜_from_香瓜种子_1020067.png,4a30dcd3-efd2-4ff2-b406-7e81ab3f4923.281bf.png +20038,木瓜种子,1020038,木瓜,木瓜_from_木瓜种子_1020038.png,8816cd72-1ebd-4904-bd50-509a8e3798fb.32cf3.png +20053,桂圆种子,1020053,桂圆,桂圆_from_桂圆种子_1020053.png,ef610bda-bc53-46e7-a0fa-a519a8b180d0.db94d.png +20080,月柿种子,1020080,月柿,月柿_from_月柿种子_1020080.png,727dfe57-ed34-4e96-b8e2-bc856211c39f.4f667.png +20039,杨桃种子,1020039,杨桃,杨桃_from_杨桃种子_1020039.png,3be855b2-ac62-4e8f-b754-8ef8d8d1e46a.e08ca.png +20056,哈密瓜种子,1020056,哈密瓜,哈密瓜_from_哈密瓜种子_1020056.png,0e787d09-1443-4c0f-b496-e0f037983758.59eff.png +20075,桑葚种子,1020075,桑葚,桑葚_from_桑葚种子_1020075.png,e1c7e781-d74f-478d-b44e-cd37fc54294e.b2f01.png +20042,柠檬种子,1020042,柠檬,柠檬_from_柠檬种子_1020042.png,d78193cf-a7a5-40b4-ae67-d1519480503e.188c4.png +20057,芒果种子,1020057,芒果,芒果_from_芒果种子_1020057.png,44bd1002-5c47-47be-b9bc-1ae0fb9a26f2.4c8b1.png +20048,杨梅种子,1020048,杨梅,杨梅_from_杨梅种子_1020048.png,9eb47072-a3ae-4239-99b3-5d1fde9129d9.415cc.png +20058,榴莲种子,1020058,榴莲,榴莲_from_榴莲种子_1020058.png,7816ce22-6077-4175-8632-9f147402c98f.d5a93.png +20079,番石榴种子,1020079,番石榴,番石榴_from_番石榴种子_1020079.png,57fada3e-788a-4384-9ff7-b0d50b6a0e57.2fc48.png +20218,瓶子树种子,1020218,瓶子树,瓶子树_from_瓶子树种子_1020218.png,189f164a-a8d8-40ec-918f-567249467619.be91f.png +20077,蓝莓种子,1020077,蓝莓,蓝莓_from_蓝莓种子_1020077.png,80ae7506-850f-451f-9e18-57c11f62bca2.c3710.png +20220,猪笼草种子,1020220,猪笼草,猪笼草_from_猪笼草种子_1020220.png,7d2e3b7f-57e1-4491-8d55-cc485b8a548e.c5826.png +20076,山竹种子,1020076,山竹,山竹_from_山竹种子_1020076.png,8c490c1a-5085-4f5f-bf06-0a35b39e10e9.1c974.png +20116,曼陀罗华种子,1020116,曼陀罗华,曼陀罗华_from_曼陀罗华种子_1020116.png,6bb50aa4-3d41-426d-8b97-7b3829f045bf.ef4d0.png +20126,曼珠沙华种子,1020126,曼珠沙华,曼珠沙华_from_曼珠沙华种子_1020126.png,a7780718-b070-4922-953e-42e3234d0a6b.d2211.png +20063,苦瓜种子,1020063,苦瓜,苦瓜_from_苦瓜种子_1020063.png,7565aa2d-1ff4-43f9-aaa4-be0d1a937122.f5ab6.png +20221,天堂鸟种子,1020221,天堂鸟,天堂鸟_from_天堂鸟种子_1020221.png,bbeb2945-da57-4d63-bad8-78c046ffe46d.878ce.png +20068,冬瓜种子,1020068,冬瓜,冬瓜_from_冬瓜种子_1020068.png,95fe19b7-bb30-4b1a-b70f-fa4fd8e3d893.1ec42.png +20222,豹皮花种子,1020222,豹皮花,豹皮花_from_豹皮花种子_1020222.png,8df2f4e2-f8e1-44b3-a5cb-0e9f2617228d.e1dbc.png +20078,杏子种子,1020078,杏子,杏子_from_杏子种子_1020078.png,522a790b-e625-4743-bc1d-8a8d1d52738c.67da8.png +20074,金桔种子,1020074,金桔,金桔_from_金桔种子_1020074.png,b0811647-bd43-4402-a071-e60e05312943.d1674.png diff --git a/211/web/public/shop_plants_organized/丝瓜_from_丝瓜种子_1020044.png b/211/web/public/shop_plants_organized/丝瓜_from_丝瓜种子_1020044.png new file mode 100644 index 0000000..a7d99df Binary files /dev/null and b/211/web/public/shop_plants_organized/丝瓜_from_丝瓜种子_1020044.png differ diff --git a/211/web/public/shop_plants_organized/冬瓜_from_冬瓜种子_1020068.png b/211/web/public/shop_plants_organized/冬瓜_from_冬瓜种子_1020068.png new file mode 100644 index 0000000..cc4bf8b Binary files /dev/null and b/211/web/public/shop_plants_organized/冬瓜_from_冬瓜种子_1020068.png differ diff --git a/211/web/public/shop_plants_organized/南瓜_from_南瓜种子_1020010.png b/211/web/public/shop_plants_organized/南瓜_from_南瓜种子_1020010.png new file mode 100644 index 0000000..6e56fcb Binary files /dev/null and b/211/web/public/shop_plants_organized/南瓜_from_南瓜种子_1020010.png differ diff --git a/211/web/public/shop_plants_organized/向日葵_from_向日葵种子_1020145.png b/211/web/public/shop_plants_organized/向日葵_from_向日葵种子_1020145.png new file mode 100644 index 0000000..6b26ee4 Binary files /dev/null and b/211/web/public/shop_plants_organized/向日葵_from_向日葵种子_1020145.png differ diff --git a/211/web/public/shop_plants_organized/含羞草_from_含羞草种子_1020143.png b/211/web/public/shop_plants_organized/含羞草_from_含羞草种子_1020143.png new file mode 100644 index 0000000..1fcc1f9 Binary files /dev/null and b/211/web/public/shop_plants_organized/含羞草_from_含羞草种子_1020143.png differ diff --git a/211/web/public/shop_plants_organized/哈密瓜_from_哈密瓜种子_1020056.png b/211/web/public/shop_plants_organized/哈密瓜_from_哈密瓜种子_1020056.png new file mode 100644 index 0000000..dcb7d2f Binary files /dev/null and b/211/web/public/shop_plants_organized/哈密瓜_from_哈密瓜种子_1020056.png differ diff --git a/211/web/public/shop_plants_organized/四叶草_from_四叶草种子_1020062.png b/211/web/public/shop_plants_organized/四叶草_from_四叶草种子_1020062.png new file mode 100644 index 0000000..5947357 Binary files /dev/null and b/211/web/public/shop_plants_organized/四叶草_from_四叶草种子_1020062.png differ diff --git a/211/web/public/shop_plants_organized/土豆_from_土豆种子_1020005.png b/211/web/public/shop_plants_organized/土豆_from_土豆种子_1020005.png new file mode 100644 index 0000000..f85f77c Binary files /dev/null and b/211/web/public/shop_plants_organized/土豆_from_土豆种子_1020005.png differ diff --git a/211/web/public/shop_plants_organized/大白菜_from_大白菜种子_1020059.png b/211/web/public/shop_plants_organized/大白菜_from_大白菜种子_1020059.png new file mode 100644 index 0000000..f2420e5 Binary files /dev/null and b/211/web/public/shop_plants_organized/大白菜_from_大白菜种子_1020059.png differ diff --git a/211/web/public/shop_plants_organized/大葱_from_大葱种子_1020064.png b/211/web/public/shop_plants_organized/大葱_from_大葱种子_1020064.png new file mode 100644 index 0000000..975d431 Binary files /dev/null and b/211/web/public/shop_plants_organized/大葱_from_大葱种子_1020064.png differ diff --git a/211/web/public/shop_plants_organized/大蒜_from_大蒜种子_1020065.png b/211/web/public/shop_plants_organized/大蒜_from_大蒜种子_1020065.png new file mode 100644 index 0000000..36b45e2 Binary files /dev/null and b/211/web/public/shop_plants_organized/大蒜_from_大蒜种子_1020065.png differ diff --git a/211/web/public/shop_plants_organized/天堂鸟_from_天堂鸟种子_1020221.png b/211/web/public/shop_plants_organized/天堂鸟_from_天堂鸟种子_1020221.png new file mode 100644 index 0000000..a8b2a79 Binary files /dev/null and b/211/web/public/shop_plants_organized/天堂鸟_from_天堂鸟种子_1020221.png differ diff --git a/211/web/public/shop_plants_organized/天香百合_from_天香百合种子_1020103.png b/211/web/public/shop_plants_organized/天香百合_from_天香百合种子_1020103.png new file mode 100644 index 0000000..0d5beb0 Binary files /dev/null and b/211/web/public/shop_plants_organized/天香百合_from_天香百合种子_1020103.png differ diff --git a/211/web/public/shop_plants_organized/小白菜_from_小白菜种子_1020071.png b/211/web/public/shop_plants_organized/小白菜_from_小白菜种子_1020071.png new file mode 100644 index 0000000..d04909b Binary files /dev/null and b/211/web/public/shop_plants_organized/小白菜_from_小白菜种子_1020071.png differ diff --git a/211/web/public/shop_plants_organized/小雏菊_from_小雏菊种子_1020105.png b/211/web/public/shop_plants_organized/小雏菊_from_小雏菊种子_1020105.png new file mode 100644 index 0000000..203f3ce Binary files /dev/null and b/211/web/public/shop_plants_organized/小雏菊_from_小雏菊种子_1020105.png differ diff --git a/211/web/public/shop_plants_organized/小麦_from_小麦种子_1020061.png b/211/web/public/shop_plants_organized/小麦_from_小麦种子_1020061.png new file mode 100644 index 0000000..c2b3851 Binary files /dev/null and b/211/web/public/shop_plants_organized/小麦_from_小麦种子_1020061.png differ diff --git a/211/web/public/shop_plants_organized/山楂_from_山楂种子_1020091.png b/211/web/public/shop_plants_organized/山楂_from_山楂种子_1020091.png new file mode 100644 index 0000000..2c1051d Binary files /dev/null and b/211/web/public/shop_plants_organized/山楂_from_山楂种子_1020091.png differ diff --git a/211/web/public/shop_plants_organized/山竹_from_山竹种子_1020076.png b/211/web/public/shop_plants_organized/山竹_from_山竹种子_1020076.png new file mode 100644 index 0000000..9c2f333 Binary files /dev/null and b/211/web/public/shop_plants_organized/山竹_from_山竹种子_1020076.png differ diff --git a/211/web/public/shop_plants_organized/无花果_from_无花果种子_1020043.png b/211/web/public/shop_plants_organized/无花果_from_无花果种子_1020043.png new file mode 100644 index 0000000..d39e5db Binary files /dev/null and b/211/web/public/shop_plants_organized/无花果_from_无花果种子_1020043.png differ diff --git a/211/web/public/shop_plants_organized/曼珠沙华_from_曼珠沙华种子_1020126.png b/211/web/public/shop_plants_organized/曼珠沙华_from_曼珠沙华种子_1020126.png new file mode 100644 index 0000000..b3217e8 Binary files /dev/null and b/211/web/public/shop_plants_organized/曼珠沙华_from_曼珠沙华种子_1020126.png differ diff --git a/211/web/public/shop_plants_organized/曼陀罗华_from_曼陀罗华种子_1020116.png b/211/web/public/shop_plants_organized/曼陀罗华_from_曼陀罗华种子_1020116.png new file mode 100644 index 0000000..11a58a7 Binary files /dev/null and b/211/web/public/shop_plants_organized/曼陀罗华_from_曼陀罗华种子_1020116.png differ diff --git a/211/web/public/shop_plants_organized/月柿_from_月柿种子_1020080.png b/211/web/public/shop_plants_organized/月柿_from_月柿种子_1020080.png new file mode 100644 index 0000000..f535d9c Binary files /dev/null and b/211/web/public/shop_plants_organized/月柿_from_月柿种子_1020080.png differ diff --git a/211/web/public/shop_plants_organized/木瓜_from_木瓜种子_1020038.png b/211/web/public/shop_plants_organized/木瓜_from_木瓜种子_1020038.png new file mode 100644 index 0000000..b0c7d6d Binary files /dev/null and b/211/web/public/shop_plants_organized/木瓜_from_木瓜种子_1020038.png differ diff --git a/211/web/public/shop_plants_organized/李子_from_李子种子_1020413.png b/211/web/public/shop_plants_organized/李子_from_李子种子_1020413.png new file mode 100644 index 0000000..179009f Binary files /dev/null and b/211/web/public/shop_plants_organized/李子_from_李子种子_1020413.png differ diff --git a/211/web/public/shop_plants_organized/杏子_from_杏子种子_1020078.png b/211/web/public/shop_plants_organized/杏子_from_杏子种子_1020078.png new file mode 100644 index 0000000..01f98df Binary files /dev/null and b/211/web/public/shop_plants_organized/杏子_from_杏子种子_1020078.png differ diff --git a/211/web/public/shop_plants_organized/杨桃_from_杨桃种子_1020039.png b/211/web/public/shop_plants_organized/杨桃_from_杨桃种子_1020039.png new file mode 100644 index 0000000..418bd2b Binary files /dev/null and b/211/web/public/shop_plants_organized/杨桃_from_杨桃种子_1020039.png differ diff --git a/211/web/public/shop_plants_organized/杨梅_from_杨梅种子_1020048.png b/211/web/public/shop_plants_organized/杨梅_from_杨梅种子_1020048.png new file mode 100644 index 0000000..02a442b Binary files /dev/null and b/211/web/public/shop_plants_organized/杨梅_from_杨梅种子_1020048.png differ diff --git a/211/web/public/shop_plants_organized/枇杷_from_枇杷种子_1020055.png b/211/web/public/shop_plants_organized/枇杷_from_枇杷种子_1020055.png new file mode 100644 index 0000000..bf3cb4b Binary files /dev/null and b/211/web/public/shop_plants_organized/枇杷_from_枇杷种子_1020055.png differ diff --git a/211/web/public/shop_plants_organized/柚子_from_柚子种子_1020026.png b/211/web/public/shop_plants_organized/柚子_from_柚子种子_1020026.png new file mode 100644 index 0000000..dd3ba2b Binary files /dev/null and b/211/web/public/shop_plants_organized/柚子_from_柚子种子_1020026.png differ diff --git a/211/web/public/shop_plants_organized/柠檬_from_柠檬种子_1020042.png b/211/web/public/shop_plants_organized/柠檬_from_柠檬种子_1020042.png new file mode 100644 index 0000000..ffd2f88 Binary files /dev/null and b/211/web/public/shop_plants_organized/柠檬_from_柠檬种子_1020042.png differ diff --git a/211/web/public/shop_plants_organized/栗子_from_栗子种子_1020095.png b/211/web/public/shop_plants_organized/栗子_from_栗子种子_1020095.png new file mode 100644 index 0000000..b54802e Binary files /dev/null and b/211/web/public/shop_plants_organized/栗子_from_栗子种子_1020095.png differ diff --git a/211/web/public/shop_plants_organized/核桃_from_核桃种子_1020308.png b/211/web/public/shop_plants_organized/核桃_from_核桃种子_1020308.png new file mode 100644 index 0000000..f2f1a01 Binary files /dev/null and b/211/web/public/shop_plants_organized/核桃_from_核桃种子_1020308.png differ diff --git a/211/web/public/shop_plants_organized/桂圆_from_桂圆种子_1020053.png b/211/web/public/shop_plants_organized/桂圆_from_桂圆种子_1020053.png new file mode 100644 index 0000000..7d85652 Binary files /dev/null and b/211/web/public/shop_plants_organized/桂圆_from_桂圆种子_1020053.png differ diff --git a/211/web/public/shop_plants_organized/桃子_from_桃子种子_1020018.png b/211/web/public/shop_plants_organized/桃子_from_桃子种子_1020018.png new file mode 100644 index 0000000..e761b21 Binary files /dev/null and b/211/web/public/shop_plants_organized/桃子_from_桃子种子_1020018.png differ diff --git a/211/web/public/shop_plants_organized/桑葚_from_桑葚种子_1020075.png b/211/web/public/shop_plants_organized/桑葚_from_桑葚种子_1020075.png new file mode 100644 index 0000000..e9d5cbf Binary files /dev/null and b/211/web/public/shop_plants_organized/桑葚_from_桑葚种子_1020075.png differ diff --git a/211/web/public/shop_plants_organized/梨_from_梨种子_1020054.png b/211/web/public/shop_plants_organized/梨_from_梨种子_1020054.png new file mode 100644 index 0000000..e0a5939 Binary files /dev/null and b/211/web/public/shop_plants_organized/梨_from_梨种子_1020054.png differ diff --git a/211/web/public/shop_plants_organized/椰子_from_椰子种子_1020029.png b/211/web/public/shop_plants_organized/椰子_from_椰子种子_1020029.png new file mode 100644 index 0000000..e32edae Binary files /dev/null and b/211/web/public/shop_plants_organized/椰子_from_椰子种子_1020029.png differ diff --git a/211/web/public/shop_plants_organized/榛子_from_榛子种子_1020072.png b/211/web/public/shop_plants_organized/榛子_from_榛子种子_1020072.png new file mode 100644 index 0000000..2cfff48 Binary files /dev/null and b/211/web/public/shop_plants_organized/榛子_from_榛子种子_1020072.png differ diff --git a/211/web/public/shop_plants_organized/榴莲_from_榴莲种子_1020058.png b/211/web/public/shop_plants_organized/榴莲_from_榴莲种子_1020058.png new file mode 100644 index 0000000..1d09335 Binary files /dev/null and b/211/web/public/shop_plants_organized/榴莲_from_榴莲种子_1020058.png differ diff --git a/211/web/public/shop_plants_organized/樱桃_from_樱桃种子_1020034.png b/211/web/public/shop_plants_organized/樱桃_from_樱桃种子_1020034.png new file mode 100644 index 0000000..d441558 Binary files /dev/null and b/211/web/public/shop_plants_organized/樱桃_from_樱桃种子_1020034.png differ diff --git a/211/web/public/shop_plants_organized/橙子_from_橙子种子_1020019.png b/211/web/public/shop_plants_organized/橙子_from_橙子种子_1020019.png new file mode 100644 index 0000000..dfe73eb Binary files /dev/null and b/211/web/public/shop_plants_organized/橙子_from_橙子种子_1020019.png differ diff --git a/211/web/public/shop_plants_organized/水稻_from_水稻种子_1020060.png b/211/web/public/shop_plants_organized/水稻_from_水稻种子_1020060.png new file mode 100644 index 0000000..633011a Binary files /dev/null and b/211/web/public/shop_plants_organized/水稻_from_水稻种子_1020060.png differ diff --git a/211/web/public/shop_plants_organized/油菜_from_油菜种子_1020099.png b/211/web/public/shop_plants_organized/油菜_from_油菜种子_1020099.png new file mode 100644 index 0000000..3c833ea Binary files /dev/null and b/211/web/public/shop_plants_organized/油菜_from_油菜种子_1020099.png differ diff --git a/211/web/public/shop_plants_organized/满天星_from_满天星种子_1020110.png b/211/web/public/shop_plants_organized/满天星_from_满天星种子_1020110.png new file mode 100644 index 0000000..4c2ce2d Binary files /dev/null and b/211/web/public/shop_plants_organized/满天星_from_满天星种子_1020110.png differ diff --git a/211/web/public/shop_plants_organized/火绒草_from_火绒草种子_1020135.png b/211/web/public/shop_plants_organized/火绒草_from_火绒草种子_1020135.png new file mode 100644 index 0000000..44c100b Binary files /dev/null and b/211/web/public/shop_plants_organized/火绒草_from_火绒草种子_1020135.png differ diff --git a/211/web/public/shop_plants_organized/火龙果_from_火龙果种子_1020033.png b/211/web/public/shop_plants_organized/火龙果_from_火龙果种子_1020033.png new file mode 100644 index 0000000..cded9e1 Binary files /dev/null and b/211/web/public/shop_plants_organized/火龙果_from_火龙果种子_1020033.png differ diff --git a/211/web/public/shop_plants_organized/牵牛花_from_牵牛花种子_1020147.png b/211/web/public/shop_plants_organized/牵牛花_from_牵牛花种子_1020147.png new file mode 100644 index 0000000..c94f0b5 Binary files /dev/null and b/211/web/public/shop_plants_organized/牵牛花_from_牵牛花种子_1020147.png differ diff --git a/211/web/public/shop_plants_organized/猕猴桃_from_猕猴桃种子_1020045.png b/211/web/public/shop_plants_organized/猕猴桃_from_猕猴桃种子_1020045.png new file mode 100644 index 0000000..1307b25 Binary files /dev/null and b/211/web/public/shop_plants_organized/猕猴桃_from_猕猴桃种子_1020045.png differ diff --git a/211/web/public/shop_plants_organized/猪笼草_from_猪笼草种子_1020220.png b/211/web/public/shop_plants_organized/猪笼草_from_猪笼草种子_1020220.png new file mode 100644 index 0000000..aa21bd9 Binary files /dev/null and b/211/web/public/shop_plants_organized/猪笼草_from_猪笼草种子_1020220.png differ diff --git a/211/web/public/shop_plants_organized/玉米_from_玉米种子_1020004.png b/211/web/public/shop_plants_organized/玉米_from_玉米种子_1020004.png new file mode 100644 index 0000000..d18417b Binary files /dev/null and b/211/web/public/shop_plants_organized/玉米_from_玉米种子_1020004.png differ diff --git a/211/web/public/shop_plants_organized/瓶子树_from_瓶子树种子_1020218.png b/211/web/public/shop_plants_organized/瓶子树_from_瓶子树种子_1020218.png new file mode 100644 index 0000000..3051ecc Binary files /dev/null and b/211/web/public/shop_plants_organized/瓶子树_from_瓶子树种子_1020218.png differ diff --git a/211/web/public/shop_plants_organized/甘蔗_from_甘蔗种子_1020047.png b/211/web/public/shop_plants_organized/甘蔗_from_甘蔗种子_1020047.png new file mode 100644 index 0000000..e02b10f Binary files /dev/null and b/211/web/public/shop_plants_organized/甘蔗_from_甘蔗种子_1020047.png differ diff --git a/211/web/public/shop_plants_organized/生菜_from_生菜种子_1020096.png b/211/web/public/shop_plants_organized/生菜_from_生菜种子_1020096.png new file mode 100644 index 0000000..ef07c87 Binary files /dev/null and b/211/web/public/shop_plants_organized/生菜_from_生菜种子_1020096.png differ diff --git a/211/web/public/shop_plants_organized/番石榴_from_番石榴种子_1020079.png b/211/web/public/shop_plants_organized/番石榴_from_番石榴种子_1020079.png new file mode 100644 index 0000000..fd25a6f Binary files /dev/null and b/211/web/public/shop_plants_organized/番石榴_from_番石榴种子_1020079.png differ diff --git a/211/web/public/shop_plants_organized/番茄_from_番茄种子_1020007.png b/211/web/public/shop_plants_organized/番茄_from_番茄种子_1020007.png new file mode 100644 index 0000000..6ef99ad Binary files /dev/null and b/211/web/public/shop_plants_organized/番茄_from_番茄种子_1020007.png differ diff --git a/211/web/public/shop_plants_organized/白萝卜_from_白萝卜种子_1020002.png b/211/web/public/shop_plants_organized/白萝卜_from_白萝卜种子_1020002.png new file mode 100644 index 0000000..1e83c6a Binary files /dev/null and b/211/web/public/shop_plants_organized/白萝卜_from_白萝卜种子_1020002.png differ diff --git a/211/web/public/shop_plants_organized/睡莲_from_睡莲种子_1020442.png b/211/web/public/shop_plants_organized/睡莲_from_睡莲种子_1020442.png new file mode 100644 index 0000000..44a5d81 Binary files /dev/null and b/211/web/public/shop_plants_organized/睡莲_from_睡莲种子_1020442.png differ diff --git a/211/web/public/shop_plants_organized/石榴_from_石榴种子_1020023.png b/211/web/public/shop_plants_organized/石榴_from_石榴种子_1020023.png new file mode 100644 index 0000000..479670a Binary files /dev/null and b/211/web/public/shop_plants_organized/石榴_from_石榴种子_1020023.png differ diff --git a/211/web/public/shop_plants_organized/秋菊红色_from_红色秋菊种子_1020162.png b/211/web/public/shop_plants_organized/秋菊红色_from_红色秋菊种子_1020162.png new file mode 100644 index 0000000..66c9032 Binary files /dev/null and b/211/web/public/shop_plants_organized/秋菊红色_from_红色秋菊种子_1020162.png differ diff --git a/211/web/public/shop_plants_organized/秋菊黄色_from_黄色秋菊种子_1020161.png b/211/web/public/shop_plants_organized/秋菊黄色_from_黄色秋菊种子_1020161.png new file mode 100644 index 0000000..796d914 Binary files /dev/null and b/211/web/public/shop_plants_organized/秋菊黄色_from_黄色秋菊种子_1020161.png differ diff --git a/211/web/public/shop_plants_organized/竹笋_from_竹笋种子_1020100.png b/211/web/public/shop_plants_organized/竹笋_from_竹笋种子_1020100.png new file mode 100644 index 0000000..f47e055 Binary files /dev/null and b/211/web/public/shop_plants_organized/竹笋_from_竹笋种子_1020100.png differ diff --git a/211/web/public/shop_plants_organized/箬竹_from_箬竹种子_1020036.png b/211/web/public/shop_plants_organized/箬竹_from_箬竹种子_1020036.png new file mode 100644 index 0000000..3fc261b Binary files /dev/null and b/211/web/public/shop_plants_organized/箬竹_from_箬竹种子_1020036.png differ diff --git a/211/web/public/shop_plants_organized/红枣_from_红枣种子_1020051.png b/211/web/public/shop_plants_organized/红枣_from_红枣种子_1020051.png new file mode 100644 index 0000000..6ec3af1 Binary files /dev/null and b/211/web/public/shop_plants_organized/红枣_from_红枣种子_1020051.png differ diff --git a/211/web/public/shop_plants_organized/红玫瑰_from_红玫瑰种子_1020041.png b/211/web/public/shop_plants_organized/红玫瑰_from_红玫瑰种子_1020041.png new file mode 100644 index 0000000..238ac06 Binary files /dev/null and b/211/web/public/shop_plants_organized/红玫瑰_from_红玫瑰种子_1020041.png differ diff --git a/211/web/public/shop_plants_organized/胡萝卜_from_胡萝卜种子_1020003.png b/211/web/public/shop_plants_organized/胡萝卜_from_胡萝卜种子_1020003.png new file mode 100644 index 0000000..ba0e1c3 Binary files /dev/null and b/211/web/public/shop_plants_organized/胡萝卜_from_胡萝卜种子_1020003.png differ diff --git a/211/web/public/shop_plants_organized/芒果_from_芒果种子_1020057.png b/211/web/public/shop_plants_organized/芒果_from_芒果种子_1020057.png new file mode 100644 index 0000000..b98d415 Binary files /dev/null and b/211/web/public/shop_plants_organized/芒果_from_芒果种子_1020057.png differ diff --git a/211/web/public/shop_plants_organized/花生_from_花生种子_1020049.png b/211/web/public/shop_plants_organized/花生_from_花生种子_1020049.png new file mode 100644 index 0000000..787e792 Binary files /dev/null and b/211/web/public/shop_plants_organized/花生_from_花生种子_1020049.png differ diff --git a/211/web/public/shop_plants_organized/花菜_from_花菜种子_1020098.png b/211/web/public/shop_plants_organized/花菜_from_花菜种子_1020098.png new file mode 100644 index 0000000..8034801 Binary files /dev/null and b/211/web/public/shop_plants_organized/花菜_from_花菜种子_1020098.png differ diff --git a/211/web/public/shop_plants_organized/花香根鸢尾_from_花香根鸢尾种子_1020141.png b/211/web/public/shop_plants_organized/花香根鸢尾_from_花香根鸢尾种子_1020141.png new file mode 100644 index 0000000..ba9af1a Binary files /dev/null and b/211/web/public/shop_plants_organized/花香根鸢尾_from_花香根鸢尾种子_1020141.png differ diff --git a/211/web/public/shop_plants_organized/芹菜_from_芹菜种子_1020306.png b/211/web/public/shop_plants_organized/芹菜_from_芹菜种子_1020306.png new file mode 100644 index 0000000..a70c55a Binary files /dev/null and b/211/web/public/shop_plants_organized/芹菜_from_芹菜种子_1020306.png differ diff --git a/211/web/public/shop_plants_organized/苦瓜_from_苦瓜种子_1020063.png b/211/web/public/shop_plants_organized/苦瓜_from_苦瓜种子_1020063.png new file mode 100644 index 0000000..580cca6 Binary files /dev/null and b/211/web/public/shop_plants_organized/苦瓜_from_苦瓜种子_1020063.png differ diff --git a/211/web/public/shop_plants_organized/苹果_from_苹果种子_1020011.png b/211/web/public/shop_plants_organized/苹果_from_苹果种子_1020011.png new file mode 100644 index 0000000..03cad0b Binary files /dev/null and b/211/web/public/shop_plants_organized/苹果_from_苹果种子_1020011.png differ diff --git a/211/web/public/shop_plants_organized/茄子_from_茄子种子_1020006.png b/211/web/public/shop_plants_organized/茄子_from_茄子种子_1020006.png new file mode 100644 index 0000000..e93c18a Binary files /dev/null and b/211/web/public/shop_plants_organized/茄子_from_茄子种子_1020006.png differ diff --git a/211/web/public/shop_plants_organized/茉莉花_from_茉莉花种子_1020128.png b/211/web/public/shop_plants_organized/茉莉花_from_茉莉花种子_1020128.png new file mode 100644 index 0000000..5acb03c Binary files /dev/null and b/211/web/public/shop_plants_organized/茉莉花_from_茉莉花种子_1020128.png differ diff --git a/211/web/public/shop_plants_organized/草莓_from_草莓种子_1020001.png b/211/web/public/shop_plants_organized/草莓_from_草莓种子_1020001.png new file mode 100644 index 0000000..89d3d0b Binary files /dev/null and b/211/web/public/shop_plants_organized/草莓_from_草莓种子_1020001.png differ diff --git a/211/web/public/shop_plants_organized/荔枝_from_荔枝种子_1020035.png b/211/web/public/shop_plants_organized/荔枝_from_荔枝种子_1020035.png new file mode 100644 index 0000000..bcc11e3 Binary files /dev/null and b/211/web/public/shop_plants_organized/荔枝_from_荔枝种子_1020035.png differ diff --git a/211/web/public/shop_plants_organized/莲藕_from_莲藕种子_1020037.png b/211/web/public/shop_plants_organized/莲藕_from_莲藕种子_1020037.png new file mode 100644 index 0000000..41c7deb Binary files /dev/null and b/211/web/public/shop_plants_organized/莲藕_from_莲藕种子_1020037.png differ diff --git a/211/web/public/shop_plants_organized/菠菜_from_菠菜种子_1020073.png b/211/web/public/shop_plants_organized/菠菜_from_菠菜种子_1020073.png new file mode 100644 index 0000000..bae6064 Binary files /dev/null and b/211/web/public/shop_plants_organized/菠菜_from_菠菜种子_1020073.png differ diff --git a/211/web/public/shop_plants_organized/菠萝_from_菠萝种子_1020027.png b/211/web/public/shop_plants_organized/菠萝_from_菠萝种子_1020027.png new file mode 100644 index 0000000..e06db9e Binary files /dev/null and b/211/web/public/shop_plants_organized/菠萝_from_菠萝种子_1020027.png differ diff --git a/211/web/public/shop_plants_organized/葡萄_from_葡萄种子_1020013.png b/211/web/public/shop_plants_organized/葡萄_from_葡萄种子_1020013.png new file mode 100644 index 0000000..29bd745 Binary files /dev/null and b/211/web/public/shop_plants_organized/葡萄_from_葡萄种子_1020013.png differ diff --git a/211/web/public/shop_plants_organized/葫芦_from_葫芦种子_1020031.png b/211/web/public/shop_plants_organized/葫芦_from_葫芦种子_1020031.png new file mode 100644 index 0000000..f1c4e69 Binary files /dev/null and b/211/web/public/shop_plants_organized/葫芦_from_葫芦种子_1020031.png differ diff --git a/211/web/public/shop_plants_organized/蒲公英_from_蒲公英种子_1020120.png b/211/web/public/shop_plants_organized/蒲公英_from_蒲公英种子_1020120.png new file mode 100644 index 0000000..0a0026f Binary files /dev/null and b/211/web/public/shop_plants_organized/蒲公英_from_蒲公英种子_1020120.png differ diff --git a/211/web/public/shop_plants_organized/蓝莓_from_蓝莓种子_1020077.png b/211/web/public/shop_plants_organized/蓝莓_from_蓝莓种子_1020077.png new file mode 100644 index 0000000..1212e50 Binary files /dev/null and b/211/web/public/shop_plants_organized/蓝莓_from_蓝莓种子_1020077.png differ diff --git a/211/web/public/shop_plants_organized/蘑菇_from_蘑菇种子_1020050.png b/211/web/public/shop_plants_organized/蘑菇_from_蘑菇种子_1020050.png new file mode 100644 index 0000000..23e4a7a Binary files /dev/null and b/211/web/public/shop_plants_organized/蘑菇_from_蘑菇种子_1020050.png differ diff --git a/211/web/public/shop_plants_organized/虞美人_from_虞美人种子_1020142.png b/211/web/public/shop_plants_organized/虞美人_from_虞美人种子_1020142.png new file mode 100644 index 0000000..ee87ae5 Binary files /dev/null and b/211/web/public/shop_plants_organized/虞美人_from_虞美人种子_1020142.png differ diff --git a/211/web/public/shop_plants_organized/西瓜_from_西瓜种子_1020014.png b/211/web/public/shop_plants_organized/西瓜_from_西瓜种子_1020014.png new file mode 100644 index 0000000..4138680 Binary files /dev/null and b/211/web/public/shop_plants_organized/西瓜_from_西瓜种子_1020014.png differ diff --git a/211/web/public/shop_plants_organized/豌豆_from_豌豆种子_1020008.png b/211/web/public/shop_plants_organized/豌豆_from_豌豆种子_1020008.png new file mode 100644 index 0000000..95f7ab3 Binary files /dev/null and b/211/web/public/shop_plants_organized/豌豆_from_豌豆种子_1020008.png differ diff --git a/211/web/public/shop_plants_organized/豹皮花_from_豹皮花种子_1020222.png b/211/web/public/shop_plants_organized/豹皮花_from_豹皮花种子_1020222.png new file mode 100644 index 0000000..97a2f92 Binary files /dev/null and b/211/web/public/shop_plants_organized/豹皮花_from_豹皮花种子_1020222.png differ diff --git a/211/web/public/shop_plants_organized/辣椒_from_辣椒种子_1020009.png b/211/web/public/shop_plants_organized/辣椒_from_辣椒种子_1020009.png new file mode 100644 index 0000000..fa9d318 Binary files /dev/null and b/211/web/public/shop_plants_organized/辣椒_from_辣椒种子_1020009.png differ diff --git a/211/web/public/shop_plants_organized/迎春花_from_迎春花种子_1020396.png b/211/web/public/shop_plants_organized/迎春花_from_迎春花种子_1020396.png new file mode 100644 index 0000000..291466a Binary files /dev/null and b/211/web/public/shop_plants_organized/迎春花_from_迎春花种子_1020396.png differ diff --git a/211/web/public/shop_plants_organized/金桔_from_金桔种子_1020074.png b/211/web/public/shop_plants_organized/金桔_from_金桔种子_1020074.png new file mode 100644 index 0000000..62b04a3 Binary files /dev/null and b/211/web/public/shop_plants_organized/金桔_from_金桔种子_1020074.png differ diff --git a/211/web/public/shop_plants_organized/金针菇_from_金针菇种子_1020052.png b/211/web/public/shop_plants_organized/金针菇_from_金针菇种子_1020052.png new file mode 100644 index 0000000..ccc7afd Binary files /dev/null and b/211/web/public/shop_plants_organized/金针菇_from_金针菇种子_1020052.png differ diff --git a/211/web/public/shop_plants_organized/银莲花_from_银莲花种子_1020259.png b/211/web/public/shop_plants_organized/银莲花_from_银莲花种子_1020259.png new file mode 100644 index 0000000..01df145 Binary files /dev/null and b/211/web/public/shop_plants_organized/银莲花_from_银莲花种子_1020259.png differ diff --git a/211/web/public/shop_plants_organized/非洲菊_from_非洲菊种子_1020104.png b/211/web/public/shop_plants_organized/非洲菊_from_非洲菊种子_1020104.png new file mode 100644 index 0000000..1135d46 Binary files /dev/null and b/211/web/public/shop_plants_organized/非洲菊_from_非洲菊种子_1020104.png differ diff --git a/211/web/public/shop_plants_organized/韭菜_from_韭菜种子_1020305.png b/211/web/public/shop_plants_organized/韭菜_from_韭菜种子_1020305.png new file mode 100644 index 0000000..b227554 Binary files /dev/null and b/211/web/public/shop_plants_organized/韭菜_from_韭菜种子_1020305.png differ diff --git a/211/web/public/shop_plants_organized/香瓜_from_香瓜种子_1020067.png b/211/web/public/shop_plants_organized/香瓜_from_香瓜种子_1020067.png new file mode 100644 index 0000000..2d1e6d7 Binary files /dev/null and b/211/web/public/shop_plants_organized/香瓜_from_香瓜种子_1020067.png differ diff --git a/211/web/public/shop_plants_organized/香蕉_from_香蕉种子_1020015.png b/211/web/public/shop_plants_organized/香蕉_from_香蕉种子_1020015.png new file mode 100644 index 0000000..778dabb Binary files /dev/null and b/211/web/public/shop_plants_organized/香蕉_from_香蕉种子_1020015.png differ diff --git a/211/web/public/shop_plants_organized/鲜姜_from_鲜姜种子_1020066.png b/211/web/public/shop_plants_organized/鲜姜_from_鲜姜种子_1020066.png new file mode 100644 index 0000000..06ed9b1 Binary files /dev/null and b/211/web/public/shop_plants_organized/鲜姜_from_鲜姜种子_1020066.png differ diff --git a/211/web/public/shop_plants_organized/黄瓜_from_黄瓜种子_1020097.png b/211/web/public/shop_plants_organized/黄瓜_from_黄瓜种子_1020097.png new file mode 100644 index 0000000..02a89d7 Binary files /dev/null and b/211/web/public/shop_plants_organized/黄瓜_from_黄瓜种子_1020097.png differ diff --git a/211/web/public/shop_plants_organized/黄豆_from_黄豆种子_1020070.png b/211/web/public/shop_plants_organized/黄豆_from_黄豆种子_1020070.png new file mode 100644 index 0000000..4d645c4 Binary files /dev/null and b/211/web/public/shop_plants_organized/黄豆_from_黄豆种子_1020070.png differ diff --git a/211/web/public/verified_items/100003_化肥礼包_4_d13af7af.png b/211/web/public/verified_items/100003_化肥礼包_4_d13af7af.png new file mode 100644 index 0000000..b8360a9 Binary files /dev/null and b/211/web/public/verified_items/100003_化肥礼包_4_d13af7af.png differ diff --git a/211/web/public/verified_items/1001_金币_4_65088be3.png b/211/web/public/verified_items/1001_金币_4_65088be3.png new file mode 100644 index 0000000..e53a72c Binary files /dev/null and b/211/web/public/verified_items/1001_金币_4_65088be3.png differ diff --git a/211/web/public/verified_items/1002_点券_50_bd489f54.png b/211/web/public/verified_items/1002_点券_50_bd489f54.png new file mode 100644 index 0000000..2f359b5 Binary files /dev/null and b/211/web/public/verified_items/1002_点券_50_bd489f54.png differ diff --git a/211/web/public/verified_items/1004_钻石_7_ff3c1c6f.png b/211/web/public/verified_items/1004_钻石_7_ff3c1c6f.png new file mode 100644 index 0000000..13ea0fd Binary files /dev/null and b/211/web/public/verified_items/1004_钻石_7_ff3c1c6f.png differ diff --git a/211/web/public/verified_items/1011_普通化肥容器_12_3ad710ed.png b/211/web/public/verified_items/1011_普通化肥容器_12_3ad710ed.png new file mode 100644 index 0000000..5d4e433 Binary files /dev/null and b/211/web/public/verified_items/1011_普通化肥容器_12_3ad710ed.png differ diff --git a/211/web/public/verified_items/1012_有机化肥容器_49_fe1c87b7.png b/211/web/public/verified_items/1012_有机化肥容器_49_fe1c87b7.png new file mode 100644 index 0000000..65aca2e Binary files /dev/null and b/211/web/public/verified_items/1012_有机化肥容器_49_fe1c87b7.png differ diff --git a/211/web/public/verified_items/80001_化肥1小时_1_cb2a91eb.png b/211/web/public/verified_items/80001_化肥1小时_1_cb2a91eb.png new file mode 100644 index 0000000..349a109 Binary files /dev/null and b/211/web/public/verified_items/80001_化肥1小时_1_cb2a91eb.png differ diff --git a/211/web/public/verified_items/80002_化肥4小时_1_af16205d.png b/211/web/public/verified_items/80002_化肥4小时_1_af16205d.png new file mode 100644 index 0000000..06a0de3 Binary files /dev/null and b/211/web/public/verified_items/80002_化肥4小时_1_af16205d.png differ diff --git a/211/web/public/verified_items/80003_化肥8小时_1_c3700ffc.png b/211/web/public/verified_items/80003_化肥8小时_1_c3700ffc.png new file mode 100644 index 0000000..d796998 Binary files /dev/null and b/211/web/public/verified_items/80003_化肥8小时_1_c3700ffc.png differ diff --git a/211/web/public/verified_items/80004_化肥12小时_1_179fe100.png b/211/web/public/verified_items/80004_化肥12小时_1_179fe100.png new file mode 100644 index 0000000..ee01b46 Binary files /dev/null and b/211/web/public/verified_items/80004_化肥12小时_1_179fe100.png differ diff --git a/211/web/public/verified_items/80011_有机化肥1小时_2_8fe4411f.png b/211/web/public/verified_items/80011_有机化肥1小时_2_8fe4411f.png new file mode 100644 index 0000000..04b7d1e Binary files /dev/null and b/211/web/public/verified_items/80011_有机化肥1小时_2_8fe4411f.png differ diff --git a/211/web/public/verified_items/80012_有机化肥4小时_1_03357e47.png b/211/web/public/verified_items/80012_有机化肥4小时_1_03357e47.png new file mode 100644 index 0000000..c8757ed Binary files /dev/null and b/211/web/public/verified_items/80012_有机化肥4小时_1_03357e47.png differ diff --git a/211/web/public/verified_items/80013_有机化肥8小时_1_242b3363.png b/211/web/public/verified_items/80013_有机化肥8小时_1_242b3363.png new file mode 100644 index 0000000..1c16bde Binary files /dev/null and b/211/web/public/verified_items/80013_有机化肥8小时_1_242b3363.png differ diff --git a/211/web/public/verified_items/80014_有机化肥12小时_2_5b6e11c0.png b/211/web/public/verified_items/80014_有机化肥12小时_2_5b6e11c0.png new file mode 100644 index 0000000..3ae8366 Binary files /dev/null and b/211/web/public/verified_items/80014_有机化肥12小时_2_5b6e11c0.png differ diff --git a/211/web/public/verified_items/90001_田园犬_1.png b/211/web/public/verified_items/90001_田园犬_1.png new file mode 100644 index 0000000..7ea129d Binary files /dev/null and b/211/web/public/verified_items/90001_田园犬_1.png differ diff --git a/211/web/public/verified_items/90002_牧羊犬_1_0a3a47cb.png b/211/web/public/verified_items/90002_牧羊犬_1_0a3a47cb.png new file mode 100644 index 0000000..e7ec0bd Binary files /dev/null and b/211/web/public/verified_items/90002_牧羊犬_1_0a3a47cb.png differ diff --git a/211/web/public/verified_items/90003_斑点狗_1_ccf8c0ff.png b/211/web/public/verified_items/90003_斑点狗_1_ccf8c0ff.png new file mode 100644 index 0000000..5e54b20 Binary files /dev/null and b/211/web/public/verified_items/90003_斑点狗_1_ccf8c0ff.png differ diff --git a/211/web/public/verified_items/90004_1天狗粮_1_ec8e4ace.png b/211/web/public/verified_items/90004_1天狗粮_1_ec8e4ace.png new file mode 100644 index 0000000..04f52c4 Binary files /dev/null and b/211/web/public/verified_items/90004_1天狗粮_1_ec8e4ace.png differ diff --git a/211/web/public/verified_items/90005_3天狗粮_1_835fa5b1.png b/211/web/public/verified_items/90005_3天狗粮_1_835fa5b1.png new file mode 100644 index 0000000..d307615 Binary files /dev/null and b/211/web/public/verified_items/90005_3天狗粮_1_835fa5b1.png differ diff --git a/211/web/public/verified_items/90006_5天狗粮_1_a04b7acf.png b/211/web/public/verified_items/90006_5天狗粮_1_a04b7acf.png new file mode 100644 index 0000000..5dc661f Binary files /dev/null and b/211/web/public/verified_items/90006_5天狗粮_1_a04b7acf.png differ diff --git a/211/web/public/verified_items/index.html b/211/web/public/verified_items/index.html new file mode 100644 index 0000000..bda47db --- /dev/null +++ b/211/web/public/verified_items/index.html @@ -0,0 +1,184 @@ + + + + + + Verified Items + + + +

Verified Items (20)

+
+ +
+ 1001_金币_4_65088be3.png +
+
1001
+
1001_金币_4_65088be3.png
+
+
+ +
+ 1002_点券_50_bd489f54.png +
+
1002
+
1002_点券_50_bd489f54.png
+
+
+ +
+ 1004_钻石_7_ff3c1c6f.png +
+
1004
+
1004_钻石_7_ff3c1c6f.png
+
+
+ +
+ 1011_普通化肥容器_12_3ad710ed.png +
+
1011
+
1011_普通化肥容器_12_3ad710ed.png
+
+
+ +
+ 1012_有机化肥容器_49_fe1c87b7.png +
+
1012
+
1012_有机化肥容器_49_fe1c87b7.png
+
+
+ +
+ 80001_化肥1小时_1_cb2a91eb.png +
+
80001
+
80001_化肥1小时_1_cb2a91eb.png
+
+
+ +
+ 80002_化肥4小时_1_af16205d.png +
+
80002
+
80002_化肥4小时_1_af16205d.png
+
+
+ +
+ 80003_化肥8小时_1_c3700ffc.png +
+
80003
+
80003_化肥8小时_1_c3700ffc.png
+
+
+ +
+ 80004_化肥12小时_1_179fe100.png +
+
80004
+
80004_化肥12小时_1_179fe100.png
+
+
+ +
+ 80011_有机化肥1小时_2_8fe4411f.png +
+
80011
+
80011_有机化肥1小时_2_8fe4411f.png
+
+
+ +
+ 80012_有机化肥4小时_1_03357e47.png +
+
80012
+
80012_有机化肥4小时_1_03357e47.png
+
+
+ +
+ 80013_有机化肥8小时_1_242b3363.png +
+
80013
+
80013_有机化肥8小时_1_242b3363.png
+
+
+ +
+ 80014_有机化肥12小时_2_5b6e11c0.png +
+
80014
+
80014_有机化肥12小时_2_5b6e11c0.png
+
+
+ +
+ 90002_牧羊犬_1_0a3a47cb.png +
+
90002
+
90002_牧羊犬_1_0a3a47cb.png
+
+
+ +
+ 90003_斑点狗_1_ccf8c0ff.png +
+
90003
+
90003_斑点狗_1_ccf8c0ff.png
+
+
+ +
+ 90004_1天狗粮_1_ec8e4ace.png +
+
90004
+
90004_1天狗粮_1_ec8e4ace.png
+
+
+ +
+ 90005_3天狗粮_1_835fa5b1.png +
+
90005
+
90005_3天狗粮_1_835fa5b1.png
+
+
+ +
+ 90006_5天狗粮_1_a04b7acf.png +
+
90006
+
90006_5天狗粮_1_a04b7acf.png
+
+
+ +
+ 100003_化肥礼包_4_d13af7af.png +
+
100003
+
100003_化肥礼包_4_d13af7af.png
+
+
+ +
+ 田园犬 +
+
90001
+
田园犬
+
+
+ +
+ + + \ No newline at end of file diff --git a/211/web/public/verified_items/mapping.json b/211/web/public/verified_items/mapping.json new file mode 100644 index 0000000..495a009 --- /dev/null +++ b/211/web/public/verified_items/mapping.json @@ -0,0 +1,103 @@ +[ + { + "id": "1001", + "filename": "1001_金币_4_65088be3.png", + "path": "verified_items/1001_金币_4_65088be3.png" + }, + { + "id": "1002", + "filename": "1002_点券_50_bd489f54.png", + "path": "verified_items/1002_点券_50_bd489f54.png" + }, + { + "id": "1004", + "filename": "1004_钻石_7_ff3c1c6f.png", + "path": "verified_items/1004_钻石_7_ff3c1c6f.png" + }, + { + "id": "1011", + "filename": "1011_普通化肥容器_12_3ad710ed.png", + "path": "verified_items/1011_普通化肥容器_12_3ad710ed.png" + }, + { + "id": "1012", + "filename": "1012_有机化肥容器_49_fe1c87b7.png", + "path": "verified_items/1012_有机化肥容器_49_fe1c87b7.png" + }, + { + "id": "80001", + "filename": "80001_化肥1小时_1_cb2a91eb.png", + "path": "verified_items/80001_化肥1小时_1_cb2a91eb.png" + }, + { + "id": "80002", + "filename": "80002_化肥4小时_1_af16205d.png", + "path": "verified_items/80002_化肥4小时_1_af16205d.png" + }, + { + "id": "80003", + "filename": "80003_化肥8小时_1_c3700ffc.png", + "path": "verified_items/80003_化肥8小时_1_c3700ffc.png" + }, + { + "id": "80004", + "filename": "80004_化肥12小时_1_179fe100.png", + "path": "verified_items/80004_化肥12小时_1_179fe100.png" + }, + { + "id": "80011", + "filename": "80011_有机化肥1小时_2_8fe4411f.png", + "path": "verified_items/80011_有机化肥1小时_2_8fe4411f.png" + }, + { + "id": "80012", + "filename": "80012_有机化肥4小时_1_03357e47.png", + "path": "verified_items/80012_有机化肥4小时_1_03357e47.png" + }, + { + "id": "80013", + "filename": "80013_有机化肥8小时_1_242b3363.png", + "path": "verified_items/80013_有机化肥8小时_1_242b3363.png" + }, + { + "id": "80014", + "filename": "80014_有机化肥12小时_2_5b6e11c0.png", + "path": "verified_items/80014_有机化肥12小时_2_5b6e11c0.png" + }, + { + "id": "90002", + "filename": "90002_牧羊犬_1_0a3a47cb.png", + "path": "verified_items/90002_牧羊犬_1_0a3a47cb.png" + }, + { + "id": "90003", + "filename": "90003_斑点狗_1_ccf8c0ff.png", + "path": "verified_items/90003_斑点狗_1_ccf8c0ff.png" + }, + { + "id": "90004", + "filename": "90004_1天狗粮_1_ec8e4ace.png", + "path": "verified_items/90004_1天狗粮_1_ec8e4ace.png" + }, + { + "id": "90005", + "filename": "90005_3天狗粮_1_835fa5b1.png", + "path": "verified_items/90005_3天狗粮_1_835fa5b1.png" + }, + { + "id": "90006", + "filename": "90006_5天狗粮_1_a04b7acf.png", + "path": "verified_items/90006_5天狗粮_1_a04b7acf.png" + }, + { + "id": "100003", + "filename": "100003_化肥礼包_4_d13af7af.png", + "path": "verified_items/100003_化肥礼包_4_d13af7af.png" + }, + { + "id": "90001", + "filename": "90001_田园犬_1.png", + "path": "verified_items/90001_田园犬_1.png", + "name": "田园犬" + } +] \ No newline at end of file diff --git a/211/web/public/verified_items/土地.png b/211/web/public/verified_items/土地.png new file mode 100644 index 0000000..1c4af5c Binary files /dev/null and b/211/web/public/verified_items/土地.png differ diff --git a/211/web/public/vite.svg b/211/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/211/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/211/web/src/App.css b/211/web/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/211/web/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/211/web/src/App.tsx b/211/web/src/App.tsx new file mode 100644 index 0000000..2632e38 --- /dev/null +++ b/211/web/src/App.tsx @@ -0,0 +1,24 @@ +import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom" +import Login from "./pages/Login" +import Dashboard from "./pages/Dashboard" + +function App() { + return ( + +
+
+ + } /> + } /> + } /> + +
+
+ Made By ♥Karriis +
+
+
+ ) +} + +export default App diff --git a/211/web/src/assets/fonts/MapleMono-NF-CN-Regular.woff2 b/211/web/src/assets/fonts/MapleMono-NF-CN-Regular.woff2 new file mode 100644 index 0000000..1e207c6 Binary files /dev/null and b/211/web/src/assets/fonts/MapleMono-NF-CN-Regular.woff2 differ diff --git a/211/web/src/assets/logo.png b/211/web/src/assets/logo.png new file mode 100644 index 0000000..5e92acb Binary files /dev/null and b/211/web/src/assets/logo.png differ diff --git a/211/web/src/assets/react.svg b/211/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/211/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/211/web/src/components/FarmGrid.tsx b/211/web/src/components/FarmGrid.tsx new file mode 100644 index 0000000..05b4103 --- /dev/null +++ b/211/web/src/components/FarmGrid.tsx @@ -0,0 +1,244 @@ +import { useEffect, useState } from "react" +import { Sprout, Lock, Droplets, Bug, Skull, Shovel, Flower, Ban, Hand } from "lucide-react" +import { cn } from "@/lib/utils" + +export interface LandData { + id: number + type: 'locked' | 'empty' | 'planted' + status?: 'growing' | 'mature' | 'dead' + plantName?: string + plantId?: number + phase?: number + phaseName?: string + fertilized?: boolean + needs?: { + water: boolean + weed: boolean + bug: boolean + } + canSteal?: boolean + couldUnlock?: boolean + unlockCondition?: { + needLevel: number + needGold: number + } +} + +interface FarmGridProps { + lands: LandData[] + isLoading?: boolean + onLandClick?: (land: LandData) => void + onLandUnlock?: (land: LandData) => void + selectionEnabled?: boolean + selectedLandIds?: number[] + userLevel?: number + userGold?: number + unlockingLandIds?: number[] +} + +export function FarmGrid({ lands, isLoading = false, onLandClick, onLandUnlock, selectionEnabled = false, selectedLandIds = [], userLevel = 0, userGold = 0, unlockingLandIds = [] }: FarmGridProps) { + const [plantImageMap, setPlantImageMap] = useState>({}) + + useEffect(() => { + let cancelled = false + fetch('/shop_plants_organized/mapping.csv') + .then(res => res.text()) + .then(text => { + if (cancelled) return + const lines = text.split(/\r?\n/).filter(Boolean) + if (lines.length <= 1) { + setPlantImageMap({}) + return + } + const map: Record = {} + for (let i = 1; i < lines.length; i += 1) { + const line = lines[i].trim() + if (!line) continue + const parts = line.split(',') + if (parts.length < 5) continue + const plantId = Number(parts[2]) + const fileName = parts[4] + if (!Number.isNaN(plantId) && fileName) { + map[plantId] = fileName + } + } + setPlantImageMap(map) + }) + .catch(() => { + if (!cancelled) setPlantImageMap({}) + }) + return () => { + cancelled = true + } + }, []) + + // Generate default 18 slots if lands is empty (initial state) + const displayLands = lands.length > 0 ? lands : Array.from({ length: 18 }, (_, i) => ({ + id: i + 1, + type: 'locked' as const + })) + + if (isLoading && lands.length === 0) { + return ( +
+ {Array.from({ length: 18 }).map((_, i) => ( +
+ ))} +
+ ) + } + + return ( +
+ {displayLands.map((land) => ( + onLandClick?.(land)} + onUnlock={() => onLandUnlock?.(land)} + userLevel={userLevel} + userGold={userGold} + isUnlocking={unlockingLandIds.includes(land.id)} + selected={selectionEnabled && selectedLandIds.includes(land.id)} + selectionEnabled={selectionEnabled} + /> + ))} +
+ ) +} + +function LandCard({ land, plantImageMap, onClick, onUnlock, userLevel, userGold, isUnlocking, selected, selectionEnabled }: { land: LandData, plantImageMap: Record, onClick?: () => void, onUnlock?: () => void, userLevel: number, userGold: number, isUnlocking: boolean, selected?: boolean, selectionEnabled?: boolean }) { + const isLocked = land.type === 'locked' + const isEmpty = land.type === 'empty' + const isPlanted = land.type === 'planted' + + const isDead = land.status === 'dead' + const isMature = land.status === 'mature' + const isGrowing = land.status === 'growing' + const isFertilized = land.fertilized === true + const plantImage = land.plantId ? plantImageMap[land.plantId] : undefined + const needLevel = land.unlockCondition?.needLevel ?? 0 + const needGold = land.unlockCondition?.needGold ?? 0 + const canUnlock = isLocked && !!land.couldUnlock && userLevel >= needLevel && userGold >= needGold + + return ( +
+ + {/* Status Badge (ID) */} + + #{land.id} + + {canUnlock && ( +
+ +
+ )} + + {/* Main Icon */} +
+ {isLocked && } + {isEmpty && } + + {isPlanted && ( + <> + {plantImage ? ( + {land.plantName + ) : ( + <> + {isDead && } + {isGrowing && } + {isMature && } + + )} + + {land.plantName || '等待中...'} + + + {`${land.phaseName || '未知'}/${isFertilized ? '已施肥' : '未施肥'}`} + + + )} +
+ + {/* Action Indicators */} + {isPlanted && !isDead && land.needs && ( +
+ {land.needs.water && ( +
+ +
+ )} + {land.needs.bug && ( +
+ +
+ )} + {land.needs.weed && ( +
+ +
+ )} +
+ )} + + {/* Steal Indicator */} + {land.canSteal && ( +
+ +
+ )} + + {/* Background decoration for mature */} + {isMature && ( +
+ )} +
+ ) +} diff --git a/211/web/src/components/FriendList.tsx b/211/web/src/components/FriendList.tsx new file mode 100644 index 0000000..3a88e04 --- /dev/null +++ b/211/web/src/components/FriendList.tsx @@ -0,0 +1,108 @@ +import { useCallback, useEffect, useState } from "react" +import { Users, User, Crown, RefreshCw } from "lucide-react" +import { ScrollArea } from "@/components/ui/scroll-area" +import { cn } from "@/lib/utils" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" + +interface Friend { + uin: string + userName: string + headPic: string + yellowLevel: number + exp: number + money: number +} + +export function FriendList({ email, onVisit }: { email: string, onVisit: (uid: string, name: string) => void }) { + const [friends, setFriends] = useState([]) + const [isLoading, setIsLoading] = useState(false) + + const fetchFriends = useCallback(async () => { + if (!email) return + setIsLoading(true) + try { + const res = await fetch(`/api/friends?email=${email}`) + const json = await res.json() + if (json.friends) { + setFriends(json.friends) + } + } catch (e) { + console.error(e) + } finally { + setIsLoading(false) + } + }, [email]) + + useEffect(() => { + fetchFriends() + }, [fetchFriends]) + + return ( +
+
+

+ + + + 好友列表 + + {friends.length} + +

+ +
+ + +
+ {friends.map((friend) => ( +
onVisit(friend.uin, friend.userName)} + > +
+ + + + + + + {friend.yellowLevel > 0 && ( +
+ Lv{friend.yellowLevel} +
+ )} +
+ +
+
+ {friend.userName} +
+
+ + 💰 {friend.money.toLocaleString()} + + + Exp {friend.exp} + +
+
+
+ ))} + + {friends.length === 0 && !isLoading && ( +
+ + 暂无好友数据 +
+ )} +
+
+
+ ) +} diff --git a/211/web/src/components/LogPanel.tsx b/211/web/src/components/LogPanel.tsx new file mode 100644 index 0000000..c71f245 --- /dev/null +++ b/211/web/src/components/LogPanel.tsx @@ -0,0 +1,265 @@ +import { useCallback, useEffect, useRef, useState } from "react" +import { ScrollArea } from "@/components/ui/scroll-area" +import { cn } from "@/lib/utils" +import { io, Socket } from "socket.io-client" +import { Terminal, Info, AlertTriangle, CheckCircle2, Clock } from "lucide-react" + +export interface LogEntry { + id: string + tag: string + msg: string + type: 'info' | 'warn' | 'error' | 'success' + time: number +} + +interface LogPanelProps { + className?: string + email?: string +} + +interface BotLogPayload { + tag?: string + msg?: unknown + type?: LogEntry['type'] + time?: number +} + +interface BotErrorPayload { + message?: string +} + +interface BotStatusPayload { + status?: string +} + +export function LogPanel({ className, email }: LogPanelProps) { + const [logs, setLogs] = useState([]) + const [isConnected, setIsConnected] = useState(false) + const scrollRef = useRef(null) + const socketRef = useRef(null) + const retryCountRef = useRef(0) + const disconnectNotifiedRef = useRef(false) + const retryNotifiedRef = useRef(false) + + const addLog = useCallback((log: Omit) => { + setLogs(prev => [...prev.slice(-200), { ...log, id: Math.random().toString(36).slice(2, 11) }]) + }, []) + + const addSystemLog = useCallback((msg: string, type: LogEntry['type'] = 'info') => { + addLog({ tag: 'System', msg, type, time: Date.now() }) + }, [addLog]) + + useEffect(() => { + if (!email) return + const loadHistory = async () => { + try { + const res = await fetch(`/api/logs?email=${encodeURIComponent(email)}`) + if (!res.ok) return + const data = await res.json() + if (!Array.isArray(data.logs)) return + const normalized = data.logs.map((entry: LogEntry) => ({ + id: entry.id || Math.random().toString(36).slice(2, 11), + tag: entry.tag || 'System', + msg: entry.msg || '', + type: entry.type || 'info', + time: entry.time || Date.now() + })) + setLogs(normalized) + } catch { + addSystemLog('历史日志加载失败', 'warn') + } + } + loadHistory() + + // Connect to WebSocket + // In production this should be an env var, here we assume same host + const socket = io('/', { + transports: ['websocket'], + reconnectionAttempts: 3, + reconnectionDelay: 1000, + reconnectionDelayMax: 3000 + }) + + socketRef.current = socket + + socket.on('connect', () => { + setIsConnected(true) + retryCountRef.current = 0 + disconnectNotifiedRef.current = false + retryNotifiedRef.current = false + addSystemLog('已连接到农场服务器') + }) + + socket.on('disconnect', () => { + setIsConnected(false) + if (!disconnectNotifiedRef.current) { + disconnectNotifiedRef.current = true + addSystemLog('与服务器断开连接', 'warn') + } + }) + + socket.on('reconnect_attempt', (attempt: number) => { + retryCountRef.current = attempt + if (!retryNotifiedRef.current) { + retryNotifiedRef.current = true + addSystemLog('连接断开,正在重试(最多3次)', 'warn') + } + }) + + socket.on('reconnect_failed', () => { + addSystemLog('连接失败,请重新绑定', 'error') + }) + + socket.on(`bot-log-${email}`, (data: unknown) => { + const payload = (typeof data === 'object' && data !== null ? data : {}) as BotLogPayload + // 格式化日志内容(如果它是对象) + const rawMsg = payload.msg + let message = '' + if (typeof rawMsg === 'string') { + message = rawMsg + } else if (rawMsg === null || rawMsg === undefined) { + message = '' + } else if (typeof rawMsg === 'object') { + try { + message = JSON.stringify(rawMsg) + } catch { + message = String(rawMsg) + } + } else { + message = String(rawMsg) + } + + const normalizedType: LogEntry['type'] = + payload.type === 'info' || payload.type === 'warn' || payload.type === 'error' || payload.type === 'success' + ? payload.type + : 'info' + + addLog({ + tag: payload.tag || 'System', + msg: message, + type: normalizedType, + time: payload.time || Date.now() + }) + }) + + socket.on(`bot-error-${email}`, (data: unknown) => { + const payload = (typeof data === 'object' && data !== null ? data : {}) as BotErrorPayload + addLog({ + tag: 'Error', + msg: payload.message || '未知错误', + type: 'error', + time: Date.now() + }) + }) + + socket.on(`bot-status-${email}`, (data: unknown) => { + const payload = (typeof data === 'object' && data !== null ? data : {}) as BotStatusPayload + const statusText = payload.status || 'unknown' + addSystemLog(`Bot状态更新: ${statusText}`, statusText === 'running' ? 'success' : 'warn') + }) + + return () => { + socket.off(`bot-log-${email}`) + socket.off(`bot-error-${email}`) + socket.off(`bot-status-${email}`) + socket.off('connect') + socket.off('disconnect') + socket.off('reconnect_attempt') + socket.off('reconnect_failed') + socket.disconnect() + } + }, [email, addLog, addSystemLog]) + + // Auto-scroll to bottom + useEffect(() => { + if (scrollRef.current) { + const scrollElement = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]') + if (scrollElement) { + scrollElement.scrollTop = scrollElement.scrollHeight + } + } + }, [logs]) + + return ( +
+ {/* Header */} +
+
+
+ +
+

运行日志

+
+
+ + + {isConnected ? 'Live' : 'Offline'} + +
+
+ + {/* Logs Area */} + +
+ {logs.length === 0 && ( +
+ +

等待活动...

+
+ )} + + {logs.map((log) => ( +
+ {/* Icon */} +
+ {log.type === 'info' && } + {log.type === 'success' && } + {log.type === 'warn' && } + {log.type === 'error' && } +
+ + {/* Content */} +
+
+ + {log.tag} + + + {new Date(log.time).toLocaleTimeString()} + +
+

+ {log.msg} +

+
+
+ ))} +
+
+
+ ) +} diff --git a/211/web/src/components/ShopPanel.tsx b/211/web/src/components/ShopPanel.tsx new file mode 100644 index 0000000..079b1e6 --- /dev/null +++ b/211/web/src/components/ShopPanel.tsx @@ -0,0 +1,274 @@ +import { useCallback, useEffect, useState } from "react" +import { RefreshCw, Coins, Search } from "lucide-react" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { cn } from "@/lib/utils" + +interface GoodsItem { + goodsId: number + itemId: number + name: string + price: number + limitCount: number + boughtNum: number + unlocked: boolean + itemCount: number +} + +export function ShopPanel({ email, refreshKey }: { email: string, refreshKey?: number }) { + const [goods, setGoods] = useState([]) + const [filteredGoods, setFilteredGoods] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [isBuying, setIsBuying] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [buyDialogOpen, setBuyDialogOpen] = useState(false) + const [selectedGoods, setSelectedGoods] = useState(null) + const [buyAmount, setBuyAmount] = useState(1) + const [message, setMessage] = useState("") + const [imageMap, setImageMap] = useState>({}) + + const fetchShop = useCallback(async () => { + if (!email) return + setIsLoading(true) + try { + const res = await fetch(`/api/shop?email=${email}`) + const json = await res.json() + if (json.goods) { + setGoods(json.goods) + setFilteredGoods(json.goods) + } + } catch (e) { + console.error(e) + } finally { + setIsLoading(false) + } + }, [email]) + + useEffect(() => { + fetchShop() + }, [fetchShop, refreshKey]) + + useEffect(() => { + if (!searchQuery) { + setFilteredGoods(goods) + } else { + const query = searchQuery.toLowerCase() + setFilteredGoods(goods.filter(g => g.name.toLowerCase().includes(query))) + } + }, [searchQuery, goods]) + + useEffect(() => { + let cancelled = false + fetch('/shop_plants_organized/mapping.csv') + .then(res => res.text()) + .then(text => { + if (cancelled) return + const lines = text.split(/\r?\n/).filter(Boolean) + if (lines.length <= 1) { + setImageMap({}) + return + } + const map: Record = {} + for (let i = 1; i < lines.length; i += 1) { + const line = lines[i].trim() + if (!line) continue + const parts = line.split(',') + if (parts.length < 5) continue + const seedId = Number(parts[0]) + const fileName = parts[4] + if (!Number.isNaN(seedId) && fileName) { + map[seedId] = fileName + } + } + setImageMap(map) + }) + .catch(() => { + if (!cancelled) setImageMap({}) + }) + return () => { + cancelled = true + } + }, []) + + const openBuyDialog = (item: GoodsItem) => { + setSelectedGoods(item) + setBuyAmount(1) + setMessage("") + setBuyDialogOpen(true) + } + + const handleBuy = async () => { + if (!selectedGoods) return + + setIsBuying(true) + setMessage("") + + try { + const res = await fetch('/api/shop/buy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + goodsId: selectedGoods.goodsId, + count: buyAmount, + price: selectedGoods.price + }) + }) + const json = await res.json() + + if (json.success) { + setMessage(`购买成功!获得 ${selectedGoods.name} x${buyAmount * selectedGoods.itemCount}`) + setTimeout(() => { + setBuyDialogOpen(false) + fetchShop() // Refresh limits/stock if any + }, 1500) + } else { + setMessage(`购买失败: ${json.error}`) + } + } catch (error) { + console.error(error) + setMessage("网络错误") + } finally { + setIsBuying(false) + } + } + + return ( +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-8" + /> +
+ +
+ + +
+ {filteredGoods.map((item) => ( +
+ {imageMap[item.itemId] && ( +
+ {item.name} +
+ )} +
+
{item.name}
+
ID: {item.itemId}
+
+ +
+ + {item.price} +
+ + + + {item.limitCount > 0 && ( +
+ 限购 {item.boughtNum}/{item.limitCount} +
+ )} +
+ ))} + {filteredGoods.length === 0 && !isLoading && ( +
+ 未找到商品 +
+ )} +
+
+ + + + + 购买 {selectedGoods?.name} + + 单价: {selectedGoods?.price} 金币 + + + +
+
+ +
{buyAmount}
+ +
+ +
+ 总价: {(selectedGoods?.price || 0) * buyAmount} 金币 +
+ + {message && ( +
+ {message} +
+ )} +
+ + + + + +
+
+
+ ) +} diff --git a/211/web/src/components/StatusBar.tsx b/211/web/src/components/StatusBar.tsx new file mode 100644 index 0000000..de9aaa9 --- /dev/null +++ b/211/web/src/components/StatusBar.tsx @@ -0,0 +1,117 @@ +import { User, TrendingUp, Activity } from "lucide-react" +import { cn } from "@/lib/utils" + +interface StatusBarProps { + nickname?: string + level?: number + exp?: number + nextLevelExp?: number + gold?: number + tickets?: number + normalFertilizerHours?: number + organicFertilizerHours?: number + status?: 'running' | 'stopped' | 'error' + avatarUrl?: string +} + +export function StatusBar({ + nickname = "Farmer", + level = 1, + exp = 0, + nextLevelExp = 100, + gold = 0, + tickets = 0, + normalFertilizerHours = 0, + organicFertilizerHours = 0, + status = 'stopped', + avatarUrl +}: StatusBarProps) { + const expPercentage = Math.min(100, Math.max(0, (exp / nextLevelExp) * 100)) + + return ( +
+ {/* User Info */} +
+
+ {avatarUrl ? ( + Avatar + ) : ( + + )} +
+
+ {nickname} +
+ + + {status === 'running' ? '运行中' : '已停止'} + + + {level} 级 + +
+
+
+ + {/* Stats Group */} +
+ {/* Experience Bar */} +
+
+ 经验 + {Math.floor(expPercentage)}% +
+
+
+
+
+ +
+
+ 点券 +
+
+ 点券 + {tickets.toLocaleString()} +
+
+ +
+
+ 普通化肥 +
+
+ 普通化肥余量 + {normalFertilizerHours.toFixed(1)}h +
+
+ +
+
+ 有机化肥 +
+
+ 有机化肥余量 + {organicFertilizerHours.toFixed(1)}h +
+
+ +
+
+ 金币 +
+
+ 金币 + {gold.toLocaleString()} +
+
+
+
+ ) +} diff --git a/211/web/src/components/SystemMonitor.tsx b/211/web/src/components/SystemMonitor.tsx new file mode 100644 index 0000000..2006baa --- /dev/null +++ b/211/web/src/components/SystemMonitor.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react" +import { Cpu, Database, Activity, ChevronLeft, ChevronRight } from "lucide-react" + +interface SystemStats { + memory: { + rss: number + heapTotal: number + heapUsed: number + } + uptime: number + activeBots: number +} + +export function SystemMonitor() { + const [stats, setStats] = useState(null) + const [collapsed, setCollapsed] = useState(true) + + useEffect(() => { + const fetchStats = async () => { + try { + const response = await fetch('/api/system/stats') + if (response.ok) { + const data = await response.json() + setStats(data) + } + } catch (error) { + console.error('Failed to fetch system stats:', error) + } + } + + fetchStats() + const interval = setInterval(fetchStats, 5000) // Update every 5s + return () => clearInterval(interval) + }, []) + + if (!stats) { + return ( +
+ + Loading stats... +
+ ) + } + + return ( +
+ +
+
+ + 运行时间: {formatUptime(stats.uptime)} +
+
+ + 内存: {stats.memory.rss} MB (堆: {stats.memory.heapUsed} MB) +
+
+ + 挂机数量: {stats.activeBots} +
+
+
+ ) +} + +function formatUptime(seconds: number) { + const h = Math.floor(seconds / 3600) + const m = Math.floor((seconds % 3600) / 60) + const s = seconds % 60 + return `${h}h ${m}m ${s}s` +} diff --git a/211/web/src/components/WarehousePanel.tsx b/211/web/src/components/WarehousePanel.tsx new file mode 100644 index 0000000..4fc433b --- /dev/null +++ b/211/web/src/components/WarehousePanel.tsx @@ -0,0 +1,595 @@ +import { useCallback, useEffect, useState, type ComponentType, type SVGProps } from "react" +import { Package, Leaf, Apple, Zap, RefreshCw, CheckCircle2, Circle, Coins } from "lucide-react" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { cn } from "@/lib/utils" + +interface Item { + id: number + uid: number + name: string + count: number + type: 'seed' | 'produce' | 'other' +} + +interface WarehouseData { + seeds: Item[] + produce: Item[] + others: Item[] +} +interface Gain { + id: number + name: string + count: number +} + +export function WarehousePanel({ email, refreshKey, onRefresh }: { email: string, refreshKey?: number, onRefresh?: () => void }) { + const [data, setData] = useState({ seeds: [], produce: [], others: [] }) + const [isLoading, setIsLoading] = useState(false) + const [isSelling, setIsSelling] = useState(false) + const [selectedItems, setSelectedItems] = useState>(new Set()) // Format: "id-uid" + const [isSelectionMode, setIsSelectionMode] = useState(false) + const [activeTab, setActiveTab] = useState("seeds") + const [message, setMessage] = useState("") + const [seedImageMap, setSeedImageMap] = useState>({}) + const [nameImageMap, setNameImageMap] = useState>({}) + const [verifiedItemMap, setVerifiedItemMap] = useState>({}) + const [useDialogOpen, setUseDialogOpen] = useState(false) + const [useTargets, setUseTargets] = useState([]) + const [useQuantities, setUseQuantities] = useState>({}) + const [gainDialogOpen, setGainDialogOpen] = useState(false) + const [gains, setGains] = useState([]) + + const fetchWarehouse = useCallback(async (silent = false) => { + if (!email) return + if (!silent) setIsLoading(true) + try { + const res = await fetch(`/api/warehouse?email=${email}`) + const json = await res.json() + if (json.seeds) { + setData(json) + } + } catch (e) { + console.error(e) + } finally { + if (!silent) setIsLoading(false) + } + }, [email]) + + useEffect(() => { + fetchWarehouse() + const interval = setInterval(() => fetchWarehouse(true), 5000) + return () => clearInterval(interval) + }, [fetchWarehouse, refreshKey]) + + useEffect(() => { + let cancelled = false + fetch('/shop_plants_organized/mapping.csv') + .then(res => res.text()) + .then(text => { + if (cancelled) return + const lines = text.split(/\r?\n/).filter(Boolean) + if (lines.length <= 1) { + setSeedImageMap({}) + setNameImageMap({}) + return + } + const map: Record = {} + const nameMap: Record = {} + for (let i = 1; i < lines.length; i += 1) { + const line = lines[i].trim() + if (!line) continue + const parts = line.split(',') + if (parts.length < 5) continue + const seedId = Number(parts[0]) + const fileName = parts[4] + if (!Number.isNaN(seedId) && fileName) { + map[seedId] = fileName + } + if (fileName) { + const name = fileName.split('_from_')[0] + if (name) nameMap[name] = fileName + } + } + setSeedImageMap(map) + setNameImageMap(nameMap) + }) + .catch(() => { + if (!cancelled) { + setSeedImageMap({}) + setNameImageMap({}) + } + }) + return () => { + cancelled = true + } + }, []) + + useEffect(() => { + let cancelled = false + fetch('/verified_items/mapping.json') + .then(res => res.json()) + .then((list: Array<{ id: string, path: string }>) => { + if (cancelled || !Array.isArray(list)) return + const map: Record = {} + for (const item of list) { + const id = Number(item.id) + if (!Number.isNaN(id) && item.path) { + map[id] = item.path + } + } + setVerifiedItemMap(map) + }) + .catch(() => { + if (!cancelled) setVerifiedItemMap({}) + }) + return () => { + cancelled = true + } + }, []) + + const toggleSelection = (item: Item) => { + const key = `${item.id}-${item.uid}` + const newSet = new Set(selectedItems) + if (newSet.has(key)) { + newSet.delete(key) + } else { + newSet.add(key) + } + setSelectedItems(newSet) + } + + const collectSelectedItems = useCallback(() => { + if (selectedItems.size === 0) return [] + const allItems = [...data.seeds, ...data.produce, ...data.others] + const result: Item[] = [] + selectedItems.forEach(key => { + const [idStr, uidStr] = key.split('-') + const id = Number(idStr) + const uid = Number(uidStr) + const item = allItems.find(i => i.id === id && i.uid === uid) + if (item) result.push(item) + }) + return result + }, [data.others, data.produce, data.seeds, selectedItems]) + + const closeUseDialog = useCallback(() => { + setUseDialogOpen(false) + setUseTargets([]) + setUseQuantities({}) + }, []) + + const openUseDialogForItems = useCallback((list: Item[]) => { + if (list.length === 0) return + const nextQuantities: Record = {} + list.forEach(item => { + nextQuantities[`${item.id}-${item.uid}`] = 1 + }) + setUseTargets(list) + setUseQuantities(nextQuantities) + setUseDialogOpen(true) + }, []) + + const openUseDialog = useCallback(() => { + const list = collectSelectedItems() + openUseDialogForItems(list) + }, [collectSelectedItems, openUseDialogForItems]) + + const updateUseQuantity = useCallback((key: string, value: string, maxCount: number) => { + const parsed = Number(value) + const next = Number.isNaN(parsed) ? 1 : Math.floor(parsed) + const clamped = Math.max(1, Math.min(maxCount, next)) + setUseQuantities(prev => { + if (prev[key] === clamped) return prev + return { ...prev, [key]: clamped } + }) + }, []) + + const handleSell = async () => { + if (selectedItems.size === 0) return + + if (activeTab === 'others') { + openUseDialog() + return + } + + setIsSelling(true) + setMessage("") + + const selectedList = collectSelectedItems() + if (selectedList.length === 0) { + setIsSelling(false) + return + } + const itemsToSell: { id: number, count: number, uid: number }[] = [] + selectedList.forEach(item => { + itemsToSell.push({ id: item.id, count: item.count, uid: item.uid }) + }) + + try { + const res = await fetch('/api/warehouse/sell', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, items: itemsToSell }) + }) + const json = await res.json() + + if (json.success) { + setMessage(`出售成功!获得 ${json.gold} 金币`) + setSelectedItems(new Set()) + setIsSelectionMode(false) + fetchWarehouse() + onRefresh?.() + } else { + setMessage(`出售失败: ${json.error}`) + } + } catch (error) { + console.error(error) + setMessage("网络错误") + } finally { + setIsSelling(false) + setTimeout(() => setMessage(""), 3000) + } + } + + const handleUse = async () => { + if (useTargets.length === 0) return + + setIsSelling(true) + setMessage("") + setGains([]) + + const itemsToUse: { id: number, count: number, uid: number }[] = [] + useTargets.forEach(item => { + const key = `${item.id}-${item.uid}` + const selectedCount = useQuantities[key] ?? 1 + const count = Math.max(1, Math.min(item.count, selectedCount)) + itemsToUse.push({ id: item.id, count, uid: item.uid }) + }) + if (itemsToUse.length === 0) { + setIsSelling(false) + return + } + + try { + const res = await fetch('/api/warehouse/use', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, items: itemsToUse }) + }) + const json = await res.json() + if (json.success) { + setMessage(json.message || "使用成功") + if (Array.isArray(json.gains) && json.gains.length > 0) { + setGains(json.gains) + setGainDialogOpen(true) + } + setSelectedItems(new Set()) + setIsSelectionMode(false) + fetchWarehouse() + onRefresh?.() + closeUseDialog() + } else { + setMessage(`使用失败: ${json.error}`) + } + } catch (error) { + console.error(error) + setMessage("网络错误") + } finally { + setIsSelling(false) + setTimeout(() => setMessage(""), 3000) + } + } + + const getGainImage = (id: number, name?: string) => { + if (verifiedItemMap[id]) return `/${verifiedItemMap[id]}` + if (seedImageMap[id]) return `/shop_plants_organized/${seedImageMap[id]}` + if (name && nameImageMap[name]) return `/shop_plants_organized/${nameImageMap[name]}` + return undefined + } + + return ( +
+ { if (!open) closeUseDialog() }}> + + + 使用数量 + +
+ {useTargets.map(item => { + const key = `${item.id}-${item.uid}` + const value = useQuantities[key] ?? 1 + return ( +
+
+
{item.name}
+
拥有 {item.count}
+
+
+ updateUseQuantity(key, event.target.value, item.count)} + className="text-center" + /> +
+
+ ) + })} +
+
+ + +
+
+
+ + + + 获得物品 + +
+ {gains.map(gain => { + const imagePath = getGainImage(gain.id, gain.name) + return ( +
+ {imagePath ? ( +
+ {gain.name} +
+ ) : ( +
+ +
+ )} +
+
{gain.name}
+
x{gain.count}
+
+
+ ) + })} +
+
+ +
+
+
+
+

+ + + + 我的仓库 +

+
+ {message && ( + + {message} + + )} + + +
+
+ + { + setActiveTab(val) + setSelectedItems(new Set()) + setIsSelectionMode(false) + }}> + + + 种子 + + + 果实 + + + 道具 + + + + + + + + + + + + { + if (isSelectionMode) return + setSelectedItems(new Set()) + openUseDialogForItems([item]) + }} + seedImageMap={seedImageMap} + nameImageMap={nameImageMap} + verifiedItemMap={verifiedItemMap} + /> + + + + + {isSelectionMode && selectedItems.size > 0 && ( +
+ +
+ )} +
+ ) +} + +function ItemGrid({ + items, + color, + icon: Icon, + emptyMsg, + isSelectionMode, + selectedItems, + onToggle, + onItemClick, + seedImageMap, + nameImageMap, + verifiedItemMap +}: { + items: Item[], + color: string, + icon: ComponentType>, + emptyMsg: string, + isSelectionMode: boolean, + selectedItems: Set, + onToggle: (item: Item) => void, + onItemClick?: (item: Item) => void, + seedImageMap: Record, + nameImageMap: Record, + verifiedItemMap: Record +}) { + if (items.length === 0) { + return ( +
+ + {emptyMsg} +
+ ) + } + + const colorStyles = { + green: "bg-green-50 border-green-100 text-green-700", + red: "bg-red-50 border-red-100 text-red-700", + purple: "bg-purple-50 border-purple-100 text-purple-700", + } + + return ( +
+ {items.map(item => { + const key = `${item.id}-${item.uid}` + const isSelected = selectedItems.has(key) + const imagePath = item.type === 'seed' + ? seedImageMap[item.id] + ? `/shop_plants_organized/${seedImageMap[item.id]}` + : undefined + : item.type === 'produce' + ? nameImageMap[item.name] + ? `/shop_plants_organized/${nameImageMap[item.name]}` + : undefined + : item.type === 'other' + ? verifiedItemMap[item.id] + ? `/${verifiedItemMap[item.id]}` + : undefined + : undefined + + return ( +
{ + if (isSelectionMode) { + onToggle(item) + return + } + onItemClick?.(item) + }} + className={cn( + "relative group flex flex-col items-center p-4 rounded-2xl border transition-all duration-300", + colorStyles[color as keyof typeof colorStyles] || colorStyles.green, + isSelectionMode ? "cursor-pointer hover:shadow-md" : (onItemClick ? "cursor-pointer hover:shadow-md" : "cursor-default hover:scale-[1.02]"), + isSelected && "ring-2 ring-offset-2 ring-orange-400 bg-orange-50 border-orange-200" + )} + > + {isSelectionMode && ( +
+ {isSelected ? ( + + ) : ( + + )} +
+ )} + + {imagePath ? ( +
+ {item.name} +
+ ) : ( +
+ +
+ )} + {item.name} + x{item.count} +
+ ) + })} +
+ ) +} diff --git a/211/web/src/components/ui/avatar.tsx b/211/web/src/components/ui/avatar.tsx new file mode 100644 index 0000000..41249d7 --- /dev/null +++ b/211/web/src/components/ui/avatar.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Avatar.displayName = "Avatar" + +const AvatarImage = React.forwardRef< + HTMLImageElement, + React.ImgHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = "AvatarImage" + +const AvatarFallback = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AvatarFallback.displayName = "AvatarFallback" + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/211/web/src/components/ui/button.tsx b/211/web/src/components/ui/button.tsx new file mode 100644 index 0000000..1239422 --- /dev/null +++ b/211/web/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button } diff --git a/211/web/src/components/ui/card.tsx b/211/web/src/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/211/web/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/211/web/src/components/ui/dialog.tsx b/211/web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..9dbeaa0 --- /dev/null +++ b/211/web/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/211/web/src/components/ui/input.tsx b/211/web/src/components/ui/input.tsx new file mode 100644 index 0000000..5c17e74 --- /dev/null +++ b/211/web/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export type InputProps = React.InputHTMLAttributes + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/211/web/src/components/ui/scroll-area.tsx b/211/web/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..77bd0c4 --- /dev/null +++ b/211/web/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/211/web/src/components/ui/tabs.tsx b/211/web/src/components/ui/tabs.tsx new file mode 100644 index 0000000..0f4caeb --- /dev/null +++ b/211/web/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/211/web/src/index.css b/211/web/src/index.css new file mode 100644 index 0000000..3dcefa2 --- /dev/null +++ b/211/web/src/index.css @@ -0,0 +1,111 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@font-face { + font-family: 'MapleMono'; + src: url('./assets/fonts/MapleMono-NF-CN-Regular.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@layer base { + :root { + /* Base background - Very soft warm white */ + --background: 0 0% 100%; + /* Foreground - Soft Charcoal, not harsh black */ + --foreground: 0 0% 3.9%; + + /* Card background - White with slight transparency for glassmorphism */ + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + /* Primary - Soft Sky Blue (Pastel) */ + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + /* Secondary - Light Pink (Pastel) */ + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + /* Muted - Soft Lavender */ + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + /* Accent - Mint Green or Buttercream (used for highlights) */ + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + /* Destructive - Soft Coral/Salmon */ + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + + /* Ring - Matches Primary but slightly stronger */ + --ring: 0 0% 3.9%; + + --radius: 0.5rem; /* More rounded corners for softer feel */ --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground font-sans; + margin: 0; + } +} + + + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/211/web/src/lib/utils.ts b/211/web/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/211/web/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/211/web/src/main.tsx b/211/web/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/211/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/211/web/src/pages/Dashboard.tsx b/211/web/src/pages/Dashboard.tsx new file mode 100644 index 0000000..cb27f83 --- /dev/null +++ b/211/web/src/pages/Dashboard.tsx @@ -0,0 +1,1703 @@ +import { useCallback, useEffect, useState, useRef } from "react" +import { useNavigate } from "react-router-dom" +import { StatusBar } from "@/components/StatusBar" +import { FarmGrid } from "@/components/FarmGrid" +import type { LandData } from "@/components/FarmGrid" +import { LogPanel } from "@/components/LogPanel" +import { ShopPanel } from "@/components/ShopPanel" +import { WarehousePanel } from "@/components/WarehousePanel" +import { SystemMonitor } from "@/components/SystemMonitor" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Cloud, Heart, Star, LogOut, QrCode, RefreshCw, Sprout, Settings, Trophy, ClipboardList, CheckCircle2, Circle, Shovel, Gift, Leaf, PawPrint, Sparkles, Package } from "lucide-react" +import logo from "@/assets/logo.png" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +interface FarmStatus { + nickname: string + level: number + exp: number + nextLevelExp: number + gold: number + tickets: number + normalFertilizerHours: number + organicFertilizerHours: number + status: 'running' | 'stopped' | 'error' + avatarUrl?: string + freeMallClaimed?: boolean +} + +interface User { + email: string + hasCode: boolean + code?: string +} + +type SeedStrategy = 'default' | 'forceLowest' +type IdleStrategy = 'task' | 'exp' + +interface BotSettings { + farmIntervalSec: number + friendIntervalSec: number + seedStrategy: SeedStrategy + enableFriendOps: boolean + enableSteal: boolean + allowTicketFertilizerPurchase: boolean + enableNormalFertilize: boolean + enableOrganicFertilize: boolean + enableAutoSell: boolean + allowBuySeeds: boolean + allowRemove: boolean + idleStrategy: IdleStrategy +} + +interface UnluckyItem { + name: string + count: number +} + +type PaidMallCategory = 'daily' | 'organic' | 'fertilizer' | 'dog' + +interface PaidMallItem { + id: number + name: string + currency?: string + price?: number + discount?: string + items: { name: string; count: number }[] + category: PaidMallCategory +} + +const paidMallItems: PaidMallItem[] = [ + { + id: 1001, + name: "每日福利", + items: [{ name: "化肥(1小时)", count: 1 }], + category: 'daily' + }, + { + id: 1002, + name: "10小时有机化肥", + currency: "点券", + price: 42, + discount: "9.1折", + items: [ + { name: "有机化肥(1小时)", count: 2 }, + { name: "有机化肥(8小时)", count: 1 } + ], + category: 'organic' + }, + { + id: 1003, + name: "10小时化肥", + currency: "点券", + price: 34, + discount: "9.4折", + items: [ + { name: "化肥(1小时)", count: 2 }, + { name: "化肥(8小时)", count: 1 } + ], + category: 'fertilizer' + }, + { + id: 1004, + name: "20小时有机化肥", + currency: "钻石", + price: 80, + discount: "9.1折", + items: [ + { name: "有机化肥(8小时)", count: 1 }, + { name: "有机化肥(12小时)", count: 1 } + ], + category: 'organic' + }, + { + id: 1005, + name: "20小时化肥", + currency: "钻石", + price: 64, + discount: "9.4折", + items: [ + { name: "化肥(8小时)", count: 1 }, + { name: "化肥(12小时)", count: 1 } + ], + category: 'fertilizer' + }, + { + id: 1006, + name: "狗粮礼包", + currency: "点券", + price: 33, + items: [ + { name: "1天狗粮", count: 1 }, + { name: "3天狗粮", count: 1 }, + { name: "5天狗粮", count: 1 } + ], + category: 'dog' + }, + { + id: 1007, + name: "有机化肥(1小时)", + currency: "钻石", + price: 5, + items: [{ name: "有机化肥(1小时)", count: 1 }], + category: 'organic' + }, + { + id: 1008, + name: "有机化肥(4小时)", + currency: "钻石", + price: 19, + items: [{ name: "有机化肥(4小时)", count: 1 }], + category: 'organic' + }, + { + id: 1009, + name: "有机化肥(8小时)", + currency: "钻石", + price: 36, + items: [{ name: "有机化肥(8小时)", count: 1 }], + category: 'organic' + }, + { + id: 1010, + name: "有机化肥(12小时)", + currency: "钻石", + price: 52, + items: [{ name: "有机化肥(12小时)", count: 1 }], + category: 'organic' + }, + { + id: 1011, + name: "化肥(1小时)", + currency: "钻石", + price: 4, + items: [{ name: "化肥(1小时)", count: 1 }], + category: 'fertilizer' + }, + { + id: 1012, + name: "化肥(4小时)", + currency: "钻石", + price: 15, + items: [{ name: "化肥(4小时)", count: 1 }], + category: 'fertilizer' + }, + { + id: 1013, + name: "化肥(8小时)", + currency: "钻石", + price: 28, + items: [{ name: "化肥(8小时)", count: 1 }], + category: 'fertilizer' + }, + { + id: 1014, + name: "化肥(12小时)", + currency: "钻石", + price: 40, + items: [{ name: "化肥(12小时)", count: 1 }], + category: 'fertilizer' + }, + { + id: 1015, + name: "1天狗粮", + currency: "钻石", + price: 4, + items: [{ name: "1天狗粮", count: 1 }], + category: 'dog' + }, + { + id: 1016, + name: "3天狗粮", + currency: "钻石", + price: 11, + items: [{ name: "3天狗粮", count: 1 }], + category: 'dog' + }, + { + id: 1017, + name: "5天狗粮", + currency: "钻石", + price: 18, + items: [{ name: "5天狗粮", count: 1 }], + category: 'dog' + } +] + +function getPaidMallIcon(category: PaidMallCategory) { + if (category === 'daily') return + if (category === 'organic') return + if (category === 'dog') return + return +} + +interface TaskReward { + id: number + count: number +} + +type TaskCategory = 'growth' | 'daily' | 'normal' + +interface TaskItem { + id: number + desc: string + progress: number + totalProgress: number + isClaimed: boolean + isUnlocked: boolean + shareMultiple: number + rewards: TaskReward[] + category: TaskCategory +} + +function TaskPanel({ email }: { email: string }) { + const [tasks, setTasks] = useState([]) + const [loading, setLoading] = useState(false) + const [claimingId, setClaimingId] = useState(null) + const [showCompleted, setShowCompleted] = useState(false) + const [error, setError] = useState("") + const [lastClaim, setLastClaim] = useState<{ claimedCount: number; failed: number } | null>(null) + const inflightRef = useRef(false) + const claimedCacheRef = useRef>(new Map()) + const autoCompleteRef = useRef(false) + + const getTaskKey = useCallback((task: TaskItem) => { + return `${(task.desc || '').trim()}|${task.totalProgress}` + }, []) + + const mergeWithClaimedCache = useCallback((nextTasks: TaskItem[]) => { + const resultMap = new Map() + for (const task of nextTasks) { + const key = getTaskKey(task) + resultMap.set(key, task) + if (task.isClaimed) { + claimedCacheRef.current.set(key, task) + } + } + for (const [key, cached] of claimedCacheRef.current.entries()) { + const current = resultMap.get(key) + if (!current || (cached.isClaimed && !current.isClaimed)) { + resultMap.set(key, cached) + } + } + return Array.from(resultMap.values()) + }, [getTaskKey]) + + const fetchTasks = useCallback(async (withClaim: boolean) => { + if (!email) return + if (inflightRef.current) return + inflightRef.current = true + setLoading(true) + setError("") + try { + if (withClaim) { + const res = await fetch('/api/tasks/claim', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }) + if (!res.ok) throw new Error('claim failed') + const data = await res.json() + const list = Array.isArray(data.tasks) ? data.tasks : [] + setTasks(mergeWithClaimedCache(list)) + const failed = Array.isArray(data.failed) ? data.failed.length : 0 + setLastClaim({ claimedCount: Number(data.claimedCount) || 0, failed }) + } else { + const res = await fetch(`/api/tasks?email=${encodeURIComponent(email)}`) + if (!res.ok) throw new Error('fetch failed') + const data = await res.json() + const list = Array.isArray(data.tasks) ? data.tasks : [] + setTasks(mergeWithClaimedCache(list)) + } + } catch { + setError("任务获取失败") + } finally { + setLoading(false) + inflightRef.current = false + } + }, [email, mergeWithClaimedCache]) + + const claimSingle = useCallback(async (task: TaskItem) => { + if (!email) return + if (claimingId !== null) return + setClaimingId(task.id) + setError("") + try { + const res = await fetch('/api/tasks/claim-one', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, taskId: task.id, doShared: task.shareMultiple > 1 }) + }) + if (!res.ok) throw new Error('claim failed') + const data = await res.json() + const list = Array.isArray(data.tasks) ? data.tasks : [] + const claimedTask = { ...task, isClaimed: true } + claimedCacheRef.current.set(getTaskKey(claimedTask), claimedTask) + setTasks(mergeWithClaimedCache(list)) + const claimedCount = Number(data.claimedCount) || 0 + const failedCount = Number(data.failedCount) || 0 + if (claimedCount > 0 || failedCount > 0) { + setLastClaim({ claimedCount, failed: failedCount }) + } + } catch { + setError("任务完成失败") + } finally { + setClaimingId(null) + } + }, [claimingId, email, getTaskKey, mergeWithClaimedCache]) + + const autoCompleteNext = useCallback(() => { + if (autoCompleteRef.current) return + const nextTask = tasks.find((task) => { + if (task.isClaimed) return false + if (!task.isUnlocked) return false + if (task.totalProgress <= 0) return false + return task.progress >= task.totalProgress + }) + if (!nextTask) return + autoCompleteRef.current = true + claimSingle(nextTask).finally(() => { + autoCompleteRef.current = false + }) + }, [claimSingle, tasks]) + + useEffect(() => { + if (!email) return + fetchTasks(false) + const timer = setInterval(() => fetchTasks(false), 15000) + return () => clearInterval(timer) + }, [email, fetchTasks]) + + useEffect(() => { + autoCompleteNext() + }, [autoCompleteNext]) + + const completedCount = tasks.filter(task => task.isClaimed).length + const visibleTasks = showCompleted ? tasks : tasks.filter(task => !task.isClaimed) + const buckets: Record = { growth: [], daily: [], normal: [] } + for (const task of visibleTasks) { + if (task.category === 'growth' || task.category === 'daily' || task.category === 'normal') { + buckets[task.category].push(task) + } else { + buckets.normal.push(task) + } + } + + const categoryMeta: Array<{ key: TaskCategory; label: string; badge: string }> = [ + { key: 'growth', label: '成长', badge: 'bg-indigo-50 text-indigo-600 border-indigo-100' }, + { key: 'daily', label: '日常', badge: 'bg-emerald-50 text-emerald-600 border-emerald-100' }, + { key: 'normal', label: '普通', badge: 'bg-slate-50 text-slate-600 border-slate-200' } + ] + + return ( +
+
+
+
+ + 当前任务 +
+
+
+ + +
+
+ + {lastClaim && (lastClaim.claimedCount > 0 || lastClaim.failed > 0) && ( +
+ 已完成 {lastClaim.claimedCount} 个任务 {lastClaim.failed > 0 ? `,失败 ${lastClaim.failed} 个` : ''} +
+ )} + + {error && ( +
{error}
+ )} + + {visibleTasks.length === 0 && !loading ? ( +
暂无任务
+ ) : ( +
+ {categoryMeta.map(({ key, label, badge }) => { + const items = buckets[key] + if (items.length === 0) return null + return ( +
+
+ {label} + {items.length} 项 +
+
+ {items.map(task => { + const claimable = task.isUnlocked && !task.isClaimed && task.totalProgress > 0 && task.progress >= task.totalProgress + const progressText = task.totalProgress > 0 ? `${task.progress}/${task.totalProgress}` : "-" + const progressPercent = task.totalProgress > 0 ? Math.min(100, Math.round((task.progress / task.totalProgress) * 100)) : 0 + return ( +
+
+
+ {task.isClaimed ? ( + + ) : claimable ? ( + + ) : ( + + )} +
+
{task.desc || `任务#${task.id}`}
+
+ 进度 {progressText} + {task.shareMultiple > 1 ? ` · 分享翻倍 x${task.shareMultiple}` : ''} +
+
+
+
+ +
+ {task.isClaimed ? "已完成" : "进行中"} +
+
+
+
+
+
+
+ ) + })} +
+
+ ) + })} +
+ )} +
+ ) +} + +const defaultSettings: BotSettings = { + farmIntervalSec: 1, + friendIntervalSec: 10, + seedStrategy: 'default', + enableFriendOps: true, + enableSteal: true, + allowTicketFertilizerPurchase: false, + enableNormalFertilize: false, + enableOrganicFertilize: false, + enableAutoSell: true, + allowBuySeeds: true, + allowRemove: true, + idleStrategy: 'exp' +} + +export default function Dashboard() { + const navigate = useNavigate() + const [user, setUser] = useState(null) + const [farmStatus, setFarmStatus] = useState({ + nickname: "加载中...", + level: 1, + exp: 0, + nextLevelExp: 100, + gold: 0, + tickets: 0, + normalFertilizerHours: 0, + organicFertilizerHours: 0, + status: 'stopped', + freeMallClaimed: false + }) + const [warehouseRefreshKey, setWarehouseRefreshKey] = useState(0) + const [lands, setLands] = useState([]) + const [removeMode, setRemoveMode] = useState(false) + const [selectedLandIds, setSelectedLandIds] = useState([]) + const [isRemoving, setIsRemoving] = useState(false) + const [unlockingLandIds, setUnlockingLandIds] = useState([]) + const [showBindDialog, setShowBindDialog] = useState(false) + const [isStarting, setIsStarting] = useState(false) + const [settings, setSettings] = useState(defaultSettings) + const [isSavingSettings, setIsSavingSettings] = useState(false) + const [settingsHint, setSettingsHint] = useState("") + const [unluckyList, setUnluckyList] = useState([]) + const [announcementMessage, setAnnouncementMessage] = useState("") + const [announcementOpen, setAnnouncementOpen] = useState(false) + const unluckyLoadingRef = useRef(false) + const [activePage, setActivePage] = useState<'farm' | 'shop' | 'paymall' | 'settings' | 'task' | 'leaderboard'>('farm') + const [navOpen, setNavOpen] = useState(false) + const [payMallDialogOpen, setPayMallDialogOpen] = useState(false) + const [payMallSelected, setPayMallSelected] = useState(null) + const [payMallCount, setPayMallCount] = useState(1) + const [payMallMessage, setPayMallMessage] = useState("") + const [payMallBuying, setPayMallBuying] = useState(false) + const isPayMallClaimed = payMallSelected?.id === 1001 && farmStatus.freeMallClaimed === true + + // QR Code States + const [qrCodeUrl, setQrCodeUrl] = useState("") + const [qrSig, setQrSig] = useState("") + const [qrStatus, setQrStatus] = useState("init") // init, waiting, scanned, success, expired + const [qrMsg, setQrMsg] = useState("") + + // Ref to track current status for stale closure prevention in setInterval + const statusRef = useRef(farmStatus.status) + + // Sync ref with state + useEffect(() => { + statusRef.current = farmStatus.status + }, [farmStatus.status]) + + useEffect(() => { + if (removeMode) return + if (selectedLandIds.length === 0) return + setSelectedLandIds([]) + }, [removeMode, selectedLandIds.length]) + + // Initialize user from local storage + useEffect(() => { + const storedUser = localStorage.getItem('farm_user') + if (!storedUser) { + navigate('/login') + return + } + const userData = JSON.parse(storedUser) + setUser(userData) + + // Check if user needs to bind farm + if (!userData.hasCode && !userData.code) { + setShowBindDialog(true) + } + }, [navigate]) + + useEffect(() => { + if (!user) return + let cancelled = false + const loadAnnouncement = async () => { + try { + const res = await fetch('/api/announcement') + if (!res.ok) return + const data = await res.json() + const message = String(data?.message || '').trim() + if (!message) return + if (cancelled) return + setAnnouncementMessage(message) + setAnnouncementOpen(true) + } catch { + return + } + } + loadAnnouncement() + return () => { + cancelled = true + } + }, [user]) + + const normalizeSettings = useCallback((value: BotSettings): BotSettings => { + const farmIntervalSec = Math.max(1, Math.floor(Number(value.farmIntervalSec) || 1)) + const friendIntervalSec = Math.max(1, Math.floor(Number(value.friendIntervalSec) || 1)) + const seedStrategy: SeedStrategy = value.seedStrategy === 'forceLowest' ? 'forceLowest' : 'default' + const enableFriendOps = value.enableFriendOps !== false + const enableSteal = value.enableSteal !== false + const allowTicketFertilizerPurchase = value.allowTicketFertilizerPurchase === true + const enableNormalFertilize = value.enableNormalFertilize === true + const enableOrganicFertilize = value.enableOrganicFertilize === true + const enableAutoSell = value.enableAutoSell !== false + const allowBuySeeds = value.allowBuySeeds !== false + const allowRemove = value.allowRemove !== false + const idleStrategy: IdleStrategy = value.idleStrategy === 'task' ? 'task' : 'exp' + return { farmIntervalSec, friendIntervalSec, seedStrategy, enableFriendOps, enableSteal, allowTicketFertilizerPurchase, enableNormalFertilize, enableOrganicFertilize, enableAutoSell, allowBuySeeds, allowRemove, idleStrategy } + }, []) + + const openPayMallDialog = useCallback((item: PaidMallItem) => { + setPayMallSelected(item) + setPayMallCount(1) + setPayMallMessage("") + setPayMallDialogOpen(true) + }, []) + + const handlePayMallBuy = useCallback(async () => { + if (!payMallSelected) return + if (!user?.email) { + setPayMallMessage("请先登录") + return + } + if (isPayMallClaimed) { + setPayMallMessage("今日已领取") + return + } + if (payMallBuying) return + setPayMallBuying(true) + setPayMallMessage("") + try { + const res = await fetch('/api/paymall/buy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: user.email, + itemId: payMallSelected.id, + count: payMallCount + }) + }) + const json = await res.json() + if (json.success) { + setPayMallMessage("购买成功") + if (payMallSelected.id === 1001) { + setFarmStatus(prev => ({ ...prev, freeMallClaimed: true })) + } + setTimeout(() => { + setPayMallDialogOpen(false) + }, 1200) + return + } + setPayMallMessage(`购买失败: ${json.error || '未知错误'}`) + } catch { + setPayMallMessage("网络错误") + } finally { + setPayMallBuying(false) + } + }, [isPayMallClaimed, payMallBuying, payMallCount, payMallSelected, user?.email]) + + useEffect(() => { + const storedSettings = localStorage.getItem('farm_settings') + if (!storedSettings) return + try { + const parsed = JSON.parse(storedSettings) + const next = normalizeSettings({ ...defaultSettings, ...parsed }) + setSettings(next) + } catch { + setSettings(defaultSettings) + } + }, [normalizeSettings]) + + const fetchStatus = useCallback(async () => { + if (!user) return + + try { + const res = await fetch(`/api/status?email=${user.email}`) + const data = await res.json() + + if (data.status === 'stopped') { + // Detect disconnect: if we were running but now stopped, prompt rebind + if (statusRef.current === 'running') { + setQrStatus('init') + setShowBindDialog(true) + } + setFarmStatus(prev => ({ ...prev, status: 'stopped' })) + } else { + setFarmStatus({ + nickname: data.user?.nickname || 'Farmer', + level: data.user?.level || 1, + exp: data.user?.exp || 0, + nextLevelExp: data.user?.nextLevelExp || 100, + gold: data.user?.gold || 0, + tickets: data.user?.tickets || 0, + normalFertilizerHours: data.user?.normalFertilizerHours || 0, + organicFertilizerHours: data.user?.organicFertilizerHours || 0, + status: 'running', + avatarUrl: data.user?.avatarUrl, + freeMallClaimed: data.user?.freeMallClaimed === true + }) + } + } catch (error) { + console.error("Failed to fetch status", error) + } + }, [user]) + + const fetchLands = useCallback(async () => { + if (!user || farmStatus.status !== 'running') return + + try { + const res = await fetch(`/api/lands?email=${user.email}`) + if (res.ok) { + const data = await res.json() + setLands(data.lands || []) + } + } catch (error) { + console.error("Failed to fetch lands", error) + } + }, [farmStatus.status, user]) + + const handleWarehouseRefresh = useCallback(() => { + setWarehouseRefreshKey(prev => prev + 1) + fetchStatus() + fetchLands() + }, [fetchLands, fetchStatus]) + + const handleLandClick = useCallback((land: LandData) => { + if (!removeMode) return + if (land.type !== 'planted') return + setSelectedLandIds(prev => { + if (prev.includes(land.id)) { + return prev.filter(id => id !== land.id) + } + return [...prev, land.id] + }) + }, [removeMode]) + + const handleRemoveSelected = useCallback(async () => { + if (!user) return + if (isRemoving) return + if (selectedLandIds.length === 0) return + setIsRemoving(true) + try { + const res = await fetch('/api/lands/remove', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: user.email, landIds: selectedLandIds }) + }) + if (!res.ok) throw new Error('remove failed') + await fetchLands() + setSelectedLandIds([]) + setRemoveMode(false) + } catch (error) { + console.error("Remove plants failed", error) + } finally { + setIsRemoving(false) + } + }, [fetchLands, isRemoving, selectedLandIds, user]) + + const handleUnlockLand = useCallback(async (land: LandData) => { + if (!user) return + if (unlockingLandIds.includes(land.id)) return + setUnlockingLandIds(prev => prev.includes(land.id) ? prev : [...prev, land.id]) + try { + const res = await fetch('/api/lands/unlock', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: user.email, landId: land.id }) + }) + if (!res.ok) throw new Error('unlock failed') + await Promise.all([fetchLands(), fetchStatus()]) + } catch (error) { + console.error("Unlock land failed", error) + } finally { + setUnlockingLandIds(prev => prev.filter(id => id !== land.id)) + } + }, [fetchLands, fetchStatus, unlockingLandIds, user]) + + // QR Code Logic + const generateQRCode = useCallback(async () => { + setQrStatus("waiting") + setQrMsg("正在获取二维码...") + try { + const res = await fetch('/api/qr/create', { method: 'POST' }) + const data = await res.json() + if (data.success) { + setQrCodeUrl(data.qrcode) + setQrSig(data.qrsig) + setQrMsg("请使用手机QQ扫码") + } else { + setQrStatus("error") + setQrMsg("获取二维码失败") + } + } catch (error) { + console.error("Generate QR error", error) + setQrStatus("error") + setQrMsg("网络错误") + } + }, []) + + const handleBindSuccess = useCallback(async (code: string) => { + if (!user) return + setIsStarting(true) + + try { + const normalizedSettings = normalizeSettings(settings) + // Start bot + const res = await fetch('/api/bot/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: user.email, code, settings: normalizedSettings }) + }) + + const data = await res.json() + if (data.success) { + // Update local user state + const newUser = { ...user, hasCode: true, code } + setUser(newUser) + localStorage.setItem('farm_user', JSON.stringify(newUser)) + + setShowBindDialog(false) + fetchStatus() + } else { + setQrMsg(`启动失败: ${data.error}`) + } + } catch (error) { + console.error("Bind error", error) + setQrMsg("启动服务失败") + } finally { + setIsStarting(false) + } + }, [fetchStatus, normalizeSettings, settings, user]) + + useEffect(() => { + let timer: ReturnType | undefined + if (showBindDialog && qrStatus === 'init') { + generateQRCode() + } + + if (showBindDialog && qrStatus === 'waiting' && qrSig) { + timer = setInterval(async () => { + try { + const res = await fetch('/api/qr/check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ qrsig: qrSig }) + }) + const data = await res.json() + + if (data.success) { + if (data.status === 'OK') { + setQrStatus('success') + setQrMsg('登录成功!正在绑定...') + clearInterval(timer) + + // Start bot with new code + await handleBindSuccess(data.code) + } else if (data.status === 'Used') { + setQrStatus('expired') + setQrMsg('二维码已失效,请刷新') + clearInterval(timer) + } + } + } catch (error) { + console.error("Check QR error", error) + } + }, 3000) + } + + return () => { + if (timer) clearInterval(timer) + } + }, [generateQRCode, handleBindSuccess, qrSig, qrStatus, showBindDialog]) + + useEffect(() => { + if (!user) return + + // Initial fetch + fetchStatus() + + // Poll status every 5 seconds + const statusTimer = setInterval(fetchStatus, 5000) + + return () => clearInterval(statusTimer) + }, [fetchStatus, user]) + + useEffect(() => { + if (farmStatus.status === 'running') { + fetchLands() + const landTimer = setInterval(fetchLands, 10000) + return () => clearInterval(landTimer) + } + }, [farmStatus.status, fetchLands]) + + const handleLogout = () => { + localStorage.removeItem('farm_user') + navigate('/login') + } + + const handleStartBot = async () => { + if (!user) return + setIsStarting(true) + try { + const normalizedSettings = normalizeSettings(settings) + const res = await fetch('/api/bot/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: user.email, settings: normalizedSettings }) + }) + const data = await res.json() + if (data.success) { + fetchStatus() + } else { + console.error(data.error || 'Failed to start bot') + } + } catch (e) { + console.error('Failed to start bot', e) + } finally { + setIsStarting(false) + } + } + + const handleSaveSettings = async () => { + if (isSavingSettings) return + setIsSavingSettings(true) + const normalizedSettings = normalizeSettings(settings) + setSettings(normalizedSettings) + localStorage.setItem('farm_settings', JSON.stringify(normalizedSettings)) + setSettingsHint("已保存,下次启动生效") + setTimeout(() => setSettingsHint(""), 2500) + setIsSavingSettings(false) + } + + const fetchUnlucky = useCallback(async () => { + if (!user) return + if (unluckyLoadingRef.current) return + unluckyLoadingRef.current = true + try { + const res = await fetch(`/api/leaderboard/unlucky?email=${encodeURIComponent(user.email)}`) + if (!res.ok) return + const data = await res.json() + setUnluckyList(Array.isArray(data.items) ? data.items : []) + } catch (error) { + console.error("Failed to fetch unlucky list", error) + } finally { + unluckyLoadingRef.current = false + } + }, [user]) + + useEffect(() => { + if (!user) return + fetchUnlucky() + const timer = setInterval(fetchUnlucky, 10000) + return () => clearInterval(timer) + }, [fetchUnlucky, user]) + + return ( +
+ {/* Decorative Background Elements */} +
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ Logo +
+

谢尔达莱群岛

+
+
+
+ + {navOpen && ( +
+ + + + + + +
+ )} +
+ {farmStatus.status !== 'running' && user?.hasCode && ( + + )} + + +
+
+ + + {activePage === 'farm' && ( +
+
+
+
+

+ + 我的土地 +

+
+ + {removeMode && ( + + )} + +
+
+ {removeMode && ( +
+ 铲除模式已开启,点击已种植土地进行多选 +
+ )} +
+ +
+
+ +
+
+

+ + 运行日志 +

+
+ +
+
+ +
+ )} + + {activePage === 'shop' && ( +
+
+
+

+ + 商店 +

+
+
+ +
+
+
+
+

+ + 仓库 +

+
+ +
+
+ )} + + {activePage === 'paymall' && ( +
+
+

+ + 付费商城 +

+
共 {paidMallItems.length} 件
+
+
+ {paidMallItems.map((item) => { + const isDailyFree = item.id === 1001 + const isClaimed = isDailyFree && farmStatus.freeMallClaimed === true + return ( +
+
+
+ {getPaidMallIcon(item.category)} +
+ {isClaimed ? ( + + 已领取 + + ) : item.discount && ( + + {item.discount} + + )} +
+
+
{item.name}
+
+ {item.items.map((sub) => ( + + {sub.name} x{sub.count} + + ))} +
+
+
+
+ {item.price ? `${item.price} ${item.currency || ''}` : "免费"} +
+ +
+
+ )})} +
+
+ )} + + + + + 购买 {payMallSelected?.name} + + {payMallSelected?.price ? `${payMallSelected.price} ${payMallSelected.currency || ''}` : '免费'} + + +
+
+ +
{payMallCount}
+ +
+
+ {payMallSelected?.price + ? `合计 ${payMallSelected.price * payMallCount} ${payMallSelected.currency || ''}` + : '合计 免费'} +
+ {payMallMessage && ( +
+ {payMallMessage} +
+ )} +
+
+ +
+
+
+ + {activePage === 'settings' && ( +
+
+

+ + 设置 +

+
+
+
+ + setSettings(prev => ({ ...prev, farmIntervalSec: Number(e.target.value) }))} + className="h-11 rounded-2xl bg-white/70 border-white/60 focus:border-emerald-200 focus:ring-emerald-100" + /> +
+
+ + setSettings(prev => ({ ...prev, friendIntervalSec: Number(e.target.value) }))} + disabled={!settings.enableFriendOps} + className={`h-11 rounded-2xl bg-white/70 border-white/60 focus:border-emerald-200 focus:ring-emerald-100 ${settings.enableFriendOps ? "" : "opacity-60 cursor-not-allowed"}`} + /> +
+
+
是否帮忙(好友相关)
+
+ + +
+
+
+
是否偷菜
+
+ + +
+
+
+
肥料使用
+
+ + + + +
+
+
+
是否使用点券购买肥料
+
+ + +
+
+
+
种子策略
+
+ + +
+
+
+
挂机逻辑
+
+ + +
+
+
+
是否出售
+
+ + +
+
+
+
仓库无种子时自动购买
+
+ + +
+
+
+
允许铲除
+
+ + +
+
+
+
{settingsHint}
+ +
+
+
+ )} + + {activePage === 'task' && ( +
+
+

+ + 任务 +

+
+ +
+ )} + + {activePage === 'leaderboard' && ( +
+
+

+ + 榜单 +

+ 已偷榜 +
+
+ {unluckyList.length === 0 ? ( +
暂无数据
+ ) : ( +
+ {unluckyList.slice(0, 10).map((item, index) => ( +
+
+
+ {index + 1} +
+ {item.name} +
+
+ + {item.count} 次 +
+
+ ))} +
+ )} +
+
+ )} +
+
+ + + + + 公告 +
+ +
+ +
+ +
+ + {/* Bind Farm Dialog */} + + + + 绑定 QQ 农场 + + 请使用手机 QQ 扫描下方二维码进行登录 + + + +
+ {qrStatus === 'waiting' && qrCodeUrl ? ( +
+ Scan QR Code +
+ 请扫码 +
+
+ ) : qrStatus === 'success' ? ( +
+
+ +
+ 扫码成功 +
+ ) : ( +
+ +
+ )} + +

+ {qrMsg || "等待操作..."} +

+
+
+
+ {isStarting && ( +
+
+ + 正在启动农场... +
+
+ )} +
+ ) +} diff --git a/211/web/src/pages/Login.tsx b/211/web/src/pages/Login.tsx new file mode 100644 index 0000000..9ea5bbb --- /dev/null +++ b/211/web/src/pages/Login.tsx @@ -0,0 +1,238 @@ +import { useEffect, useState } from "react" +import { useNavigate } from "react-router-dom" +import { Heart, Star, Cloud, Loader2 } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import logo from "@/assets/logo.png" + +export default function Login() { + const [isRegister, setIsRegister] = useState(false) + const [showPasswordLogin, setShowPasswordLogin] = useState(false) + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState("") + const [oauthEnabled, setOauthEnabled] = useState(false) + const navigate = useNavigate() + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + const oauthEmail = params.get('email') + const oauthStatus = params.get('oauth') + if (oauthStatus === 'success' && oauthEmail) { + const loadProfile = async () => { + let hasCode = false + try { + const res = await fetch(`/api/auth/profile?email=${encodeURIComponent(oauthEmail)}`) + if (res.ok) { + const data = await res.json() + hasCode = Boolean(data?.user?.hasCode) + } + } catch { + hasCode = false + } + const user = { email: oauthEmail, auth_provider: 'authentik', hasCode } + localStorage.setItem('farm_user', JSON.stringify(user)) + localStorage.removeItem('farm_code') + navigate("/dashboard") + } + loadProfile() + return + } + if (params.get('error') === 'oauth_failed') { + setError("OAuth 登录失败,请重试") + } + }, [navigate]) + + useEffect(() => { + let cancelled = false + fetch('/api/auth/oauth/status') + .then((res) => res.json()) + .then((data) => { + if (!cancelled) setOauthEnabled(Boolean(data?.enabled)) + }) + .catch(() => { + if (!cancelled) setOauthEnabled(false) + }) + return () => { + cancelled = true + } + }, []) + + const handleOAuthLogin = () => { + window.location.href = '/api/auth/oauth/authentik'; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!email || !password) return + if (isRegister && password !== confirmPassword) { + setError("两次输入的密码不一致") + return + } + + setIsLoading(true) + setError("") + + try { + const endpoint = isRegister ? '/api/auth/register' : '/api/auth/login' + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || (isRegister ? '注册失败' : '登录失败')) + } + + // Save user info to localStorage + localStorage.setItem('farm_user', JSON.stringify(data.user)) + // Clear old code if exists + localStorage.removeItem('farm_code') + + navigate("/dashboard") + } catch (err) { + const message = err instanceof Error ? err.message : '出错了' + setError(message || '出错了') + } finally { + setIsLoading(false) + } + } + + return ( +
+ {/* Decorative Background Elements */} +
+ +
+
+ +
+
+ +
+ + +
+ + +
+ Logo +
+ + {isRegister ? '加入谢尔达莱群岛' : '谢尔达莱群岛'} + + + {isRegister ? '开启你的田园生活' : '欢迎回到你的温馨农场'}
+ 今天也要种出美好的事物哦 +
+
+ +
+ +
+ {oauthEnabled && ( + + )} + + + + {showPasswordLogin && ( +
+
+ setEmail(e.target.value)} + disabled={isLoading} + className="h-12 text-center text-lg bg-slate-50/50 border-slate-100 focus:border-blue-200 focus:ring-blue-100 rounded-2xl transition-all duration-300 hover:bg-white" + /> + setPassword(e.target.value)} + disabled={isLoading} + className="h-12 text-center text-lg bg-slate-50/50 border-slate-100 focus:border-blue-200 focus:ring-blue-100 rounded-2xl transition-all duration-300 hover:bg-white" + /> + {isRegister && ( + setConfirmPassword(e.target.value)} + disabled={isLoading} + className="h-12 text-center text-lg bg-slate-50/50 border-slate-100 focus:border-blue-200 focus:ring-blue-100 rounded-2xl transition-all duration-300 hover:bg-white animate-in slide-in-from-top-2 fade-in duration-300" + /> + )} +
+ {error && ( +
+ {error} +
+ )} + + +
+ {isRegister ? '已有账号?' : '还没有账号?'} + +
+
+ )} +
+
+ + +
+ +
+ 专为 Karriis 设计 🌸 +
+
+ ) +} diff --git a/211/web/tailwind.config.js b/211/web/tailwind.config.js new file mode 100644 index 0000000..9f5bc60 --- /dev/null +++ b/211/web/tailwind.config.js @@ -0,0 +1,79 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ["class"], + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + fontFamily: { + sans: [ + 'MapleMono', + 'ui-sans-serif', + 'system-ui', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ] + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + } + } + }, + plugins: [require("tailwindcss-animate")], + } diff --git a/211/web/tsconfig.app.json b/211/web/tsconfig.app.json new file mode 100644 index 0000000..0a7e47d --- /dev/null +++ b/211/web/tsconfig.app.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client", "node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Paths */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/211/web/tsconfig.json b/211/web/tsconfig.json new file mode 100644 index 0000000..fec8c8e --- /dev/null +++ b/211/web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/211/web/tsconfig.node.json b/211/web/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/211/web/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/211/web/vite.config.ts b/211/web/vite.config.ts new file mode 100644 index 0000000..9ae1fcd --- /dev/null +++ b/211/web/vite.config.ts @@ -0,0 +1,26 @@ +import path from "path" +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + '/socket.io': { + target: 'http://localhost:3000', + changeOrigin: true, + ws: true, + } + } + } +}) diff --git a/server/bot-worker.js b/server/bot-worker.js new file mode 100644 index 0000000..40afb41 --- /dev/null +++ b/server/bot-worker.js @@ -0,0 +1,575 @@ +const { createClient } = require('redis'); +const { FarmBot } = require('./src/core/FarmBot'); +const { loadProto, types } = require('./src/proto'); +const { log, toLong, toNum } = require('./src/utils'); +const { getLevelExpProgress, getPlantNameBySeedId } = require('./src/gameConfig'); + +const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379'; +const redisPublisher = createClient({ url: redisUrl }); +const redisSubscriber = createClient({ url: redisUrl }); + +const bots = new Map(); +const locks = new Map(); + +function safeParseJson(input) { + try { + return JSON.parse(input); + } catch { + return {}; + } +} + +function publish(channel, payload) { + if (!redisPublisher.isOpen) return; + const body = JSON.stringify(payload || {}); + redisPublisher.publish(channel, body).catch(() => {}); +} + +function getTodayKey() { + const now = new Date(); + const y = now.getFullYear(); + const m = `${now.getMonth() + 1}`.padStart(2, '0'); + const d = `${now.getDate()}`.padStart(2, '0'); + return `${y}-${m}-${d}`; +} + +async function withEmailLock(email, task) { + if (!email) return task(); + const prev = locks.get(email) || Promise.resolve(); + let release; + const next = new Promise((resolve) => { release = resolve; }); + locks.set(email, prev.then(() => next)); + await prev; + try { + return await task(); + } finally { + release(); + if (locks.get(email) === next) locks.delete(email); + } +} + +async function fetchTaskInfo(bot) { + const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish(); + const { body: replyBody } = await bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body); + const reply = types.TaskInfoReply.decode(replyBody); + return reply.task_info || null; +} + +function buildTaskList(taskInfo) { + const tasks = []; + if (!taskInfo) return tasks; + const seen = new Set(); + const seenContent = new Set(); + const addTasks = (list, category) => { + if (!list) return; + for (const task of list) { + const id = toNum(task.id); + if (seen.has(id)) continue; + const progress = toNum(task.progress); + const totalProgress = toNum(task.total_progress); + const isClaimed = Boolean(task.is_claimed); + const isUnlocked = Boolean(task.is_unlocked); + const shareMultiple = toNum(task.share_multiple); + const rewards = []; + const rewardList = task.rewards || []; + for (const reward of rewardList) { + rewards.push({ + id: toNum(reward.id), + count: toNum(reward.count) + }); + } + const desc = task.desc || `任务#${id}`; + const contentKey = `${desc.trim()}|${progress}|${totalProgress}`; + if (seenContent.has(contentKey)) continue; + tasks.push({ + id, + desc, + progress, + totalProgress, + isClaimed, + isUnlocked, + shareMultiple, + rewards, + category + }); + seen.add(id); + seenContent.add(contentKey); + } + }; + + addTasks(taskInfo.growth_tasks, 'growth'); + addTasks(taskInfo.daily_tasks, 'daily'); + addTasks(taskInfo.tasks, 'normal'); + return tasks; +} + +function isTaskClaimable(task) { + if (!task.isUnlocked || task.isClaimed) return false; + if (task.totalProgress <= 0) return false; + return task.progress >= task.totalProgress; +} + +async function claimTaskReward(bot, taskId, doShared) { + const body = types.ClaimTaskRewardRequest.encode(types.ClaimTaskRewardRequest.create({ + id: toLong(taskId), + do_shared: Boolean(doShared) + })).finish(); + const { body: replyBody } = await bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'ClaimTaskReward', body); + return types.ClaimTaskRewardReply.decode(replyBody); +} + +function attachBotEvents(email, bot) { + bot.on('started', () => { + publish(`bot-status-${email}`, { status: 'running' }); + }); + bot.on('stopped', () => { + publish(`bot-status-${email}`, { status: 'stopped' }); + bots.delete(email); + }); + bot.on('error', (err) => { + publish(`bot-error-${email}`, { message: err.message }); + }); + bot.on('log', (logData) => { + publish(`bot-log-${email}`, logData); + }); + bot.on('stealRecord', (data) => { + publish(`bot-steal-${email}`, data || {}); + }); +} + +async function buildStatus(bot) { + const user = bot.user || {}; + const todayKey = getTodayKey(); + const freeMallClaimed = user.freeMallClaimDate === todayKey; + const levelProgress = getLevelExpProgress(user.level || 0, user.exp || 0); + const tickets = user.tickets || 0; + const fertilizerContainer = Number(user.fertilizerContainer) || 0; + const fallbackHours = fertilizerContainer >= 3600 ? fertilizerContainer / 3600 : fertilizerContainer; + const organicFertilizerContainer = Number(user.organicFertilizerContainer) || 0; + const fallbackOrganicHours = organicFertilizerContainer >= 3600 ? organicFertilizerContainer / 3600 : organicFertilizerContainer; + let fertilizerHours = Number.isFinite(user.fertilizerHours) ? user.fertilizerHours : fallbackHours; + let organicFertilizerHours = Number.isFinite(user.organicFertilizerHours) ? user.organicFertilizerHours : fallbackOrganicHours; + if (bot.warehouseManager) { + try { + const bagReply = await bot.warehouseManager.getBag(); + const items = bot.warehouseManager.getBagItems(bagReply); + let normalContainer = 0; + let organicContainer = 0; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1011) normalContainer = count; + if (id === 1012) organicContainer = count; + } + const normalContainerHours = normalContainer >= 3600 ? normalContainer / 3600 : normalContainer; + const organicContainerHours = organicContainer >= 3600 ? organicContainer / 3600 : organicContainer; + const normalHours = normalContainerHours; + const organicHours = organicContainerHours; + fertilizerHours = normalHours; + user.normalFertilizerHours = normalHours; + user.organicFertilizerHours = organicHours; + organicFertilizerHours = organicHours; + } catch (error) { + log('状态', `获取背包失败: ${error.message}`); + } + } + return { + status: 'running', + user: { + nickname: user.name || 'Farmer', + level: user.level || 0, + exp: levelProgress.current || 0, + nextLevelExp: levelProgress.needed || 100, + gold: user.gold || 0, + tickets, + fertilizerHours, + normalFertilizerHours: Number.isFinite(user.normalFertilizerHours) ? user.normalFertilizerHours : fertilizerHours, + organicFertilizerHours, + avatarUrl: user.avatarUrl || '', + freeMallClaimed + } + }; +} + +function rpcOk(id, data) { + return { id, ok: true, data }; +} + +function rpcErr(id, error, status = 500) { + return { id, ok: false, error, status }; +} + +async function handleRpcRequest(req) { + const id = req && req.id; + const action = req && req.action; + const email = req && req.email; + const payload = (req && req.payload) || {}; + if (!id || !action) return null; + try { + if (action === 'worker.ping') return rpcOk(id, { ok: true }); + if (action === 'worker.stats') { + const memory = process.memoryUsage(); + return rpcOk(id, { + activeBots: bots.size, + memory: { + rss: Math.round(memory.rss / 1024 / 1024), + heapTotal: Math.round(memory.heapTotal / 1024 / 1024), + heapUsed: Math.round(memory.heapUsed / 1024 / 1024) + }, + uptime: Math.floor(process.uptime()) + }); + } + + if (action === 'worker.stopAll') { + let stopped = 0; + for (const bot of bots.values()) { + try { + bot.stop(); + stopped += 1; + } catch { + continue; + } + } + bots.clear(); + return rpcOk(id, { success: true, stopped }); + } + + if (action === 'worker.shutdown') { + for (const bot of bots.values()) { + try { + bot.stop(); + } catch { + continue; + } + } + bots.clear(); + setTimeout(() => process.exit(0), 200); + return rpcOk(id, { success: true }); + } + + if (action === 'bot.isRunning') { + return rpcOk(id, { running: bots.has(email) }); + } + + if (action === 'bot.start') { + if (!email) return rpcErr(id, 'Email is required', 400); + const rawCode = typeof payload.code === 'string' ? payload.code.trim() : ''; + payload.code = rawCode; + const code = rawCode; + if (!code) return rpcErr(id, 'Game code is required. Please provide it.', 400); + const response = await withEmailLock(email, async () => { + const existing = bots.get(email); + if (existing) { + existing.stop(); + bots.delete(email); + } + const bot = new FarmBot(payload); + attachBotEvents(email, bot); + const started = await bot.start(); + if (!started) return rpcErr(id, bot.lastStartError || '启动失败', 503); + bots.set(email, bot); + return rpcOk(id, { success: true }); + }); + return response; + } + + if (action === 'bot.stop') { + if (!email) return rpcErr(id, 'Email is required', 400); + const response = await withEmailLock(email, async () => { + const bot = bots.get(email); + if (!bot) return rpcErr(id, 'Bot is not running', 404); + bot.stop(); + bots.delete(email); + return rpcOk(id, { success: true }); + }); + return response; + } + + if (action === 'bot.status') { + if (!email) return rpcErr(id, 'Email is required', 400); + const bot = bots.get(email); + if (!bot) return rpcOk(id, { status: 'stopped', user: null }); + const data = await buildStatus(bot); + return rpcOk(id, data); + } + + if (action === 'bot.updateConfig') { + if (!email) return rpcErr(id, 'Email is required', 400); + const response = await withEmailLock(email, async () => { + const bot = bots.get(email); + if (!bot) return rpcErr(id, 'Bot not running', 404); + const settings = payload && typeof payload === 'object' + ? (payload.settings && typeof payload.settings === 'object' ? payload.settings : payload) + : {}; + bot.applyConfig(settings); + return rpcOk(id, { success: true }); + }); + return response; + } + + if (!email) return rpcErr(id, 'Email is required', 400); + const bot = bots.get(email); + if (!bot) return rpcErr(id, 'Bot not running', 404); + + if (action === 'friends.list') { + if (bot.config && bot.config.enableFriendOps === false) { + return rpcErr(id, 'Friend operations disabled', 403); + } + const reply = await bot.friendManager.getAllFriends(); + const friends = (reply.friends || []).map(f => ({ + uin: f.uin ? f.uin.toString() : '', + userName: f.userName, + headPic: f.headPic, + yellowLevel: f.yellowLevel, + exp: f.exp, + money: f.money + })); + return rpcOk(id, { friends }); + } + + if (action === 'friends.visit') { + if (bot.config && bot.config.enableFriendOps === false) { + return rpcErr(id, 'Friend operations disabled', 403); + } + const friendUid = payload.friendUid; + const reply = await bot.friendManager.enterFriendFarm(friendUid); + const lands = reply && reply.farm && reply.farm.lands ? reply.farm.lands : []; + if (lands.length === 0) return rpcOk(id, { lands: [] }); + const analysis = bot.friendManager.analyzeFriendLands(lands, bot.user.gid); + const formattedLands = lands.map(land => { + const landId = Number(land.id); + if (!land.unlocked) return { id: landId, type: 'locked' }; + const plant = land.plant; + if (!plant || !plant.phases || plant.phases.length === 0) { + return { id: landId, type: 'empty' }; + } + const currentPhase = bot.farmManager.getCurrentPhase(plant.phases, false); + const phaseVal = currentPhase ? currentPhase.phase : 0; + let status = 'growing'; + if (phaseVal === 7) status = 'dead'; + else if (phaseVal === 6) status = 'mature'; + return { + id: landId, + type: 'planted', + status, + plantId: Number(plant.id), + plantName: getPlantNameBySeedId(Number(plant.id)) || `作物${plant.id}`, + phase: phaseVal, + needs: { + water: analysis.needWater.includes(landId), + weed: analysis.needWeed.includes(landId), + bug: analysis.needBug.includes(landId) + }, + canSteal: analysis.stealable.includes(landId) + }; + }); + return rpcOk(id, { lands: formattedLands, farmUser: reply.farm.user }); + } + + if (action === 'friends.action') { + if (bot.config && bot.config.enableFriendOps === false) { + return rpcErr(id, 'Friend operations disabled', 403); + } + if (payload.actionType === 'steal' && bot.config && bot.config.enableSteal === false) { + return rpcErr(id, 'Steal disabled', 403); + } + let result; + let message = ''; + if (payload.actionType === 'water') { + result = await bot.friendManager.helpWater(payload.friendUid, payload.landIds); + message = '浇水成功'; + } else if (payload.actionType === 'weed') { + result = await bot.friendManager.helpWeed(payload.friendUid, payload.landIds); + message = '除草成功'; + } else if (payload.actionType === 'insecticide') { + result = await bot.friendManager.helpInsecticide(payload.friendUid, payload.landIds); + message = '除虫成功'; + } else if (payload.actionType === 'steal') { + result = await bot.friendManager.stealHarvest(payload.friendUid, payload.landIds); + message = '偷菜成功'; + } else { + return rpcErr(id, 'Unknown action type', 400); + } + return rpcOk(id, { success: true, message, result }); + } + + if (action === 'warehouse.list') { + const data = await bot.warehouseManager.getFormattedBag(); + return rpcOk(id, data); + } + + if (action === 'warehouse.sell') { + const reply = await bot.warehouseManager.sellItems(payload.items); + const gold = bot.warehouseManager.extractGold(reply); + return rpcOk(id, { success: true, gold }); + } + + if (action === 'warehouse.use') { + const result = await bot.warehouseManager.useItems(payload.items); + return rpcOk(id, result); + } + + if (action === 'tasks.list') { + const taskInfo = await fetchTaskInfo(bot); + const tasks = buildTaskList(taskInfo); + return rpcOk(id, { tasks }); + } + + if (action === 'tasks.claimAll') { + const taskInfo = await fetchTaskInfo(bot); + const tasks = buildTaskList(taskInfo); + const claimable = []; + for (const task of tasks) { + if (isTaskClaimable(task)) claimable.push(task); + } + let claimedCount = 0; + const failed = []; + for (const task of claimable) { + try { + await claimTaskReward(bot, task.id, task.shareMultiple > 1); + claimedCount += 1; + } catch (err) { + failed.push({ id: task.id, message: err.message }); + } + } + const latestTaskInfo = await fetchTaskInfo(bot); + const latestTasks = buildTaskList(latestTaskInfo); + return rpcOk(id, { claimedCount, failed, tasks: latestTasks }); + } + + if (action === 'tasks.claimOne') { + const idNum = Number(payload.taskId); + if (!Number.isFinite(idNum) || idNum <= 0) return rpcErr(id, 'Invalid taskId', 400); + await claimTaskReward(bot, idNum, Boolean(payload.doShared)); + const latestTaskInfo = await fetchTaskInfo(bot); + const latestTasks = buildTaskList(latestTaskInfo); + return rpcOk(id, { claimedCount: 1, failedCount: 0, tasks: latestTasks }); + } + + if (action === 'lands.list') { + const lands = await bot.farmManager.getFormattedLands(); + return rpcOk(id, { lands }); + } + + if (action === 'lands.remove') { + const ids = Array.isArray(payload.landIds) ? payload.landIds.map(Number).filter(idNum => Number.isFinite(idNum) && idNum > 0) : []; + if (ids.length === 0) return rpcErr(id, 'Invalid landIds', 400); + await bot.farmManager.removePlant(ids); + return rpcOk(id, { success: true, removed: ids.length }); + } + + if (action === 'lands.unlock') { + const landId = Number(payload.landId); + if (!Number.isFinite(landId) || landId <= 0) return rpcErr(id, 'Invalid landId', 400); + const reply = await bot.farmManager.unlockLand(landId, Boolean(payload.doShared)); + let land = reply && reply.land ? reply.land : null; + if (!land || !land.unlocked) { + const landsReply = await bot.farmManager.getAllLands(); + const lands = landsReply && landsReply.lands ? landsReply.lands : []; + land = lands.find(item => Number(item.id) === landId) || null; + if (!land || !land.unlocked) { + return rpcErr(id, 'Land not unlocked', 409); + } + } + return rpcOk(id, { success: true, landId, land }); + } + + if (action === 'shop.list') { + const goods = await bot.shopManager.getSeedShopList(); + return rpcOk(id, { goods }); + } + + if (action === 'shop.buy') { + await bot.shopManager.buyGoods(payload.goodsId, payload.count, payload.price); + return rpcOk(id, { success: true }); + } + + if (action === 'shop.paymallBuy') { + const itemId = Number(payload.itemId); + const buyCount = Number(payload.count ?? 1); + if (!Number.isFinite(itemId) || itemId <= 0) return rpcErr(id, 'Invalid itemId', 400); + if (!Number.isFinite(buyCount) || buyCount <= 0) return rpcErr(id, 'Invalid count', 400); + await bot.shopManager.purchaseMallItem(itemId, buyCount); + if (itemId === 1001) { + bot.user.freeMallClaimDate = getTodayKey(); + bot.emit('userUpdate', bot.user); + } + return rpcOk(id, { success: true }); + } + + if (action === 'debug.fertilizer') { + const bagReply = await bot.warehouseManager.getBag(); + const items = bot.warehouseManager.getBagItems(bagReply); + let normalContainer = 0; + let normal1 = 0; + let normal4 = 0; + let normal8 = 0; + let normal12 = 0; + let organicContainer = 0; + let organic1 = 0; + let organic4 = 0; + let organic8 = 0; + let organic12 = 0; + for (const item of items) { + const idNum = toNum(item.id); + const countNum = toNum(item.count); + if (idNum === 1011) normalContainer = countNum; + if (idNum === 80001) normal1 = countNum; + if (idNum === 80002) normal4 = countNum; + if (idNum === 80003) normal8 = countNum; + if (idNum === 80004) normal12 = countNum; + if (idNum === 1012) organicContainer = countNum; + if (idNum === 80011) organic1 = countNum; + if (idNum === 80012) organic4 = countNum; + if (idNum === 80013) organic8 = countNum; + if (idNum === 80014) organic12 = countNum; + } + const normalContainerHours = normalContainer >= 3600 ? normalContainer / 3600 : normalContainer; + const organicContainerHours = organicContainer >= 3600 ? organicContainer / 3600 : organicContainer; + const normalHours = normalContainerHours + normal1 + normal4 * 4 + normal8 * 8 + normal12 * 12; + const organicHours = organicContainerHours + organic1 + organic4 * 4 + organic8 * 8 + organic12 * 12; + return rpcOk(id, { + normal: { + container: normalContainer, + hour1: normal1, + hour4: normal4, + hour8: normal8, + hour12: normal12, + hours: normalHours + }, + organic: { + container: organicContainer, + hour1: organic1, + hour4: organic4, + hour8: organic8, + hour12: organic12, + hours: organicHours + } + }); + } + + return rpcErr(id, 'Unknown action', 400); + } catch (err) { + const message = err && err.message ? err.message : 'Worker error'; + return rpcErr(id, message, 500); + } +} + +async function startWorker() { + await loadProto(); + await redisPublisher.connect(); + await redisSubscriber.connect(); + await redisSubscriber.subscribe('bot:rpc:req', async (message) => { + const req = safeParseJson(message); + const response = await handleRpcRequest(req); + if (!response) return; + const body = JSON.stringify(response); + redisPublisher.publish('bot:rpc:res', body).catch(() => {}); + }); + log('Worker', 'Worker ready'); +} + +startWorker().catch((err) => { + log('Worker', `Worker failed: ${err.message}`); + process.exit(1); +}); diff --git a/server/client.js b/server/client.js new file mode 100644 index 0000000..b5392c8 --- /dev/null +++ b/server/client.js @@ -0,0 +1,139 @@ +/** + * QQ经典农场 挂机脚本 - 入口文件 + * + * 模块结构: + * src/config.js - 配置常量与枚举 + * src/utils.js - 通用工具函数 + * src/proto.js - Protobuf 加载与类型管理 + * src/core/ - 核心逻辑 (FarmBot, Network, FarmManager, FriendManager) + * src/decode.js - PB解码/验证工具模式 + */ + +const { CONFIG } = require('./src/config'); +const { loadProto } = require('./src/proto'); +const { FarmBot } = require('./src/core/FarmBot'); +const { verifyMode, decodeMode } = require('./src/decode'); +const { emitRuntimeHint, sleep } = require('./src/utils'); + +// ============ 帮助信息 ============ +function showHelp() { + console.log(` +QQ经典农场 挂机脚本 (重构版) +==================== + +用法: + node client.js --code <登录code> [--wx] [--interval <秒>] [--friend-interval <秒>] + node client.js --verify + node client.js --decode <数据> [--hex] [--gate] [--type <消息类型>] + +参数: + --code 小程序 login() 返回的临时凭证 (必需) + --wx 使用微信登录 (默认为QQ小程序) + --interval 自己农场巡查完成后等待秒数, 默认10秒, 最低10秒 + --friend-interval 好友巡查完成后等待秒数, 默认1秒, 最低1秒 + --verify 验证proto定义 + --decode 解码PB数据 (运行 --decode 无参数查看详细帮助) + +功能: + - 自动收获成熟作物 → 购买种子 → 种植 → 施肥 + - 自动除草、除虫、浇水 + - 自动铲除枯死作物 + - 自动巡查好友农场: 帮忙浇水/除草/除虫 + 偷菜 + - 自动领取任务奖励 (支持分享翻倍) + - 每分钟自动出售仓库果实 + - 启动时读取 share.txt 处理邀请码 (仅微信) + - 心跳保活 +`); +} + +// ============ 参数解析 ============ +function parseArgs(args) { + const options = { + code: '', + deleteAccountMode: false, + name: '', + certId: '', + certType: 0, + }; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--code' && args[i + 1]) { + options.code = args[++i]; + } + if (args[i] === '--wx') { + CONFIG.platform = 'wx'; + } + if (args[i] === '--interval' && args[i + 1]) { + const sec = parseInt(args[++i]); + CONFIG.farmCheckInterval = Math.max(sec, 1) * 1000; + } + if (args[i] === '--friend-interval' && args[i + 1]) { + const sec = parseInt(args[++i]); + CONFIG.friendCheckInterval = Math.max(sec, 1) * 1000; // 最低1秒 + } + } + return options; +} + +// ============ 主函数 ============ +async function main() { + const args = process.argv.slice(2); + + // 加载 proto 定义 + await loadProto(); + + // 验证模式 + if (args.includes('--verify')) { + await verifyMode(); + return; + } + + // 解码模式 + if (args.includes('--decode')) { + await decodeMode(args); + return; + } + + // 正常运行模式 + const options = parseArgs(args); + + if (!options.code) { + showHelp(); + return; + } + + // 更新 CONFIG 中的 code (注意: FarmBot 构造函数会合并 CONFIG 和传入的 config) + // 这里我们可以直接修改全局 CONFIG,或者传入 options + // 为了兼容旧代码习惯,我们修改全局 CONFIG + // 但 FarmBot 建议传入 config + const botConfig = { + code: options.code, + // 其他参数已通过 modify global CONFIG 生效,或者也可以显式传入 + }; + + console.log('正在启动 FarmBot...'); + const bot = new FarmBot(botConfig); + + // 优雅退出 + process.on('SIGINT', () => { + console.log('\n正在停止...'); + bot.stop(); + process.exit(0); + }); + + try { + await bot.start(); + + // 保持进程运行 (如果 start() 返回后没有挂起的操作) + // FarmBot 内部启动了 interval,所以进程应该会保持运行 + + } catch (error) { + console.error('Bot 运行出错:', error); + process.exit(1); + } +} + +main().catch(err => { + console.error('未捕获的异常:', err); + process.exit(1); +}); diff --git a/server/data/users.db b/server/data/users.db new file mode 100644 index 0000000..6b13462 Binary files /dev/null and b/server/data/users.db differ diff --git a/server/gameConfig/Plant.json b/server/gameConfig/Plant.json new file mode 100644 index 0000000..9a1d24c --- /dev/null +++ b/server/gameConfig/Plant.json @@ -0,0 +1,4061 @@ +[ + { + "id": 2020002, + "name": "白萝卜", + "mutant": "", + "fruit": { + "id": 40002, + "count": 5 + }, + "seed_id": 29999, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:1;发芽:1;成熟:0;", + "exp": 1, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020002, + "name": "白萝卜", + "mutant": "1:102003;2:1020059", + "fruit": { + "id": 40002, + "count": 5 + }, + "seed_id": 20002, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:30;发芽:30;成熟:0;", + "exp": 1, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020003, + "name": "胡萝卜", + "mutant": "", + "fruit": { + "id": 40003, + "count": 10 + }, + "seed_id": 20003, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:30;发芽:30;小叶子:30;大叶子:30;成熟:0;", + "exp": 2, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020059, + "name": "大白菜", + "mutant": "", + "fruit": { + "id": 40059, + "count": 20 + }, + "seed_id": 20059, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:60;发芽:60;幼苗:60;成株:60;卷心:60;成熟:0;", + "exp": 5, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020065, + "name": "大蒜", + "mutant": "", + "fruit": { + "id": 40065, + "count": 20 + }, + "seed_id": 20065, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:120;发芽:120;幼苗:120;伸长:120;初熟:120;成熟:0;", + "exp": 10, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020064, + "name": "大葱", + "mutant": "", + "fruit": { + "id": 40064, + "count": 30 + }, + "seed_id": 20064, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:300;发芽:300;小叶子:300;大叶子:300;成熟:0;", + "exp": 20, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020060, + "name": "水稻", + "mutant": "", + "fruit": { + "id": 40060, + "count": 30 + }, + "seed_id": 20060, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:480;幼苗:480;秧苗:480;幼穗:480;开花:480;成熟:0;", + "exp": 41, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020061, + "name": "小麦", + "mutant": "", + "fruit": { + "id": 40061, + "count": 40 + }, + "seed_id": 20061, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:720;发芽:720;小叶子:720;大叶子:720;幼穗:720;成熟:0;", + "exp": 62, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020004, + "name": "玉米", + "mutant": "", + "fruit": { + "id": 40004, + "count": 40 + }, + "seed_id": 20004, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:960;发芽:960;小叶子:960;大叶子:960;开花:960;成熟:0;", + "exp": 82, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020066, + "name": "鲜姜", + "mutant": "", + "fruit": { + "id": 40066, + "count": 60 + }, + "seed_id": 20066, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:1500;发芽:1500;小叶子:1500;大叶子:1500;成熟:0;", + "exp": 106, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020005, + "name": "土豆", + "mutant": "", + "fruit": { + "id": 40005, + "count": 60 + }, + "seed_id": 20005, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:1440;发芽:1440;小叶子:1440;大叶子:1440;初熟:1440;成熟:0;", + "exp": 128, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020071, + "name": "小白菜", + "mutant": "", + "fruit": { + "id": 40071, + "count": 80 + }, + "seed_id": 20071, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2250;发芽:2250;小叶子:2250;大叶子:2250;成熟:0;", + "exp": 160, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020096, + "name": "生菜", + "mutant": "", + "fruit": { + "id": 40096, + "count": 80 + }, + "seed_id": 20096, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2160;发芽:2160;小叶子:2160;大叶子:2160;初熟:2160;成熟:0;", + "exp": 192, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020099, + "name": "油菜", + "mutant": "", + "fruit": { + "id": 40099, + "count": 200 + }, + "seed_id": 20099, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:3600;发芽:3600;小叶子:3600;大叶子:3600;成熟:0;", + "exp": 272, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020006, + "name": "茄子", + "mutant": "", + "fruit": { + "id": 40006, + "count": 200 + }, + "seed_id": 20006, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 544, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020051, + "name": "红枣", + "mutant": "", + "fruit": { + "id": 40051, + "count": 200 + }, + "seed_id": 20051, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 816, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020120, + "name": "蒲公英", + "mutant": "", + "fruit": { + "id": 40120, + "count": 200 + }, + "seed_id": 20120, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:17280;成熟:0;", + "exp": 1632, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020259, + "name": "银莲花", + "mutant": "", + "fruit": { + "id": 40259, + "count": 200 + }, + "seed_id": 20259, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0;", + "exp": 288, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020007, + "name": "番茄", + "mutant": "", + "fruit": { + "id": 40007, + "count": 200 + }, + "seed_id": 20007, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 576, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020098, + "name": "花菜", + "mutant": "", + "fruit": { + "id": 40098, + "count": 200 + }, + "seed_id": 20098, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;幼苗:8640;卷心:8640;初熟:8640;成熟:0;", + "exp": 864, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020305, + "name": "韭菜", + "mutant": "", + "fruit": { + "id": 40305, + "count": 200 + }, + "seed_id": 20305, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0;", + "exp": 1728, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020105, + "name": "小雏菊", + "mutant": "", + "fruit": { + "id": 40105, + "count": 200 + }, + "seed_id": 20105, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0;", + "exp": 304, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020008, + "name": "豌豆", + "mutant": "", + "fruit": { + "id": 40008, + "count": 200 + }, + "seed_id": 20008, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0;", + "exp": 608, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020037, + "name": "莲藕", + "mutant": "", + "fruit": { + "id": 40037, + "count": 200 + }, + "seed_id": 20037, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 912, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020041, + "name": "红玫瑰", + "mutant": "", + "fruit": { + "id": 40041, + "count": 200 + }, + "seed_id": 20041, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0;", + "exp": 1824, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020161, + "name": "秋菊(黄色)", + "mutant": "", + "fruit": { + "id": 40161, + "count": 200 + }, + "seed_id": 20161, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0;", + "exp": 324, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020110, + "name": "满天星", + "mutant": "", + "fruit": { + "id": 40110, + "count": 200 + }, + "seed_id": 20110, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0;", + "exp": 648, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020143, + "name": "含羞草", + "mutant": "", + "fruit": { + "id": 40143, + "count": 200 + }, + "seed_id": 20143, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:7200;花蕾:7200;盛开:7200;成熟:0;", + "exp": 972, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020147, + "name": "牵牛花", + "mutant": "", + "fruit": { + "id": 40147, + "count": 200 + }, + "seed_id": 20147, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0;", + "exp": 1944, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020162, + "name": "秋菊(红色)", + "mutant": "", + "fruit": { + "id": 40162, + "count": 200 + }, + "seed_id": 20162, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0;", + "exp": 344, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020009, + "name": "辣椒", + "mutant": "", + "fruit": { + "id": 40009, + "count": 200 + }, + "seed_id": 20009, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0;", + "exp": 688, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020097, + "name": "黄瓜", + "mutant": "", + "fruit": { + "id": 40097, + "count": 200 + }, + "seed_id": 20097, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 1032, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020306, + "name": "芹菜", + "mutant": "", + "fruit": { + "id": 40306, + "count": 200 + }, + "seed_id": 20306, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0;", + "exp": 2064, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020103, + "name": "天香百合", + "mutant": "", + "fruit": { + "id": 40103, + "count": 200 + }, + "seed_id": 20103, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0;", + "exp": 368, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020010, + "name": "南瓜", + "mutant": "", + "fruit": { + "id": 40010, + "count": 200 + }, + "seed_id": 20010, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0;", + "exp": 736, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020308, + "name": "核桃", + "mutant": "", + "fruit": { + "id": 40308, + "count": 200 + }, + "seed_id": 20308, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:10800;发芽:10800;小叶子:10800;大叶子:10800;成熟:0;", + "exp": 1104, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020091, + "name": "山楂", + "mutant": "", + "fruit": { + "id": 40091, + "count": 200 + }, + "seed_id": 20091, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:14400;开花:14400;初熟:14400;成熟:0;", + "exp": 2208, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020073, + "name": "菠菜", + "mutant": "", + "fruit": { + "id": 40073, + "count": 200 + }, + "seed_id": 20073, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0;", + "exp": 392, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020001, + "name": "草莓", + "mutant": "", + "fruit": { + "id": 40001, + "count": 200 + }, + "seed_id": 20001, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 784, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020011, + "name": "苹果", + "mutant": "", + "fruit": { + "id": 40011, + "count": 200 + }, + "seed_id": 20011, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 1176, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020062, + "name": "四叶草", + "mutant": "", + "fruit": { + "id": 40062, + "count": 200 + }, + "seed_id": 20062, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0;", + "exp": 2352, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020104, + "name": "非洲菊", + "mutant": "", + "fruit": { + "id": 40104, + "count": 200 + }, + "seed_id": 20104, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:2400;花蕾:2400;盛开:2400;成熟:0;", + "exp": 420, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020135, + "name": "火绒草", + "mutant": "", + "fruit": { + "id": 40135, + "count": 200 + }, + "seed_id": 20135, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0;", + "exp": 840, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020141, + "name": "花香根鸢尾", + "mutant": "", + "fruit": { + "id": 40141, + "count": 200 + }, + "seed_id": 20141, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;花蕾:8640;盛开:0;", + "exp": 1260, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020142, + "name": "虞美人", + "mutant": "", + "fruit": { + "id": 40142, + "count": 200 + }, + "seed_id": 20142, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0;", + "exp": 2520, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020145, + "name": "向日葵", + "mutant": "", + "fruit": { + "id": 40145, + "count": 200 + }, + "seed_id": 20145, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:2400;开花:2400;初熟:2400;成熟:0;", + "exp": 448, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020014, + "name": "西瓜", + "mutant": "", + "fruit": { + "id": 40014, + "count": 200 + }, + "seed_id": 20014, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 896, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020070, + "name": "黄豆", + "mutant": "", + "fruit": { + "id": 40070, + "count": 200 + }, + "seed_id": 20070, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;初熟:8640;成熟:0;", + "exp": 1344, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020015, + "name": "香蕉", + "mutant": "", + "fruit": { + "id": 40015, + "count": 200 + }, + "seed_id": 20015, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0;", + "exp": 2688, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020100, + "name": "竹笋", + "mutant": "", + "fruit": { + "id": 40100, + "count": 200 + }, + "seed_id": 20100, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;幼苗:2880;伸长:2880;初熟:2880;成熟:0;", + "exp": 476, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020018, + "name": "桃子", + "mutant": "", + "fruit": { + "id": 40018, + "count": 200 + }, + "seed_id": 20018, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;开花:5760;小叶子:5760;大叶子:5760;成熟:0;", + "exp": 952, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020047, + "name": "甘蔗", + "mutant": "", + "fruit": { + "id": 40047, + "count": 200 + }, + "seed_id": 20047, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;幼苗:8640;分叶:8640;伸长:8640;成熟:0;", + "exp": 1428, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020019, + "name": "橙子", + "mutant": "", + "fruit": { + "id": 40019, + "count": 200 + }, + "seed_id": 20019, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0;", + "exp": 2856, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020128, + "name": "茉莉花", + "mutant": "", + "fruit": { + "id": 40128, + "count": 200 + }, + "seed_id": 20128, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0;", + "exp": 508, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020013, + "name": "葡萄", + "mutant": "", + "fruit": { + "id": 40013, + "count": 200 + }, + "seed_id": 20013, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 1016, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020044, + "name": "丝瓜", + "mutant": "", + "fruit": { + "id": 40044, + "count": 200 + }, + "seed_id": 20044, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;长枝:8640;开花:8640;小叶子:8640;大叶子:8640;结果:0;", + "exp": 1524, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020072, + "name": "榛子", + "mutant": "", + "fruit": { + "id": 40072, + "count": 200 + }, + "seed_id": 20072, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;开花:17280;小叶子:17280;大叶子:17280;成熟:0;", + "exp": 3048, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020396, + "name": "迎春花", + "mutant": "", + "fruit": { + "id": 40396, + "count": 200 + }, + "seed_id": 20396, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:2880;幼芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0;", + "exp": 540, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020023, + "name": "石榴", + "mutant": "", + "fruit": { + "id": 40023, + "count": 200 + }, + "seed_id": 20023, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0;", + "exp": 1080, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020095, + "name": "栗子", + "mutant": "", + "fruit": { + "id": 40095, + "count": 200 + }, + "seed_id": 20095, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0;", + "exp": 1620, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020026, + "name": "柚子", + "mutant": "", + "fruit": { + "id": 40026, + "count": 200 + }, + "seed_id": 20026, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0;", + "exp": 3240, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020050, + "name": "蘑菇", + "mutant": "", + "fruit": { + "id": 40050, + "count": 200 + }, + "seed_id": 20050, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:3600;发芽:3600;大叶子:3600;初熟:3600;成熟:0;", + "exp": 429, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020027, + "name": "菠萝", + "mutant": "", + "fruit": { + "id": 40027, + "count": 200 + }, + "seed_id": 20027, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 858, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020036, + "name": "箬竹", + "mutant": "", + "fruit": { + "id": 40036, + "count": 200 + }, + "seed_id": 20036, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0;", + "exp": 1287, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020043, + "name": "无花果", + "mutant": "", + "fruit": { + "id": 40043, + "count": 200 + }, + "seed_id": 20043, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 2574, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020029, + "name": "椰子", + "mutant": "", + "fruit": { + "id": 40029, + "count": 200 + }, + "seed_id": 20029, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 456, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020049, + "name": "花生", + "mutant": "", + "fruit": { + "id": 40049, + "count": 200 + }, + "seed_id": 20049, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 912, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020052, + "name": "金针菇", + "mutant": "", + "fruit": { + "id": 40052, + "count": 200 + }, + "seed_id": 20052, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发菌:7200;出菇:7200;幼菇:10800;初熟:10800;成熟:0;", + "exp": 1368, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020031, + "name": "葫芦", + "mutant": "", + "fruit": { + "id": 40031, + "count": 200 + }, + "seed_id": 20031, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 2736, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020045, + "name": "猕猴桃", + "mutant": "", + "fruit": { + "id": 40045, + "count": 200 + }, + "seed_id": 20045, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 480, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020054, + "name": "梨", + "mutant": "", + "fruit": { + "id": 40054, + "count": 200 + }, + "seed_id": 20054, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 960, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020442, + "name": "睡莲", + "mutant": "", + "fruit": { + "id": 40442, + "count": 200 + }, + "seed_id": 20442, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;幼株:10800;成熟:0;", + "exp": 1440, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020033, + "name": "火龙果", + "mutant": "", + "fruit": { + "id": 40033, + "count": 200 + }, + "seed_id": 20033, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 2880, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020055, + "name": "枇杷", + "mutant": "", + "fruit": { + "id": 40055, + "count": 200 + }, + "seed_id": 20055, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 510, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020034, + "name": "樱桃", + "mutant": "", + "fruit": { + "id": 40034, + "count": 200 + }, + "seed_id": 20034, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 1020, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020413, + "name": "李子", + "mutant": "", + "fruit": { + "id": 40413, + "count": 200 + }, + "seed_id": 20413, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1530, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020035, + "name": "荔枝", + "mutant": "", + "fruit": { + "id": 40035, + "count": 200 + }, + "seed_id": 20035, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 3060, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020067, + "name": "香瓜", + "mutant": "", + "fruit": { + "id": 40067, + "count": 200 + }, + "seed_id": 20067, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 537, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020038, + "name": "木瓜", + "mutant": "", + "fruit": { + "id": 40038, + "count": 200 + }, + "seed_id": 20038, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 1074, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020053, + "name": "桂圆", + "mutant": "", + "fruit": { + "id": 40053, + "count": 200 + }, + "seed_id": 20053, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1611, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020080, + "name": "月柿", + "mutant": "", + "fruit": { + "id": 40080, + "count": 200 + }, + "seed_id": 20080, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 3222, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020039, + "name": "杨桃", + "mutant": "", + "fruit": { + "id": 40039, + "count": 200 + }, + "seed_id": 20039, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 567, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020056, + "name": "哈密瓜", + "mutant": "", + "fruit": { + "id": 40056, + "count": 200 + }, + "seed_id": 20056, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 1134, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020075, + "name": "桑葚", + "mutant": "", + "fruit": { + "id": 40075, + "count": 200 + }, + "seed_id": 20075, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1701, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020042, + "name": "柠檬", + "mutant": "", + "fruit": { + "id": 40042, + "count": 200 + }, + "seed_id": 20042, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 3402, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020057, + "name": "芒果", + "mutant": "", + "fruit": { + "id": 40057, + "count": 200 + }, + "seed_id": 20057, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 597, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020048, + "name": "杨梅", + "mutant": "", + "fruit": { + "id": 40048, + "count": 200 + }, + "seed_id": 20048, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0;", + "exp": 1194, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020058, + "name": "榴莲", + "mutant": "", + "fruit": { + "id": 40058, + "count": 200 + }, + "seed_id": 20058, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1791, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020079, + "name": "番石榴", + "mutant": "", + "fruit": { + "id": 40079, + "count": 200 + }, + "seed_id": 20079, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 3582, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020218, + "name": "瓶子树", + "mutant": "", + "fruit": { + "id": 40218, + "count": 200 + }, + "seed_id": 20218, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;长枝:2400;小叶子:2400;大叶子:3600;初熟:3600;成树:0;", + "exp": 627, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020077, + "name": "蓝莓", + "mutant": "", + "fruit": { + "id": 40077, + "count": 200 + }, + "seed_id": 20077, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 1254, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020220, + "name": "猪笼草", + "mutant": "", + "fruit": { + "id": 40220, + "count": 200 + }, + "seed_id": 20220, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0;", + "exp": 1881, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020076, + "name": "山竹", + "mutant": "", + "fruit": { + "id": 40076, + "count": 200 + }, + "seed_id": 20076, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 3762, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020116, + "name": "曼陀罗华", + "mutant": "", + "fruit": { + "id": 40116, + "count": 200 + }, + "seed_id": 20116, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;花蕾:3600;盛开:0;", + "exp": 660, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020126, + "name": "曼珠沙华", + "mutant": "", + "fruit": { + "id": 40126, + "count": 200 + }, + "seed_id": 20126, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 1320, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020063, + "name": "苦瓜", + "mutant": "", + "fruit": { + "id": 40063, + "count": 200 + }, + "seed_id": 20063, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 1980, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020221, + "name": "天堂鸟", + "mutant": "", + "fruit": { + "id": 40221, + "count": 200 + }, + "seed_id": 20221, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0;", + "exp": 3960, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020068, + "name": "冬瓜", + "mutant": "", + "fruit": { + "id": 40068, + "count": 200 + }, + "seed_id": 20068, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 693, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020222, + "name": "豹皮花", + "mutant": "", + "fruit": { + "id": 40222, + "count": 200 + }, + "seed_id": 20222, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0;", + "exp": 1386, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020078, + "name": "杏子", + "mutant": "", + "fruit": { + "id": 40078, + "count": 200 + }, + "seed_id": 20078, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0;", + "exp": 2079, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020074, + "name": "金桔", + "mutant": "", + "fruit": { + "id": 40074, + "count": 200 + }, + "seed_id": 20074, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 4158, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020083, + "name": "红毛丹", + "mutant": "", + "fruit": { + "id": 40083, + "count": 200 + }, + "seed_id": 20083, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 726, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020225, + "name": "宝华玉兰", + "mutant": "", + "fruit": { + "id": 40225, + "count": 200 + }, + "seed_id": 20225, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0;", + "exp": 1452, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020084, + "name": "芭蕉", + "mutant": "", + "fruit": { + "id": 40084, + "count": 200 + }, + "seed_id": 20084, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0;", + "exp": 2178, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020226, + "name": "依米花", + "mutant": "", + "fruit": { + "id": 40226, + "count": 200 + }, + "seed_id": 20226, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0;", + "exp": 4356, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020085, + "name": "番荔枝", + "mutant": "", + "fruit": { + "id": 40085, + "count": 200 + }, + "seed_id": 20085, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0;", + "exp": 762, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020227, + "name": "大王花", + "mutant": "", + "fruit": { + "id": 40227, + "count": 200 + }, + "seed_id": 20227, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;幼蕾:4800;含苞:7200;初放:7200;盛开:0;", + "exp": 1524, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020086, + "name": "橄榄", + "mutant": "", + "fruit": { + "id": 40086, + "count": 200 + }, + "seed_id": 20086, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 2286, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020228, + "name": "人参果", + "mutant": "", + "fruit": { + "id": 40228, + "count": 200 + }, + "seed_id": 20228, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0;", + "exp": 4572, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020087, + "name": "百香果", + "mutant": "", + "fruit": { + "id": 40087, + "count": 200 + }, + "seed_id": 20087, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0;", + "exp": 795, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020235, + "name": "金花茶", + "mutant": "", + "fruit": { + "id": 40235, + "count": 200 + }, + "seed_id": 20235, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0;", + "exp": 1590, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020088, + "name": "灯笼果", + "mutant": "", + "fruit": { + "id": 40088, + "count": 200 + }, + "seed_id": 20088, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0;", + "exp": 2385, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020201, + "name": "天山雪莲", + "mutant": "", + "fruit": { + "id": 40201, + "count": 200 + }, + "seed_id": 20201, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;幼株:21600;成熟:0;", + "exp": 4770, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020089, + "name": "芦荟", + "mutant": "", + "fruit": { + "id": 40089, + "count": 200 + }, + "seed_id": 20089, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0;", + "exp": 831, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020202, + "name": "金边灵芝", + "mutant": "", + "fruit": { + "id": 40202, + "count": 200 + }, + "seed_id": 20202, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;芝蕾:4800;幼芝:7200;初熟:7200;成熟:0;", + "exp": 1662, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020090, + "name": "薄荷", + "mutant": "", + "fruit": { + "id": 40090, + "count": 200 + }, + "seed_id": 20090, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 2493, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020229, + "name": "何首乌", + "mutant": "", + "fruit": { + "id": 40229, + "count": 200 + }, + "seed_id": 20229, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 4986, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020016, + "name": "菠萝蜜", + "mutant": "", + "fruit": { + "id": 40016, + "count": 200 + }, + "seed_id": 20016, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0;", + "exp": 867, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020204, + "name": "人参", + "mutant": "", + "fruit": { + "id": 40204, + "count": 200 + }, + "seed_id": 20204, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0;", + "exp": 1734, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020022, + "name": "鳄梨", + "mutant": "", + "fruit": { + "id": 40022, + "count": 200 + }, + "seed_id": 20022, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0;", + "exp": 2601, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1020242, + "name": "似血杜鹃", + "mutant": "", + "fruit": { + "id": 40242, + "count": 200 + }, + "seed_id": 20242, + "land_level_need": 1, + "seasons": 2, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0;", + "exp": 5202, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 1021542, + "name": "新春红包", + "mutant": "", + "fruit": { + "id": 41542, + "count": 20 + }, + "seed_id": 21542, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0;", + "exp": 688, + "size": 0, + "offsetPosition": { + "x": 0, + "y": 0 + }, + "mutantEffectScale": { + "x": 1, + "y": 1 + }, + "harvestOffsetPosition": { + "x": -35, + "y": 40 + }, + "harvestRandom": false, + "harvestAllSpineRes": "", + "harvestAllOffsetPosition": "", + "all_state_spine": "spine/v2/xiyouzhongzi/red packet_tree", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "" + }, + { + "id": 2029998, + "name": "哈哈南瓜", + "mutant": "", + "fruit": { + "id": 40416, + "count": 50 + }, + "seed_id": 29998, + "land_level_need": 1, + "seasons": 1, + "grow_phases": "种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0;", + "exp": 1, + "size": 2, + "offsetPosition": { + "x": 100, + "y": 0 + }, + "mutantEffectScale": { + "x": 1.75, + "y": 1.5 + }, + "harvestOffsetPosition": { + "x": 0, + "y": 0 + }, + "harvestRandom": true, + "harvestAllSpineRes": "spine/v2/shouge/Crop_sg_final", + "harvestAllOffsetPosition": "5:30;50:-30;150:-75;200:-25;150:-30", + "all_state_spine": "spine/v2/xiyouzhongzi/xiyouzhongzi", + "mature_effect": "effect/prefab/effect_plant_maturation", + "rare_plant_light_pos": "{\"x\":113,\"y\":125,\"rotation\":0,\"scale\":{\"x\":1,\"y\":1}}" + } +] \ No newline at end of file diff --git a/server/gameConfig/RoleLevel.json b/server/gameConfig/RoleLevel.json new file mode 100644 index 0000000..403a309 --- /dev/null +++ b/server/gameConfig/RoleLevel.json @@ -0,0 +1,802 @@ +[ + { + "level": 1, + "exp": 0 + }, + { + "level": 2, + "exp": 100 + }, + { + "level": 3, + "exp": 300 + }, + { + "level": 4, + "exp": 700 + }, + { + "level": 5, + "exp": 1300 + }, + { + "level": 6, + "exp": 2300 + }, + { + "level": 7, + "exp": 4000 + }, + { + "level": 8, + "exp": 6600 + }, + { + "level": 9, + "exp": 10100 + }, + { + "level": 10, + "exp": 14300 + }, + { + "level": 11, + "exp": 19300 + }, + { + "level": 12, + "exp": 25100 + }, + { + "level": 13, + "exp": 31800 + }, + { + "level": 14, + "exp": 39500 + }, + { + "level": 15, + "exp": 48300 + }, + { + "level": 16, + "exp": 58300 + }, + { + "level": 17, + "exp": 69500 + }, + { + "level": 18, + "exp": 82000 + }, + { + "level": 19, + "exp": 95900 + }, + { + "level": 20, + "exp": 111300 + }, + { + "level": 21, + "exp": 128300 + }, + { + "level": 22, + "exp": 146900 + }, + { + "level": 23, + "exp": 167200 + }, + { + "level": 24, + "exp": 189300 + }, + { + "level": 25, + "exp": 213300 + }, + { + "level": 26, + "exp": 239300 + }, + { + "level": 27, + "exp": 267300 + }, + { + "level": 28, + "exp": 297400 + }, + { + "level": 29, + "exp": 329700 + }, + { + "level": 30, + "exp": 364300 + }, + { + "level": 31, + "exp": 401300 + }, + { + "level": 32, + "exp": 440700 + }, + { + "level": 33, + "exp": 482600 + }, + { + "level": 34, + "exp": 527100 + }, + { + "level": 35, + "exp": 574300 + }, + { + "level": 36, + "exp": 624300 + }, + { + "level": 37, + "exp": 677100 + }, + { + "level": 38, + "exp": 732800 + }, + { + "level": 39, + "exp": 791500 + }, + { + "level": 40, + "exp": 853300 + }, + { + "level": 41, + "exp": 918300 + }, + { + "level": 42, + "exp": 986500 + }, + { + "level": 43, + "exp": 1058000 + }, + { + "level": 44, + "exp": 1132900 + }, + { + "level": 45, + "exp": 1211300 + }, + { + "level": 46, + "exp": 1293300 + }, + { + "level": 47, + "exp": 1378900 + }, + { + "level": 48, + "exp": 1468200 + }, + { + "level": 49, + "exp": 1561300 + }, + { + "level": 50, + "exp": 1658300 + }, + { + "level": 51, + "exp": 1759300 + }, + { + "level": 52, + "exp": 1864300 + }, + { + "level": 53, + "exp": 1973400 + }, + { + "level": 54, + "exp": 2086700 + }, + { + "level": 55, + "exp": 2204300 + }, + { + "level": 56, + "exp": 2326300 + }, + { + "level": 57, + "exp": 2452700 + }, + { + "level": 58, + "exp": 2583600 + }, + { + "level": 59, + "exp": 2719100 + }, + { + "level": 60, + "exp": 2859300 + }, + { + "level": 61, + "exp": 3004300 + }, + { + "level": 62, + "exp": 3154100 + }, + { + "level": 63, + "exp": 3308800 + }, + { + "level": 64, + "exp": 3468500 + }, + { + "level": 65, + "exp": 3633300 + }, + { + "level": 66, + "exp": 3803300 + }, + { + "level": 67, + "exp": 3978500 + }, + { + "level": 68, + "exp": 4159000 + }, + { + "level": 69, + "exp": 4344900 + }, + { + "level": 70, + "exp": 4536300 + }, + { + "level": 71, + "exp": 4733300 + }, + { + "level": 72, + "exp": 4935900 + }, + { + "level": 73, + "exp": 5144200 + }, + { + "level": 74, + "exp": 5358300 + }, + { + "level": 75, + "exp": 5578300 + }, + { + "level": 76, + "exp": 5804300 + }, + { + "level": 77, + "exp": 6036300 + }, + { + "level": 78, + "exp": 6274400 + }, + { + "level": 79, + "exp": 6518700 + }, + { + "level": 80, + "exp": 6769300 + }, + { + "level": 81, + "exp": 7026300 + }, + { + "level": 82, + "exp": 7289700 + }, + { + "level": 83, + "exp": 7559600 + }, + { + "level": 84, + "exp": 7836100 + }, + { + "level": 85, + "exp": 8119300 + }, + { + "level": 86, + "exp": 8409300 + }, + { + "level": 87, + "exp": 8706100 + }, + { + "level": 88, + "exp": 9009800 + }, + { + "level": 89, + "exp": 9320500 + }, + { + "level": 90, + "exp": 9638300 + }, + { + "level": 91, + "exp": 9963300 + }, + { + "level": 92, + "exp": 10295500 + }, + { + "level": 93, + "exp": 10635000 + }, + { + "level": 94, + "exp": 10981900 + }, + { + "level": 95, + "exp": 11336300 + }, + { + "level": 96, + "exp": 11698300 + }, + { + "level": 97, + "exp": 12067900 + }, + { + "level": 98, + "exp": 12445200 + }, + { + "level": 99, + "exp": 12830300 + }, + { + "level": 100, + "exp": 13223300 + }, + { + "level": 101, + "exp": 13624300 + }, + { + "level": 102, + "exp": 14185200 + }, + { + "level": 103, + "exp": 14760100 + }, + { + "level": 104, + "exp": 15349200 + }, + { + "level": 105, + "exp": 15952700 + }, + { + "level": 106, + "exp": 16570700 + }, + { + "level": 107, + "exp": 17203500 + }, + { + "level": 108, + "exp": 17851200 + }, + { + "level": 109, + "exp": 18513900 + }, + { + "level": 110, + "exp": 19191900 + }, + { + "level": 111, + "exp": 19885400 + }, + { + "level": 112, + "exp": 20594500 + }, + { + "level": 113, + "exp": 21319400 + }, + { + "level": 114, + "exp": 22060300 + }, + { + "level": 115, + "exp": 22817400 + }, + { + "level": 116, + "exp": 23590900 + }, + { + "level": 117, + "exp": 24381000 + }, + { + "level": 118, + "exp": 25187800 + }, + { + "level": 119, + "exp": 26011600 + }, + { + "level": 120, + "exp": 26852500 + }, + { + "level": 121, + "exp": 27710700 + }, + { + "level": 122, + "exp": 28586400 + }, + { + "level": 123, + "exp": 29479800 + }, + { + "level": 124, + "exp": 30391100 + }, + { + "level": 125, + "exp": 31320500 + }, + { + "level": 126, + "exp": 32268100 + }, + { + "level": 127, + "exp": 33234200 + }, + { + "level": 128, + "exp": 34218900 + }, + { + "level": 129, + "exp": 35222400 + }, + { + "level": 130, + "exp": 36245000 + }, + { + "level": 131, + "exp": 37286800 + }, + { + "level": 132, + "exp": 38348000 + }, + { + "level": 133, + "exp": 39428800 + }, + { + "level": 134, + "exp": 40529400 + }, + { + "level": 135, + "exp": 41650000 + }, + { + "level": 136, + "exp": 42790800 + }, + { + "level": 137, + "exp": 43952000 + }, + { + "level": 138, + "exp": 45133800 + }, + { + "level": 139, + "exp": 46336400 + }, + { + "level": 140, + "exp": 47559900 + }, + { + "level": 141, + "exp": 48804600 + }, + { + "level": 142, + "exp": 50070700 + }, + { + "level": 143, + "exp": 51358300 + }, + { + "level": 144, + "exp": 52667700 + }, + { + "level": 145, + "exp": 53999100 + }, + { + "level": 146, + "exp": 55352600 + }, + { + "level": 147, + "exp": 56728500 + }, + { + "level": 148, + "exp": 58127000 + }, + { + "level": 149, + "exp": 59548200 + }, + { + "level": 150, + "exp": 60992400 + }, + { + "level": 151, + "exp": 62459800 + }, + { + "level": 152, + "exp": 63950500 + }, + { + "level": 153, + "exp": 65464800 + }, + { + "level": 154, + "exp": 67002900 + }, + { + "level": 155, + "exp": 68564900 + }, + { + "level": 156, + "exp": 70151100 + }, + { + "level": 157, + "exp": 71761700 + }, + { + "level": 158, + "exp": 73396900 + }, + { + "level": 159, + "exp": 75056900 + }, + { + "level": 160, + "exp": 76741900 + }, + { + "level": 161, + "exp": 78452100 + }, + { + "level": 162, + "exp": 80187700 + }, + { + "level": 163, + "exp": 81948900 + }, + { + "level": 164, + "exp": 83735900 + }, + { + "level": 165, + "exp": 85548900 + }, + { + "level": 166, + "exp": 87388200 + }, + { + "level": 167, + "exp": 89253900 + }, + { + "level": 168, + "exp": 91146300 + }, + { + "level": 169, + "exp": 93065500 + }, + { + "level": 170, + "exp": 95011800 + }, + { + "level": 171, + "exp": 96985300 + }, + { + "level": 172, + "exp": 98986300 + }, + { + "level": 173, + "exp": 101015000 + }, + { + "level": 174, + "exp": 103071600 + }, + { + "level": 175, + "exp": 105156300 + }, + { + "level": 176, + "exp": 107269400 + }, + { + "level": 177, + "exp": 109411000 + }, + { + "level": 178, + "exp": 111581300 + }, + { + "level": 179, + "exp": 113780600 + }, + { + "level": 180, + "exp": 116009100 + }, + { + "level": 181, + "exp": 118267000 + }, + { + "level": 182, + "exp": 120554500 + }, + { + "level": 183, + "exp": 122871800 + }, + { + "level": 184, + "exp": 125219100 + }, + { + "level": 185, + "exp": 127596600 + }, + { + "level": 186, + "exp": 130004600 + }, + { + "level": 187, + "exp": 132443200 + }, + { + "level": 188, + "exp": 134912700 + }, + { + "level": 189, + "exp": 137413300 + }, + { + "level": 190, + "exp": 139945200 + }, + { + "level": 191, + "exp": 142508700 + }, + { + "level": 192, + "exp": 145103900 + }, + { + "level": 193, + "exp": 147731100 + }, + { + "level": 194, + "exp": 150390400 + }, + { + "level": 195, + "exp": 153082100 + }, + { + "level": 196, + "exp": 155806500 + }, + { + "level": 197, + "exp": 158563700 + }, + { + "level": 198, + "exp": 161353900 + }, + { + "level": 199, + "exp": 164177400 + }, + { + "level": 200, + "exp": 167034400 + } +] \ No newline at end of file diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..0921c83 --- /dev/null +++ b/server/index.js @@ -0,0 +1,945 @@ +const crypto = require('crypto'); +const { spawn } = require('child_process'); +const axios = require('axios'); +const express = require('express'); +const http = require('http'); +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '.env') }); +require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const updateRouter = require('./src/routes/update'); // 新增更新路由 +const { createClient } = require('redis'); +const { log } = require('./src/utils'); +const userManager = require('./src/userManager'); +const { MiniProgramLoginSession } = require('./src/qrlib/session'); + +// OAuth Dependencies +const session = require('express-session'); +const passport = require('passport'); +const OAuth2Strategy = require('passport-oauth2'); + +// 初始化 Express App +const app = express(); +const server = http.createServer(app); +const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379'; +const redisPublisher = createClient({ url: redisUrl }); +const redisSubscriber = createClient({ url: redisUrl }); +const rpcPending = new Map(); +let workerStarting = null; +let workerProcess = null; +let updateRunning = null; +const workerMode = (process.env.FARMBOT_WORKER_MODE || 'detached').toLowerCase(); +const adminWhitelistRaw = process.env.ADMIN_WHITELIST || ''; +const adminWhitelist = new Set( + adminWhitelistRaw.split(/[,;\s]+/).map(item => item.trim().toLowerCase()).filter(Boolean) +); +redisPublisher.on('error', (err) => { + log('Redis', `连接异常: ${err.message}`); +}); +redisPublisher.connect().then(() => { + log('Redis', 'Publisher 已连接'); +}).catch((err) => { + log('Redis', `连接失败: ${err.message}`); +}); +redisSubscriber.on('error', (err) => { + log('Redis', `订阅异常: ${err.message}`); +}); +redisSubscriber.connect().then(async () => { + log('Redis', 'Subscriber 已连接'); + await redisSubscriber.subscribe('bot:rpc:res', (message) => { + const data = safeParseJson(message); + const id = data && data.id; + if (!id) return; + const resolver = rpcPending.get(id); + if (!resolver) return; + rpcPending.delete(id); + resolver(data); + }); + await redisSubscriber.pSubscribe('bot-log-*', (message, channel) => { + const email = typeof channel === 'string' ? channel.slice('bot-log-'.length) : ''; + if (!email) return; + const data = safeParseJson(message); + userManager.saveBotLog(email, data); + }); + await redisSubscriber.pSubscribe('bot-steal-*', (message, channel) => { + const email = typeof channel === 'string' ? channel.slice('bot-steal-'.length) : ''; + if (!email) return; + const data = safeParseJson(message); + const name = data && data.name; + const count = Number(data && data.count); + userManager.saveUnlucky(email, name, count); + }); +}).catch((err) => { + log('Redis', `订阅连接失败: ${err.message}`); +}); + +function safeParseJson(input) { + try { + return JSON.parse(input); + } catch { + return {}; + } +} + +function isAdminEmail(email) { + if (!email) return false; + return adminWhitelist.has(String(email).toLowerCase()); +} + +function getRequestEmail(req) { + const headerEmail = req.headers['x-admin-email']; + if (headerEmail) return String(headerEmail); + if (req.query && req.query.email) return String(req.query.email); + if (req.body && req.body.email) return String(req.body.email); + return ''; +} + +function requireAdmin(req, res, next) { + const email = getRequestEmail(req); + if (!isAdminEmail(email)) { + return res.status(403).json({ error: 'Admin only' }); + } + req.adminEmail = email; + return next(); +} + +function startWorkerProcess() { + if (workerMode === 'external') { + log('Worker', '外部模式,跳过拉起'); + return; + } + if (workerProcess && !workerProcess.killed) return; + const detached = workerMode === 'detached'; + workerProcess = spawn(process.execPath, ['bot-worker.js'], { + cwd: __dirname, + stdio: detached ? 'ignore' : 'inherit', + detached, + windowsHide: true + }); + if (detached) { + workerProcess.unref(); + } else { + workerProcess.on('exit', (code, signal) => { + log('Worker', `Worker exited (code=${code}, signal=${signal || 'none'})`); + }); + } +} + +async function sendRpc(action, email, payload, timeoutMs = 6000) { + if (!redisPublisher.isOpen || !redisSubscriber.isOpen) { + return { ok: false, error: 'Redis 未就绪', status: 503 }; + } + const id = crypto.randomUUID(); + const body = JSON.stringify({ + id, + action, + email, + payload, + ts: Date.now() + }); + return new Promise((resolve) => { + const timer = setTimeout(() => { + rpcPending.delete(id); + resolve({ ok: false, error: 'Worker 响应超时', status: 504 }); + }, timeoutMs); + rpcPending.set(id, (response) => { + clearTimeout(timer); + resolve(response); + }); + redisPublisher.publish('bot:rpc:req', body).catch((err) => { + clearTimeout(timer); + rpcPending.delete(id); + resolve({ ok: false, error: err.message || 'RPC 发送失败', status: 503 }); + }); + }); +} + +async function ensureWorkerReady() { + if (workerStarting) return workerStarting; + workerStarting = (async () => { + const ping = await sendRpc('worker.ping', null, null, 800); + if (ping.ok) return true; + startWorkerProcess(); + await new Promise((resolve) => setTimeout(resolve, 500)); + const retry = await sendRpc('worker.ping', null, null, 1500); + if (!retry.ok) { + return { ok: false, error: retry.error || 'Worker 未就绪' }; + } + return true; + })(); + try { + return await workerStarting; + } finally { + workerStarting = null; + } +} + +async function callWorker(action, email, payload, timeoutMs) { + try { + const ready = await ensureWorkerReady(); + if (ready !== true) { + return { ok: false, error: ready && ready.error ? ready.error : 'Worker 未就绪', status: 503 }; + } + return sendRpc(action, email, payload, timeoutMs); + } catch (err) { + return { ok: false, error: err && err.message ? err.message : 'Worker 未就绪', status: 503 }; + } +} + +function runGit(args, cwd) { + return new Promise((resolve) => { + const child = spawn('git', args, { cwd, windowsHide: true }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + child.on('error', (err) => { + resolve({ code: 1, stdout: '', stderr: err && err.message ? err.message : 'spawn failed' }); + }); + child.on('close', (code) => { + resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() }); + }); + }); +} + +async function fetchRemoteUpdateSummary() { + const repo = 'Tony/Tieldalaes'; + const base = 'https://gitea.infras.host/api/v1'; + const commitsUrl = `${base}/repos/${repo}/commits?limit=5`; + const releasesUrl = `${base}/repos/${repo}/releases?limit=5`; + const [commitsRes, releasesRes] = await Promise.allSettled([ + axios.get(commitsUrl, { timeout: 6000 }), + axios.get(releasesUrl, { timeout: 6000 }) + ]); + const commits = commitsRes.status === 'fulfilled' && Array.isArray(commitsRes.value.data) + ? commitsRes.value.data.map((item) => ({ + sha: String(item?.sha || '').slice(0, 7), + message: String(item?.commit?.message || '').split('\n')[0], + author: String(item?.commit?.author?.name || ''), + date: String(item?.commit?.author?.date || '') + })) + : []; + const releases = releasesRes.status === 'fulfilled' && Array.isArray(releasesRes.value.data) + ? releasesRes.value.data.map((item) => ({ + tag: String(item?.tag_name || ''), + name: String(item?.name || ''), + date: String(item?.published_at || item?.created_at || ''), + body: String(item?.body || '') + })) + : []; + return { commits, releases }; +} + +const oauthEnabled = Boolean(process.env.OAUTH_AUTHENTIK_ISSUER) + && Boolean(process.env.OAUTH_AUTHENTIK_CLIENT_ID) + && Boolean(process.env.OAUTH_AUTHENTIK_CLIENT_SECRET) + && Boolean(process.env.OAUTH_AUTHENTIK_REDIRECT_URI) + && Boolean(process.env.OAUTH_AUTHENTIK_SCOPE); + +// OAuth Setup +if (oauthEnabled) { + app.use(session({ + secret: 'farm-session-secret', + resave: false, + saveUninitialized: false + })); + app.use(passport.initialize()); + app.use(passport.session()); + + passport.serializeUser((user, done) => done(null, user.email)); + passport.deserializeUser((email, done) => { + const user = userManager.getUser(email); + done(null, user); + }); + + passport.use('authentik', new OAuth2Strategy({ + authorizationURL: `${process.env.OAUTH_AUTHENTIK_ISSUER}authorize/`, + tokenURL: `${process.env.OAUTH_AUTHENTIK_ISSUER}token/`, + clientID: process.env.OAUTH_AUTHENTIK_CLIENT_ID, + clientSecret: process.env.OAUTH_AUTHENTIK_CLIENT_SECRET, + callbackURL: process.env.OAUTH_AUTHENTIK_REDIRECT_URI, + scope: process.env.OAUTH_AUTHENTIK_SCOPE.split(' ') + }, async (accessToken, refreshToken, profile, cb) => { + try { + // Fetch user info using accessToken + const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); + const userInfoRes = await fetch(`${process.env.OAUTH_AUTHENTIK_ISSUER}userinfo/`, { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + const userInfo = await userInfoRes.json(); + + const email = userInfo.email || userInfo.preferred_username; // Adjust based on Authentik response + + // Register or Login + const user = await userManager.register(email, null, 'authentik', userInfo.sub); + return cb(null, user); + } catch (err) { + return cb(err); + } + })); + + // OAuth Routes + app.get('/api/auth/oauth/authentik', passport.authenticate('authentik', { state: true })); + + app.get('/api/auth/oauth/authentik/callback', (req, res, next) => { + passport.authenticate('authentik', { state: true }, (err, user, info) => { + const frontendUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:5173' : ''; + + if (err) { + console.error('OAuth Error:', err); + return res.redirect(`${frontendUrl}/login?error=oauth_failed`); + } + if (!user) { + console.error('OAuth Failed: No user returned'); + return res.redirect(`${frontendUrl}/login?error=oauth_failed`); + } + + req.logIn(user, (loginErr) => { + if (loginErr) { + console.error('Login Error:', loginErr); + return res.redirect(`${frontendUrl}/login?error=login_err`); + } + res.redirect(`${frontendUrl}/login?email=${encodeURIComponent(user.email)}&oauth=success&_t=${Date.now()}`); + }); + })(req, res, next); + }); +} + +// 中间件 +app.use(cors()); +app.use(bodyParser.json()); +if (process.env.NODE_ENV === 'production') { + app.use(express.static(path.join(__dirname, '../web/dist'))); +} + +app.use('/api/update', updateRouter); // 注册更新路由 + +app.get('/api/auth/oauth/status', (req, res) => { + res.json({ enabled: oauthEnabled }); +}); + + +function normalizeSettings(settings) { + if (!settings || typeof settings !== 'object') return {}; + const result = {}; + const farmIntervalSec = Number(settings.farmIntervalSec); + if (Number.isFinite(farmIntervalSec) && farmIntervalSec >= 1) { + result.farmCheckInterval = Math.floor(farmIntervalSec * 1000); + } + const friendIntervalSec = Number(settings.friendIntervalSec); + if (Number.isFinite(friendIntervalSec) && friendIntervalSec >= 1) { + result.friendCheckInterval = Math.floor(friendIntervalSec * 1000); + } + if (settings.seedStrategy === 'forceLowest') { + result.forceLowestLevelCrop = true; + } else if (settings.seedStrategy === 'default') { + result.forceLowestLevelCrop = false; + } + if (typeof settings.enableFriendOps === 'boolean') { + result.enableFriendOps = settings.enableFriendOps; + } + if (typeof settings.enableSteal === 'boolean') { + result.enableSteal = settings.enableSteal; + } + if (typeof settings.enableNormalFertilize === 'boolean') { + result.enableNormalFertilize = settings.enableNormalFertilize; + } + if (typeof settings.enableOrganicFertilize === 'boolean') { + result.enableOrganicFertilize = settings.enableOrganicFertilize; + } + if (typeof settings.allowTicketFertilizerPurchase === 'boolean') { + result.allowTicketFertilizerPurchase = settings.allowTicketFertilizerPurchase; + } + if (typeof settings.enableAutoSell === 'boolean') { + result.enableAutoSell = settings.enableAutoSell; + } + if (typeof settings.allowBuySeeds === 'boolean') { + result.allowBuySeeds = settings.allowBuySeeds; + } + if (typeof settings.allowRemove === 'boolean') { + result.allowRemove = settings.allowRemove; + } + if (settings.idleStrategy === 'task') { + result.idleStrategy = 'task'; + } else if (settings.idleStrategy === 'exp') { + result.idleStrategy = 'exp'; + } + return result; +} + +async function handleRpcResponse(res, response, fallback = {}) { + if (!response || response.ok !== true) { + const status = response && response.status ? response.status : 500; + const error = response && response.error ? response.error : 'Worker 错误'; + res.status(status).json({ error }); + return false; + } + res.json(response.data ?? fallback); + return true; +} + +function getTodayKey() { + const now = new Date(); + const y = now.getFullYear(); + const m = `${now.getMonth() + 1}`.padStart(2, '0'); + const d = `${now.getDate()}`.padStart(2, '0'); + return `${y}-${m}-${d}`; +} + +const qrLogState = new Map(); +function logQrOnce(message, minIntervalMs = 20000) { + const now = Date.now(); + const lastAt = qrLogState.get(message) || 0; + if (now - lastAt < minIntervalMs) return; + qrLogState.set(message, now); + log('扫码', message); +} + +// ============ API 路由 ============ + +// QR Code Login API (Mini Program) +app.post('/api/qr/create', async (req, res) => { + try { + logQrOnce('正在获取登录二维码...'); + const result = await MiniProgramLoginSession.requestLoginCode(); + logQrOnce('已获取登录二维码'); + res.json({ + success: true, + qrsig: result.code, + qrcode: `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(result.url)}`, + url: result.url + }); + } catch (error) { + console.error('QR Create Error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +app.post('/api/qr/check', async (req, res) => { + const { qrsig } = req.body; + if (!qrsig) { + return res.status(400).json({ success: false, message: 'Missing qrsig/code' }); + } + + try { + const result = await MiniProgramLoginSession.queryStatus(qrsig); + + if (result.status === 'Wait') { + res.json({ success: true, status: 'Wait', msg: '等待扫码...' }); + } else if (result.status === 'Used') { + res.json({ success: true, status: 'Used', msg: '二维码已失效' }); + } else if (result.status === 'OK') { + const ticket = result.ticket; + // Determine AppID (Farm AppID) + const appid = MiniProgramLoginSession.Presets.farm.appid; // QQ Classic Farm + const code = await MiniProgramLoginSession.getAuthCode(ticket, appid); + + res.json({ + success: true, + status: 'OK', + msg: '登录成功', + code: code // This is the Game Code we need + }); + } else { + res.json({ success: true, status: 'Error', msg: '状态查询错误' }); + } + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +// 注册 API +app.post('/api/auth/register', async (req, res) => { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: '请输入邮箱和密码' }); + } + + try { + if (userManager.getUser(email)) { + return res.status(400).json({ error: '该邮箱已被注册' }); + } + + const user = userManager.register(email, password); + log('Auth', `New user registered: ${email}`); + + res.json({ + success: true, + user: { + email: user.email, + hasCode: !!user.code + } + }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// 登录 API (仅登录) +app.post('/api/auth/login', async (req, res) => { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: '请输入邮箱和密码' }); + } + + try { + const user = userManager.login(email, password); + if (!user) { + return res.status(401).json({ error: '邮箱或密码错误' }); + } + + const rpcRes = await callWorker('bot.isRunning', email, null, 2000); + const botRunning = rpcRes && rpcRes.ok && rpcRes.data && rpcRes.data.running === true; + res.json({ + success: true, + user: { + email: user.email, + code: user.code, + hasCode: !!user.code + }, + botRunning + }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +app.get('/api/auth/profile', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const user = userManager.getUser(email); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json({ + user: { + email: user.email, + hasCode: !!user.code, + auth_provider: user.auth_provider || 'local' + } + }); +}); + +// 好友 API +app.get('/api/friends', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('friends.list', email, null); + await handleRpcResponse(res, response, { friends: [] }); +}); + +// 访问好友农场 API +app.post('/api/friends/visit', async (req, res) => { + const { email, friendUid } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('friends.visit', email, { friendUid }); + await handleRpcResponse(res, response, { lands: [] }); +}); + +// 好友互动 API (偷菜/浇水等) +app.post('/api/friends/action', async (req, res) => { + const { email, friendUid, actionType, landIds } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('friends.action', email, { friendUid, actionType, landIds }); + await handleRpcResponse(res, response, { success: true }); +}); + +// 启动 Bot API +app.post('/api/bot/start', async (req, res) => { + const { email, code, settings } = req.body; // 如果提供了 code,更新并启动 + + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + + const user = userManager.getUser(email); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + // 如果提供了新 code,更新用户数据 + let gameCode = typeof code === 'string' ? code.trim() : code; + if (gameCode) { + userManager.updateCode(email, gameCode); + } else { + gameCode = typeof user.code === 'string' ? user.code.trim() : user.code; + } + + if (!gameCode) { + return res.status(400).json({ error: 'Game code is required. Please provide it.' }); + } + + try { + const botConfig = { code: gameCode, ...normalizeSettings(settings) }; + const response = await callWorker('bot.start', email, botConfig, 12000); + await handleRpcResponse(res, response, { success: true, message: 'Bot started successfully.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 停止 Bot API +app.post('/api/bot/stop', (req, res) => { + const { email } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + callWorker('bot.stop', email, null).then((response) => { + handleRpcResponse(res, response, { success: true }); + }).catch((err) => { + res.status(500).json({ error: err.message }); + }); +}); + +app.post('/api/bot/settings', async (req, res) => { + const { email, settings } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const user = userManager.getUser(email); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + try { + const botConfig = normalizeSettings(settings); + const response = await callWorker('bot.updateConfig', email, { settings: botConfig }, 8000); + await handleRpcResponse(res, response, { success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 获取状态 API +app.get('/api/status', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('bot.status', email, null, 5000); + if (!response || response.ok !== true) { + return res.json({ status: 'stopped', user: null }); + } + res.json(response.data); +}); + +app.get('/api/announcement', (req, res) => { + const message = typeof process.env.LOGIN_ANNOUNCEMENT === 'string' ? process.env.LOGIN_ANNOUNCEMENT : ''; + res.json({ message }); +}); + +app.get('/api/logs', async (req, res) => { + const { email, limit } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const count = Number(limit); + try { + const logs = await userManager.getBotLogs(email, count); + res.json({ logs }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/admin/me', (req, res) => { + const { email } = req.query; + res.json({ isAdmin: isAdminEmail(email) }); +}); + +app.get('/api/admin/logs', requireAdmin, async (req, res) => { + const { email, limit } = req.query; + const count = Number(limit); + try { + const logs = await userManager.getAllBotLogs(count, email); + res.json({ logs }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/admin/worker/stop-all', requireAdmin, async (req, res) => { + const response = await callWorker('worker.stopAll', null, null, 5000); + await handleRpcResponse(res, response, { success: true }); +}); + +app.post('/api/admin/worker/shutdown', requireAdmin, async (req, res) => { + const response = await callWorker('worker.shutdown', null, null, 5000); + await handleRpcResponse(res, response, { success: true }); +}); + +app.post('/api/admin/worker/start', requireAdmin, async (req, res) => { + const ready = await ensureWorkerReady(); + if (ready === true) { + return res.json({ success: true }); + } + res.status(503).json({ error: ready && ready.error ? ready.error : 'Worker not ready' }); +}); + +app.get('/api/admin/update/summary', requireAdmin, async (req, res) => { + const repoRoot = path.join(__dirname, '..'); + try { + const [localHeadRes, remoteHeadRes, branchRes, remoteSummary] = await Promise.all([ + runGit(['rev-parse', 'HEAD'], repoRoot), + runGit(['ls-remote', 'origin', '-h', 'refs/heads/main'], repoRoot), + runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot), + fetchRemoteUpdateSummary() + ]); + const localHead = localHeadRes.stdout || ''; + const remoteLine = remoteHeadRes.stdout || ''; + const remoteHead = remoteLine.split('\t')[0] || ''; + const branch = branchRes.stdout || ''; + res.json({ + local: { head: localHead, branch }, + remote: { head: remoteHead }, + updateNeeded: Boolean(localHead && remoteHead && localHead !== remoteHead), + commits: remoteSummary.commits, + releases: remoteSummary.releases, + repo: 'https://gitea.infras.host/Tony/Tieldalaes' + }); + } catch (error) { + res.status(500).json({ error: error && error.message ? error.message : '更新信息获取失败' }); + } +}); + +app.post('/api/admin/update/run', requireAdmin, async (req, res) => { + if (updateRunning) { + return res.status(409).json({ error: '更新进行中' }); + } + const repoRoot = path.join(__dirname, '..'); + updateRunning = (async () => { + const before = await runGit(['rev-parse', 'HEAD'], repoRoot); + const pull = await runGit(['pull', '--rebase', 'origin', 'main'], repoRoot); + const after = await runGit(['rev-parse', 'HEAD'], repoRoot); + return { + ok: pull.code === 0, + before: before.stdout || '', + after: after.stdout || '', + output: pull.stdout || '', + error: pull.stderr || '' + }; + })(); + try { + const result = await updateRunning; + if (!result.ok) { + return res.status(500).json({ error: result.error || '更新失败', output: result.output, before: result.before, after: result.after }); + } + res.json({ success: true, output: result.output, before: result.before, after: result.after }); + } finally { + updateRunning = null; + } +}); + +app.get('/api/debug/fertilizer', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('debug.fertilizer', email, null, 12000); + await handleRpcResponse(res, response, {}); +}); + +app.get('/api/leaderboard/unlucky', (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + userManager.getUnluckyTop(email, 10).then((items) => { + res.json({ items }); + }).catch((err) => { + res.status(500).json({ error: err.message }); + }); +}); + +app.get('/api/tasks', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('tasks.list', email, null); + await handleRpcResponse(res, response, { tasks: [] }); +}); + +app.post('/api/tasks/claim', async (req, res) => { + const { email } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('tasks.claimAll', email, null, 12000); + await handleRpcResponse(res, response, { claimedCount: 0, failed: [], tasks: [] }); +}); + +app.post('/api/tasks/claim-one', async (req, res) => { + const { email, taskId, doShared } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const id = Number(taskId); + if (!Number.isFinite(id) || id <= 0) { + return res.status(400).json({ error: 'Invalid taskId' }); + } + const response = await callWorker('tasks.claimOne', email, { taskId: id, doShared: Boolean(doShared) }, 12000); + await handleRpcResponse(res, response, { claimedCount: 0, failedCount: 0, tasks: [] }); +}); + +// Get lands API +app.get('/api/lands', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('lands.list', email, null); + await handleRpcResponse(res, response, { lands: [] }); +}); + +app.post('/api/lands/remove', async (req, res) => { + const { email, landIds } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + if (!Array.isArray(landIds)) { + return res.status(400).json({ error: 'Invalid landIds' }); + } + const ids = landIds.map(Number).filter(id => Number.isFinite(id) && id > 0); + if (ids.length === 0) { + return res.status(400).json({ error: 'Invalid landIds' }); + } + const response = await callWorker('lands.remove', email, { landIds: ids }); + await handleRpcResponse(res, response, { success: true, removed: ids.length }); +}); + +app.post('/api/lands/unlock', async (req, res) => { + const { email, landId, doShared } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const id = Number(landId); + if (!Number.isFinite(id) || id <= 0) { + return res.status(400).json({ error: 'Invalid landId' }); + } + const response = await callWorker('lands.unlock', email, { landId: id, doShared: Boolean(doShared) }, 12000); + await handleRpcResponse(res, response, { success: true, landId: id }); +}); + +// Shop API +app.get('/api/shop', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('shop.list', email, null); + await handleRpcResponse(res, response, { goods: [] }); +}); + +app.post('/api/shop/buy', async (req, res) => { + const { email, goodsId, count, price } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('shop.buy', email, { goodsId, count, price }, 12000); + await handleRpcResponse(res, response, { success: true }); +}); + +app.post('/api/paymall/buy', async (req, res) => { + const { email, itemId, count } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const id = Number(itemId); + if (!Number.isFinite(id) || id <= 0) { + return res.status(400).json({ error: 'Invalid itemId' }); + } + const buyCount = Number(count ?? 1); + if (!Number.isFinite(buyCount) || buyCount <= 0) { + return res.status(400).json({ error: 'Invalid count' }); + } + const response = await callWorker('shop.paymallBuy', email, { itemId: id, count: buyCount }, 12000); + await handleRpcResponse(res, response, { success: true }); +}); + +// Warehouse API +app.get('/api/warehouse', async (req, res) => { + const { email } = req.query; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('warehouse.list', email, null); + await handleRpcResponse(res, response, {}); +}); + +app.post('/api/warehouse/sell', async (req, res) => { + const { email, items } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('warehouse.sell', email, { items }); + await handleRpcResponse(res, response, { success: true, gold: 0 }); +}); + +app.post('/api/warehouse/use', async (req, res) => { + const { email, items } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + const response = await callWorker('warehouse.use', email, { items }, 12000); + await handleRpcResponse(res, response, {}); +}); + +// System stats API +app.get('/api/system/stats', (req, res) => { + const memory = process.memoryUsage(); + // Convert to MB + const memoryUsage = { + rss: Math.round(memory.rss / 1024 / 1024), + heapTotal: Math.round(memory.heapTotal / 1024 / 1024), + heapUsed: Math.round(memory.heapUsed / 1024 / 1024), + }; + callWorker('worker.stats', null, null, 2000).then((response) => { + const workerData = response && response.ok && response.data ? response.data : null; + const activeBots = workerData && Number.isFinite(workerData.activeBots) ? workerData.activeBots : 0; + res.json({ + memory: memoryUsage, + uptime: Math.floor(process.uptime()), + activeBots, + worker: workerData ? { + memory: workerData.memory || null, + uptime: workerData.uptime || 0, + activeBots + } : null + }); + }).catch(() => { + res.json({ + memory: memoryUsage, + uptime: Math.floor(process.uptime()), + activeBots: 0, + worker: null + }); + }); +}); + +// 所有其他请求返回前端 index.html (SPA 支持) +app.get('*', (req, res) => { + if (process.env.NODE_ENV === 'production') { + res.sendFile(path.join(__dirname, '../web/dist/index.html')); + } else { + res.status(404).send('Not Found (Development Mode: Use Frontend Dev Server at port 5173)'); + } +}); + +// 启动服务器 +const PORT = process.env.PORT || 3000; +server.listen(PORT, () => { + log('Server', `Server running on http://localhost:${PORT}`); +}); diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..3032c92 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,3480 @@ +{ + "name": "farm", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "farm", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.5.16", + "axios": "^1.13.5", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^4.18.2", + "express-session": "^1.19.0", + "jsonwebtoken": "^9.0.3", + "long": "^5.3.2", + "mysql2": "^3.17.1", + "node-fetch": "^3.3.2", + "passport": "^0.7.0", + "passport-oauth2": "^1.8.0", + "protobufjs": "^8.0.0", + "redis": "^4.7.0", + "simple-git": "^3.31.1", + "socket.io": "^4.8.3", + "sqlite3": "^5.1.7", + "ws": "^8.19.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "peer": true, + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.2.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "license": "MIT", + "dependencies": { + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "~5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.1.tgz", + "integrity": "sha512-UzIzdVwPXPoZm+FaJ4lNsRt28HtUwt68gpLH7NP1oSjd91M5Qn1XJzbIsSRMRc5CV3pvktLNshmbaFfMYqPBhQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.3", + "named-placeholders": "^1.1.6", + "seq-queue": "^0.0.5", + "sql-escaper": "^1.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "license": "MIT" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-8.0.0.tgz", + "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-git": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.31.1.tgz", + "integrity": "sha512-oiWP4Q9+kO8q9hHqkX35uuHmxiEbZNTrZ5IPxgMGrJwN76pzjm/jabkZO0ItEcqxAincqGAzL3QHSaHt4+knBg==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.2.tgz", + "integrity": "sha512-lp+ZDVfSjHt+qAK1jXBTIXBNYnbo7gnaAGwoYTH9bE89kNkXwcu6g0WjJGRsdTKVpY1z70u3Y0IgmnBOoRybHw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..21e7c7c --- /dev/null +++ b/server/package.json @@ -0,0 +1,33 @@ +{ + "name": "farm", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "adm-zip": "^0.5.16", + "axios": "^1.13.5", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^4.18.2", + "express-session": "^1.19.0", + "jsonwebtoken": "^9.0.3", + "long": "^5.3.2", + "mysql2": "^3.17.1", + "node-fetch": "^3.3.2", + "passport": "^0.7.0", + "passport-oauth2": "^1.8.0", + "protobufjs": "^8.0.0", + "redis": "^4.7.0", + "simple-git": "^3.31.1", + "socket.io": "^4.8.3", + "sqlite3": "^5.1.7", + "ws": "^8.19.0" + } +} diff --git a/server/proto/corepb.proto b/server/proto/corepb.proto new file mode 100644 index 0000000..2ec2b42 --- /dev/null +++ b/server/proto/corepb.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package corepb; + +// ============ 通用物品 ============ +message Item { + int64 id = 1; // 物品ID + int64 count = 2; // 数量 + int64 expire_time = 3; // 过期时间 + // field 4 reserved + // google.protobuf.Any detail = 5; // 详情 (略) + int64 uid = 6; // UID + bool is_new = 7; // 是否新获得 + repeated int64 mutant_types = 8; // 变异类型 + // ItemShow show = 100; // 展示信息 (略) +} + +// 背包(物品列表) +message ItemBag { + repeated Item items = 1; +} + +// ============ 物品变化 ============ +message ItemChg { + Item item = 1; // 物品信息 + int64 delta = 2; // 变化量 (正数增加, 负数减少) +} diff --git a/server/proto/friendpb.proto b/server/proto/friendpb.proto new file mode 100644 index 0000000..365e307 --- /dev/null +++ b/server/proto/friendpb.proto @@ -0,0 +1,120 @@ +syntax = "proto3"; + +package gamepb.friendpb; + +// ============ 好友农场摘要信息 ============ +message Plant { + int64 dry_time_sec = 1; // 缺水倒计时秒 + int64 weed_time_sec = 2; // 长草倒计时秒 + int64 insect_time_sec = 3; // 生虫倒计时秒 + int64 ripe_time_sec = 4; // 成熟倒计时秒 + int64 ripe_fruit_id = 5; // 成熟果实ID + int64 steal_plant_num = 6; // 可偷数量 + int64 dry_num = 7; // 缺水地块数 + int64 weed_num = 8; // 有草地块数 + int64 insect_num = 9; // 有虫地块数 +} + +// ============ 标签 ============ +message Tags { + bool is_new = 1; + bool is_follow = 2; +} + +// ============ 好友信息 ============ +message GameFriend { + int64 gid = 1; + string open_id = 2; + string name = 3; + string avatar_url = 4; + string remark = 5; + int64 level = 6; + int64 gold = 7; + Tags tags = 8; + Plant plant = 9; + int32 authorized_status = 10; + // field 11 reserved + // Illustrated illustrated = 12; // 略 + // repeated AvatarFrame equip_avatar_frames = 13; // 略 +} + +// ============ 请求/回复 ============ + +// --- 获取所有好友 --- +message GetAllRequest {} + +message GetAllReply { + repeated GameFriend game_friends = 1; + // repeated Invitation invitations = 2; // 略 + int64 application_count = 3; +} + +// --- 同步好友 (带 open_ids) --- +message SyncAllRequest { + // field 1 reserved + repeated string open_ids = 2; +} + +message SyncAllReply { + repeated GameFriend game_friends = 1; + // repeated Invitation invitations = 2; // 略 + int64 application_count = 3; +} + +// ============ 好友申请 (微信同玩) ============ + +// 申请信息 +message Application { + int64 gid = 1; + int64 time_at = 2; + string open_id = 3; + string name = 4; + string avatar_url = 5; + int64 level = 6; + // repeated AvatarFrame equip_avatar_frames = 7; // 略 +} + +// --- 获取好友申请列表 --- +message GetApplicationsRequest {} + +message GetApplicationsReply { + repeated Application applications = 1; + bool block_applications = 2; // 是否屏蔽申请 +} + +// --- 同意好友申请 --- +message AcceptFriendsRequest { + repeated int64 friend_gids = 1; +} + +message AcceptFriendsReply { + repeated GameFriend friends = 1; +} + +// --- 拒绝好友申请 --- +message RejectFriendsRequest { + repeated int64 friend_gids = 1; +} + +message RejectFriendsReply {} + +// --- 设置屏蔽申请 --- +message SetBlockApplicationsRequest { + bool block = 1; +} + +message SetBlockApplicationsReply { + bool block = 1; +} + +// ============ 服务器推送通知 ============ + +// 收到好友申请通知 +message FriendApplicationReceivedNotify { + repeated Application applications = 1; +} + +// 好友添加成功通知 (对方同意后) +message FriendAddedNotify { + repeated GameFriend friends = 1; +} diff --git a/server/proto/game.proto b/server/proto/game.proto new file mode 100644 index 0000000..a0e75fe --- /dev/null +++ b/server/proto/game.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +// ============ gatepb ============ +// 网关层协议 - 所有WS消息的外壳 + +package gatepb; + +// Meta.Type 枚举 +enum MessageType { + None = 0; + Request = 1; + Response = 2; + Notify = 3; +} + +// 消息元信息 +message Meta { + string service_name = 1; + string method_name = 2; + int32 message_type = 3; // MessageType enum + int64 client_seq = 4; + int64 server_seq = 5; + int64 error_code = 6; + string error_message = 7; + map metadata = 8; +} + +// 每个WS帧的结构 +message Message { + Meta meta = 1; + bytes body = 2; +} + +// 服务器推送事件 +message EventMessage { + string message_type = 1; + bytes body = 2; +} + +// 被踢下线通知 +message KickoutNotify { + int64 reason = 1; + string reason_message = 2; +} diff --git a/server/proto/itempb.proto b/server/proto/itempb.proto new file mode 100644 index 0000000..5446399 --- /dev/null +++ b/server/proto/itempb.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +package gamepb.itempb; + +import "corepb.proto"; + +// ============ 背包/仓库 ============ + +// 获取背包 +message BagRequest {} + +message BagReply { + corepb.ItemBag item_bag = 1; // 与 game 一致,背包数据在 item_bag 里 +} + +// ============ 出售物品 ============ + +message SellRequest { + repeated corepb.Item items = 1; // 要出售的物品列表 (id + count) +} + +message SellReply { + repeated corepb.Item sell_items = 1; // 出售的物品 + repeated corepb.Item get_items = 2; // 获得的物品(id=1001为金币, id=1002为点券) +} + +// ============ 使用物品 ============ + +message UseRequest { + int64 item_id = 1; + int64 count = 2; + repeated int64 land_ids = 3; +} + +message UseReply { + repeated corepb.Item items = 1; +} + +// ============ 批量使用物品 ============ + +message UseItem { + int64 item_id = 1; + int64 count = 2; + int64 land_count = 6; // 根据抓包,字段6为18(推测为土地数) +} + +message BatchUseRequest { + repeated UseItem items = 1; +} + +message BatchUseReply { + repeated corepb.Item items = 1; +} diff --git a/server/proto/notifypb.proto b/server/proto/notifypb.proto new file mode 100644 index 0000000..a23ceb6 --- /dev/null +++ b/server/proto/notifypb.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package gamepb.itempb; + +import "corepb.proto"; + +// ============ 物品变化通知 ============ +message ItemNotify { + repeated corepb.ItemChg items = 1; +} diff --git a/server/proto/plantpb.proto b/server/proto/plantpb.proto new file mode 100644 index 0000000..516fb72 --- /dev/null +++ b/server/proto/plantpb.proto @@ -0,0 +1,266 @@ +syntax = "proto3"; + +package gamepb.plantpb; + +// ============ 生长阶段枚举 ============ +enum PlantPhase { + PHASE_UNKNOWN = 0; + SEED = 1; // 种子 + GERMINATION = 2; // 发芽 + SMALL_LEAVES = 3; // 小叶 + LARGE_LEAVES = 4; // 大叶 + BLOOMING = 5; // 开花 + MATURE = 6; // 成熟 (可收获) + DEAD = 7; // 枯死 +} + +// ============ 土地信息 ============ +message LandInfo { + int64 id = 1; + bool unlocked = 2; + int64 level = 3; + int64 max_level = 4; + bool could_unlock = 5; + bool could_upgrade = 6; + LandUnlockCondition unlock_condition = 7; + LandUpgradeCondition upgrade_condition = 8; + Buff buff = 9; + PlantInfo plant = 10; + bool is_shared = 11; + bool can_share = 12; + int64 master_land_id = 13; + repeated int64 slave_land_ids = 14; + int64 land_size = 15; + int64 lands_level = 16; + + // 土地buff + message Buff { + int64 plant_yield_bonus = 1; + int64 planting_time_reduction = 2; + int64 plant_exp_bonus = 3; + } +} + +// 土地解锁条件 (简化) +message LandUnlockCondition { + int64 need_level = 1; + int64 need_gold = 2; + // ... 其他条件字段 +} + +// 土地升级条件 (简化) +message LandUpgradeCondition { + int64 need_level = 1; + int64 need_gold = 2; + // ... 其他条件字段 +} + +// ============ 植物信息 ============ +message PlantInfo { + int64 id = 1; // 植物/种子ID + string name = 2; // 名称 + // field 3 reserved + repeated PlantPhaseInfo phases = 4; // 生长阶段列表 + int64 season = 5; // 季节 + int64 dry_num = 6; // 缺水次数 (>0需要浇水) + // fields 7,8 reserved + int64 stole_num = 9; // 被偷次数 + int64 fruit_id = 10; // 果实ID + int64 fruit_num = 11; // 果实数量 + repeated int64 weed_owners = 12; // 放草的人 (非空=有草) + repeated int64 insect_owners = 13; // 放虫的人 (非空=有虫) + repeated int64 stealers = 14; // 偷菜的人 + int64 grow_sec = 15; // 生长秒数 + bool stealable = 16; // 是否可偷 + int64 left_inorc_fert_times = 17; // 剩余施肥次数 + int64 left_fruit_num = 18; // 剩余果实数 + int64 steal_intimacy_level = 19; // 偷菜亲密度等级 + repeated int64 mutant_config_ids = 20; // 变异配置ID + bool is_nudged = 21; // 是否被催熟 +} + +// ============ 生长阶段详情 ============ +message PlantPhaseInfo { + int32 phase = 1; // PlantPhase 枚举值 + int64 begin_time = 2; // 阶段开始时间 + int64 phase_id = 3; // 阶段ID + // fields 4,5 reserved + int64 dry_time = 6; // 变干时间 (>0 需要浇水) + int64 weeds_time = 7; // 长草时间 (>0 有杂草) + int64 insect_time = 8; // 生虫时间 (>0 有虫害) + map ferts_used = 9; // 已用肥料 + repeated MutantInfo mutants = 10; // 变异信息 +} + +// 变异信息 +message MutantInfo { + int64 mutant_time = 1; + int64 mutant_config_id = 2; + int64 weather_id = 3; +} + +// ============ 操作限制 ============ +// 每种操作的每日限制信息 +message OperationLimit { + int64 id = 1; // 操作类型ID + int64 day_times = 2; // 今日已操作次数 + int64 day_times_lt = 3; // 每日操作上限 + int64 day_share_id = 4; // 分享ID + int64 day_exp_times = 5; // 今日已获得经验次数 + int64 day_ex_times_lt = 6; // 每日可获得经验上限 + int64 day_exp_share_id = 7; // 经验分享ID +} +// 操作类型ID: +// 10001 = 帮好友浇水 +// 10002 = 帮好友除虫 +// 10003 = 帮好友除草 +// 10004 = 偷菜 +// 10005 = 放虫 +// 10006 = 放草 + +// ============ 请求/回复消息 ============ + +// --- 获取所有土地 --- +message AllLandsRequest { + int64 host_gid = 1; // 0或自己的GID=查看自己 +} + +message AllLandsReply { + repeated LandInfo lands = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 收获 --- +message HarvestRequest { + repeated int64 land_ids = 1; // 要收获的土地ID列表 + int64 host_gid = 2; // 农场主GID + bool is_all = 3; // 是否全部收获 +} + +message HarvestReply { + repeated LandInfo land = 1; + // repeated Item items = 2; // corepb.Item + // repeated Item lost_items = 3; + repeated OperationLimit operation_limits = 4; +} + +// --- 浇水 --- +message WaterLandRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message WaterLandReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 除草 --- +message WeedOutRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message WeedOutReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 除虫 --- +message InsecticideRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message InsecticideReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 播种 --- +message PlantItem { + int64 seed_id = 1; // 种子ID + repeated int64 land_ids = 2; // 要种植的土地ID列表 + bool auto_slave = 3; // 是否自动副产 +} + +message PlantRequest { + map land_and_seed = 1; // land_id -> seed_id (旧版) + repeated PlantItem items = 2; // 新版: 按种子分组种植 +} + +message PlantReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 移除植物 --- +message RemovePlantRequest { + repeated int64 land_ids = 1; +} + +message RemovePlantReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 施肥 --- +message FertilizeRequest { + repeated int64 land_ids = 1; + int64 fertilizer_id = 2; +} + +message FertilizeReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; + int64 fertilizer = 3; // 剩余肥料时间 +} + +// --- 升级土地 --- +message UpgradeLandRequest { + int64 land_id = 1; +} + +message UpgradeLandReply { + LandInfo land = 1; +} + +// --- 解锁土地 --- +message UnlockLandRequest { + int64 land_id = 1; + bool do_shared = 2; +} + +message UnlockLandReply { + LandInfo land = 1; +} + +// --- 放虫 --- +message PutInsectsRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message PutInsectsReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// --- 放草 --- +message PutWeedsRequest { + repeated int64 land_ids = 1; + int64 host_gid = 2; +} + +message PutWeedsReply { + repeated LandInfo land = 1; + repeated OperationLimit operation_limits = 2; +} + +// ============ 服务器推送通知 ============ + +// 土地状态变化通知 (被放虫/放草/偷菜等) +message LandsNotify { + repeated LandInfo lands = 1; // 变化的土地列表 + int64 host_gid = 2; // 农场主GID +} diff --git a/server/proto/shoppb.proto b/server/proto/shoppb.proto new file mode 100644 index 0000000..175bbb7 --- /dev/null +++ b/server/proto/shoppb.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; + +package gamepb.shoppb; + +import "corepb.proto"; + +// ============ 商店类型: 1=道具商店, 2=种子商店, 3=宠物商店 ============ + +// ============ 商店概览 ============ +message ShopProfile { + int64 shop_id = 1; // 商店ID + string shop_name = 2; // 商店名称 + int32 shop_type = 3; // 商店类型 +} + +// ============ 商品信息 ============ +message GoodsInfo { + int64 id = 1; // 商品ID (用于购买) + int64 bought_num = 2; // 已购买数量 + int64 price = 3; // 价格 (金币) + int64 limit_count = 4; // 限购数量 (0=不限购) + bool unlocked = 5; // 是否已解锁 + int64 item_id = 6; // 物品ID (即种子ID) + int64 item_count = 7; // 每次购买获得数量 + repeated Cond conds = 8; // 解锁条件 +} + +// ============ 解锁条件 ============ +enum CondType { + COND_TYPE_UNKNOWN = 0; + MIN_LEVEL = 1; // 最低等级要求 + UNLOCK_CARD = 2; // 需要解锁卡 +} + +message Cond { + int32 type = 1; // CondType + int64 param = 2; // 参数值 (如等级要求的等级数) +} + +// ============ 请求/回复 ============ + +// --- 获取商店列表 --- +message ShopProfilesRequest {} + +message ShopProfilesReply { + repeated ShopProfile shop_profiles = 1; +} + +// --- 获取商店商品 --- +message ShopInfoRequest { + int64 shop_id = 1; +} + +message ShopInfoReply { + repeated GoodsInfo goods_list = 1; +} + +// --- 购买商品 --- +message BuyGoodsRequest { + int64 goods_id = 1; // 商品ID (GoodsInfo.id) + int64 num = 2; // 购买数量 + int64 price = 3; // 单价 +} + +message BuyGoodsReply { + GoodsInfo goods = 1; // 更新后的商品信息 + repeated corepb.Item get_items = 2; // 获得的物品 + repeated corepb.Item cost_items = 3; // 消耗的物品 +} + +// --- 商品解锁推送 --- +message GoodsUnlockNotify { + repeated GoodsInfo goods_list = 1; +} diff --git a/server/proto/taskpb.proto b/server/proto/taskpb.proto new file mode 100644 index 0000000..e6d953b --- /dev/null +++ b/server/proto/taskpb.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package gamepb.taskpb; + +import "corepb.proto"; + +// ============ 任务类型 ============ +// 1 = 成长任务 (GROWTH_TASK) +// 2 = 每日任务 (DAILY_TASK) + +// ============ 单个任务 ============ +message Task { + int64 id = 1; // 任务ID + int64 progress = 2; // 当前进度 + bool is_claimed = 3; // 是否已领取 + bool is_unlocked = 4; // 是否已解锁 + repeated corepb.Item rewards = 5; // 奖励列表 + int64 total_progress = 6; // 总进度 + int64 share_multiple = 7; // 分享倍数 (>1 表示可分享翻倍) + repeated string params = 8; // 参数 + string desc = 9; // 描述 + int32 task_type = 10; // 任务类型 + int64 group = 11; // 分组 + int64 cond_type = 12; // 条件类型 + int64 is_show_text = 13; // 是否显示文本 +} + +// ============ 活跃度奖励 ============ +message Active { + int64 id = 1; + int32 status = 2; // 0=未完成, 1=已完成, 2=未完成 + repeated corepb.Item items = 3; +} + +// ============ 任务信息汇总 ============ +message TaskInfo { + repeated Task growth_tasks = 1; // 成长任务列表 + repeated Task daily_tasks = 2; // 每日任务列表 + repeated Task tasks = 3; // 其他任务 + repeated Active actives = 4; // 活跃度奖励 +} + +// ============ 请求/回复 ============ + +// --- 获取任务信息 --- +message TaskInfoRequest {} + +message TaskInfoReply { + TaskInfo task_info = 1; +} + +// --- 领取单个任务奖励 --- +message ClaimTaskRewardRequest { + int64 id = 1; // 任务ID + bool do_shared = 2; // 是否使用分享翻倍 (true=翻倍, false=普通领取) +} + +message ClaimTaskRewardReply { + repeated corepb.Item items = 1; // 获得的物品 + TaskInfo task_info = 2; // 更新后的任务信息 + repeated corepb.Item compensated_items = 3; // 补偿物品 +} + +// --- 批量领取任务奖励 --- +message BatchClaimTaskRewardRequest { + repeated int64 ids = 1; // 任务ID列表 + bool do_shared = 2; // 是否使用分享翻倍 +} + +message BatchClaimTaskRewardReply { + repeated corepb.Item items = 1; + TaskInfo task_info = 2; + repeated corepb.Item compensated_items = 3; +} + +// ============ 服务器推送 ============ + +// 任务状态变化通知 +message TaskInfoNotify { + TaskInfo task_info = 1; +} diff --git a/server/proto/userpb.proto b/server/proto/userpb.proto new file mode 100644 index 0000000..6b987aa --- /dev/null +++ b/server/proto/userpb.proto @@ -0,0 +1,123 @@ +syntax = "proto3"; + +package gamepb.userpb; + +// ============ 登录请求 ============ +message LoginRequest { + // 字段 1,2 保留未使用 + int64 sharer_id = 3; + string sharer_open_id = 4; + DeviceInfo device_info = 5; + int64 share_cfg_id = 6; + string scene_id = 7; + ReportData report_data = 8; +} + +// 设备信息 +message DeviceInfo { + string client_version = 1; // e.g. "1.6.0.8_20251224" + string sys_software = 2; // e.g. "Windows Unknown x64" + string sys_hardware = 3; + string telecom_oper = 4; + string network = 5; // e.g. "wifi" + int64 screen_width = 6; + int64 screen_height = 7; + float density = 8; + string cpu = 9; // e.g. "microsoft" + int64 memory = 10; + string gl_render = 11; + string gl_version = 12; + string device_id = 13; + string android_oaid = 14; + string ios_caid = 15; +} + +// 上报数据 +message ReportData { + string callback = 1; + string cd_extend_info = 2; + string click_id = 3; + string clue_token = 4; + string minigame_channel = 5; + int32 minigame_platid = 6; + string req_id = 7; + string trackid = 8; +} + +// ============ 登录回复 ============ +message LoginReply { + BasicInfo basic = 1; + // ItemBag item_bag = 2; // corepb.ItemBag, 复杂结构先跳过 + int64 time_now_millis = 3; + bool is_first_login = 4; + // GuideInfo guide_info = 5; + repeated QQGroupInfo qq_group_infos = 6; + // Illustrated illustrated = 7; + // repeated SystemUnlockItem unlocked_items = 8; + VersionInfo version_info = 9; + // MallMsg mall_msg = 10; + int64 qq_friend_recommend_authorized = 11; +} + +// 用户基本信息 +message BasicInfo { + int64 gid = 1; + string name = 2; + int64 level = 3; + int64 exp = 4; + int64 gold = 5; + string open_id = 6; + string avatar_url = 7; + string remark = 8; + string signature = 9; + int32 gender = 10; + // repeated AvatarFrame equip_avatar_frames = 11; + // map share_infos = 12; + int32 authorized_status = 13; + bool disable_nudge = 14; +} + +// QQ群信息 +message QQGroupInfo { + string qq_group_id = 1; + string qq_group_name = 2; +} + +// 版本信息 +message VersionInfo { + int32 status = 1; + string version_recommend = 2; + string version_force = 3; + string res_version = 4; +} + +// ============ 心跳 ============ +message HeartbeatRequest { + int64 gid = 1; + string client_version = 2; +} + +message HeartbeatReply { + int64 server_time = 1; + VersionInfo version_info = 2; +} + +// ============ 上报分享点击 ============ +// 用于已登录状态下处理分享链接(触发好友申请) +message ReportArkClickRequest { + int64 sharer_id = 1; + string sharer_open_id = 2; + string scene_id = 3; + int64 share_cfg_id = 4; +} + +message ReportArkClickReply { + // 通常为空响应 +} + +// ============ 服务器推送通知 ============ + +// 基本信息变化通知 (升级/金币变化等) +message BasicNotify { + BasicInfo basic = 1; +} \ No newline at end of file diff --git a/server/proto/visitpb.proto b/server/proto/visitpb.proto new file mode 100644 index 0000000..ffc074a --- /dev/null +++ b/server/proto/visitpb.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package gamepb.visitpb; + +import "plantpb.proto"; +import "userpb.proto"; + +// ============ 访问好友农场服务 ============ +// service VisitService { +// rpc Enter(EnterRequest) returns (EnterReply); +// rpc Leave(LeaveRequest) returns (LeaveReply); +// } + +// ============ 进入原因枚举 ============ +enum EnterReason { + ENTER_REASON_UNKNOWN = 0; + ENTER_REASON_BUBBLE = 1; + ENTER_REASON_FRIEND = 2; + ENTER_REASON_INTERACT = 3; +} + +// ============ 进入好友农场 ============ +message EnterRequest { + int64 host_gid = 1; // 好友的GID + int32 reason = 2; // EnterReason 进入原因 +} + +message EnterReply { + gamepb.userpb.BasicInfo basic = 1; // 好友基本信息 + repeated gamepb.plantpb.LandInfo lands = 2; // 好友的所有土地 + // field 3: brief_dog_info (不需要) + // field 4: nudge_info (不需要) +} + +// ============ 离开好友农场 ============ +message LeaveRequest { + int64 host_gid = 1; // 好友的GID +} + +message LeaveReply { + // 空消息 +} diff --git a/server/share.txt b/server/share.txt new file mode 100644 index 0000000..e69de29 diff --git a/server/src/config.js b/server/src/config.js new file mode 100644 index 0000000..d220d29 --- /dev/null +++ b/server/src/config.js @@ -0,0 +1,53 @@ +/** + * 配置常量与枚举定义 + */ + +const CONFIG = { + serverUrl: process.env.FARMBOT_SERVER_URL || 'wss://gate-obt.nqf.qq.com/prod/ws', + clientVersion: process.env.FARMBOT_CLIENT_VERSION || '1.6.0.14_20251224', + platform: 'qq', // 平台: qq 或 wx (可通过 --wx 切换为微信) + os: 'iOS', + heartbeatInterval: 25000, // 心跳间隔 25秒 + farmCheckInterval: 1000, // 自己农场巡查完成后等待间隔 (可通过 --interval 修改, 最低1秒) + friendCheckInterval: 10000, // 好友巡查完成后等待间隔 (可通过 --friend-interval 修改, 最低1秒) + forceLowestLevelCrop: false, // 开启后固定种最低等级作物(通常是白萝卜),跳过经验效率分析 + enableNormalFertilize: false, + enableOrganicFertilize: false, + allowTicketFertilizerPurchase: false, + enableFriendOps: true, + enableSteal: true, + enableAutoSell: true, + allowBuySeeds: true, + allowRemove: true, + idleStrategy: 'exp', +}; + +// 运行期提示文案(做了简单编码,避免明文散落) +const RUNTIME_HINT_MASK = 23; +const RUNTIME_HINT_DATA = [ + 12295, 22759, 26137, 12294, 26427, 39022, 30457, 24343, 28295, 20826, + 36142, 65307, 20018, 31126, 20485, 21313, 12309, 35808, 20185, 20859, + 24343, 20164, 24196, 20826, 36142, 33696, 21441, 12309, +]; + +// 生长阶段枚举 +const PlantPhase = { + UNKNOWN: 0, + SEED: 1, + GERMINATION: 2, + SMALL_LEAVES: 3, + LARGE_LEAVES: 4, + BLOOMING: 5, + MATURE: 6, + DEAD: 7, +}; + +const PHASE_NAMES = ['未知', '种子', '发芽', '小叶', '大叶', '开花', '成熟', '枯死']; + +module.exports = { + CONFIG, + PlantPhase, + PHASE_NAMES, + RUNTIME_HINT_MASK, + RUNTIME_HINT_DATA, +}; diff --git a/server/src/core/FarmBot.js b/server/src/core/FarmBot.js new file mode 100644 index 0000000..65045b7 --- /dev/null +++ b/server/src/core/FarmBot.js @@ -0,0 +1,215 @@ +const EventEmitter = require('events'); +const { NetworkClient } = require('./Network'); +const { FarmManager } = require('./FarmManager'); +const { FriendManager } = require('./FriendManager'); +const { WarehouseManager } = require('./WarehouseManager'); +const { ShopManager } = require('./ShopManager'); +const { log, logWarn, emitRuntimeHint } = require('../utils'); +const { CONFIG } = require('../config'); + +class FarmBot extends EventEmitter { + constructor(config = {}) { + super(); + // Merge provided config with default CONFIG + // Deep copy to avoid modifying global CONFIG if passed by reference (though usually it's shallow copy) + this.config = { ...CONFIG, ...config }; + + // Initialize components + this.network = new NetworkClient(this); + this.farmManager = new FarmManager(this); + this.friendManager = new FriendManager(this); + this.warehouseManager = new WarehouseManager(this); + this.shopManager = new ShopManager(this); + this.dailyClaimTimer = null; + + // State + this.isRunning = false; + this.user = { + gid: 0, + name: 'Loading...', + level: 0, + gold: 0, + exp: 0, + tickets: 0, + fertilizer: 0, + freeMallClaimDate: '' + }; + + // Error handling + this.network.on('error', (err) => { + log('机器人', `网络错误: ${err.message}`); + this.emit('error', err); + }); + + this.network.on('disconnected', () => { + if (this.isRunning) { + log('机器人', '连接断开,正在尝试重新连接...'); + // Reconnection logic is handled in NetworkClient, but we can monitor it here + } + }); + } + + async start() { + if (this.isRunning) return; + this.isRunning = true; + + this.log('系统', `正在启动农场小助手... 平台: ${this.config.platform}`); + emitRuntimeHint(true); + const codeLen = typeof this.config.code === 'string' ? this.config.code.length : 0; + this.log('系统', `连接参数: url=${this.config.serverUrl} version=${this.config.clientVersion} codeLen=${codeLen}`); + + try { + // 1. Connect + this.log('系统', '正在连接服务器...'); + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('连接超时')), 10000); + this.network.connect((success) => { + clearTimeout(timeout); + if (success) resolve(); + else reject(new Error('连接失败')); + }); + }); + + // 2. Login + this.log('系统', '正在登录...'); + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('登录超时')), 10000); + this.network.sendLogin((success) => { + clearTimeout(timeout); + if (success) resolve(); + else reject(new Error('登录失败')); + }); + }); + + // 3. Start Loops + this.log('系统', '开始自动化作业...'); + this.farmManager.startLoop(); + if (this.config.enableFriendOps !== false) { + this.friendManager.startLoop(); + } + if (this.config.enableAutoSell !== false) { + this.warehouseManager.startLoop(); + } + this.startDailyClaimLoop(); + + this.emit('started'); + this.log('系统', '农场小助手启动成功!'); + return true; + + } catch (error) { + const message = error && error.message ? error.message : '启动失败'; + this.log('系统', `启动失败: ${message}`); + this.stop(); + this.lastStartError = message; + return false; + } + } + + stop() { + this.isRunning = false; + this.farmManager.stopLoop(); + this.friendManager.stopLoop(); + this.warehouseManager.stopLoop(); + this.stopDailyClaimLoop(); + + // Stop network + if (this.network.ws) { + this.network.ws.close(); + } + + this.emit('stopped'); + log('系统', '农场小助手已停止'); + } + + applyConfig(nextConfig = {}) { + if (!nextConfig || typeof nextConfig !== 'object') return; + const prevFriendOps = this.config.enableFriendOps !== false; + const prevAutoSell = this.config.enableAutoSell !== false; + Object.assign(this.config, nextConfig); + if (!this.isRunning) return; + const nextFriendOps = this.config.enableFriendOps !== false; + const nextAutoSell = this.config.enableAutoSell !== false; + if (prevFriendOps !== nextFriendOps) { + if (nextFriendOps) { + this.friendManager.startLoop(); + } else { + this.friendManager.stopLoop(); + } + } + if (prevAutoSell !== nextAutoSell) { + if (nextAutoSell) { + this.warehouseManager.startLoop(); + } else { + this.warehouseManager.stopLoop(); + } + } + } + + log(tag, msg) { + log(tag, msg); + this.emit('log', { + tag, + msg, + time: Date.now() + }); + } + + logWarn(tag, msg) { + logWarn(tag, msg); + this.emit('log', { + tag, + msg, + type: 'warn', + time: Date.now() + }); + } + + getTodayKey() { + const now = new Date(); + const y = now.getFullYear(); + const m = `${now.getMonth() + 1}`.padStart(2, '0'); + const d = `${now.getDate()}`.padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + async runDailyClaims() { + if (!this.isRunning) return; + await this.network.tryClaimShareReward(1); + const todayKey = this.getTodayKey(); + if (this.user.freeMallClaimDate === todayKey) return; + try { + await this.shopManager.purchaseMallItem(1001, 1); + this.user.freeMallClaimDate = todayKey; + this.emit('userUpdate', this.user); + this.log('商城', '已领取每日免费礼包'); + } catch (e) { + this.logWarn('商城', `领取失败: ${e.message}`); + } + } + + startDailyClaimLoop() { + if (this.dailyClaimTimer) return; + const scheduleNext = () => { + if (!this.isRunning) return; + const now = new Date(); + const next = new Date(now); + next.setHours(24, 0, 0, 0); + const delay = Math.max(1000, next.getTime() - now.getTime()); + this.dailyClaimTimer = setTimeout(async () => { + this.dailyClaimTimer = null; + await this.runDailyClaims(); + scheduleNext(); + }, delay); + }; + scheduleNext(); + } + + stopDailyClaimLoop() { + if (this.dailyClaimTimer) { + clearTimeout(this.dailyClaimTimer); + this.dailyClaimTimer = null; + } + } +} + +module.exports = { FarmBot }; diff --git a/server/src/core/FarmManager.js b/server/src/core/FarmManager.js new file mode 100644 index 0000000..5a396b3 --- /dev/null +++ b/server/src/core/FarmManager.js @@ -0,0 +1,1164 @@ +const protobuf = require('protobufjs'); +const { PlantPhase, PHASE_NAMES } = require('../config'); +const { types } = require('../proto'); +const { toLong, toNum, getServerTimeSec, toTimeSec, sleep } = require('../utils'); +const { getPlantNameBySeedId, getPlantName, getPlantExp, formatGrowTime, getPlantGrowTime, getSeedIdByPlantName, getPlantByName } = require('../gameConfig'); +const { getPlantingRecommendation } = require('../../tools/calc-exp-yield'); + +const PLANT_COOLDOWN_MS = 5000; +const PLANT_ALREADY_COOLDOWN_MS = 30000; +const PLANT_PENDING_MS = 15000; +const FERT_PENDING_MS = 15000; + +function isTaskNotCompleteError(message) { + if (!message) return false; + return message.includes('code=1008001') || message.includes('任务未完成'); +} + +function isTaskAlreadyClaimedError(message) { + if (!message) return false; + return message.includes('code=1008002') || message.includes('任务已领取'); +} + +class FarmManager { + constructor(bot) { + this.bot = bot; + + // Internal state + this.isCheckingFarm = false; + this.isFirstFarmCheck = true; + this.farmCheckTimer = null; + this.farmLoopRunning = false; + this.taskExecuting = false; + this.taskCache = null; + this.taskCacheAt = 0; + this.lastTaskActionAt = 0; + this.taskClaiming = false; + this.plantNextAllowed = new Map(); + this.pendingPlantUntil = new Map(); + this.pendingFertilizeUntil = new Map(); + + // Listen for lands changed push + this.bot.on('landsChanged', this.onLandsChangedPush.bind(this)); + this.bot.on('taskInfoNotify', (taskInfo) => { + this.taskCache = taskInfo || null; + this.taskCacheAt = Date.now(); + this.autoClaimTasks(taskInfo).catch(() => {}); + }); + } + + // ============ Farm API ============ + + async getAllLands() { + const body = types.AllLandsRequest.encode(types.AllLandsRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'AllLands', body); + const reply = types.AllLandsReply.decode(replyBody); + + // Emit operation limits update (for FriendManager) + if (reply.operation_limits) { + this.bot.emit('operationLimits', reply.operation_limits); + } + return reply; + } + + async harvest(landIds) { + const body = types.HarvestRequest.encode(types.HarvestRequest.create({ + land_ids: landIds, + host_gid: toLong(this.bot.user.gid), + is_all: true, + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body); + return types.HarvestReply.decode(replyBody); + } + + async waterLand(landIds) { + const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({ + land_ids: landIds, + host_gid: toLong(this.bot.user.gid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body); + return types.WaterLandReply.decode(replyBody); + } + + async weedOut(landIds) { + const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({ + land_ids: landIds, + host_gid: toLong(this.bot.user.gid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body); + return types.WeedOutReply.decode(replyBody); + } + + async insecticide(landIds) { + const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({ + land_ids: landIds, + host_gid: toLong(this.bot.user.gid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body); + return types.InsecticideReply.decode(replyBody); + } + + async fertilize(landIds, fertilizerId = 1011) { + let successCount = 0; + for (const landId of landIds) { + try { + const body = types.FertilizeRequest.encode(types.FertilizeRequest.create({ + land_ids: [toLong(landId)], + fertilizer_id: toLong(fertilizerId), + })).finish(); + await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Fertilize', body); + successCount++; + this.pendingFertilizeUntil.set(landId, Date.now() + FERT_PENDING_MS); + } catch (e) { + break; + } + if (landIds.length > 1) await sleep(50); + } + return successCount; + } + + async removePlant(landIds) { + const body = types.RemovePlantRequest.encode(types.RemovePlantRequest.create({ + land_ids: landIds.map(id => toLong(id)), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'RemovePlant', body); + return types.RemovePlantReply.decode(replyBody); + } + + async unlockLand(landId, doShared = false) { + const body = types.UnlockLandRequest.encode(types.UnlockLandRequest.create({ + land_id: toLong(landId), + do_shared: Boolean(doShared), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'UnlockLand', body); + return types.UnlockLandReply.decode(replyBody); + } + + // ============ Shop API ============ + + async getShopProfiles() { + const body = types.ShopProfilesRequest.encode(types.ShopProfilesRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopProfiles', body); + return types.ShopProfilesReply.decode(replyBody); + } + + async getShopInfo(shopId) { + const body = types.ShopInfoRequest.encode(types.ShopInfoRequest.create({ + shop_id: toLong(shopId), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopInfo', body); + return types.ShopInfoReply.decode(replyBody); + } + + async buyGoods(goodsId, num, price) { + const body = types.BuyGoodsRequest.encode(types.BuyGoodsRequest.create({ + goods_id: toLong(goodsId), + num: toLong(num), + price: toLong(price), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'BuyGoods', body); + return types.BuyGoodsReply.decode(replyBody); + } + + async buyNormalFertilizer(count) { + if (!Number.isFinite(count) || count <= 0) return 0; + const fertilizerItemId = 1011; + let propsShopId = null; + try { + const profilesReply = await this.getShopProfiles(); + const profiles = profilesReply.shop_profiles || []; + const propsShop = profiles.find(profile => toNum(profile.shop_type) === 1); + if (propsShop) { + propsShopId = toNum(propsShop.shop_id); + } + } catch (e) { + this.bot.logWarn('商店', `获取商店列表失败: ${e.message}`); + return 0; + } + + if (!propsShopId) { + this.bot.logWarn('商店', '未找到道具商店'); + return 0; + } + + let goods = null; + try { + const shopReply = await this.getShopInfo(propsShopId); + const goodsList = shopReply.goods_list || []; + goods = goodsList.find(item => toNum(item.item_id) === fertilizerItemId && item.unlocked); + } catch (e) { + this.bot.logWarn('商店', `获取道具商店商品失败: ${e.message}`); + return 0; + } + + if (!goods) { + this.bot.logWarn('商店', '普通化肥不可购买'); + return 0; + } + + const limitCount = toNum(goods.limit_count); + const boughtNum = toNum(goods.bought_num); + let buyCount = count; + if (limitCount > 0) { + const remaining = Math.max(0, limitCount - boughtNum); + buyCount = Math.min(buyCount, remaining); + } + if (buyCount <= 0) return 0; + + try { + const buyReply = await this.buyGoods(toNum(goods.id), buyCount, toNum(goods.price)); + const items = buyReply.get_items || []; + let gotCount = 0; + for (const item of items) { + if (toNum(item.id) === fertilizerItemId) { + gotCount += toNum(item.count); + } + } + return gotCount; + } catch (e) { + this.bot.logWarn('购买', `购买普通化肥失败: ${e.message}`); + return 0; + } + } + + async fetchTaskInfo() { + const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body); + const reply = types.TaskInfoReply.decode(replyBody); + return reply.task_info || null; + } + + async claimTaskReward(taskId, doShared) { + const body = types.ClaimTaskRewardRequest.encode(types.ClaimTaskRewardRequest.create({ + id: toLong(taskId), + do_shared: Boolean(doShared) + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'ClaimTaskReward', body); + return types.ClaimTaskRewardReply.decode(replyBody); + } + + async autoClaimTasks(taskInfo) { + if (this.taskClaiming) return; + if (!taskInfo) return; + + const claimable = []; + const addTasks = (list) => { + if (!list) return; + for (const task of list) { + if (!task.is_unlocked || task.is_claimed) continue; + const progress = toNum(task.progress); + const totalProgress = toNum(task.total_progress); + if (totalProgress <= 0 || progress < totalProgress) continue; + const id = toNum(task.id); + claimable.push({ + id, + desc: task.desc || `任务#${id}`, + shareMultiple: toNum(task.share_multiple) + }); + } + }; + addTasks(taskInfo.growth_tasks); + addTasks(taskInfo.daily_tasks); + addTasks(taskInfo.tasks); + + if (claimable.length === 0) return; + + this.taskClaiming = true; + try { + for (const task of claimable) { + try { + const useShare = task.shareMultiple > 1; + const multipleStr = useShare ? ` (${task.shareMultiple}倍)` : ''; + await this.claimTaskReward(task.id, useShare); + this.bot.log('任务', `领取: ${task.desc}${multipleStr}`); + await sleep(300); + } catch (e) { + const message = e && e.message ? e.message : ''; + if (isTaskNotCompleteError(message)) { + this.bot.log('任务', `任务未完成,跳过领取 #${task.id}`); + continue; + } + if (isTaskAlreadyClaimedError(message)) { + this.bot.log('任务', `任务已领取,跳过领取 #${task.id}`); + continue; + } + this.bot.logWarn('任务', `领取失败 #${task.id}: ${message}`); + } + } + } finally { + this.taskClaiming = false; + } + } + + async getTaskInfoCached(maxAgeMs = 15000) { + const now = Date.now(); + if (this.taskCache && now - this.taskCacheAt < maxAgeMs) return this.taskCache; + const taskInfo = await this.fetchTaskInfo(); + if (taskInfo) { + this.taskCache = taskInfo; + this.taskCacheAt = now; + } + return taskInfo; + } + + buildTaskList(taskInfo) { + const tasks = []; + if (!taskInfo) return tasks; + const addTasks = (list) => { + if (!list) return; + for (const task of list) { + tasks.push({ + id: toNum(task.id), + desc: task.desc || '', + progress: toNum(task.progress), + totalProgress: toNum(task.total_progress), + isClaimed: Boolean(task.is_claimed), + isUnlocked: Boolean(task.is_unlocked), + }); + } + }; + addTasks(taskInfo.growth_tasks); + addTasks(taskInfo.daily_tasks); + addTasks(taskInfo.tasks); + return tasks; + } + + parseTaskDesc(desc) { + if (!desc) return null; + let match = desc.match(/购买(\d+)个(.+?)种子/); + if (match) { + return { type: 'buySeed', count: Number(match[1]) || 0, name: match[2].trim() }; + } + match = desc.match(/种植(\d+)个(.+)/); + if (match) { + return { type: 'plantSeed', count: Number(match[1]) || 0, name: match[2].trim() }; + } + match = desc.match(/收获(\d+)个(.+)/); + if (match) { + return { type: 'harvest', count: Number(match[1]) || 0, name: match[2].trim() }; + } + return null; + } + + pickTaskByType(tasks, type) { + for (const task of tasks) { + if (!task.isUnlocked || task.isClaimed) continue; + const parsed = this.parseTaskDesc(task.desc); + if (!parsed || parsed.type !== type) continue; + const remaining = task.totalProgress > 0 ? Math.max(0, task.totalProgress - task.progress) : parsed.count; + if (remaining <= 0) continue; + return { ...parsed, remaining }; + } + return null; + } + + async findSeedGoods(seedId) { + const seedShopId = 2; + const shopReply = await this.getShopInfo(seedShopId); + const goodsList = shopReply.goods_list || []; + const goods = goodsList.find(item => toNum(item.item_id) === seedId && item.unlocked); + if (!goods) return null; + return { + seedId, + goodsId: toNum(goods.id), + price: toNum(goods.price), + limitCount: toNum(goods.limit_count), + boughtNum: toNum(goods.bought_num), + }; + } + + async executeBuySeedTask(task) { + if (!task || !task.name) return false; + if (this.bot.config.allowBuySeeds === false) { + this.bot.log('任务', '已关闭自动购买种子,跳过任务购买'); + return false; + } + const seedId = getSeedIdByPlantName(task.name); + if (!seedId) return false; + const seedGoods = await this.findSeedGoods(seedId); + if (!seedGoods) { + this.bot.logWarn('任务', `未找到种子: ${task.name}`); + return false; + } + let buyCount = task.remaining || task.count || 0; + if (buyCount <= 0) return false; + if (seedGoods.limitCount > 0) { + const remainingLimit = Math.max(0, seedGoods.limitCount - seedGoods.boughtNum); + buyCount = Math.min(buyCount, remainingLimit); + } + const affordable = Math.floor(this.bot.user.gold / seedGoods.price); + buyCount = Math.min(buyCount, affordable); + if (buyCount <= 0) return false; + try { + const buyReply = await this.buyGoods(seedGoods.goodsId, buyCount, seedGoods.price); + if (buyReply.cost_items) { + for (const item of buyReply.cost_items) { + this.bot.user.gold -= toNum(item.count); + } + } + this.bot.log('任务', `购买种子: ${task.name} x${buyCount}`); + return true; + } catch (e) { + this.bot.logWarn('任务', `购买失败: ${e.message}`); + return false; + } + } + + // ============ Planting Logic ============ + + encodePlantRequest(seedId, landIds) { + const writer = protobuf.Writer.create(); + const itemWriter = writer.uint32(18).fork(); + itemWriter.uint32(8).int64(seedId); + const idsWriter = itemWriter.uint32(18).fork(); + for (const id of landIds) { + idsWriter.int64(id); + } + idsWriter.ldelim(); + itemWriter.ldelim(); + return writer.finish(); + } + + async plantSeeds(seedId, landIds) { + let successCount = 0; + const now = Date.now(); + for (const landId of landIds) { + const nextAllowed = this.plantNextAllowed.get(landId) || 0; + if (now < nextAllowed) continue; + this.plantNextAllowed.set(landId, now + PLANT_COOLDOWN_MS); + try { + const body = this.encodePlantRequest(seedId, [landId]); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Plant', body); + types.PlantReply.decode(replyBody); + successCount++; + this.pendingPlantUntil.set(landId, now + PLANT_PENDING_MS); + } catch (e) { + const msg = String(e && e.message ? e.message : e); + if (msg.includes('1001008') || msg.includes('土地已种植')) { + this.plantNextAllowed.set(landId, now + PLANT_ALREADY_COOLDOWN_MS); + this.pendingPlantUntil.set(landId, now + PLANT_ALREADY_COOLDOWN_MS); + this.bot.log('种植', `土地#${landId} 已种植,跳过`); + } else { + this.bot.logWarn('种植', `土地#${landId} 失败: ${msg}`); + } + } + if (landIds.length > 1) await sleep(50); + } + return successCount; + } + + async findBestSeed(landsCount, useFertilizerOverride) { + const SEED_SHOP_ID = 2; + const shopReply = await this.getShopInfo(SEED_SHOP_ID); + if (!shopReply.goods_list || shopReply.goods_list.length === 0) { + this.bot.logWarn('商店', '种子商店暂无商品'); + return null; + } + + const available = []; + for (const goods of shopReply.goods_list) { + if (!goods.unlocked) continue; + + let meetsConditions = true; + let requiredLevel = 0; + const conds = goods.conds || []; + for (const cond of conds) { + if (toNum(cond.type) === 1) { + requiredLevel = toNum(cond.param); + if (this.bot.user.level < requiredLevel) { + meetsConditions = false; + break; + } + } + } + if (!meetsConditions) continue; + + const limitCount = toNum(goods.limit_count); + const boughtNum = toNum(goods.bought_num); + if (limitCount > 0 && boughtNum >= limitCount) continue; + + available.push({ + goods, + goodsId: toNum(goods.id), + seedId: toNum(goods.item_id), + price: toNum(goods.price), + requiredLevel, + }); + } + + if (available.length === 0) { + this.bot.logWarn('商店', '没有找到可购买的种子'); + return null; + } + + if (this.bot.config.forceLowestLevelCrop) { + available.sort((a, b) => a.requiredLevel - b.requiredLevel || a.price - b.price); + return available[0]; + } + + try { + const rec = getPlantingRecommendation(this.bot.user.level, landsCount == null ? 18 : landsCount, { top: 50 }); + const useFertilizer = typeof useFertilizerOverride === 'boolean' + ? useFertilizerOverride + : (this.bot.config.enableNormalFertilize || this.bot.config.enableOrganicFertilize); + let candidates = useFertilizer ? rec.candidatesNormalFert : rec.candidatesNoFert; + if (!candidates) candidates = rec.candidatesNoFert; // fallback + + let rankedSeedIds = candidates.map(x => x.seedId); + + if (this.bot.user.level > 30) { + const minLevel = Math.max(1, this.bot.user.level - 20); + const filtered = rec.candidatesNoFert.filter(x => x.requiredLevel >= minLevel); + if (filtered.length > 0) { + rankedSeedIds = filtered.map(x => x.seedId); + this.bot.log('商店', `高等级策略生效 (Lv${this.bot.user.level}),仅考虑 Lv${minLevel}+ 作物`); + } + } + for (const seedId of rankedSeedIds) { + const hit = available.find(x => x.seedId === seedId); + if (hit) return hit; + } + } catch (e) { + this.bot.logWarn('商店', `经验效率推荐失败: ${e.message}`); + } + + if(this.bot.user.level && this.bot.user.level <= 28){ + available.sort((a, b) => a.requiredLevel - b.requiredLevel); + }else{ + available.sort((a, b) => b.requiredLevel - a.requiredLevel); + } + return available[0]; + } + + async autoPlantEmptyLands(deadLandIds, emptyLandIds, unlockedLandCount, preferredSeedId = 0, preferredCount = 0) { + let landsToPlant = emptyLandIds.length > 0 ? [...emptyLandIds] : []; + + if (deadLandIds.length > 0) { + if (this.bot.config.allowRemove === false) { + this.bot.log('铲除', '已关闭自动铲除,跳过枯萎土地'); + } else { + try { + await this.removePlant(deadLandIds); + this.bot.log('铲除', `已铲除 ${deadLandIds.length} 块土地 (${deadLandIds.join(',')})`); + landsToPlant.push(...deadLandIds); + } catch (e) { + this.bot.logWarn('铲除', `批量铲除失败: ${e.message}`); + landsToPlant.push(...deadLandIds); + } + } + } + + if (landsToPlant.length === 0) return 0; + if (landsToPlant.length > 1) { + landsToPlant = Array.from(new Set(landsToPlant)); + } + if (landsToPlant.length > 0) { + const now = Date.now(); + const filtered = []; + for (const landId of landsToPlant) { + const nextAllowed = this.plantNextAllowed.get(landId) || 0; + if (now >= nextAllowed) { + filtered.push(landId); + } + } + landsToPlant = filtered; + } + if (landsToPlant.length === 0) return 0; + if (preferredCount > 0 && landsToPlant.length > preferredCount) { + landsToPlant = landsToPlant.slice(0, preferredCount); + } + + let bagItems = null; + try { + const bagReply = await this.bot.warehouseManager.getBag(); + bagItems = this.bot.warehouseManager.getBagItems(bagReply); + } catch (e) { + this.bot.logWarn('背包', `获取背包失败: ${e.message}`); + } + + const enableNormal = this.bot.config.enableNormalFertilize === true; + const enableOrganic = this.bot.config.enableOrganicFertilize === true; + let normalCount = 0; + let organicCount = 0; + let fertilizerInventoryEmpty = false; + if (bagItems) { + for (const item of bagItems) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1011) normalCount = count; + if (id === 1012) organicCount = count; + } + if ((enableNormal || enableOrganic) && normalCount <= 0 && organicCount <= 0) { + fertilizerInventoryEmpty = true; + this.bot.log('施肥', '化肥容器为空,切换为无化肥策略'); + } + } + const useFertilizer = (enableNormal || enableOrganic) && !fertilizerInventoryEmpty; + + let targetSeedGoods = null; + let targetSeedId = preferredSeedId; + let targetSeedName = preferredSeedId ? getPlantNameBySeedId(preferredSeedId) : ''; + if (!preferredSeedId) { + try { + targetSeedGoods = await this.findBestSeed(unlockedLandCount, useFertilizer); + } catch (e) { + this.bot.logWarn('商店', `查询失败: ${e.message}`); + return 0; + } + if (!targetSeedGoods) return 0; + targetSeedId = targetSeedGoods.seedId; + targetSeedName = getPlantNameBySeedId(targetSeedId); + } else { + try { + targetSeedGoods = await this.findSeedGoods(preferredSeedId); + } catch (e) { + this.bot.logWarn('商店', `查询失败: ${e.message}`); + } + } + + let selectedSeed = null; + if (bagItems && targetSeedId) { + for (const item of bagItems) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id !== targetSeedId || count <= 0) continue; + selectedSeed = { id, count, name: targetSeedName || getPlantNameBySeedId(id) }; + break; + } + } + + let actualSeedId = 0; + let seedName = ''; + if (selectedSeed) { + actualSeedId = selectedSeed.id; + seedName = selectedSeed.name; + if (landsToPlant.length > selectedSeed.count) { + landsToPlant = landsToPlant.slice(0, selectedSeed.count); + } + this.bot.log('种植', `优先使用背包种子: ${seedName} x${landsToPlant.length}`); + } else if (this.bot.config.allowBuySeeds === false) { + if (targetSeedName) { + this.bot.log('种植', `已关闭购买种子,背包无 ${targetSeedName} 种子`); + } else { + this.bot.log('种植', '已关闭购买种子,背包无可用种子'); + } + return 0; + } else { + if (!targetSeedGoods) { + try { + targetSeedGoods = await this.findSeedGoods(targetSeedId); + } catch (e) { + this.bot.logWarn('商店', `查询失败: ${e.message}`); + return 0; + } + } + if (!targetSeedGoods) return 0; + + seedName = targetSeedName || getPlantNameBySeedId(targetSeedGoods.seedId); + const growTime = getPlantGrowTime(1020000 + (targetSeedGoods.seedId - 20000)); + const growTimeStr = growTime > 0 ? ` 生长${formatGrowTime(growTime)}` : ''; + this.bot.log('商店', `最佳作物: ${seedName} (${targetSeedGoods.seedId}) 价格: ${targetSeedGoods.price}金币${growTimeStr}`); + + const needCount = landsToPlant.length; + const totalCost = targetSeedGoods.price * needCount; + if (totalCost > this.bot.user.gold) { + const canBuy = Math.floor(this.bot.user.gold / targetSeedGoods.price); + if (canBuy <= 0) return 0; + landsToPlant = landsToPlant.slice(0, canBuy); + this.bot.log('商店', `金币不足,本次仅种植 ${canBuy} 块土地`); + } + + actualSeedId = targetSeedGoods.seedId; + try { + const buyReply = await this.buyGoods(targetSeedGoods.goodsId, landsToPlant.length, targetSeedGoods.price); + if (buyReply.get_items && buyReply.get_items.length > 0) { + const gotItem = buyReply.get_items[0]; + const gotId = toNum(gotItem.id); + if (gotId > 0) actualSeedId = gotId; + } + if (buyReply.cost_items) { + for (const item of buyReply.cost_items) { + this.bot.user.gold -= toNum(item.count); + } + } + } catch (e) { + this.bot.logWarn('购买', e.message); + return 0; + } + } + + let plantedLands = []; + try { + const planted = await this.plantSeeds(actualSeedId, landsToPlant); + this.bot.log('种植', `成功种植 ${planted} 块土地`); + if (planted > 0) { + plantedLands = landsToPlant.slice(0, planted); + } + } catch (e) { + this.bot.logWarn('种植', e.message); + } + + if (plantedLands.length > 0) { + if (!enableNormal && !enableOrganic) { + return plantedLands.length; + } + if (fertilizerInventoryEmpty) { + return plantedLands.length; + } + + let items = bagItems; + if (!items) { + try { + const bagReply = await this.bot.warehouseManager.getBag(); + items = this.bot.warehouseManager.getBagItems(bagReply); + } catch (e) { + this.bot.logWarn('施肥', `检查背包失败: ${e.message}`); + } + } + + if (!items) { + if (enableNormal) normalCount = plantedLands.length; + if (enableOrganic) organicCount = plantedLands.length; + } else if (!bagItems) { + normalCount = 0; + organicCount = 0; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1011) normalCount = count; + if (id === 1012) organicCount = count; + } + } + + let remainingLands = [...plantedLands]; + if (enableOrganic && organicCount > 0) { + const organicTargets = remainingLands.slice(0, organicCount); + const fertilized = await this.fertilize(organicTargets, 1012); + if (fertilized > 0) { + this.bot.log('施肥', `有机化肥施肥 ${fertilized} 块土地`); + } + remainingLands = remainingLands.slice(organicTargets.length); + } + + if (remainingLands.length > 0 && enableNormal) { + if (normalCount < remainingLands.length && this.bot.config.allowTicketFertilizerPurchase) { + const needBuy = remainingLands.length - normalCount; + const bought = await this.buyNormalFertilizer(needBuy); + if (bought > 0) { + normalCount += bought; + this.bot.log('施肥', `自动购买普通化肥 ${bought} 个`); + } + } + if (normalCount < remainingLands.length) { + this.bot.log('施肥', `普通化肥不足 (库存: ${normalCount}, 需要: ${remainingLands.length}),仅施肥部分土地`); + remainingLands = remainingLands.slice(0, normalCount); + } + if (remainingLands.length > 0) { + const fertilized = await this.fertilize(remainingLands, 1011); + if (fertilized > 0) { + this.bot.log('施肥', `普通化肥施肥 ${fertilized} 块土地`); + } + } + } + } + + return plantedLands.length; + } + + // ============ Analysis ============ + + getCurrentPhase(phases, debug, landLabel) { + if (!phases || phases.length === 0) return null; + const nowSec = getServerTimeSec(); + + for (let i = phases.length - 1; i >= 0; i--) { + const beginTime = toTimeSec(phases[i].begin_time); + if (beginTime > 0 && beginTime <= nowSec) { + return phases[i]; + } + } + return phases[0]; + } + + analyzeLands(lands) { + const result = { + harvestable: [], needWater: [], needWeed: [], needBug: [], + growing: [], empty: [], dead: [], + harvestableInfo: [], + }; + + const nowSec = getServerTimeSec(); + + for (const land of lands) { + const id = toNum(land.id); + if (!land.unlocked) continue; + + const plant = land.plant; + if (!plant) { + const pendingUntil = this.pendingPlantUntil.get(id) || 0; + if (pendingUntil > Date.now()) { + result.growing.push(id); + } else { + result.empty.push(id); + } + continue; + } + if (!plant.phases || plant.phases.length === 0) { + const plantId = toNum(plant.id); + if (plantId > 0 || plant.name) { + result.growing.push(id); + } else { + const pendingUntil = this.pendingPlantUntil.get(id) || 0; + if (pendingUntil > Date.now()) { + result.growing.push(id); + } else { + result.empty.push(id); + } + } + continue; + } + + const currentPhase = this.getCurrentPhase(plant.phases, false, `土地#${id}`); + if (!currentPhase) { + result.empty.push(id); + continue; + } + const phaseVal = currentPhase.phase; + + if (phaseVal === PlantPhase.DEAD) { + result.dead.push(id); + continue; + } + + if (phaseVal === PlantPhase.MATURE) { + result.harvestable.push(id); + const plantId = toNum(plant.id); + result.harvestableInfo.push({ + landId: id, + plantId, + name: getPlantName(plantId), + exp: getPlantExp(plantId), + }); + continue; + } + + const dryNum = toNum(plant.dry_num); + const dryTime = toTimeSec(currentPhase.dry_time); + if (dryNum > 0 || (dryTime > 0 && dryTime <= nowSec)) { + result.needWater.push(id); + } + + const weedsTime = toTimeSec(currentPhase.weeds_time); + const hasWeeds = (plant.weed_owners && plant.weed_owners.length > 0) || (weedsTime > 0 && weedsTime <= nowSec); + if (hasWeeds) { + result.needWeed.push(id); + } + + const insectTime = toTimeSec(currentPhase.insect_time); + const hasBugs = (plant.insect_owners && plant.insect_owners.length > 0) || (insectTime > 0 && insectTime <= nowSec); + if (hasBugs) { + result.needBug.push(id); + } + + result.growing.push(id); + } + + return result; + } + + // ============ Loop ============ + + async getFormattedLands() { + try { + const landsReply = await this.getAllLands(); + if (!landsReply || !landsReply.lands) return []; + + const lands = landsReply.lands; + const analysis = this.analyzeLands(lands); + + return lands.map(land => { + const id = toNum(land.id); + if (!land.unlocked) { + const unlockCondition = land.unlock_condition || {}; + return { + id, + type: 'locked', + couldUnlock: !!land.could_unlock, + unlockCondition: { + needLevel: toNum(unlockCondition.need_level || 0), + needGold: toNum(unlockCondition.need_gold || 0), + } + }; + } + + const plant = land.plant; + if (!plant) { + const pendingUntil = this.pendingPlantUntil.get(id) || 0; + if (pendingUntil > Date.now()) { + return { + id, + type: 'planted', + status: 'growing', + plantId: 0, + plantName: '', + phase: 0, + phaseName: PHASE_NAMES[0], + fertilized: false, + needs: { water: false, weed: false, bug: false } + }; + } + return { id, type: 'empty' }; + } + if (!plant.phases || plant.phases.length === 0) { + const plantId = toNum(plant.id); + const plantName = plant.name || getPlantName(plantId); + if (plantId > 0 || plantName) { + return { + id, + type: 'planted', + status: 'growing', + plantId, + plantName, + phase: 0, + phaseName: PHASE_NAMES[0], + fertilized: false, + needs: { water: false, weed: false, bug: false } + }; + } + const pendingUntil = this.pendingPlantUntil.get(id) || 0; + if (pendingUntil > Date.now()) { + return { + id, + type: 'planted', + status: 'growing', + plantId: 0, + plantName: '', + phase: 0, + phaseName: PHASE_NAMES[0], + fertilized: false, + needs: { water: false, weed: false, bug: false } + }; + } + return { id, type: 'empty' }; + } + + const currentPhase = this.getCurrentPhase(plant.phases, false); + const phaseVal = currentPhase ? currentPhase.phase : 0; + const serverFertsUsed = currentPhase && currentPhase.ferts_used && Object.keys(currentPhase.ferts_used).length > 0; + const pendingFert = (this.pendingFertilizeUntil.get(id) || 0) > Date.now(); + const fertilized = !!(serverFertsUsed || pendingFert); + + let status = 'growing'; + if (phaseVal === PlantPhase.DEAD) status = 'dead'; + else if (phaseVal === PlantPhase.MATURE) status = 'mature'; + + const phaseName = PHASE_NAMES[phaseVal] || PHASE_NAMES[0]; + return { + id, + type: 'planted', + status, + plantId: toNum(plant.id), + plantName: getPlantName(toNum(plant.id)), + phase: phaseVal, + phaseName, + fertilized, + needs: { + water: analysis.needWater.includes(id), + weed: analysis.needWeed.includes(id), + bug: analysis.needBug.includes(id) + } + }; + }); + } catch (e) { + this.bot.logWarn('API', `Failed to get lands: ${e.message}`); + return []; + } + } + + async check() { + if (this.isCheckingFarm || !this.bot.user.gid) return; + this.isCheckingFarm = true; + + try { + const now = Date.now(); + const canUseTask = now - this.lastTaskActionAt >= 12000; + let tasks = []; + let buyTask = null; + let plantTask = null; + let harvestTask = null; + + if (canUseTask) { + try { + const taskInfo = await this.getTaskInfoCached(); + tasks = this.buildTaskList(taskInfo); + this.autoClaimTasks(taskInfo).catch(() => {}); + buyTask = this.pickTaskByType(tasks, 'buySeed'); + plantTask = this.pickTaskByType(tasks, 'plantSeed'); + harvestTask = this.pickTaskByType(tasks, 'harvest'); + } catch (e) { } + } + + if (canUseTask && buyTask && !this.taskExecuting) { + this.taskExecuting = true; + let bought = false; + try { + bought = await this.executeBuySeedTask(buyTask); + } finally { + this.taskExecuting = false; + } + if (bought) { + this.lastTaskActionAt = Date.now(); + return; + } + } + + const landsReply = await this.getAllLands(); + if (!landsReply.lands || landsReply.lands.length === 0) { + return; + } + + const lands = landsReply.lands; + const status = this.analyzeLands(lands); + const unlockedLandCount = lands.filter(land => land && land.unlocked).length; + this.isFirstFarmCheck = false; + + if (canUseTask && harvestTask) { + const plant = getPlantByName(harvestTask.name); + if (plant) { + const targetIds = status.harvestableInfo + .filter(info => info.plantId === plant.id) + .map(info => info.landId); + if (targetIds.length > 0) { + try { + await this.harvest(targetIds); + this.bot.log('任务', `收获 ${harvestTask.name} ${targetIds.length} 块`); + this.lastTaskActionAt = Date.now(); + return; + } catch (e) { + this.bot.logWarn('任务', `收获失败: ${e.message}`); + } + } + } + } + + const statusParts = []; + if (status.harvestable.length) statusParts.push(`待收获 ${status.harvestable.length} 块`); + if (status.needWeed.length) statusParts.push(`需除草 ${status.needWeed.length} 块`); + if (status.needBug.length) statusParts.push(`需除虫 ${status.needBug.length} 块`); + if (status.needWater.length) statusParts.push(`需浇水 ${status.needWater.length} 块`); + if (status.dead.length) statusParts.push(`枯萎 ${status.dead.length} 块`); + if (status.empty.length) statusParts.push(`空闲 ${status.empty.length} 块`); + if (status.growing.length) statusParts.push(`生长中 ${status.growing.length} 块`); + + const hasWork = status.harvestable.length || status.needWeed.length || status.needBug.length + || status.needWater.length || status.dead.length || status.empty.length; + + const actions = []; + const batchOps = []; + + if (status.needWeed.length > 0) { + batchOps.push(this.weedOut(status.needWeed).then(() => actions.push(`除草 ${status.needWeed.length} 块`)).catch(e => this.bot.logWarn('除草', e.message))); + } + if (status.needBug.length > 0) { + batchOps.push(this.insecticide(status.needBug).then(() => actions.push(`除虫 ${status.needBug.length} 块`)).catch(e => this.bot.logWarn('除虫', e.message))); + } + if (status.needWater.length > 0) { + batchOps.push(this.waterLand(status.needWater).then(() => actions.push(`浇水 ${status.needWater.length} 块`)).catch(e => this.bot.logWarn('浇水', e.message))); + } + if (batchOps.length > 0) await Promise.all(batchOps); + + let harvestedLandIds = []; + if (status.harvestable.length > 0) { + try { + await this.harvest(status.harvestable); + actions.push(`收获 ${status.harvestable.length} 块`); + harvestedLandIds = [...status.harvestable]; + } catch (e) { this.bot.logWarn('收获', e.message); } + } + + const allDeadLands = [...status.dead]; + const allEmptyLands = [...status.empty, ...harvestedLandIds]; + if (canUseTask && plantTask && this.bot.config.idleStrategy === 'task') { + const seedId = getSeedIdByPlantName(plantTask.name); + if (seedId && allEmptyLands.length > 0) { + try { + const planted = await this.autoPlantEmptyLands([], allEmptyLands, unlockedLandCount, seedId, plantTask.remaining || plantTask.count); + if (planted > 0) { + this.bot.log('任务', `种植 ${plantTask.name} ${planted} 块`); + this.lastTaskActionAt = Date.now(); + return; + } + } catch (e) { + this.bot.logWarn('任务', `种植失败: ${e.message}`); + } + } + } + if (canUseTask && plantTask && this.bot.config.idleStrategy !== 'task') { + const seedId = getSeedIdByPlantName(plantTask.name); + if (seedId && (allDeadLands.length > 0 || allEmptyLands.length > 0)) { + try { + const planted = await this.autoPlantEmptyLands(allDeadLands, allEmptyLands, unlockedLandCount, seedId, plantTask.remaining || plantTask.count); + if (planted > 0) { + this.bot.log('任务', `种植 ${plantTask.name} ${planted} 块`); + this.lastTaskActionAt = Date.now(); + return; + } + } catch (e) { + this.bot.logWarn('任务', `种植失败: ${e.message}`); + } + } + } + if (allDeadLands.length > 0 || allEmptyLands.length > 0) { + try { + await this.autoPlantEmptyLands(allDeadLands, allEmptyLands, unlockedLandCount); + actions.push(`种植 ${allDeadLands.length + allEmptyLands.length} 块`); + } catch (e) { this.bot.logWarn('种植', e.message); } + } + + if(hasWork) { + this.bot.log('农场', `土地状态: ${statusParts.join(',')}${actions.length > 0 ? '。正在执行: ' + actions.join(',') : ''}`); + } + } catch (err) { + this.bot.logWarn('巡田', `检查失败: ${err.message}`); + } finally { + this.isCheckingFarm = false; + } + } + + async loop() { + while (this.farmLoopRunning) { + await this.check(); + if (!this.farmLoopRunning) break; + await sleep(this.bot.config.farmCheckInterval); + } + } + + startLoop() { + if (this.farmLoopRunning) return; + this.farmLoopRunning = true; + this.farmCheckTimer = setTimeout(() => this.loop(), 2000); + } + + stopLoop() { + this.farmLoopRunning = false; + if (this.farmCheckTimer) { clearTimeout(this.farmCheckTimer); this.farmCheckTimer = null; } + } + + lastPushTime = 0; + onLandsChangedPush(lands) { + if (this.isCheckingFarm) return; + const now = Date.now(); + if (now - this.lastPushTime < 500) return; + + this.lastPushTime = now; + this.bot.log('农场', `收到推送: ${lands.length}块土地变化,检查中...`); + + setTimeout(async () => { + if (!this.isCheckingFarm) { + await this.check(); + } + }, 100); + } +} + +module.exports = { FarmManager }; diff --git a/server/src/core/FriendManager.js b/server/src/core/FriendManager.js new file mode 100644 index 0000000..5bf3cdf --- /dev/null +++ b/server/src/core/FriendManager.js @@ -0,0 +1,497 @@ +const { PlantPhase } = require('../config'); +const { types } = require('../proto'); +const { toLong, toNum, sleep, getServerTimeSec, toTimeSec } = require('../utils'); +const { getPlantName } = require('../gameConfig'); + +const OP_NAMES = { + 10001: '收获', 10002: '铲除', 10003: '放草', 10004: '放虫', + 10005: '除草', 10006: '除虫', 10007: '浇水', 10008: '偷菜', +}; + +class FriendManager { + constructor(bot) { + this.bot = bot; + + // Internal state + this.isCheckingFriends = false; + this.isFirstFriendCheck = true; + this.friendCheckTimer = null; + this.friendLoopRunning = false; + this.lastResetDate = ''; + this.operationLimits = new Map(); + + // Listen for events + this.bot.on('operationLimits', this.updateOperationLimits.bind(this)); + this.bot.on('friendApplicationReceived', this.onFriendApplicationReceived.bind(this)); + } + + // ============ Helper Methods ============ + + checkDailyReset() { + const today = new Date().toISOString().slice(0, 10); + if (this.lastResetDate !== today) { + if (this.lastResetDate !== '') { + this.bot.log('系统', '跨日重置,已清空操作限制缓存'); + } + this.operationLimits.clear(); + this.lastResetDate = today; + } + } + + updateOperationLimits(limits) { + if (!limits || limits.length === 0) return; + this.checkDailyReset(); + for (const limit of limits) { + const id = toNum(limit.id); + if (id > 0) { + this.operationLimits.set(id, { + dayTimes: toNum(limit.day_times), + dayTimesLimit: toNum(limit.day_times_lt), + dayExpTimes: toNum(limit.day_exp_times), + dayExpTimesLimit: toNum(limit.day_ex_times_lt), + }); + } + } + } + + canGetExp(opId) { + const limit = this.operationLimits.get(opId); + if (!limit) return false; + if (limit.dayExpTimesLimit <= 0) return true; + return limit.dayExpTimes < limit.dayExpTimesLimit; + } + + canOperate(opId) { + const limit = this.operationLimits.get(opId); + if (!limit) return true; + if (limit.dayTimesLimit <= 0) return true; + return limit.dayTimes < limit.dayTimesLimit; + } + + getRemainingTimes(opId) { + const limit = this.operationLimits.get(opId); + if (!limit || limit.dayTimesLimit <= 0) return 999; + return Math.max(0, limit.dayTimesLimit - limit.dayTimes); + } + + // ============ Friend API ============ + + async getAllFriends() { + const body = types.GetAllFriendsRequest.encode(types.GetAllFriendsRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'GetAll', body); + return types.GetAllFriendsReply.decode(replyBody); + } + + async getApplications() { + const body = types.GetApplicationsRequest.encode(types.GetApplicationsRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'GetApplications', body); + return types.GetApplicationsReply.decode(replyBody); + } + + async acceptFriends(gids) { + const body = types.AcceptFriendsRequest.encode(types.AcceptFriendsRequest.create({ + friend_gids: gids.map(g => toLong(g)), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'AcceptFriends', body); + return types.AcceptFriendsReply.decode(replyBody); + } + + async enterFriendFarm(friendGid) { + const body = types.VisitEnterRequest.encode(types.VisitEnterRequest.create({ + host_gid: toLong(friendGid), + reason: 2, + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.visitpb.VisitService', 'Enter', body); + return types.VisitEnterReply.decode(replyBody); + } + + async leaveFriendFarm(friendGid) { + const body = types.VisitLeaveRequest.encode(types.VisitLeaveRequest.create({ + host_gid: toLong(friendGid), + })).finish(); + try { + await this.bot.network.sendMsgAsync('gamepb.visitpb.VisitService', 'Leave', body); + } catch (e) { } + } + + async helpWater(friendGid, landIds) { + const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body); + const reply = types.WaterLandReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async helpWeed(friendGid, landIds) { + const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body); + const reply = types.WeedOutReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async helpInsecticide(friendGid, landIds) { + const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body); + const reply = types.InsecticideReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async stealHarvest(friendGid, landIds) { + const body = types.HarvestRequest.encode(types.HarvestRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + is_all: true, + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body); + const reply = types.HarvestReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async putInsects(friendGid, landIds) { + const body = types.PutInsectsRequest.encode(types.PutInsectsRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'PutInsects', body); + const reply = types.PutInsectsReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + async putWeeds(friendGid, landIds) { + const body = types.PutWeedsRequest.encode(types.PutWeedsRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'PutWeeds', body); + const reply = types.PutWeedsReply.decode(replyBody); + this.updateOperationLimits(reply.operation_limits); + return reply; + } + + // ============ Logic ============ + + getCurrentPhase(phases) { + if (!phases || phases.length === 0) return null; + const nowSec = getServerTimeSec(); + for (let i = phases.length - 1; i >= 0; i--) { + const beginTime = toTimeSec(phases[i].begin_time); + if (beginTime > 0 && beginTime <= nowSec) { + return phases[i]; + } + } + return phases[0]; + } + + analyzeFriendLands(lands, myGid) { + const result = { + stealable: [], stealableInfo: [], + needWater: [], needWeed: [], needBug: [], + canPutWeed: [], canPutBug: [], + }; + + const nowSec = getServerTimeSec(); + + for (const land of lands) { + const id = toNum(land.id); + const plant = land.plant; + + if (!plant || !plant.phases || plant.phases.length === 0) continue; + + const currentPhase = this.getCurrentPhase(plant.phases); + if (!currentPhase) continue; + const phaseVal = currentPhase.phase; + + if (phaseVal === PlantPhase.MATURE) { + if (plant.stealable) { + result.stealable.push(id); + const plantId = toNum(plant.id); + const plantName = getPlantName(plantId) || plant.name || '未知'; + result.stealableInfo.push({ landId: id, plantId, name: plantName }); + } + continue; + } + + if (phaseVal === PlantPhase.DEAD) continue; + + const dryNum = toNum(plant.dry_num); + const dryTime = toTimeSec(currentPhase.dry_time); + if (dryNum > 0 || (dryTime > 0 && dryTime <= nowSec)) result.needWater.push(id); + + const weedsTime = toTimeSec(currentPhase.weeds_time); + const hasWeeds = (plant.weed_owners && plant.weed_owners.length > 0) || (weedsTime > 0 && weedsTime <= nowSec); + if (hasWeeds) result.needWeed.push(id); + + const insectTime = toTimeSec(currentPhase.insect_time); + const hasBugs = (plant.insect_owners && plant.insect_owners.length > 0) || (insectTime > 0 && insectTime <= nowSec); + if (hasBugs) result.needBug.push(id); + + const weedOwners = plant.weed_owners || []; + const insectOwners = plant.insect_owners || []; + const iAlreadyPutWeed = weedOwners.some(gid => toNum(gid) === myGid); + const iAlreadyPutBug = insectOwners.some(gid => toNum(gid) === myGid); + + if (weedOwners.length < 2 && !iAlreadyPutWeed) result.canPutWeed.push(id); + if (insectOwners.length < 2 && !iAlreadyPutBug) result.canPutBug.push(id); + } + return result; + } + + async visitFriend(friend, totalActions, myGid) { + const { gid, name } = friend; + + let enterReply; + try { + enterReply = await this.enterFriendFarm(gid); + } catch (e) { + this.bot.logWarn('好友', `进入 ${name} 农场失败: ${e.message}`); + return; + } + + const lands = enterReply.lands || []; + if (lands.length === 0) { + await this.leaveFriendFarm(gid); + return; + } + + const status = this.analyzeFriendLands(lands, myGid); + const actions = []; + const HELP_ONLY_WITH_EXP = true; + // Note: Logic copied from original friend.js + + if (status.needWeed.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10005); + if (shouldHelp) { + let ok = 0; + for (const landId of status.needWeed) { + try { await this.helpWeed(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`除草 ${ok} 块`); totalActions.weed += ok; } + } + } + + if (status.needBug.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10006); + if (shouldHelp) { + let ok = 0; + for (const landId of status.needBug) { + try { await this.helpInsecticide(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`除虫 ${ok} 块`); totalActions.bug += ok; } + } + } + + if (status.needWater.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10007); + if (shouldHelp) { + let ok = 0; + for (const landId of status.needWater) { + try { await this.helpWater(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`浇水 ${ok} 块`); totalActions.water += ok; } + } + } + + const canSteal = !(this.bot.config && this.bot.config.enableSteal === false); + if (canSteal && status.stealable.length > 0) { + let ok = 0; + const stolenPlants = []; + for (let i = 0; i < status.stealable.length; i++) { + const landId = status.stealable[i]; + try { + await this.stealHarvest(gid, [landId]); + ok++; + if (status.stealableInfo[i]) { + stolenPlants.push(status.stealableInfo[i].name); + } + } catch (e) { } + await sleep(100); + } + if (ok > 0) { + const plantNames = [...new Set(stolenPlants)].join('、'); + actions.push(`偷取 ${ok} 块${plantNames ? ' (' + plantNames + ')' : ''}`); + totalActions.steal += ok; + this.bot.emit('stealRecord', { name, count: ok }); + } + } + + const ENABLE_PUT_BAD_THINGS = false; // Copied from original + + if (ENABLE_PUT_BAD_THINGS && status.canPutBug.length > 0 && this.canOperate(10004)) { + let ok = 0; + const remaining = this.getRemainingTimes(10004); + const toProcess = status.canPutBug.slice(0, remaining); + for (const landId of toProcess) { + if (!this.canOperate(10004)) break; + try { await this.putInsects(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`放虫 ${ok} 块`); totalActions.putBug += ok; } + } + + if (ENABLE_PUT_BAD_THINGS && status.canPutWeed.length > 0 && this.canOperate(10003)) { + let ok = 0; + const remaining = this.getRemainingTimes(10003); + const toProcess = status.canPutWeed.slice(0, remaining); + for (const landId of toProcess) { + if (!this.canOperate(10003)) break; + try { await this.putWeeds(gid, [landId]); ok++; } catch (e) { } + await sleep(100); + } + if (ok > 0) { actions.push(`放草 ${ok} 块`); totalActions.putWeed += ok; } + } + + if (actions.length > 0) { + this.bot.log('好友', `[${name}] 执行: ${actions.join(',')}`); + } + + await this.leaveFriendFarm(gid); + } + + async check() { + if (this.isCheckingFriends || !this.bot.user.gid) return; + if (this.bot.config && this.bot.config.enableFriendOps === false) return; + this.isCheckingFriends = true; + this.checkDailyReset(); + + try { + const friendsReply = await this.getAllFriends(); + const friends = friendsReply.game_friends || []; + if (friends.length === 0) { this.bot.log('好友', '没有好友'); return; } + + const canPutBugOrWeed = this.canOperate(10004) || this.canOperate(10003); + const ENABLE_PUT_BAD_THINGS = false; + + const priorityFriends = []; + const otherFriends = []; + const visitedGids = new Set(); + + for (const f of friends) { + const gid = toNum(f.gid); + if (gid === this.bot.user.gid) continue; + if (visitedGids.has(gid)) continue; + const name = f.remark || f.name || `GID:${gid}`; + const p = f.plant; + + const stealNum = p ? toNum(p.steal_plant_num) : 0; + const dryNum = p ? toNum(p.dry_num) : 0; + const weedNum = p ? toNum(p.weed_num) : 0; + const insectNum = p ? toNum(p.insect_num) : 0; + + if (stealNum > 0 || dryNum > 0 || weedNum > 0 || insectNum > 0) { + priorityFriends.push({ gid, name }); + visitedGids.add(gid); + } else if (ENABLE_PUT_BAD_THINGS && canPutBugOrWeed) { + otherFriends.push({ gid, name }); + visitedGids.add(gid); + } + } + + const friendsToVisit = [...priorityFriends, ...otherFriends]; + if (friendsToVisit.length === 0) return; + + let totalActions = { steal: 0, water: 0, weed: 0, bug: 0, putBug: 0, putWeed: 0 }; + for (const friend of friendsToVisit) { + try { + await this.visitFriend(friend, totalActions, this.bot.user.gid); + } catch (e) { } + await sleep(500); + } + + const summary = []; + if (totalActions.steal > 0) summary.push(`偷取 ${totalActions.steal} 块`); + if (totalActions.weed > 0) summary.push(`除草 ${totalActions.weed} 块`); + if (totalActions.bug > 0) summary.push(`除虫 ${totalActions.bug} 块`); + if (totalActions.water > 0) summary.push(`浇水 ${totalActions.water} 块`); + if (totalActions.putBug > 0) summary.push(`放虫 ${totalActions.putBug} 块`); + if (totalActions.putWeed > 0) summary.push(`放草 ${totalActions.putWeed} 块`); + + if (summary.length > 0) { + this.bot.log('好友', `已巡查 ${friendsToVisit.length} 位好友。总计: ${summary.join(',')}`); + } + this.isFirstFriendCheck = false; + } catch (err) { + this.bot.logWarn('好友', `巡查失败: ${err.message}`); + } finally { + this.isCheckingFriends = false; + } + } + + async loop() { + while (this.friendLoopRunning) { + await this.check(); + if (!this.friendLoopRunning) break; + await sleep(this.bot.config.friendCheckInterval); + } + } + + startLoop() { + if (this.friendLoopRunning) return; + if (this.bot.config && this.bot.config.enableFriendOps === false) return; + this.friendLoopRunning = true; + + this.friendCheckTimer = setTimeout(() => this.loop(), 5000); + setTimeout(() => this.checkAndAcceptApplications(), 3000); + } + + stopLoop() { + this.friendLoopRunning = false; + if (this.friendCheckTimer) { clearTimeout(this.friendCheckTimer); this.friendCheckTimer = null; } + } + + // ============ Applications ============ + + onFriendApplicationReceived(applications) { + if (this.bot.config && this.bot.config.enableFriendOps === false) return; + const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', '); + this.bot.log('申请', `收到 ${applications.length} 个好友申请: ${names}`); + const gids = applications.map(a => toNum(a.gid)); + this.acceptFriendsWithRetry(gids); + } + + async checkAndAcceptApplications() { + if (this.bot.config && this.bot.config.enableFriendOps === false) return; + try { + const reply = await this.getApplications(); + const applications = reply.applications || []; + if (applications.length === 0) return; + + const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', '); + this.bot.log('申请', `发现 ${applications.length} 个待处理申请: ${names}`); + + const gids = applications.map(a => toNum(a.gid)); + await this.acceptFriendsWithRetry(gids); + } catch (e) { } + } + + async acceptFriendsWithRetry(gids) { + if (gids.length === 0) return; + try { + const reply = await this.acceptFriends(gids); + const friends = reply.friends || []; + if (friends.length > 0) { + const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', '); + this.bot.log('申请', `已同意 ${friends.length} 人: ${names}`); + } + } catch (e) { + this.bot.logWarn('申请', `同意失败: ${e.message}`); + } + } +} + +module.exports = { FriendManager }; diff --git a/server/src/core/Network.js b/server/src/core/Network.js new file mode 100644 index 0000000..55f4a8a --- /dev/null +++ b/server/src/core/Network.js @@ -0,0 +1,576 @@ +const WebSocket = require('ws'); +const EventEmitter = require('events'); +const protobuf = require('protobufjs'); +const { types } = require('../proto'); +const { toLong, toNum, log, logWarn, syncServerTime } = require('../utils'); + +function calcNormalFertilizerHours(containerCount) { + const containerNum = Number(containerCount) || 0; + return containerNum >= 3600 ? containerNum / 3600 : containerNum; +} + +function calcOrganicFertilizerHours(containerCount) { + const containerNum = Number(containerCount) || 0; + return containerNum >= 3600 ? containerNum / 3600 : containerNum; +} + +class NetworkClient extends EventEmitter { + constructor(bot) { + super(); + this.bot = bot; + this.ws = null; + this.clientSeq = 1; + this.serverSeq = 0; + this.heartbeatTimer = null; + this.pendingCallbacks = new Map(); + this.connected = false; + this.shareClaiming = false; + this.lastShareClaimAt = 0; + + // Heartbeat state + this.lastHeartbeatResponse = 0; + this.heartbeatMissCount = 0; + } + + connect(callback) { + const rawCode = typeof this.bot.config.code === 'string' ? this.bot.config.code.trim() : ''; + if (!rawCode) { + this.bot.logWarn('系统', '缺少游戏Code,无法连接'); + if (callback) callback(false); + return; + } + const serverUrl = this.bot.config.serverUrl || ''; + if (!serverUrl) { + this.bot.logWarn('系统', '缺少服务器地址,无法连接'); + if (callback) callback(false); + return; + } + let url = ''; + try { + const target = new URL(serverUrl); + const params = new URLSearchParams(); + const platform = this.bot.config.platform || 'qq'; + const os = this.bot.config.os || 'iOS'; + const ver = this.bot.config.clientVersion || ''; + params.set('platform', platform); + params.set('os', os); + params.set('ver', ver); + params.set('code', rawCode); + const openId = this.bot.config.openId || this.bot.config.openID || ''; + params.set('openID', openId); + target.search = params.toString(); + url = target.toString(); + } catch (e) { + this.bot.logWarn('系统', `服务器地址无效: ${e.message}`); + if (callback) callback(false); + return; + } + + this.bot.log('系统', `连接服务器... ${this.bot.config.platform}`); + + this.ws = new WebSocket(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13)', + 'Origin': 'https://gate-obt.nqf.qq.com', + }, + }); + + this.ws.binaryType = 'arraybuffer'; + + this.ws.on('open', () => { + this.bot.log('系统', 'WebSocket 连接成功'); + this.connected = true; + if (callback) callback(true); + }); + + this.ws.on('message', (data) => { + this.handleMessage(data); + }); + + this.ws.on('close', (code, reason) => { + this.bot.log('系统', `WebSocket 连接断开 (code=${code})`); + this.connected = false; + this.stopHeartbeat(); + this.cleanup(); + this.emit('disconnected'); + }); + + this.ws.on('error', (err) => { + this.bot.logWarn('系统', `WebSocket 错误: ${err.message}`); + this.emit('error', err); + }); + } + + cleanup() { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + this.pendingCallbacks.clear(); + } + + disconnect() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.cleanup(); + } + + startHeartbeat() { + if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); + this.lastHeartbeatResponse = Date.now(); + this.heartbeatMissCount = 0; + + this.heartbeatTimer = setInterval(() => { + if (!this.bot.user.gid) return; + + const timeSinceLastResponse = Date.now() - this.lastHeartbeatResponse; + if (timeSinceLastResponse > 60000) { + this.heartbeatMissCount++; + this.bot.logWarn('心跳', `连接可能已断开 (${Math.round(timeSinceLastResponse/1000)}s 无响应)`); + if (this.heartbeatMissCount >= 2) { + this.bot.log('心跳', '尝试重连...'); + // Cleanup pending callbacks to avoid leaks + this.pendingCallbacks.forEach((cb, seq) => { + try { cb(new Error('连接超时,已清理')); } catch (e) {} + }); + this.pendingCallbacks.clear(); + // Optional: trigger reconnect logic here or let the user handle it + } + } + + const body = types.HeartbeatRequest.encode(types.HeartbeatRequest.create({ + gid: toLong(this.bot.user.gid), + client_version: this.bot.config.clientVersion, + })).finish(); + + this.sendMsg('gamepb.userpb.UserService', 'Heartbeat', body, (err, replyBody) => { + if (err || !replyBody) return; + this.lastHeartbeatResponse = Date.now(); + this.heartbeatMissCount = 0; + try { + const reply = types.HeartbeatReply.decode(replyBody); + if (reply.server_time) { + // We still use the global syncServerTime for now as it's just a time offset + syncServerTime(toNum(reply.server_time)); + } + } catch (e) { } + }); + + }, this.bot.config.heartbeatInterval); + } + + stopHeartbeat() { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + } + + sendLogin(callback) { + const body = types.LoginRequest.encode(types.LoginRequest.create({ + sharer_id: toLong(0), + sharer_open_id: '', + device_info: { + client_version: this.bot.config.clientVersion, + sys_software: 'iOS 26.2.1', + network: 'wifi', + memory: '7672', + device_id: 'iPhone X', + }, + share_cfg_id: toLong(0), + scene_id: '1256', + report_data: { + callback: '', cd_extend_info: '', click_id: '', clue_token: '', + minigame_channel: 'other', minigame_platid: 2, req_id: '', trackid: '', + }, + })).finish(); + + this.sendMsg('gamepb.userpb.UserService', 'Login', body, (err, bodyBytes, meta) => { + if (err) { + this.bot.log('登录', `失败: ${err.message}`); + if (callback) callback(false); + return; + } + try { + const reply = types.LoginReply.decode(bodyBytes); + if (reply.basic) { + const basic = reply.basic; + this.bot.user.gid = toNum(basic.gid); + this.bot.user.name = basic.name || '未知'; + this.bot.user.level = toNum(basic.level); + this.bot.user.gold = toNum(basic.gold); + this.bot.user.exp = toNum(basic.exp); + this.bot.user.avatarUrl = basic.avatar_url; + + this.bot.log('系统', `登录成功: ${this.bot.user.name} (Lv${this.bot.user.level})`); + + if (reply.time_now_millis) { + syncServerTime(toNum(reply.time_now_millis)); + } + } + + this.startHeartbeat(); + this.bot.emit('loginSuccess', this.bot.user); + this.tryClaimShareReward(); + if (this.bot.warehouseManager) { + this.bot.warehouseManager.getBag().then((bagReply) => { + const items = this.bot.warehouseManager.getBagItems(bagReply); + let tickets = 0; + let fertilizerContainer = 0; + let organicFertilizerContainer = 0; + const fertilizerItems = { + 80001: 0, + 80002: 0, + 80003: 0, + 80004: 0, + }; + const organicFertilizerItems = { + 80011: 0, + 80012: 0, + 80013: 0, + 80014: 0, + }; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (id === 1002) tickets = count; + if (id === 1011) fertilizerContainer = count; + if (id === 1012) organicFertilizerContainer = count; + if (id === 80001) fertilizerItems[80001] = count; + if (id === 80002) fertilizerItems[80002] = count; + if (id === 80003) fertilizerItems[80003] = count; + if (id === 80004) fertilizerItems[80004] = count; + if (id === 80011) organicFertilizerItems[80011] = count; + if (id === 80012) organicFertilizerItems[80012] = count; + if (id === 80013) organicFertilizerItems[80013] = count; + if (id === 80014) organicFertilizerItems[80014] = count; + } + this.bot.user.tickets = tickets; + this.bot.user.fertilizerContainer = fertilizerContainer; + this.bot.user.fertilizerItems = fertilizerItems; + this.bot.user.fertilizerHours = calcNormalFertilizerHours(fertilizerContainer); + this.bot.user.organicFertilizerContainer = organicFertilizerContainer; + this.bot.user.organicFertilizerItems = organicFertilizerItems; + this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(organicFertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + }).catch(() => { }); + } + if (callback) callback(true); + } catch (e) { + this.bot.log('登录', `解码失败: ${e.message}`); + if (callback) callback(false); + } + }); + } + + async tryClaimShareReward(shareId = 1) { + if (this.shareClaiming) return; + const now = Date.now(); + if (now - this.lastShareClaimAt < 60000) return; + this.shareClaiming = true; + this.lastShareClaimAt = now; + try { + const canShare = await this.checkCanShareReward(); + if (!canShare) return; + await this.reportShare(shareId); + await this.claimShareReward(shareId); + this.bot.log('分享', '已领取分享奖励'); + } catch (e) { + const message = this.formatShareClaimError(e); + this.bot.logWarn('分享', message); + } finally { + this.shareClaiming = false; + } + } + + formatShareClaimError(error) { + const raw = error?.message ? String(error.message) : ''; + if (!raw) return '分享奖励领取失败'; + if (raw.includes('code=1009001') || raw.includes('已经领取')) { + return '分享奖励已领取,无需重复领取'; + } + return `领取失败: ${raw}`; + } + + decodeShareFlag(replyBody) { + if (!replyBody || replyBody.length === 0) return 0; + const reader = protobuf.Reader.create(replyBody); + let flag = 0; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const field = tag >>> 3; + if (field === 1) { + flag = Number(reader.int64()); + } else { + reader.skipType(tag & 7); + } + } + return flag; + } + + async checkCanShareReward() { + const { body: replyBody } = await this.sendMsgAsync('gamepb.sharepb.ShareService', 'CheckCanShare', Buffer.alloc(0)); + const flag = this.decodeShareFlag(replyBody); + return flag === 1; + } + + async reportShare(shareId) { + const writer = protobuf.Writer.create(); + writer.uint32(8).int64(toLong(shareId)); + const body = writer.finish(); + await this.sendMsgAsync('gamepb.sharepb.ShareService', 'ReportShare', body); + } + + async claimShareReward(shareId) { + const writer = protobuf.Writer.create(); + writer.uint32(8).int64(toLong(shareId)); + const body = writer.finish(); + await this.sendMsgAsync('gamepb.sharepb.ShareService', 'ClaimShareReward', body); + } + + encodeMsg(serviceName, methodName, bodyBytes) { + const msg = types.GateMessage.create({ + meta: { + service_name: serviceName, + method_name: methodName, + message_type: 1, + client_seq: toLong(this.clientSeq), + server_seq: toLong(this.serverSeq), + }, + body: bodyBytes || Buffer.alloc(0), + }); + const encoded = types.GateMessage.encode(msg).finish(); + this.clientSeq++; + return encoded; + } + + sendMsg(serviceName, methodName, bodyBytes, callback) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + return false; + } + const seq = this.clientSeq; + const encoded = this.encodeMsg(serviceName, methodName, bodyBytes); + if (callback) this.pendingCallbacks.set(seq, callback); + this.ws.send(encoded); + return true; + } + + sendMsgAsync(serviceName, methodName, bodyBytes, timeout = 10000) { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + reject(new Error(`连接未打开: ${methodName}`)); + return; + } + + const seq = this.clientSeq; + const timer = setTimeout(() => { + this.pendingCallbacks.delete(seq); + reject(new Error(`请求超时: ${methodName}`)); + }, timeout); + + const sent = this.sendMsg(serviceName, methodName, bodyBytes, (err, body, meta) => { + clearTimeout(timer); + if (err) reject(err); + else resolve({ body, meta }); + }); + + if (!sent) { + clearTimeout(timer); + reject(new Error(`发送失败: ${methodName}`)); + } + }); + } + + handleMessage(data) { + try { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); + const msg = types.GateMessage.decode(buf); + const meta = msg.meta; + if (!meta) return; + + if (meta.server_seq) { + const seq = toNum(meta.server_seq); + if (seq > this.serverSeq) this.serverSeq = seq; + } + + const msgType = meta.message_type; + + // Notify + if (msgType === 3) { + this.handleNotify(msg); + return; + } + + // Response + if (msgType === 2) { + const errorCode = toNum(meta.error_code); + const clientSeqVal = toNum(meta.client_seq); + + const cb = this.pendingCallbacks.get(clientSeqVal); + if (cb) { + this.pendingCallbacks.delete(clientSeqVal); + if (errorCode !== 0) { + cb(new Error(`${meta.service_name}.${meta.method_name} 错误: code=${errorCode} ${meta.error_message || ''}`)); + } else { + cb(null, msg.body, meta); + } + return; + } + } + } catch (err) { + this.bot.logWarn('解码', err.message); + } + } + + handleNotify(msg) { + if (!msg.body || msg.body.length === 0) return; + try { + const event = types.EventMessage.decode(msg.body); + const type = event.message_type || ''; + const eventBody = event.body; + + if (type.includes('Kickout')) { + this.bot.logWarn('推送', `被踢下线! ${type}`); + this.bot.emit('kickout', type); + return; + } + + if (type.includes('LandsNotify')) { + const notify = types.LandsNotify.decode(eventBody); + const hostGid = toNum(notify.host_gid); + const lands = notify.lands || []; + if (lands.length > 0) { + if (hostGid === this.bot.user.gid || hostGid === 0) { + this.bot.emit('landsChanged', lands); + } + } + return; + } + + if (type.includes('BasicNotify')) { + try { + const notify = types.BasicNotify.decode(eventBody); + if (notify.basic) { + const oldLevel = this.bot.user.level; + this.bot.user.level = toNum(notify.basic.level) || this.bot.user.level; + this.bot.user.gold = toNum(notify.basic.gold) || this.bot.user.gold; + const exp = toNum(notify.basic.exp); + if (exp > 0) this.bot.user.exp = exp; + + if (this.bot.user.level !== oldLevel) { + this.bot.log('系统', `升级! Lv${oldLevel} → Lv${this.bot.user.level}`); + } + this.bot.emit('userUpdate', this.bot.user); + } + } catch (e) { } + return; + } + + if (type.includes('FriendApplicationReceivedNotify')) { + try { + const notify = types.FriendApplicationReceivedNotify.decode(eventBody); + const applications = notify.applications || []; + if (applications.length > 0) { + this.bot.emit('friendApplicationReceived', applications); + } + } catch (e) { } + return; + } + + if (type.includes('FriendAddedNotify')) { + try { + const notify = types.FriendAddedNotify.decode(eventBody); + const friends = notify.friends || []; + if (friends.length > 0) { + const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', '); + this.bot.log('好友', `新好友: ${names}`); + } + } catch (e) { } + return; + } + + if (type.includes('ItemNotify')) { + const notify = types.ItemNotify.decode(eventBody); + const items = notify.items || []; + for (const itemChg of items) { + if (!itemChg.item) continue; + const id = toNum(itemChg.item.id); + const count = toNum(itemChg.item.count); + const delta = toNum(itemChg.delta); + if (id === 1101 || id === 2) { + this.bot.user.exp = count; + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 1 || id === 1001) { + this.bot.user.gold = count; + if (delta !== 0) { + this.bot.log('物品', `金币 ${delta > 0 ? '+' : ''}${delta} (当前: ${count})`); + } + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 1002) { + this.bot.user.tickets = count; + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 1011) { + this.bot.user.fertilizerContainer = count; + this.bot.user.fertilizerHours = calcNormalFertilizerHours(this.bot.user.fertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 1012) { + this.bot.user.organicFertilizerContainer = count; + this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(this.bot.user.organicFertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + continue; + } + if (id === 80001 || id === 80002 || id === 80003 || id === 80004) { + if (!this.bot.user.fertilizerItems) { + this.bot.user.fertilizerItems = { 80001: 0, 80002: 0, 80003: 0, 80004: 0 }; + } + this.bot.user.fertilizerItems[id] = count; + this.bot.user.fertilizerHours = calcNormalFertilizerHours(this.bot.user.fertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + } + if (id === 80011 || id === 80012 || id === 80013 || id === 80014) { + if (!this.bot.user.organicFertilizerItems) { + this.bot.user.organicFertilizerItems = { 80011: 0, 80012: 0, 80013: 0, 80014: 0 }; + } + this.bot.user.organicFertilizerItems[id] = count; + this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(this.bot.user.organicFertilizerContainer); + this.bot.emit('userUpdate', this.bot.user); + } + } + return; + } + + if (type.includes('GoodsUnlockNotify')) { + try { + const notify = types.GoodsUnlockNotify.decode(eventBody); + const goods = notify.goods_list || []; + if (goods.length > 0) { + this.bot.log('商店', `解锁 ${goods.length} 个新商品!`); + } + } catch (e) { } + return; + } + + if (type.includes('TaskInfoNotify')) { + try { + const notify = types.TaskInfoNotify.decode(eventBody); + if (notify.task_info) { + this.bot.emit('taskInfoNotify', notify.task_info); + } + } catch (e) { } + return; + } + } catch (e) { } + } +} + +module.exports = { NetworkClient }; diff --git a/server/src/core/ShopManager.js b/server/src/core/ShopManager.js new file mode 100644 index 0000000..dfb2243 --- /dev/null +++ b/server/src/core/ShopManager.js @@ -0,0 +1,80 @@ +const protobuf = require('protobufjs'); +const { types } = require('../proto'); +const { toLong, toNum, log } = require('../utils'); +const { getPlantNameBySeedId, getItemName } = require('../gameConfig'); + +class ShopManager { + constructor(bot) { + this.bot = bot; + } + + async getShopProfiles() { + const body = types.ShopProfilesRequest.encode(types.ShopProfilesRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopProfiles', body); + return types.ShopProfilesReply.decode(replyBody); + } + + async getShopInfo(shopId) { + const body = types.ShopInfoRequest.encode(types.ShopInfoRequest.create({ + shop_id: toLong(shopId) + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopInfo', body); + return types.ShopInfoReply.decode(replyBody); + } + + async buyGoods(goodsId, count, price) { + const body = types.BuyGoodsRequest.encode(types.BuyGoodsRequest.create({ + goods_id: toLong(goodsId), + num: toLong(count), + price: toLong(price) + })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'BuyGoods', body); + return types.BuyGoodsReply.decode(replyBody); + } + + buildMallPurchaseBody(itemId, count) { + const writer = protobuf.Writer.create(); + writer.uint32(8).int64(toLong(itemId)); + writer.uint32(16).int64(toLong(count)); + return writer.finish(); + } + + async purchaseMallItem(itemId, count) { + const body = this.buildMallPurchaseBody(itemId, count); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.mallpb.MallService', 'Purchase', body); + return replyBody; + } + + async getSeedShopList() { + // 1. 获取商店列表,找到种子商店 (type=2) + const profilesReply = await this.getShopProfiles(); + const profiles = profilesReply.shop_profiles || []; + const seedShop = profiles.find(p => p.shop_type === 2); // 2 usually means Seed Shop + + if (!seedShop) { + throw new Error('未找到种子商店'); + } + + // 2. 获取商品列表 + const infoReply = await this.getShopInfo(seedShop.shop_id); + const goodsList = infoReply.goods_list || []; + + // 3. 格式化数据 + return goodsList.map(g => { + const itemId = toNum(g.item_id); + const name = getPlantNameBySeedId(itemId) || getItemName(itemId) || `商品${g.id}`; + return { + goodsId: toNum(g.id), + itemId: itemId, + name: name, + price: toNum(g.price), + limitCount: toNum(g.limit_count), + boughtNum: toNum(g.bought_num), + unlocked: g.unlocked, + itemCount: toNum(g.item_count) + }; + }); + } +} + +module.exports = { ShopManager }; diff --git a/server/src/core/UpdateService.js b/server/src/core/UpdateService.js new file mode 100644 index 0000000..67ada59 --- /dev/null +++ b/server/src/core/UpdateService.js @@ -0,0 +1,85 @@ +const simpleGit = require('simple-git'); +const path = require('path'); +const { log } = require('../utils'); + +class UpdateService { + constructor() { + // 项目根目录(假设 server 目录在项目根目录下的 server 文件夹中) + this.rootDir = path.resolve(__dirname, '../../..'); + this.git = simpleGit(this.rootDir); + } + + /** + * 检查是否有更新 + * @returns {Promise<{hasUpdate: boolean, currentHash: string, remoteHash: string, logs: Array<{hash: string, date: string, message: string, author_name: string}>}>} + */ + async checkUpdate() { + try { + await this.git.fetch(); + + const status = await this.git.status(); + const currentHash = await this.git.revparse(['HEAD']); + const remoteHash = await this.git.revparse(['@{u}']); // upstream + + if (currentHash.trim() === remoteHash.trim()) { + return { + hasUpdate: false, + currentHash: currentHash.trim(), + remoteHash: remoteHash.trim(), + logs: [] + }; + } + + // 获取更新日志 + const logOptions = { + from: 'HEAD', + to: '@{u}', + format: { + hash: '%h', + date: '%ai', + message: '%s', + author_name: '%an' + } + }; + const logSummary = await this.git.log(logOptions); + + return { + hasUpdate: true, + currentHash: currentHash.trim(), + remoteHash: remoteHash.trim(), + logs: logSummary.all + }; + } catch (error) { + log('UpdateService', `检查更新失败: ${error.message}`); + throw error; + } + } + + /** + * 执行更新 (git pull) + * @returns {Promise<{success: boolean, message: string, summary: any}>} + */ + async doUpdate() { + try { + // 可以在这里添加备份逻辑,虽然 git pull 相对安全 + // 暂时只执行 pull + const pullResult = await this.git.pull(); + + log('UpdateService', `更新成功: ${JSON.stringify(pullResult.summary)}`); + + return { + success: true, + message: '代码拉取成功', + summary: pullResult.summary + }; + } catch (error) { + log('UpdateService', `更新失败: ${error.message}`); + + // 尝试回滚? 或者由用户手动处理冲突 + // 这里简单抛出错误 + throw error; + } + } +} + +module.exports = new UpdateService(); diff --git a/server/src/core/WarehouseManager.js b/server/src/core/WarehouseManager.js new file mode 100644 index 0000000..81d71eb --- /dev/null +++ b/server/src/core/WarehouseManager.js @@ -0,0 +1,325 @@ +const protobuf = require('protobufjs'); +const { types } = require('../proto'); +const { toLong, toNum, log, logWarn, emitRuntimeHint } = require('../utils'); +const { getFruitName, getPlantNameBySeedId, getItemNameById } = require('../gameConfig'); +const seedShopData = require('../../tools/seed-shop-merged-export.json'); + +// 游戏内金币和点券的物品 ID (GlobalData.GodItemId / DiamondItemId) +const GOLD_ITEM_ID = 1001; +const FRUIT_ID_SET = new Set( + ((seedShopData && seedShopData.rows) || []) + .map(row => Number(row.fruitId)) + .filter(Number.isFinite) +); + +class WarehouseManager { + constructor(bot) { + this.bot = bot; + this.sellTimer = null; + this.sellInterval = 60000; + } + + isFruitIdBySeedData(id) { + return FRUIT_ID_SET.has(toNum(id)); + } + + /** + * 从 SellReply 中提取获得的金币数量 + * 新版 SellReply 返回 get_items (repeated Item),其中 id=1001 为金币 + */ + extractGold(sellReply) { + if (sellReply.get_items && sellReply.get_items.length > 0) { + for (const item of sellReply.get_items) { + const id = toNum(item.id); + if (id === GOLD_ITEM_ID) { + return toNum(item.count); + } + } + return 0; + } + if (sellReply.gold !== undefined && sellReply.gold !== null) { + return toNum(sellReply.gold); + } + return 0; + } + + async getBag() { + const body = types.BagRequest.encode(types.BagRequest.create({})).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Bag', body); + return types.BagReply.decode(replyBody); + } + + /** + * 将 item 转为 Sell 请求所需格式(id/count/uid 保留 Long 或转成 Long,与游戏一致) + */ + toSellItem(item) { + const id = item.id != null ? toLong(item.id) : undefined; + const count = item.count != null ? toLong(item.count) : undefined; + const uid = item.uid != null ? toLong(item.uid) : undefined; + return { id, count, uid }; + } + + async sellItems(items) { + const payload = items.map(this.toSellItem); + const body = types.SellRequest.encode(types.SellRequest.create({ items: payload })).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Sell', body); + return types.SellReply.decode(replyBody); + } + + /** + * 从 BagReply 取出物品列表(兼容 item_bag 与旧版 items) + */ + getBagItems(bagReply) { + if (bagReply.item_bag && bagReply.item_bag.items && bagReply.item_bag.items.length) + return bagReply.item_bag.items; + return bagReply.items || []; + } + + /** + * 获取格式化的背包数据,用于前端显示 + */ + async getFormattedBag() { + const bagReply = await this.getBag(); + const items = this.getBagItems(bagReply); + + const seeds = []; + const produce = []; + const others = []; + + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + const uid = item.uid ? toNum(item.uid) : 0; + + // Basic item object + const itemObj = { + id, + uid, + count, + name: `物品 ${id}`, + type: 'other' + }; + + if (this.isFruitIdBySeedData(id) || id === 40416) { + itemObj.name = getFruitName(id); + itemObj.type = 'produce'; + produce.push(itemObj); + } else { + // 尝试识别为种子 + const plantName = getPlantNameBySeedId(id); + if (plantName && plantName !== `种子${id}`) { + itemObj.name = plantName + '种子'; + itemObj.type = 'seed'; + seeds.push(itemObj); + } else { + // 过滤掉货币类物品和特殊物品 + // 1001:金币, 1002:点券, 1101:种植经验, 3001:普通收藏点, 1011/1012:化肥容器 + if ([1001, 1002, 1101, 3001, 1011, 1012].includes(id)) { + continue; + } + + // 其他物品 + itemObj.name = getItemNameById(id); + others.push(itemObj); + } + } + } + + return { seeds, produce, others }; + } + + async sellAllFruits() { + if (!this.bot.network.connected) return; + + try { + const bagReply = await this.getBag(); + const items = this.getBagItems(bagReply); + + const toSell = []; + const names = []; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + const uid = item.uid ? toNum(item.uid) : 0; + if (this.isFruitIdBySeedData(id) && count > 0) { + if (uid === 0) continue; // 跳过无效格子 + toSell.push(item); + names.push(`${getFruitName(id)} ${count} 个`); + } + } + + if (toSell.length === 0) return; + + const reply = await this.sellItems(toSell); + const totalGold = this.extractGold(reply); + log('仓库', `成功出售: ${names.join(',')}。共获得 ${totalGold} 金币`); + emitRuntimeHint(false); + } catch (e) { + logWarn('仓库', `物品出售失败: ${e.message}`); + } + } + + async useItems(items) { + if (!items || items.length === 0) return { success: true, message: '没有选择物品' }; + + const results = []; + const fertilizers = new Set([80001, 80002, 80003, 80004, 80011, 80012, 80013, 80014]); + const formatGains = (replyItems, excludeId) => { + if (!replyItems || replyItems.length === 0) return ''; + const parts = []; + for (const replyItem of replyItems) { + const gainId = toNum(replyItem.id); + const gainCount = toNum(replyItem.count); + if (excludeId && gainId === excludeId) continue; + if (!gainId || !gainCount) continue; + parts.push(`${getItemNameById(gainId)} x${gainCount}`); + } + return parts.join(','); + }; + const buildGainList = (replyItems, excludeId) => { + if (!replyItems || replyItems.length === 0) return []; + const gains = []; + for (const replyItem of replyItems) { + const gainId = toNum(replyItem.id); + const gainCount = toNum(replyItem.count); + if (excludeId && gainId === excludeId) continue; + if (!gainId || !gainCount) continue; + gains.push({ id: gainId, count: gainCount }); + } + return gains; + }; + const buildItemCountMap = (bagItems) => { + const map = new Map(); + for (const bagItem of bagItems) { + const bagId = toNum(bagItem.id); + const bagCount = toNum(bagItem.count); + if (!bagId || !bagCount) continue; + map.set(bagId, (map.get(bagId) || 0) + bagCount); + } + return map; + }; + const diffGains = (beforeMap, afterMap, excludeId) => { + const parts = []; + for (const [id, afterCount] of afterMap.entries()) { + if (excludeId && id === excludeId) continue; + const beforeCount = beforeMap.get(id) || 0; + const delta = afterCount - beforeCount; + if (delta > 0) { + parts.push(`${getItemNameById(id)} x${delta}`); + } + } + return parts.join(','); + }; + const diffGainList = (beforeMap, afterMap, excludeId) => { + const gains = []; + for (const [id, afterCount] of afterMap.entries()) { + if (excludeId && id === excludeId) continue; + const beforeCount = beforeMap.get(id) || 0; + const delta = afterCount - beforeCount; + if (delta > 0) gains.push({ id, count: delta }); + } + return gains; + }; + const mergeGains = (gainMap, gains) => { + for (const gain of gains) { + if (!gain || !gain.id || !gain.count) continue; + gainMap.set(gain.id, (gainMap.get(gain.id) || 0) + gain.count); + } + }; + const totalGains = new Map(); + + for (const item of items) { + const id = Number(item.id); + const count = Number(item.count); + let uid = item.uid != null ? Number(item.uid) : 0; + const name = getItemNameById(id); + + try { + let usedCount = count; + let gainsText = ''; + let gainList = []; + if (id === 100003) { + const beforeReply = await this.getBag(); + const beforeItems = this.getBagItems(beforeReply); + const beforeMap = buildItemCountMap(beforeItems); + if (!uid) { + const bagReply = await this.getBag(); + const bagItems = this.getBagItems(bagReply); + const found = bagItems.find(bagItem => toNum(bagItem.id) === id && toNum(bagItem.uid) > 0); + if (found) uid = toNum(found.uid); + } + if (!uid) { + throw new Error('礼包缺少UID'); + } + const itemWriter = protobuf.Writer.create(); + itemWriter.uint32(8).int64(toLong(id)); + itemWriter.uint32(16).int64(toLong(count)); + itemWriter.uint32(48).int64(toLong(uid)); + const body = protobuf.Writer.create().uint32(10).bytes(itemWriter.finish()).finish(); + await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Use', body); + const afterReply = await this.getBag(); + const afterItems = this.getBagItems(afterReply); + const afterMap = buildItemCountMap(afterItems); + gainsText = diffGains(beforeMap, afterMap, id); + gainList = diffGainList(beforeMap, afterMap, id); + } else { + const useItem = { + item_id: toLong(id), + count: toLong(count) + }; + if (uid > 0) { + useItem.land_count = toLong(uid); + } + const payload = { + items: [useItem] + }; + const body = types.BatchUseRequest.encode(types.BatchUseRequest.create(payload)).finish(); + const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'BatchUse', body); + const reply = types.BatchUseReply.decode(replyBody); + gainsText = formatGains(reply.items, id); + gainList = buildGainList(reply.items, id); + } + + if (fertilizers.has(id)) { + results.push(`${name}: 已使用(消耗 ${usedCount} 个)${gainsText ? `,获得:${gainsText}` : ''}`); + } else { + results.push(`${name}: 已使用${gainsText ? `,获得:${gainsText}` : ''}`); + } + if (gainsText) { + log('仓库', `使用 ${name} 获得:${gainsText}`); + } + mergeGains(totalGains, gainList); + + } catch (e) { + logWarn('仓库', `物品使用失败 (${name}): ${e.message}`); + results.push(`${name}: 使用失败 - ${e.message}`); + } + } + + const gains = []; + for (const [id, count] of totalGains.entries()) { + gains.push({ id, name: getItemNameById(id), count }); + } + return { success: true, message: results.join('; '), gains }; + } + + startLoop(interval = 60000) { + if (this.sellTimer) return; + this.sellInterval = interval; + // 延迟启动,避免刚上线就请求 + setTimeout(() => { + if (!this.bot.isRunning) return; + this.sellAllFruits(); + this.sellTimer = setInterval(() => this.sellAllFruits(), this.sellInterval); + }, 10000); + } + + stopLoop() { + if (this.sellTimer) { + clearInterval(this.sellTimer); + this.sellTimer = null; + } + } +} + +module.exports = { WarehouseManager }; diff --git a/server/src/decode.js b/server/src/decode.js new file mode 100644 index 0000000..a00b929 --- /dev/null +++ b/server/src/decode.js @@ -0,0 +1,245 @@ +/** + * 解码/验证工具模式 + */ + +const protobuf = require('protobufjs'); +const Long = require('long'); +const { PHASE_NAMES } = require('./config'); +const { types, getRoot } = require('./proto'); +const { toNum } = require('./utils'); + +// ============ 辅助函数 ============ + +/** JSON.stringify replacer, 处理 Long 和 Buffer */ +function longReplacer(key, value) { + if (value && typeof value === 'object' && value.low !== undefined && value.high !== undefined) { + return Long.fromBits(value.low, value.high, value.unsigned).toString(); + } + if (value && value.type === 'Buffer' && Array.isArray(value.data)) { + return `<${value.data.length} bytes>`; + } + return value; +} + +/** 尝试将 bytes 解码为 UTF-8 字符串 */ +function tryDecodeString(bytes) { + try { + const str = Buffer.from(bytes).toString('utf8'); + const printable = str.split('').filter(c => c.charCodeAt(0) >= 32 || c === '\n' || c === '\r' || c === '\t').length; + if (printable > str.length * 0.8 && str.length > 0) return str; + } catch (e) {} + return null; +} + +/** 通用 protobuf 解码 (无 schema, 显示原始字段) */ +function tryGenericDecode(buf) { + console.log('=== 通用 protobuf 解码 (无schema) ==='); + try { + const reader = protobuf.Reader.create(buf); + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const fieldNum = tag >>> 3; + const wireType = tag & 7; + let value; + switch (wireType) { + case 0: value = reader.int64().toString(); console.log(` field ${fieldNum} (varint): ${value}`); break; + case 1: value = reader.fixed64().toString(); console.log(` field ${fieldNum} (fixed64): ${value}`); break; + case 2: { + const bytes = reader.bytes(); + const str = tryDecodeString(bytes); + if (str !== null) { + console.log(` field ${fieldNum} (bytes/${bytes.length}): "${str}"`); + } else { + console.log(` field ${fieldNum} (bytes/${bytes.length}): ${Buffer.from(bytes).toString('hex')}`); + } + break; + } + case 5: value = reader.float(); console.log(` field ${fieldNum} (float): ${value}`); break; + default: console.log(` field ${fieldNum} (wire ${wireType}): `); reader.skipType(wireType); break; + } + } + } catch (e) { + console.log(` 解码中断: ${e.message}`); + } +} + +// ============ 验证模式 ============ + +async function verifyMode() { + console.log('\n====== 验证模式 ======\n'); + + // Login Request + const loginB64 = 'CigKGWdhbWVwYi51c2VycGIuVXNlclNlcnZpY2USBUxvZ2luGAEgASgAEmEYACIAKjwKEDEuNi4wLjhfMjAyNTEyMjQSE1dpbmRvd3MgVW5rbm93biB4NjQqBHdpZmlQzL0BagltaWNyb3NvZnQwADoEMTI1NkIVCgASABoAIgAqBW90aGVyMAI6AEIA'; + try { + const msg = types.GateMessage.decode(Buffer.from(loginB64, 'base64')); + console.log(`[OK] Login Request: ${msg.meta.service_name}.${msg.meta.method_name} seq=${msg.meta.client_seq}`); + const req = types.LoginRequest.decode(msg.body); + console.log(` device=${req.device_info?.client_version} scene=${req.scene_id}`); + } catch (e) { console.log(`[FAIL] Login Request: ${e.message}`); } + + // AllLands Response + const allLandsB64 = 'ClwKG2dhbWVwYi5wbGFudHBiLlBsYW50U2VydmljZRIIQWxsTGFuZHMYAiAEKARCLQoJeC10cmFjZWlkEiBhOWZhNmZhZmYwZmI0ZDU5ZjQ5ZDJiZTJlYTY2NGU3NBK7BwpMCAEQARgBIARCDQgSEBwaBwjpBxDAmgxKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCNu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAIQARgBIARCDQgSEB0aBwjpBxCQoQ9KAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAMQARgBIARCDQgSEB4aBwjpBxDgpxJKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAQQARgBIARCDQgSEB8aBwjpBxCwrhVKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAUQARgBIARCDQgSECAaBwjpBxCAtRhKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCNu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAYQARgBIARCDQgSECEaBwjpBxCgwh5KAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQoPCAcgBDoHCAYQBRiIJ2ABCg8ICCAEOgcIBxAHGJBOYAEKEAgJIAQ6CAgIEAkYoJwBYAEKDggKIAQ6CAgJEAsYsOoBCg4ICyAEOggIChANGMC4AgoOCAwgBDoICAsQDxjg1AMKDggNIAQ6CAgMEBEYgPEECg4IDiAEOggIDRATGKCNBgoOCA8gBDoICA4QFRjAqQcKDggQIAQ6CAgPEBcY4MUICg4IESAEOggIEBAZGIDiCQoOCBIgBDoICBEQGxig/goKDggTIAQ6CAgSEB0YwJoMCg4IFCAEOggIExAfGOC2DQoOCBUgBDoICBQQIRiA0w4KDggWIAQ6CAgVECMYoO8PCg4IFyAEOggIFhAlGMCLEQoOCBggBDoICBcQJxjgpxISCQicThj/k+vcAxIJCJ1OGP+T69wDEgkInk4Y/5Pr3AMSCwiRThAJGP+T69wDEg0IlE4YZCCTTihkOJNOEhAIlU4QBxj/k+vcAygXOJVOEhAIlk4QCxj/k+vcAygLOJVOEhAIl04QBRj/k+vcAygFOJVOEgkImE4Y/5Pr3AMSDQiZThAMGP+T69wDKAwSCwiaThABGP+T69wDEg0Ikk4QCRj/k+vcAygJEg0Ik04YZCCTTihkOJNOEgkIm04Y/5Pr3AM='; + try { + const msg = types.GateMessage.decode(Buffer.from(allLandsB64, 'base64')); + const reply = types.AllLandsReply.decode(msg.body); + console.log(`[OK] AllLands Reply: ${reply.lands.length} 块土地`); + for (const land of reply.lands.slice(0, 3)) { + const id = toNum(land.id); + const unlocked = land.unlocked; + const plantName = land.plant?.name || '空'; + const phases = land.plant?.phases || []; + const lastPhase = phases.length > 0 ? phases[phases.length - 1].phase : -1; + console.log(` 土地#${id}: ${unlocked ? '已解锁' : '未解锁'} 植物=${plantName} 阶段=${PHASE_NAMES[lastPhase] || lastPhase}`); + } + if (reply.lands.length > 3) console.log(` ... 还有 ${reply.lands.length - 3} 块`); + } catch (e) { console.log(`[FAIL] AllLands Reply: ${e.message}`); } + + // Harvest Request + const harvestB64 = 'CiwKG2dhbWVwYi5wbGFudHBiLlBsYW50U2VydmljZRIHSGFydmVzdBgBIBsoGhIQCgYBAgMEBQYQyOHR8gMYAQ=='; + try { + const msg = types.GateMessage.decode(Buffer.from(harvestB64, 'base64')); + const req = types.HarvestRequest.decode(msg.body); + console.log(`[OK] Harvest Request: land_ids=[${req.land_ids.join(',')}] host_gid=${req.host_gid} is_all=${req.is_all}`); + } catch (e) { console.log(`[FAIL] Harvest Request: ${e.message}`); } + + console.log('\n====== 验证完成 ======\n'); +} + +// ============ 解码模式 ============ + +async function decodeMode(args) { + let inputData = ''; + let typeName = ''; + let isHex = false; + let isGateWrapped = false; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--decode') continue; + if (args[i] === '--type' && args[i + 1]) { typeName = args[++i]; continue; } + if (args[i] === '--hex') { isHex = true; continue; } + if (args[i] === '--gate') { isGateWrapped = true; continue; } + if (!inputData) inputData = args[i]; + } + + if (!inputData) { + console.log(` +PB数据解码工具 +============== + +用法: + node client.js --decode + node client.js --decode --hex + node client.js --decode --type <消息类型> + node client.js --decode --gate + +参数: + <数据> base64编码的pb数据 (默认), 或hex编码 (配合 --hex) + --hex 输入数据为hex编码 + --gate 外层是 gatepb.Message 包装, 自动解析 meta + body + --type 指定消息类型, 如: gatepb.Message, gamepb.plantpb.AllLandsReply 等 + +可用类型: + gatepb.Message / gatepb.Meta + gamepb.userpb.LoginRequest / LoginReply / HeartbeatRequest / HeartbeatReply + gamepb.plantpb.AllLandsRequest / AllLandsReply / HarvestRequest / HarvestReply + gamepb.plantpb.WaterLandRequest / WeedOutRequest / InsecticideRequest + gamepb.plantpb.PlantRequest / PlantReply / RemovePlantRequest / RemovePlantReply + gamepb.shoppb.ShopInfoRequest / ShopInfoReply / BuyGoodsRequest / BuyGoodsReply + gamepb.friendpb.GetAllRequest / GetAllReply / GameFriend + +示例: + node client.js --decode CigKGWdhbWVwYi... --gate + node client.js --decode 0a1c0a19... --hex --type gatepb.Message +`); + return; + } + + const root = getRoot(); + let buf; + try { + buf = isHex ? Buffer.from(inputData, 'hex') : Buffer.from(inputData, 'base64'); + } catch (e) { + console.error(`输入数据解码失败: ${e.message}`); + return; + } + console.log(`数据长度: ${buf.length} 字节\n`); + + // --gate: 先解析外层 gatepb.Message + if (isGateWrapped) { + try { + const msg = types.GateMessage.decode(buf); + const meta = msg.meta; + console.log('=== gatepb.Message (外层) ==='); + console.log(` service: ${meta.service_name}`); + console.log(` method: ${meta.method_name}`); + console.log(` type: ${meta.message_type} (${meta.message_type === 1 ? 'Request' : meta.message_type === 2 ? 'Response' : 'Notify'})`); + console.log(` client_seq: ${meta.client_seq}`); + console.log(` server_seq: ${meta.server_seq}`); + if (toNum(meta.error_code) !== 0) { + console.log(` error_code: ${meta.error_code}`); + console.log(` error_msg: ${meta.error_message}`); + } + console.log(''); + + if (msg.body && msg.body.length > 0) { + const svc = meta.service_name || ''; + const mtd = meta.method_name || ''; + const isReq = meta.message_type === 1; + const suffix = isReq ? 'Request' : 'Reply'; + const autoType = `${svc.replace('Service', '')}.${mtd}${suffix}`; + + let bodyType = null; + try { bodyType = root.lookupType(autoType); } catch (e) {} + if (!bodyType) { + const parts = svc.split('.'); + if (parts.length >= 2) { + const ns = parts.slice(0, parts.length - 1).join('.'); + try { bodyType = root.lookupType(`${ns}.${mtd}${suffix}`); } catch (e) {} + } + } + + if (bodyType) { + console.log(`=== ${bodyType.fullName} (body 自动推断) ===`); + const decoded = bodyType.decode(msg.body); + console.log(JSON.stringify(decoded.toJSON(), longReplacer, 2)); + } else { + console.log(`=== body (未能自动推断类型, 用 --type 手动指定 body 类型) ===`); + console.log(` hex: ${Buffer.from(msg.body).toString('hex')}`); + console.log(` base64: ${Buffer.from(msg.body).toString('base64')}`); + tryGenericDecode(msg.body); + } + } + } catch (e) { + console.error(`gatepb.Message 解码失败: ${e.message}`); + } + return; + } + + // --type: 指定类型解码 + if (typeName) { + try { + const msgType = root.lookupType(typeName); + const decoded = msgType.decode(buf); + console.log(`=== ${typeName} ===`); + console.log(JSON.stringify(decoded.toJSON(), longReplacer, 2)); + } catch (e) { + console.error(`解码失败 (${typeName}): ${e.message}`); + } + return; + } + + // 未指定类型,自动尝试 + console.log('未指定类型,自动尝试...\n'); + try { + const msg = types.GateMessage.decode(buf); + if (msg.meta && (msg.meta.service_name || msg.meta.method_name)) { + console.log('=== 检测为 gatepb.Message ==='); + console.log(JSON.stringify(msg.toJSON(), longReplacer, 2)); + return; + } + } catch (e) {} + + tryGenericDecode(buf); +} + +module.exports = { verifyMode, decodeMode }; diff --git a/server/src/farm.js b/server/src/farm.js new file mode 100644 index 0000000..a154cae --- /dev/null +++ b/server/src/farm.js @@ -0,0 +1,648 @@ +/** + * 自己的农场操作 - 收获/浇水/除草/除虫/铲除/种植/商店/巡田 + */ + +const protobuf = require('protobufjs'); +const { CONFIG, PlantPhase, PHASE_NAMES } = require('./config'); +const { types } = require('./proto'); +const { sendMsgAsync, getUserState, networkEvents } = require('./network'); +const { toLong, toNum, getServerTimeSec, toTimeSec, log, logWarn, sleep } = require('./utils'); +const { getPlantNameBySeedId, getPlantName, getPlantExp, formatGrowTime, getPlantGrowTime } = require('./gameConfig'); +const { getPlantingRecommendation } = require('../tools/calc-exp-yield'); + +// ============ 内部状态 ============ +let isCheckingFarm = false; +let isFirstFarmCheck = true; +let farmCheckTimer = null; +let farmLoopRunning = false; + +// ============ 农场 API ============ + +// 操作限制更新回调 (由 friend.js 设置) +let onOperationLimitsUpdate = null; +function setOperationLimitsCallback(callback) { + onOperationLimitsUpdate = callback; +} + +async function getAllLands() { + const body = types.AllLandsRequest.encode(types.AllLandsRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'AllLands', body); + const reply = types.AllLandsReply.decode(replyBody); + // 更新操作限制 + if (reply.operation_limits && onOperationLimitsUpdate) { + onOperationLimitsUpdate(reply.operation_limits); + } + return reply; +} + +async function harvest(landIds) { + const state = getUserState(); + const body = types.HarvestRequest.encode(types.HarvestRequest.create({ + land_ids: landIds, + host_gid: toLong(state.gid), + is_all: true, + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body); + return types.HarvestReply.decode(replyBody); +} + +async function waterLand(landIds) { + const state = getUserState(); + const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({ + land_ids: landIds, + host_gid: toLong(state.gid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body); + return types.WaterLandReply.decode(replyBody); +} + +async function weedOut(landIds) { + const state = getUserState(); + const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({ + land_ids: landIds, + host_gid: toLong(state.gid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body); + return types.WeedOutReply.decode(replyBody); +} + +async function insecticide(landIds) { + const state = getUserState(); + const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({ + land_ids: landIds, + host_gid: toLong(state.gid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body); + return types.InsecticideReply.decode(replyBody); +} + +// 普通肥料 ID +const NORMAL_FERTILIZER_ID = 1011; + +/** + * 施肥 - 必须逐块进行,服务器不支持批量 + * 游戏中拖动施肥间隔很短,这里用 50ms + */ +async function fertilize(landIds, fertilizerId = NORMAL_FERTILIZER_ID) { + let successCount = 0; + for (const landId of landIds) { + try { + const body = types.FertilizeRequest.encode(types.FertilizeRequest.create({ + land_ids: [toLong(landId)], + fertilizer_id: toLong(fertilizerId), + })).finish(); + await sendMsgAsync('gamepb.plantpb.PlantService', 'Fertilize', body); + successCount++; + } catch (e) { + // 施肥失败(可能肥料不足),停止继续 + break; + } + if (landIds.length > 1) await sleep(50); // 50ms 间隔 + } + return successCount; +} + +async function removePlant(landIds) { + const body = types.RemovePlantRequest.encode(types.RemovePlantRequest.create({ + land_ids: landIds.map(id => toLong(id)), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'RemovePlant', body); + return types.RemovePlantReply.decode(replyBody); +} + +// ============ 商店 API ============ + +async function getShopInfo(shopId) { + const body = types.ShopInfoRequest.encode(types.ShopInfoRequest.create({ + shop_id: toLong(shopId), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.shoppb.ShopService', 'ShopInfo', body); + return types.ShopInfoReply.decode(replyBody); +} + +async function buyGoods(goodsId, num, price) { + const body = types.BuyGoodsRequest.encode(types.BuyGoodsRequest.create({ + goods_id: toLong(goodsId), + num: toLong(num), + price: toLong(price), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.shoppb.ShopService', 'BuyGoods', body); + return types.BuyGoodsReply.decode(replyBody); +} + +// ============ 种植 ============ + +const plantNextAllowed = new Map(); +const PLANT_COOLDOWN_MS = 5000; +const PLANT_ALREADY_COOLDOWN_MS = 30000; + +function encodePlantRequest(seedId, landIds) { + const writer = protobuf.Writer.create(); + const itemWriter = writer.uint32(18).fork(); + itemWriter.uint32(8).int64(seedId); + const idsWriter = itemWriter.uint32(18).fork(); + for (const id of landIds) { + idsWriter.int64(id); + } + idsWriter.ldelim(); + itemWriter.ldelim(); + return writer.finish(); +} + +/** + * 种植 - 游戏中拖动种植间隔很短,这里用 50ms + */ +async function plantSeeds(seedId, landIds) { + let successCount = 0; + const now = Date.now(); + for (const landId of landIds) { + const nextAllowed = plantNextAllowed.get(landId) || 0; + if (now < nextAllowed) continue; + plantNextAllowed.set(landId, now + PLANT_COOLDOWN_MS); + try { + const body = encodePlantRequest(seedId, [landId]); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Plant', body); + types.PlantReply.decode(replyBody); + successCount++; + } catch (e) { + const msg = String(e && e.message ? e.message : e); + if (msg.includes('1001008') || msg.includes('土地已种植')) { + plantNextAllowed.set(landId, now + PLANT_ALREADY_COOLDOWN_MS); + log('种植', `土地#${landId} 已种植,跳过`); + } else { + logWarn('种植', `土地#${landId} 失败: ${msg}`); + } + } + if (landIds.length > 1) await sleep(50); // 50ms 间隔 + } + return successCount; +} + +async function findBestSeed(landsCount) { + const SEED_SHOP_ID = 2; + const shopReply = await getShopInfo(SEED_SHOP_ID); + if (!shopReply.goods_list || shopReply.goods_list.length === 0) { + logWarn('商店', '种子商店无商品'); + return null; + } + + const state = getUserState(); + const available = []; + for (const goods of shopReply.goods_list) { + if (!goods.unlocked) continue; + + let meetsConditions = true; + let requiredLevel = 0; + const conds = goods.conds || []; + for (const cond of conds) { + if (toNum(cond.type) === 1) { + requiredLevel = toNum(cond.param); + if (state.level < requiredLevel) { + meetsConditions = false; + break; + } + } + } + if (!meetsConditions) continue; + + const limitCount = toNum(goods.limit_count); + const boughtNum = toNum(goods.bought_num); + if (limitCount > 0 && boughtNum >= limitCount) continue; + + available.push({ + goods, + goodsId: toNum(goods.id), + seedId: toNum(goods.item_id), + price: toNum(goods.price), + requiredLevel, + }); + } + + if (available.length === 0) { + logWarn('商店', '没有可购买的种子'); + return null; + } + + if (CONFIG.forceLowestLevelCrop) { + available.sort((a, b) => a.requiredLevel - b.requiredLevel || a.price - b.price); + return available[0]; + } + + try { + log('商店', `等级: ${state.level},土地数量: ${landsCount}`); + + const rec = getPlantingRecommendation(state.level, landsCount == null ? 18 : landsCount, { top: 50 }); + const rankedSeedIds = rec.candidatesNoFert.map(x => x.seedId); + for (const seedId of rankedSeedIds) { + const hit = available.find(x => x.seedId === seedId); + if (hit) return hit; + } + } catch (e) { + logWarn('商店', `经验效率推荐失败,使用兜底策略: ${e.message}`); + } + + // 兜底:等级在28级以前还是白萝卜比较好,28级以上选最高等级的种子 + if(state.level && state.level <= 28){ + available.sort((a, b) => a.requiredLevel - b.requiredLevel); + }else{ + available.sort((a, b) => b.requiredLevel - a.requiredLevel); + } + return available[0]; +} + +async function autoPlantEmptyLands(deadLandIds, emptyLandIds, unlockedLandCount) { + let landsToPlant = emptyLandIds.length > 0 ? [...emptyLandIds] : []; + const state = getUserState(); + + // 1. 铲除枯死/收获残留植物(一键操作) + if (deadLandIds.length > 0) { + try { + await removePlant(deadLandIds); + log('铲除', `已铲除 ${deadLandIds.length} 块 (${deadLandIds.join(',')})`); + landsToPlant.push(...deadLandIds); + } catch (e) { + logWarn('铲除', `批量铲除失败: ${e.message}`); + // 失败时仍然尝试种植 + landsToPlant.push(...deadLandIds); + } + } + + if (landsToPlant.length === 0) return; + + if (landsToPlant.length > 1) { + landsToPlant = Array.from(new Set(landsToPlant)); + } + + if (landsToPlant.length > 0) { + const now = Date.now(); + const filtered = []; + for (const landId of landsToPlant) { + const nextAllowed = plantNextAllowed.get(landId) || 0; + if (now >= nextAllowed) { + filtered.push(landId); + } + } + landsToPlant = filtered; + } + + if (landsToPlant.length === 0) return; + + // 2. 查询种子商店 + let bestSeed; + try { + bestSeed = await findBestSeed(unlockedLandCount); + } catch (e) { + logWarn('商店', `查询失败: ${e.message}`); + return; + } + if (!bestSeed) return; + + const seedName = getPlantNameBySeedId(bestSeed.seedId); + const growTime = getPlantGrowTime(1020000 + (bestSeed.seedId - 20000)); // 转换为植物ID + const growTimeStr = growTime > 0 ? ` 生长${formatGrowTime(growTime)}` : ''; + log('商店', `最佳种子: ${seedName} (${bestSeed.seedId}) 价格=${bestSeed.price}金币${growTimeStr}`); + + // 3. 购买 + const needCount = landsToPlant.length; + const totalCost = bestSeed.price * needCount; + if (totalCost > state.gold) { + logWarn('商店', `金币不足! 需要 ${totalCost} 金币, 当前 ${state.gold} 金币`); + const canBuy = Math.floor(state.gold / bestSeed.price); + if (canBuy <= 0) return; + landsToPlant = landsToPlant.slice(0, canBuy); + log('商店', `金币有限,只种 ${canBuy} 块地`); + } + + let actualSeedId = bestSeed.seedId; + try { + const buyReply = await buyGoods(bestSeed.goodsId, landsToPlant.length, bestSeed.price); + if (buyReply.get_items && buyReply.get_items.length > 0) { + const gotItem = buyReply.get_items[0]; + const gotId = toNum(gotItem.id); + const gotCount = toNum(gotItem.count); + log('购买', `获得物品: id=${gotId} count=${gotCount}`); + if (gotId > 0) actualSeedId = gotId; + } + if (buyReply.cost_items) { + for (const item of buyReply.cost_items) { + state.gold -= toNum(item.count); + } + } + const boughtName = getPlantNameBySeedId(actualSeedId); + log('购买', `已购买 ${boughtName}种子 x${landsToPlant.length}, 花费 ${bestSeed.price * landsToPlant.length} 金币`); + } catch (e) { + logWarn('购买', e.message); + return; + } + + // 4. 种植(逐块拖动,间隔50ms) + let plantedLands = []; + try { + const planted = await plantSeeds(actualSeedId, landsToPlant); + log('种植', `已在 ${planted} 块地种植 (${landsToPlant.join(',')})`); + if (planted > 0) { + plantedLands = landsToPlant.slice(0, planted); + } + } catch (e) { + logWarn('种植', e.message); + } + +} + +// ============ 土地分析 ============ + +/** + * 根据服务器时间确定当前实际生长阶段 + */ +function getCurrentPhase(phases, debug, landLabel) { + if (!phases || phases.length === 0) return null; + + const nowSec = getServerTimeSec(); + + if (debug) { + console.log(` ${landLabel} 服务器时间=${nowSec} (${new Date(nowSec * 1000).toLocaleTimeString()})`); + for (let i = 0; i < phases.length; i++) { + const p = phases[i]; + const bt = toTimeSec(p.begin_time); + const phaseName = PHASE_NAMES[p.phase] || `阶段${p.phase}`; + const diff = bt > 0 ? (bt - nowSec) : 0; + const diffStr = diff > 0 ? `(未来 ${diff}s)` : diff < 0 ? `(已过 ${-diff}s)` : ''; + console.log(` ${landLabel} [${i}] ${phaseName}(${p.phase}) begin=${bt} ${diffStr} dry=${toTimeSec(p.dry_time)} weed=${toTimeSec(p.weeds_time)} insect=${toTimeSec(p.insect_time)}`); + } + } + + for (let i = phases.length - 1; i >= 0; i--) { + const beginTime = toTimeSec(phases[i].begin_time); + if (beginTime > 0 && beginTime <= nowSec) { + if (debug) { + console.log(` ${landLabel} → 当前阶段: ${PHASE_NAMES[phases[i].phase] || phases[i].phase}`); + } + return phases[i]; + } + } + + if (debug) { + console.log(` ${landLabel} → 所有阶段都在未来,使用第一个: ${PHASE_NAMES[phases[0].phase] || phases[0].phase}`); + } + return phases[0]; +} + +function analyzeLands(lands) { + const result = { + harvestable: [], needWater: [], needWeed: [], needBug: [], + growing: [], empty: [], dead: [], + harvestableInfo: [], // 收获植物的详细信息 { id, name, exp } + }; + + const nowSec = getServerTimeSec(); + const debug = false; + + if (debug) { + console.log(''); + console.log('========== 首次巡田详细日志 =========='); + console.log(` 服务器时间(秒): ${nowSec} (${new Date(nowSec * 1000).toLocaleString()})`); + console.log(` 总土地数: ${lands.length}`); + console.log(''); + } + + for (const land of lands) { + const id = toNum(land.id); + if (!land.unlocked) { + if (debug) console.log(` 土地#${id}: 未解锁`); + continue; + } + + const plant = land.plant; + if (!plant) { + result.empty.push(id); + if (debug) console.log(` 土地#${id}: 空地`); + continue; + } + if (!plant.phases || plant.phases.length === 0) { + const plantId = toNum(plant.id); + if (plantId > 0 || plant.name) { + result.growing.push(id); + if (debug) console.log(` 土地#${id}: 生长中(阶段未知)`); + } else { + result.empty.push(id); + if (debug) console.log(` 土地#${id}: 空地`); + } + continue; + } + + const plantName = plant.name || '未知作物'; + const landLabel = `土地#${id}(${plantName})`; + + if (debug) { + console.log(` ${landLabel}: phases=${plant.phases.length} dry_num=${toNum(plant.dry_num)} weed_owners=${(plant.weed_owners||[]).length} insect_owners=${(plant.insect_owners||[]).length}`); + } + + const currentPhase = getCurrentPhase(plant.phases, debug, landLabel); + if (!currentPhase) { + result.empty.push(id); + continue; + } + const phaseVal = currentPhase.phase; + + if (phaseVal === PlantPhase.DEAD) { + result.dead.push(id); + if (debug) console.log(` → 结果: 枯死`); + continue; + } + + if (phaseVal === PlantPhase.MATURE) { + result.harvestable.push(id); + // 收集植物信息用于日志 + const plantId = toNum(plant.id); + const plantNameFromConfig = getPlantName(plantId); + const plantExp = getPlantExp(plantId); + result.harvestableInfo.push({ + landId: id, + plantId, + name: plantNameFromConfig || plantName, + exp: plantExp, + }); + if (debug) console.log(` → 结果: 可收获 (${plantNameFromConfig} +${plantExp}经验)`); + continue; + } + + let landNeeds = []; + const dryNum = toNum(plant.dry_num); + const dryTime = toTimeSec(currentPhase.dry_time); + if (dryNum > 0 || (dryTime > 0 && dryTime <= nowSec)) { + result.needWater.push(id); + landNeeds.push('缺水'); + } + + const weedsTime = toTimeSec(currentPhase.weeds_time); + const hasWeeds = (plant.weed_owners && plant.weed_owners.length > 0) || (weedsTime > 0 && weedsTime <= nowSec); + if (hasWeeds) { + result.needWeed.push(id); + landNeeds.push('有草'); + } + + const insectTime = toTimeSec(currentPhase.insect_time); + const hasBugs = (plant.insect_owners && plant.insect_owners.length > 0) || (insectTime > 0 && insectTime <= nowSec); + if (hasBugs) { + result.needBug.push(id); + landNeeds.push('有虫'); + } + + result.growing.push(id); + if (debug) { + const needStr = landNeeds.length > 0 ? ` 需要: ${landNeeds.join(',')}` : ''; + console.log(` → 结果: 生长中(${PHASE_NAMES[phaseVal] || phaseVal})${needStr}`); + } + } + + if (debug) { + console.log(''); + console.log('========== 巡田分析汇总 =========='); + console.log(` 可收获: ${result.harvestable.length} [${result.harvestable.join(',')}]`); + console.log(` 生长中: ${result.growing.length} [${result.growing.join(',')}]`); + console.log(` 缺水: ${result.needWater.length} [${result.needWater.join(',')}]`); + console.log(` 有草: ${result.needWeed.length} [${result.needWeed.join(',')}]`); + console.log(` 有虫: ${result.needBug.length} [${result.needBug.join(',')}]`); + console.log(` 空地: ${result.empty.length} [${result.empty.join(',')}]`); + console.log(` 枯死: ${result.dead.length} [${result.dead.join(',')}]`); + console.log('===================================='); + console.log(''); + } + + return result; +} + +// ============ 巡田主循环 ============ + +async function checkFarm() { + const state = getUserState(); + if (isCheckingFarm || !state.gid) return; + isCheckingFarm = true; + + try { + const landsReply = await getAllLands(); + if (!landsReply.lands || landsReply.lands.length === 0) { + log('农场', '没有土地数据'); + return; + } + + const lands = landsReply.lands; + const status = analyzeLands(lands); + const unlockedLandCount = lands.filter(land => land && land.unlocked).length; + isFirstFarmCheck = false; + + // 构建状态摘要 + const statusParts = []; + if (status.harvestable.length) statusParts.push(`收:${status.harvestable.length}`); + if (status.needWeed.length) statusParts.push(`草:${status.needWeed.length}`); + if (status.needBug.length) statusParts.push(`虫:${status.needBug.length}`); + if (status.needWater.length) statusParts.push(`水:${status.needWater.length}`); + if (status.dead.length) statusParts.push(`枯:${status.dead.length}`); + if (status.empty.length) statusParts.push(`空:${status.empty.length}`); + statusParts.push(`长:${status.growing.length}`); + + const hasWork = status.harvestable.length || status.needWeed.length || status.needBug.length + || status.needWater.length || status.dead.length || status.empty.length; + + // 执行操作并收集结果 + const actions = []; + + // 一键操作:除草、除虫、浇水可以并行执行(游戏中都是一键完成) + const batchOps = []; + if (status.needWeed.length > 0) { + batchOps.push(weedOut(status.needWeed).then(() => actions.push(`除草${status.needWeed.length}`)).catch(e => logWarn('除草', e.message))); + } + if (status.needBug.length > 0) { + batchOps.push(insecticide(status.needBug).then(() => actions.push(`除虫${status.needBug.length}`)).catch(e => logWarn('除虫', e.message))); + } + if (status.needWater.length > 0) { + batchOps.push(waterLand(status.needWater).then(() => actions.push(`浇水${status.needWater.length}`)).catch(e => logWarn('浇水', e.message))); + } + if (batchOps.length > 0) { + await Promise.all(batchOps); + } + + // 收获(一键操作) + let harvestedLandIds = []; + if (status.harvestable.length > 0) { + try { + await harvest(status.harvestable); + actions.push(`收获${status.harvestable.length}`); + harvestedLandIds = [...status.harvestable]; + } catch (e) { logWarn('收获', e.message); } + } + + // 铲除 + 种植 + 施肥(需要顺序执行) + const allDeadLands = [...status.dead, ...harvestedLandIds]; + const allEmptyLands = [...status.empty]; + if (allDeadLands.length > 0 || allEmptyLands.length > 0) { + try { + await autoPlantEmptyLands(allDeadLands, allEmptyLands, unlockedLandCount); + actions.push(`种植${allDeadLands.length + allEmptyLands.length}`); + } catch (e) { logWarn('种植', e.message); } + } + + // 输出一行日志 + const actionStr = actions.length > 0 ? ` → ${actions.join('/')}` : ''; + if(hasWork) { + log('农场', `[${statusParts.join(' ')}]${actionStr}${!hasWork ? ' 无需操作' : ''}`) + } + } catch (err) { + logWarn('巡田', `检查失败: ${err.message}`); + } finally { + isCheckingFarm = false; + } +} + +/** + * 农场巡查循环 - 本次完成后等待指定秒数再开始下次 + */ +async function farmCheckLoop() { + while (farmLoopRunning) { + await checkFarm(); + if (!farmLoopRunning) break; + await sleep(CONFIG.farmCheckInterval); + } +} + +function startFarmCheckLoop() { + if (farmLoopRunning) return; + farmLoopRunning = true; + + // 监听服务器推送的土地变化事件 + networkEvents.on('landsChanged', onLandsChangedPush); + + // 延迟 2 秒后启动循环 + farmCheckTimer = setTimeout(() => farmCheckLoop(), 2000); +} + +/** + * 处理服务器推送的土地变化 + */ +let lastPushTime = 0; +function onLandsChangedPush(lands) { + if (isCheckingFarm) return; + const now = Date.now(); + if (now - lastPushTime < 500) return; // 500ms 防抖 + + lastPushTime = now; + log('农场', `收到推送: ${lands.length}块土地变化,检查中...`); + + setTimeout(async () => { + if (!isCheckingFarm) { + await checkFarm(); + } + }, 100); +} + +function stopFarmCheckLoop() { + farmLoopRunning = false; + if (farmCheckTimer) { clearTimeout(farmCheckTimer); farmCheckTimer = null; } + networkEvents.removeListener('landsChanged', onLandsChangedPush); +} + +module.exports = { + checkFarm, startFarmCheckLoop, stopFarmCheckLoop, + getCurrentPhase, + setOperationLimitsCallback, +}; diff --git a/server/src/friend.js b/server/src/friend.js new file mode 100644 index 0000000..abf84db --- /dev/null +++ b/server/src/friend.js @@ -0,0 +1,650 @@ +/** + * 好友农场操作 - 进入/离开/帮忙/偷菜/巡查 + */ + +const { CONFIG, PlantPhase, PHASE_NAMES } = require('./config'); +const { types } = require('./proto'); +const { sendMsgAsync, getUserState, networkEvents } = require('./network'); +const { toLong, toNum, getServerTimeSec, log, logWarn, sleep } = require('./utils'); +const { getCurrentPhase, setOperationLimitsCallback } = require('./farm'); +const { getPlantName } = require('./gameConfig'); + +// ============ 内部状态 ============ +let isCheckingFriends = false; +let isFirstFriendCheck = true; +let friendCheckTimer = null; +let friendLoopRunning = false; +let lastResetDate = ''; // 上次重置日期 (YYYY-MM-DD) + +// 操作限制状态 (从服务器响应中更新) +// 操作类型ID (根据游戏代码): +// 10001 = 收获, 10002 = 铲除, 10003 = 放草, 10004 = 放虫 +// 10005 = 除草(帮好友), 10006 = 除虫(帮好友), 10007 = 浇水(帮好友), 10008 = 偷菜 +const operationLimits = new Map(); + +// 操作类型名称映射 +const OP_NAMES = { + 10001: '收获', + 10002: '铲除', + 10003: '放草', + 10004: '放虫', + 10005: '除草', + 10006: '除虫', + 10007: '浇水', + 10008: '偷菜', +}; + +// 配置: 是否只在有经验时才帮助好友 +const HELP_ONLY_WITH_EXP = true; // !!!无效,暂时无法判断。有修复方法但是暂时没打算更新出来 + +// 配置: 是否启用放虫放草功能 +const ENABLE_PUT_BAD_THINGS = false; // 无效!!!开启后会多次访问朋友导致被拉黑 请勿更改暂时关闭放虫放草功能 + +// ============ 好友 API ============ + +async function getAllFriends() { + const body = types.GetAllFriendsRequest.encode(types.GetAllFriendsRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'GetAll', body); + return types.GetAllFriendsReply.decode(replyBody); +} + +// ============ 好友申请 API (微信同玩) ============ + +async function getApplications() { + const body = types.GetApplicationsRequest.encode(types.GetApplicationsRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'GetApplications', body); + return types.GetApplicationsReply.decode(replyBody); +} + +async function acceptFriends(gids) { + const body = types.AcceptFriendsRequest.encode(types.AcceptFriendsRequest.create({ + friend_gids: gids.map(g => toLong(g)), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'AcceptFriends', body); + return types.AcceptFriendsReply.decode(replyBody); +} + +async function enterFriendFarm(friendGid) { + const body = types.VisitEnterRequest.encode(types.VisitEnterRequest.create({ + host_gid: toLong(friendGid), + reason: 2, // ENTER_REASON_FRIEND + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.visitpb.VisitService', 'Enter', body); + return types.VisitEnterReply.decode(replyBody); +} + +async function leaveFriendFarm(friendGid) { + const body = types.VisitLeaveRequest.encode(types.VisitLeaveRequest.create({ + host_gid: toLong(friendGid), + })).finish(); + try { + await sendMsgAsync('gamepb.visitpb.VisitService', 'Leave', body); + } catch (e) { /* 离开失败不影响主流程 */ } +} + +/** + * 检查是否需要重置每日限制 (0点刷新) + */ +function checkDailyReset() { + const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD + if (lastResetDate !== today) { + if (lastResetDate !== '') { + log('系统', '跨日重置,清空操作限制缓存'); + } + operationLimits.clear(); + lastResetDate = today; + } +} + +/** + * 更新操作限制状态 + */ +function updateOperationLimits(limits) { + if (!limits || limits.length === 0) return; + checkDailyReset(); + for (const limit of limits) { + const id = toNum(limit.id); + if (id > 0) { + const data = { + dayTimes: toNum(limit.day_times), + dayTimesLimit: toNum(limit.day_times_lt), + dayExpTimes: toNum(limit.day_exp_times), + dayExpTimesLimit: toNum(limit.day_ex_times_lt), // 注意: 字段名是 day_ex_times_lt (少个p) + }; + operationLimits.set(id, data); + } + } +} + +/** + * 检查某操作是否还能获得经验 + */ +function canGetExp(opId) { + const limit = operationLimits.get(opId); + if (!limit) return false; // 没有限制信息,保守起见不帮助(等待农场检查获取限制) + if (limit.dayExpTimesLimit <= 0) return true; // 没有经验上限 + return limit.dayExpTimes < limit.dayExpTimesLimit; +} + +/** + * 检查某操作是否还有次数 + */ +function canOperate(opId) { + const limit = operationLimits.get(opId); + if (!limit) return true; + if (limit.dayTimesLimit <= 0) return true; + return limit.dayTimes < limit.dayTimesLimit; +} + +/** + * 获取某操作剩余次数 + */ +function getRemainingTimes(opId) { + const limit = operationLimits.get(opId); + if (!limit || limit.dayTimesLimit <= 0) return 999; + return Math.max(0, limit.dayTimesLimit - limit.dayTimes); +} + +/** + * 获取操作限制摘要 (用于日志显示) + */ +function getOperationLimitsSummary() { + const parts = []; + // 帮助好友操作 (10005=除草, 10006=除虫, 10007=浇水, 10008=偷菜) + for (const id of [10005, 10006, 10007, 10008]) { + const limit = operationLimits.get(id); + if (limit && limit.dayExpTimesLimit > 0) { + const name = OP_NAMES[id] || `#${id}`; + const expLeft = limit.dayExpTimesLimit - limit.dayExpTimes; + parts.push(`${name}${expLeft}/${limit.dayExpTimesLimit}`); + } + } + // 捣乱操作 (10003=放草, 10004=放虫) + for (const id of [10003, 10004]) { + const limit = operationLimits.get(id); + if (limit && limit.dayTimesLimit > 0) { + const name = OP_NAMES[id] || `#${id}`; + const left = limit.dayTimesLimit - limit.dayTimes; + parts.push(`${name}${left}/${limit.dayTimesLimit}`); + } + } + return parts; +} + +async function helpWater(friendGid, landIds) { + const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body); + const reply = types.WaterLandReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function helpWeed(friendGid, landIds) { + const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body); + const reply = types.WeedOutReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function helpInsecticide(friendGid, landIds) { + const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body); + const reply = types.InsecticideReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function stealHarvest(friendGid, landIds) { + const body = types.HarvestRequest.encode(types.HarvestRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + is_all: true, + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body); + const reply = types.HarvestReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function putInsects(friendGid, landIds) { + const body = types.PutInsectsRequest.encode(types.PutInsectsRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'PutInsects', body); + const reply = types.PutInsectsReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +async function putWeeds(friendGid, landIds) { + const body = types.PutWeedsRequest.encode(types.PutWeedsRequest.create({ + land_ids: landIds, + host_gid: toLong(friendGid), + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'PutWeeds', body); + const reply = types.PutWeedsReply.decode(replyBody); + updateOperationLimits(reply.operation_limits); + return reply; +} + +// ============ 好友土地分析 ============ + +// 调试开关 - 设为好友名字可只查看该好友的土地分析详情,设为 true 查看全部,false 关闭 +const DEBUG_FRIEND_LANDS = false; + +function analyzeFriendLands(lands, myGid, friendName = '') { + const result = { + stealable: [], // 可偷 + stealableInfo: [], // 可偷植物信息 { landId, plantId, name } + needWater: [], // 需要浇水 + needWeed: [], // 需要除草 + needBug: [], // 需要除虫 + canPutWeed: [], // 可以放草 + canPutBug: [], // 可以放虫 + }; + + for (const land of lands) { + const id = toNum(land.id); + const plant = land.plant; + // 是否显示此好友的调试信息 + const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === friendName; + + if (!plant || !plant.phases || plant.phases.length === 0) { + if (showDebug) console.log(` [${friendName}] 土地#${id}: 无植物或无阶段数据`); + continue; + } + + const currentPhase = getCurrentPhase(plant.phases, showDebug, `[${friendName}]土地#${id}`); + if (!currentPhase) { + if (showDebug) console.log(` [${friendName}] 土地#${id}: getCurrentPhase返回null`); + continue; + } + const phaseVal = currentPhase.phase; + + if (showDebug) { + const insectOwners = plant.insect_owners || []; + const weedOwners = plant.weed_owners || []; + console.log(` [${friendName}] 土地#${id}: phase=${phaseVal} stealable=${plant.stealable} dry=${toNum(plant.dry_num)} weed=${weedOwners.length} bug=${insectOwners.length}`); + } + + if (phaseVal === PlantPhase.MATURE) { + if (plant.stealable) { + result.stealable.push(id); + const plantId = toNum(plant.id); + const plantName = getPlantName(plantId) || plant.name || '未知'; + result.stealableInfo.push({ landId: id, plantId, name: plantName }); + } else if (showDebug) { + console.log(` [${friendName}] 土地#${id}: 成熟但stealable=false (可能已被偷过)`); + } + continue; + } + + if (phaseVal === PlantPhase.DEAD) continue; + + // 帮助操作 + if (toNum(plant.dry_num) > 0) result.needWater.push(id); + if (plant.weed_owners && plant.weed_owners.length > 0) result.needWeed.push(id); + if (plant.insect_owners && plant.insect_owners.length > 0) result.needBug.push(id); + + // 捣乱操作: 检查是否可以放草/放虫 + // 条件: 没有草且我没放过草 + const weedOwners = plant.weed_owners || []; + const insectOwners = plant.insect_owners || []; + const iAlreadyPutWeed = weedOwners.some(gid => toNum(gid) === myGid); + const iAlreadyPutBug = insectOwners.some(gid => toNum(gid) === myGid); + + // 每块地最多2个草/虫,且我没放过 + if (weedOwners.length < 2 && !iAlreadyPutWeed) { + result.canPutWeed.push(id); + } + if (insectOwners.length < 2 && !iAlreadyPutBug) { + result.canPutBug.push(id); + } + } + return result; +} + +// ============ 拜访好友 ============ + +async function visitFriend(friend, totalActions, myGid) { + const { gid, name } = friend; + const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === name; + + if (showDebug) { + console.log(`\n========== 调试: 进入好友 [${name}] 农场 ==========`); + } + + let enterReply; + try { + enterReply = await enterFriendFarm(gid); + } catch (e) { + logWarn('好友', `进入 ${name} 农场失败: ${e.message}`); + return; + } + + const lands = enterReply.lands || []; + if (showDebug) { + console.log(` [${name}] 获取到 ${lands.length} 块土地`); + } + if (lands.length === 0) { + await leaveFriendFarm(gid); + return; + } + + const status = analyzeFriendLands(lands, myGid, name); + + if (showDebug) { + console.log(` [${name}] 分析结果: 可偷=${status.stealable.length} 浇水=${status.needWater.length} 除草=${status.needWeed.length} 除虫=${status.needBug.length}`); + console.log(`========== 调试结束 ==========\n`); + } + + // 执行操作 + const actions = []; + + // 帮助操作: 只在有经验时执行 (如果启用了 HELP_ONLY_WITH_EXP) + if (status.needWeed.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10005); // 10005=除草 + if (shouldHelp) { + let ok = 0; + for (const landId of status.needWeed) { + try { await helpWeed(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`草${ok}`); totalActions.weed += ok; } + } + } + + if (status.needBug.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10006); // 10006=除虫 + if (shouldHelp) { + let ok = 0; + for (const landId of status.needBug) { + try { await helpInsecticide(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`虫${ok}`); totalActions.bug += ok; } + } + } + + if (status.needWater.length > 0) { + const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10007); // 10007=浇水 + if (shouldHelp) { + let ok = 0; + for (const landId of status.needWater) { + try { await helpWater(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`水${ok}`); totalActions.water += ok; } + } + } + + // 偷菜: 始终执行 + if (status.stealable.length > 0) { + let ok = 0; + const stolenPlants = []; + for (let i = 0; i < status.stealable.length; i++) { + const landId = status.stealable[i]; + try { + await stealHarvest(gid, [landId]); + ok++; + if (status.stealableInfo[i]) { + stolenPlants.push(status.stealableInfo[i].name); + } + } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { + const plantNames = [...new Set(stolenPlants)].join('/'); + actions.push(`偷${ok}${plantNames ? '(' + plantNames + ')' : ''}`); + totalActions.steal += ok; + } + } + + // 捣乱操作: 放虫(10004)/放草(10003) + if (ENABLE_PUT_BAD_THINGS && status.canPutBug.length > 0 && canOperate(10004)) { + let ok = 0; + const remaining = getRemainingTimes(10004); + const toProcess = status.canPutBug.slice(0, remaining); + for (const landId of toProcess) { + if (!canOperate(10004)) break; + try { await putInsects(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`放虫${ok}`); totalActions.putBug += ok; } + } + + if (ENABLE_PUT_BAD_THINGS && status.canPutWeed.length > 0 && canOperate(10003)) { + let ok = 0; + const remaining = getRemainingTimes(10003); + const toProcess = status.canPutWeed.slice(0, remaining); + for (const landId of toProcess) { + if (!canOperate(10003)) break; + try { await putWeeds(gid, [landId]); ok++; } catch (e) { /* ignore */ } + await sleep(100); + } + if (ok > 0) { actions.push(`放草${ok}`); totalActions.putWeed += ok; } + } + + if (actions.length > 0) { + log('好友', `${name}: ${actions.join('/')}`); + } + + await leaveFriendFarm(gid); +} + +// ============ 好友巡查主循环 ============ + +async function checkFriends() { + const state = getUserState(); + if (isCheckingFriends || !state.gid) return; + isCheckingFriends = true; + + // 检查是否跨日需要重置 + checkDailyReset(); + + // 经验限制状态(移到有操作时才显示) + + try { + const friendsReply = await getAllFriends(); + const friends = friendsReply.game_friends || []; + if (friends.length === 0) { log('好友', '没有好友'); return; } + + // 检查是否还有捣乱次数 (放虫/放草) + const canPutBugOrWeed = canOperate(10004) || canOperate(10003); // 10004=放虫, 10003=放草 + + // 分两类:有预览信息的优先访问,其他的放后面(用于放虫放草) + const priorityFriends = []; // 有可偷/可帮助的好友 + const otherFriends = []; // 其他好友(仅用于放虫放草) + const visitedGids = new Set(); + + for (const f of friends) { + const gid = toNum(f.gid); + if (gid === state.gid) continue; + if (visitedGids.has(gid)) continue; + const name = f.remark || f.name || `GID:${gid}`; + const p = f.plant; + + const stealNum = p ? toNum(p.steal_plant_num) : 0; + const dryNum = p ? toNum(p.dry_num) : 0; + const weedNum = p ? toNum(p.weed_num) : 0; + const insectNum = p ? toNum(p.insect_num) : 0; + + // 调试:显示指定好友的预览信息 + const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === name; + if (showDebug) { + console.log(`[调试] 好友列表预览 [${name}]: steal=${stealNum} dry=${dryNum} weed=${weedNum} insect=${insectNum}`); + } + + // 只加入有预览信息的好友 + if (stealNum > 0 || dryNum > 0 || weedNum > 0 || insectNum > 0) { + priorityFriends.push({ gid, name }); + visitedGids.add(gid); + if (showDebug) { + console.log(`[调试] 好友 [${name}] 加入优先列表 (位置: ${priorityFriends.length})`); + } + } else if (ENABLE_PUT_BAD_THINGS && canPutBugOrWeed) { + // 没有预览信息但可以放虫放草(仅在开启放虫放草功能时) + otherFriends.push({ gid, name }); + visitedGids.add(gid); + } + } + + // 合并列表:优先好友在前 + const friendsToVisit = [...priorityFriends, ...otherFriends]; + + // 调试:检查目标好友位置 + if (DEBUG_FRIEND_LANDS && typeof DEBUG_FRIEND_LANDS === 'string') { + const idx = friendsToVisit.findIndex(f => f.name === DEBUG_FRIEND_LANDS); + if (idx >= 0) { + const inPriority = idx < priorityFriends.length; + console.log(`[调试] 好友 [${DEBUG_FRIEND_LANDS}] 位置: ${idx + 1}/${friendsToVisit.length} (${inPriority ? '优先列表' : '其他列表'})`); + } else { + console.log(`[调试] 好友 [${DEBUG_FRIEND_LANDS}] 不在待访问列表中!`); + } + } + + if (friendsToVisit.length === 0) { + // 无需操作时不输出日志 + return; + } + + let totalActions = { steal: 0, water: 0, weed: 0, bug: 0, putBug: 0, putWeed: 0 }; + for (let i = 0; i < friendsToVisit.length; i++) { + const friend = friendsToVisit[i]; + const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === friend.name; + if (showDebug) { + console.log(`[调试] 准备访问 [${friend.name}] (${i + 1}/${friendsToVisit.length})`); + } + try { + await visitFriend(friend, totalActions, state.gid); + } catch (e) { + if (showDebug) { + console.log(`[调试] 访问 [${friend.name}] 出错: ${e.message}`); + } + } + await sleep(500); + // 如果捣乱次数用完了,且没有其他操作,可以提前结束 + if (!canOperate(10004) && !canOperate(10003)) { // 10004=放虫, 10003=放草 + // 继续巡查,但不再放虫放草 + } + } + + // 只在有操作时输出日志 + const summary = []; + if (totalActions.steal > 0) summary.push(`偷${totalActions.steal}`); + if (totalActions.weed > 0) summary.push(`除草${totalActions.weed}`); + if (totalActions.bug > 0) summary.push(`除虫${totalActions.bug}`); + if (totalActions.water > 0) summary.push(`浇水${totalActions.water}`); + if (totalActions.putBug > 0) summary.push(`放虫${totalActions.putBug}`); + if (totalActions.putWeed > 0) summary.push(`放草${totalActions.putWeed}`); + + if (summary.length > 0) { + log('好友', `巡查 ${friendsToVisit.length} 人 → ${summary.join('/')}`); + } + isFirstFriendCheck = false; + } catch (err) { + logWarn('好友', `巡查失败: ${err.message}`); + } finally { + isCheckingFriends = false; + } +} + +/** + * 好友巡查循环 - 本次完成后等待指定秒数再开始下次 + */ +async function friendCheckLoop() { + while (friendLoopRunning) { + await checkFriends(); + if (!friendLoopRunning) break; + await sleep(CONFIG.friendCheckInterval); + } +} + +function startFriendCheckLoop() { + if (friendLoopRunning) return; + friendLoopRunning = true; + + // 注册操作限制更新回调,从农场检查中获取限制信息 + setOperationLimitsCallback(updateOperationLimits); + + // 监听好友申请推送 (微信同玩) + networkEvents.on('friendApplicationReceived', onFriendApplicationReceived); + + // 延迟 5 秒后启动循环,等待登录和首次农场检查完成 + friendCheckTimer = setTimeout(() => friendCheckLoop(), 5000); + + // 启动时检查一次待处理的好友申请 + setTimeout(() => checkAndAcceptApplications(), 3000); +} + +function stopFriendCheckLoop() { + friendLoopRunning = false; + networkEvents.off('friendApplicationReceived', onFriendApplicationReceived); + if (friendCheckTimer) { clearTimeout(friendCheckTimer); friendCheckTimer = null; } +} + +// ============ 自动同意好友申请 (微信同玩) ============ + +/** + * 处理服务器推送的好友申请 + */ +function onFriendApplicationReceived(applications) { + const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', '); + log('申请', `收到 ${applications.length} 个好友申请: ${names}`); + + // 自动同意 + const gids = applications.map(a => toNum(a.gid)); + acceptFriendsWithRetry(gids); +} + +/** + * 检查并同意所有待处理的好友申请 + */ +async function checkAndAcceptApplications() { + try { + const reply = await getApplications(); + const applications = reply.applications || []; + if (applications.length === 0) return; + + const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', '); + log('申请', `发现 ${applications.length} 个待处理申请: ${names}`); + + const gids = applications.map(a => toNum(a.gid)); + await acceptFriendsWithRetry(gids); + } catch (e) { + // 静默失败,可能是 QQ 平台不支持 + } +} + +/** + * 同意好友申请 (带重试) + */ +async function acceptFriendsWithRetry(gids) { + if (gids.length === 0) return; + try { + const reply = await acceptFriends(gids); + const friends = reply.friends || []; + if (friends.length > 0) { + const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', '); + log('申请', `已同意 ${friends.length} 人: ${names}`); + } + } catch (e) { + logWarn('申请', `同意失败: ${e.message}`); + } +} + +module.exports = { + checkFriends, startFriendCheckLoop, stopFriendCheckLoop, + checkAndAcceptApplications, +}; diff --git a/server/src/gameConfig.js b/server/src/gameConfig.js new file mode 100644 index 0000000..1264dd7 --- /dev/null +++ b/server/src/gameConfig.js @@ -0,0 +1,269 @@ +/** + * 游戏配置数据模块 + * 从 gameConfig 目录加载配置数据 + */ + +const fs = require('fs'); +const path = require('path'); + +// ============ 等级经验表 ============ +let roleLevelConfig = null; +let levelExpTable = null; // 累计经验表,索引为等级 + +// ============ 植物配置 ============ +let plantConfig = null; +let plantMap = new Map(); // id -> plant +let seedToPlant = new Map(); // seed_id -> plant +let fruitToPlant = new Map(); // fruit_id -> plant (果实ID -> 植物) +let nameToPlant = new Map(); + +/** + * 加载配置文件 + */ +function loadConfigs() { + const configDir = path.join(__dirname, '..', 'gameConfig'); + + // 加载等级经验配置 + try { + const roleLevelPath = path.join(configDir, 'RoleLevel.json'); + if (fs.existsSync(roleLevelPath)) { + roleLevelConfig = JSON.parse(fs.readFileSync(roleLevelPath, 'utf8')); + // 构建累计经验表 + levelExpTable = []; + for (const item of roleLevelConfig) { + levelExpTable[item.level] = item.exp; + } + console.log(`[配置] 已加载等级经验表 (${roleLevelConfig.length} 级)`); + } + } catch (e) { + console.warn('[配置] 加载 RoleLevel.json 失败:', e.message); + } + + // 加载植物配置 + try { + const plantPath = path.join(configDir, 'Plant.json'); + if (fs.existsSync(plantPath)) { + plantConfig = JSON.parse(fs.readFileSync(plantPath, 'utf8')); + plantMap.clear(); + seedToPlant.clear(); + fruitToPlant.clear(); + nameToPlant.clear(); + for (const plant of plantConfig) { + plantMap.set(plant.id, plant); + if (plant.seed_id) { + seedToPlant.set(plant.seed_id, plant); + } + if (plant.fruit && plant.fruit.id) { + fruitToPlant.set(plant.fruit.id, plant); + } + if (plant.name) { + nameToPlant.set(plant.name, plant); + } + } + console.log(`[配置] 已加载植物配置 (${plantConfig.length} 种)`); + } + } catch (e) { + console.warn('[配置] 加载 Plant.json 失败:', e.message); + } +} + +// ============ 等级经验相关 ============ + +/** + * 获取等级经验表 + */ +function getLevelExpTable() { + return levelExpTable; +} + +/** + * 计算当前等级的经验进度 + * @param {number} level - 当前等级 + * @param {number} totalExp - 累计总经验 + * @returns {{ current: number, needed: number }} 当前等级经验进度 + */ +function getLevelExpProgress(level, totalExp) { + if (!levelExpTable || level <= 0) return { current: 0, needed: 0 }; + + const currentLevelStart = levelExpTable[level] || 0; + const nextLevelStart = levelExpTable[level + 1] || (currentLevelStart + 100000); + + const currentExp = Math.max(0, totalExp - currentLevelStart); + const neededExp = nextLevelStart - currentLevelStart; + + return { current: currentExp, needed: neededExp }; +} + +// ============ 植物配置相关 ============ + +/** + * 根据植物ID获取植物信息 + * @param {number} plantId - 植物ID + */ +function getPlantById(plantId) { + return plantMap.get(plantId); +} + +function getPlantByName(name) { + return nameToPlant.get(name); +} + +/** + * 根据种子ID获取植物信息 + * @param {number} seedId - 种子ID + */ +function getPlantBySeedId(seedId) { + return seedToPlant.get(seedId); +} + +function getSeedIdByPlantName(name) { + const plant = nameToPlant.get(name); + if (!plant) return 0; + return plant.seed_id || 0; +} + +/** + * 获取植物名称 + * @param {number} plantId - 植物ID + */ +function getPlantName(plantId) { + const plant = plantMap.get(plantId); + return plant ? plant.name : `植物${plantId}`; +} + +/** + * 根据种子ID获取植物名称 + * @param {number} seedId - 种子ID + */ +function getPlantNameBySeedId(seedId) { + const plant = seedToPlant.get(seedId); + return plant ? plant.name : `种子${seedId}`; +} + +/** + * 获取植物的果实信息 + * @param {number} plantId - 植物ID + * @returns {{ id: number, count: number, name: string } | null} + */ +function getPlantFruit(plantId) { + const plant = plantMap.get(plantId); + if (!plant || !plant.fruit) return null; + return { + id: plant.fruit.id, + count: plant.fruit.count, + name: plant.name, + }; +} + +/** + * 获取植物的生长时间(秒) + * @param {number} plantId - 植物ID + */ +function getPlantGrowTime(plantId) { + const plant = plantMap.get(plantId); + if (!plant || !plant.grow_phases) return 0; + + // 解析 "种子:30;发芽:30;成熟:0;" 格式 + const phases = plant.grow_phases.split(';').filter(p => p); + let totalSeconds = 0; + for (const phase of phases) { + const match = phase.match(/:(\d+)/); + if (match) { + totalSeconds += parseInt(match[1]); + } + } + return totalSeconds; +} + +/** + * 格式化时间 + * @param {number} seconds - 秒数 + */ +function formatGrowTime(seconds) { + if (seconds < 60) return `${seconds}秒`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`; + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return mins > 0 ? `${hours}小时${mins}分` : `${hours}小时`; +} + +/** + * 获取植物的收获经验 + * @param {number} plantId - 植物ID + */ +function getPlantExp(plantId) { + const plant = plantMap.get(plantId); + return plant ? plant.exp : 0; +} + +/** + * 根据果实ID获取植物名称 + * @param {number} fruitId - 果实ID + */ +function getFruitName(fruitId) { + const plant = fruitToPlant.get(fruitId); + return plant ? plant.name : `果实${fruitId}`; +} + +/** + * 根据果实ID获取植物信息 + * @param {number} fruitId - 果实ID + */ +function getPlantByFruitId(fruitId) { + return fruitToPlant.get(fruitId); +} + +const ITEM_NAME_MAP = { + 1001: '金币', + 1002: '点券', + 1011: '普通化肥容器', + 1012: '有机化肥容器', + 1013: '友谊果实', + 1014: '穗华', + 1015: '幸运币', + 3001: '普通收藏点', + 1101: '种植经验', + 80001: '普通(1小时)', + 80011: '有机(1小时)', + 80002: '普通(4小时)', + 80003: '普通(8小时)', + 80004: '普通(12小时)', + 80012: '有机(4小时)', + 80013: '有机(8小时)', + 80014: '有机(12小时)', + 90004: '1天狗粮', + 90005: '3天狗粮', + 100003: '化肥礼包', +}; + +function getItemNameById(id) { + const name = ITEM_NAME_MAP[id]; + if (name) return name; + return `物品${id}`; +} + +// 启动时加载配置 +loadConfigs(); + +module.exports = { + loadConfigs, + // 等级经验 + getLevelExpTable, + getLevelExpProgress, + // 植物配置 + getPlantById, + getPlantByName, + getPlantBySeedId, + getSeedIdByPlantName, + getPlantName, + getPlantNameBySeedId, + getPlantFruit, + getPlantGrowTime, + getPlantExp, + formatGrowTime, + // 果实配置 + getFruitName, + getPlantByFruitId, + // 物品配置 + getItemNameById, +}; diff --git a/server/src/invite.js b/server/src/invite.js new file mode 100644 index 0000000..0efe299 --- /dev/null +++ b/server/src/invite.js @@ -0,0 +1,161 @@ +/** + * 邀请码处理模块 - 读取 share.txt 并通过 ReportArkClick 申请好友 + * 注意:此功能仅在微信环境下有效 + * + * 原理: + * 1. 首次登录时,游戏会在 LoginRequest 中携带 sharer_id 和 sharer_open_id + * 2. 已登录状态下点击分享链接,游戏会发送 ReportArkClickRequest + * 3. 服务器收到后会自动向分享者发送好友申请 + * + * 我们使用 ReportArkClickRequest 来模拟已登录状态下的分享链接点击 + */ + +const fs = require('fs'); +const path = require('path'); +const { types } = require('./proto'); +const { sendMsgAsync } = require('./network'); +const { toLong, log, logWarn, sleep } = require('./utils'); +const { CONFIG } = require('./config'); + +/** + * 解析分享链接,提取 uid 和 openid + * 格式: ?uid=xxx&openid=xxx&share_source=xxx&doc_id=xxx + */ +function parseShareLink(link) { + const result = { uid: null, openid: null, shareSource: null, docId: null }; + + // 移除开头的 ? 如果有 + const queryStr = link.startsWith('?') ? link.slice(1) : link; + + // 解析参数 + const params = new URLSearchParams(queryStr); + result.uid = params.get('uid'); + result.openid = params.get('openid'); + result.shareSource = params.get('share_source'); + result.docId = params.get('doc_id'); + + return result; +} + +/** + * 读取 share.txt 文件并去重 + */ +function readShareFile() { + const shareFilePath = path.join(__dirname, '..', 'share.txt'); + + if (!fs.existsSync(shareFilePath)) { + return []; + } + + try { + const content = fs.readFileSync(shareFilePath, 'utf8'); + const lines = content.split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0 && line.includes('openid=')); + + const invites = []; + const seenUids = new Set(); // 用于去重 + + for (const line of lines) { + const parsed = parseShareLink(line); + if (parsed.openid && parsed.uid) { + // 按 uid 去重,同一个用户只处理一次 + if (!seenUids.has(parsed.uid)) { + seenUids.add(parsed.uid); + invites.push(parsed); + } + } + } + + return invites; + } catch (e) { + logWarn('邀请', `读取 share.txt 失败: ${e.message}`); + return []; + } +} + +/** + * 发送 ReportArkClick 请求 + * 模拟已登录状态下点击分享链接,触发服务器向分享者发送好友申请 + */ +async function sendReportArkClick(sharerId, sharerOpenId, shareSource) { + const body = types.ReportArkClickRequest.encode(types.ReportArkClickRequest.create({ + sharer_id: toLong(sharerId), + sharer_open_id: sharerOpenId, + share_cfg_id: toLong(shareSource || 0), + scene_id: '1256', // 模拟微信场景 + })).finish(); + + const { body: replyBody } = await sendMsgAsync('gamepb.userpb.UserService', 'ReportArkClick', body); + return types.ReportArkClickReply.decode(replyBody); +} + +// 请求间隔时间(毫秒) +const INVITE_REQUEST_DELAY = 2000; + +/** + * 处理邀请码列表 + * 仅在微信环境下执行 + */ +async function processInviteCodes() { + // 检查是否为微信环境 + if (CONFIG.platform !== 'wx') { + log('邀请', '当前为 QQ 环境,跳过邀请码处理(仅微信支持)'); + return; + } + + const invites = readShareFile(); + if (invites.length === 0) { + return; + } + + log('邀请', `读取到 ${invites.length} 个邀请码(已去重),开始逐个处理...`); + + let successCount = 0; + let failCount = 0; + + for (let i = 0; i < invites.length; i++) { + const invite = invites[i]; + + try { + // 发送 ReportArkClick 请求,模拟点击分享链接 + await sendReportArkClick(invite.uid, invite.openid, invite.shareSource); + successCount++; + log('邀请', `[${i + 1}/${invites.length}] 已向 uid=${invite.uid} 发送好友申请`); + } catch (e) { + failCount++; + logWarn('邀请', `[${i + 1}/${invites.length}] 向 uid=${invite.uid} 发送申请失败: ${e.message}`); + } + + // 每个请求之间延迟,避免请求过快被限流 + if (i < invites.length - 1) { + await sleep(INVITE_REQUEST_DELAY); + } + } + + log('邀请', `处理完成: 成功 ${successCount}, 失败 ${failCount}`); + + // 处理完成后清空文件 + clearShareFile(); +} + +/** + * 清空已处理的邀请码文件 + */ +function clearShareFile() { + const shareFilePath = path.join(__dirname, '..', 'share.txt'); + try { + fs.writeFileSync(shareFilePath, '', 'utf8'); + log('邀请', '已清空 share.txt'); + } catch (e) { + // 静默失败 + } +} + +module.exports = { + parseShareLink, + readShareFile, + sendReportArkClick, + processInviteCodes, + clearShareFile, +}; diff --git a/server/src/network.js b/server/src/network.js new file mode 100644 index 0000000..ae997b6 --- /dev/null +++ b/server/src/network.js @@ -0,0 +1,461 @@ +/** + * WebSocket 网络层 - 连接/消息编解码/登录/心跳 + */ + +const WebSocket = require('ws'); +const EventEmitter = require('events'); +const { CONFIG } = require('./config'); +const { types } = require('./proto'); +const { toLong, toNum, syncServerTime, log, logWarn } = require('./utils'); +const { updateStatusFromLogin, updateStatusGold, updateStatusLevel } = require('./status'); + +// ============ 事件发射器 (用于推送通知) ============ +const networkEvents = new EventEmitter(); + +// ============ 内部状态 ============ +let ws = null; +let clientSeq = 1; +let serverSeq = 0; +let heartbeatTimer = null; +let pendingCallbacks = new Map(); + +// ============ 用户状态 (登录后设置) ============ +const userState = { + gid: 0, + name: '', + level: 0, + gold: 0, + exp: 0, +}; + +function getUserState() { return userState; } + +// ============ 消息编解码 ============ +function encodeMsg(serviceName, methodName, bodyBytes) { + const msg = types.GateMessage.create({ + meta: { + service_name: serviceName, + method_name: methodName, + message_type: 1, + client_seq: toLong(clientSeq), + server_seq: toLong(serverSeq), + }, + body: bodyBytes || Buffer.alloc(0), + }); + const encoded = types.GateMessage.encode(msg).finish(); + clientSeq++; + return encoded; +} + +function sendMsg(serviceName, methodName, bodyBytes, callback) { + if (!ws || ws.readyState !== WebSocket.OPEN) { + log('WS', '连接未打开'); + return false; + } + const seq = clientSeq; + const encoded = encodeMsg(serviceName, methodName, bodyBytes); + if (callback) pendingCallbacks.set(seq, callback); + ws.send(encoded); + return true; +} + +/** Promise 版发送 */ +function sendMsgAsync(serviceName, methodName, bodyBytes, timeout = 10000) { + return new Promise((resolve, reject) => { + // 检查连接状态 + if (!ws || ws.readyState !== WebSocket.OPEN) { + reject(new Error(`连接未打开: ${methodName}`)); + return; + } + + const seq = clientSeq; + const timer = setTimeout(() => { + pendingCallbacks.delete(seq); + // 检查当前待处理的请求数 + const pending = pendingCallbacks.size; + reject(new Error(`请求超时: ${methodName} (seq=${seq}, pending=${pending})`)); + }, timeout); + + const sent = sendMsg(serviceName, methodName, bodyBytes, (err, body, meta) => { + clearTimeout(timer); + if (err) reject(err); + else resolve({ body, meta }); + }); + + if (!sent) { + clearTimeout(timer); + reject(new Error(`发送失败: ${methodName}`)); + } + }); +} + +// ============ 消息处理 ============ +function handleMessage(data) { + try { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); + const msg = types.GateMessage.decode(buf); + const meta = msg.meta; + if (!meta) return; + + if (meta.server_seq) { + const seq = toNum(meta.server_seq); + if (seq > serverSeq) serverSeq = seq; + } + + const msgType = meta.message_type; + + // Notify + if (msgType === 3) { + handleNotify(msg); + return; + } + + // Response + if (msgType === 2) { + const errorCode = toNum(meta.error_code); + const clientSeqVal = toNum(meta.client_seq); + + const cb = pendingCallbacks.get(clientSeqVal); + if (cb) { + pendingCallbacks.delete(clientSeqVal); + if (errorCode !== 0) { + cb(new Error(`${meta.service_name}.${meta.method_name} 错误: code=${errorCode} ${meta.error_message || ''}`)); + } else { + cb(null, msg.body, meta); + } + return; + } + + if (errorCode !== 0) { + logWarn('错误', `${meta.service_name}.${meta.method_name} code=${errorCode} ${meta.error_message || ''}`); + } + } + } catch (err) { + logWarn('解码', err.message); + } +} + +// 调试:记录所有推送类型 (设为 true 可查看所有推送) +// 注意:QQ环境下只有 ItemNotify 推送,没有 LandsNotify 推送 +const DEBUG_NOTIFY = false; + +function handleNotify(msg) { + if (!msg.body || msg.body.length === 0) return; + try { + const event = types.EventMessage.decode(msg.body); + const type = event.message_type || ''; + const eventBody = event.body; + + // 调试:显示所有推送类型 + if (DEBUG_NOTIFY) { + console.log(`[DEBUG] 收到推送: ${type}`); + } + + // 被踢下线 + if (type.includes('Kickout')) { + log('推送', `被踢下线! ${type}`); + try { + const notify = types.KickoutNotify.decode(eventBody); + log('推送', `原因: ${notify.reason_message || '未知'}`); + } catch (e) { } + return; + } + + // 土地状态变化 (被放虫/放草/偷菜等) + if (type.includes('LandsNotify')) { + try { + const notify = types.LandsNotify.decode(eventBody); + const hostGid = toNum(notify.host_gid); + const lands = notify.lands || []; + if (DEBUG_NOTIFY) { + console.log(`[DEBUG] LandsNotify: hostGid=${hostGid}, myGid=${userState.gid}, lands=${lands.length}`); + } + if (lands.length > 0) { + // 如果是自己的农场,触发事件 + if (hostGid === userState.gid || hostGid === 0) { + networkEvents.emit('landsChanged', lands); + } + } + } catch (e) { } + return; + } + + // 物品变化通知 (经验/金币等) - 仅更新状态栏 + // 金币: id=1 或 id=1001 (GodItemId) + // 经验: id=1101 (ExpItemId) 或 id=2 + if (type.includes('ItemNotify')) { + try { + const notify = types.ItemNotify.decode(eventBody); + const items = notify.items || []; + for (const itemChg of items) { + const item = itemChg.item; + if (!item) continue; + const id = toNum(item.id); + const count = toNum(item.count); + + if (id === 1101 || id === 2) { + userState.exp = count; + updateStatusLevel(userState.level, count); + } else if (id === 1 || id === 1001) { + userState.gold = count; + updateStatusGold(count); + } + } + } catch (e) { } + return; + } + + // 基本信息变化 (升级等) + if (type.includes('BasicNotify')) { + try { + const notify = types.BasicNotify.decode(eventBody); + if (notify.basic) { + const oldLevel = userState.level; + const oldExp = userState.exp || 0; + userState.level = toNum(notify.basic.level) || userState.level; + userState.gold = toNum(notify.basic.gold) || userState.gold; + const exp = toNum(notify.basic.exp); + if (exp > 0) { + userState.exp = exp; + updateStatusLevel(userState.level, exp); + } + updateStatusGold(userState.gold); + // 升级提示 + if (userState.level !== oldLevel) { + log('系统', `升级! Lv${oldLevel} → Lv${userState.level}`); + } + } + } catch (e) { } + return; + } + + // 好友申请通知 (微信同玩) + if (type.includes('FriendApplicationReceivedNotify')) { + try { + const notify = types.FriendApplicationReceivedNotify.decode(eventBody); + const applications = notify.applications || []; + if (applications.length > 0) { + networkEvents.emit('friendApplicationReceived', applications); + } + } catch (e) { } + return; + } + + // 好友添加成功通知 + if (type.includes('FriendAddedNotify')) { + try { + const notify = types.FriendAddedNotify.decode(eventBody); + const friends = notify.friends || []; + if (friends.length > 0) { + const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', '); + log('好友', `新好友: ${names}`); + } + } catch (e) { } + return; + } + + // 物品变化通知 (收获/购买/消耗等) + if (type.includes('ItemNotify')) { + try { + const notify = types.ItemNotify.decode(eventBody); + const items = notify.items || []; + for (const chg of items) { + if (!chg.item) continue; + const id = toNum(chg.item.id); + const count = toNum(chg.item.count); + const delta = toNum(chg.delta); + // 金币 ID=1 + if (id === 1) { + userState.gold = count; + updateStatusGold(count); + if (delta !== 0) { + log('物品', `金币 ${delta > 0 ? '+' : ''}${delta} (当前: ${count})`); + } + } + // 经验 ID=2 (升级由 BasicNotify 处理) + } + } catch (e) { } + return; + } + + // 商品解锁通知 (升级后解锁新种子等) + if (type.includes('GoodsUnlockNotify')) { + try { + const notify = types.GoodsUnlockNotify.decode(eventBody); + const goods = notify.goods_list || []; + if (goods.length > 0) { + log('商店', `解锁 ${goods.length} 个新商品!`); + } + } catch (e) { } + return; + } + + // 任务状态变化通知 + if (type.includes('TaskInfoNotify')) { + try { + const notify = types.TaskInfoNotify.decode(eventBody); + if (notify.task_info) { + networkEvents.emit('taskInfoNotify', notify.task_info); + } + } catch (e) { } + return; + } + + // 其他未处理的推送类型 (调试用) + // log('推送', `未处理类型: ${type}`); + } catch (e) { + logWarn('推送', `解码失败: ${e.message}`); + } +} + +// ============ 登录 ============ +function sendLogin(onLoginSuccess) { + const body = types.LoginRequest.encode(types.LoginRequest.create({ + sharer_id: toLong(0), + sharer_open_id: '', + device_info: { + client_version: CONFIG.clientVersion, + sys_software: 'iOS 26.2.1', + network: 'wifi', + memory: '7672', + device_id: 'iPhone X', + }, + share_cfg_id: toLong(0), + scene_id: '1256', + report_data: { + callback: '', cd_extend_info: '', click_id: '', clue_token: '', + minigame_channel: 'other', minigame_platid: 2, req_id: '', trackid: '', + }, + })).finish(); + + sendMsg('gamepb.userpb.UserService', 'Login', body, (err, bodyBytes, meta) => { + if (err) { + log('登录', `失败: ${err.message}`); + return; + } + try { + const reply = types.LoginReply.decode(bodyBytes); + if (reply.basic) { + userState.gid = toNum(reply.basic.gid); + userState.name = reply.basic.name || '未知'; + userState.level = toNum(reply.basic.level); + userState.gold = toNum(reply.basic.gold); + userState.exp = toNum(reply.basic.exp); + + // 更新状态栏 + updateStatusFromLogin({ + name: userState.name, + level: userState.level, + gold: userState.gold, + exp: userState.exp, + }); + + console.log(''); + console.log('========== 登录成功 =========='); + console.log(` GID: ${userState.gid}`); + console.log(` 昵称: ${userState.name}`); + console.log(` 等级: ${userState.level}`); + console.log(` 金币: ${userState.gold}`); + if (reply.time_now_millis) { + syncServerTime(toNum(reply.time_now_millis)); + console.log(` 时间: ${new Date(toNum(reply.time_now_millis)).toLocaleString()}`); + } + console.log('==============================='); + console.log(''); + } + + startHeartbeat(); + if (onLoginSuccess) onLoginSuccess(); + } catch (e) { + log('登录', `解码失败: ${e.message}`); + } + }); +} + +// ============ 心跳 ============ +let lastHeartbeatResponse = Date.now(); +let heartbeatMissCount = 0; + +function startHeartbeat() { + if (heartbeatTimer) clearInterval(heartbeatTimer); + lastHeartbeatResponse = Date.now(); + heartbeatMissCount = 0; + + heartbeatTimer = setInterval(() => { + if (!userState.gid) return; + + // 检查上次心跳响应时间,超过 60 秒没响应说明连接有问题 + const timeSinceLastResponse = Date.now() - lastHeartbeatResponse; + if (timeSinceLastResponse > 60000) { + heartbeatMissCount++; + logWarn('心跳', `连接可能已断开 (${Math.round(timeSinceLastResponse/1000)}s 无响应, pending=${pendingCallbacks.size})`); + if (heartbeatMissCount >= 2) { + log('心跳', '尝试重连...'); + // 清理待处理的回调,避免堆积 + pendingCallbacks.forEach((cb, seq) => { + try { cb(new Error('连接超时,已清理')); } catch (e) {} + }); + pendingCallbacks.clear(); + } + } + + const body = types.HeartbeatRequest.encode(types.HeartbeatRequest.create({ + gid: toLong(userState.gid), + client_version: CONFIG.clientVersion, + })).finish(); + sendMsg('gamepb.userpb.UserService', 'Heartbeat', body, (err, replyBody) => { + if (err || !replyBody) return; + lastHeartbeatResponse = Date.now(); + heartbeatMissCount = 0; + try { + const reply = types.HeartbeatReply.decode(replyBody); + if (reply.server_time) syncServerTime(toNum(reply.server_time)); + } catch (e) { } + }); + }, CONFIG.heartbeatInterval); +} + +// ============ WebSocket 连接 ============ +function connect(code, onLoginSuccess) { + const url = `${CONFIG.serverUrl}?platform=${CONFIG.platform}&os=${CONFIG.os}&ver=${CONFIG.clientVersion}&code=${code}&openID=`; + + ws = new WebSocket(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13)', + 'Origin': 'https://gate-obt.nqf.qq.com', + }, + }); + + ws.binaryType = 'arraybuffer'; + + ws.on('open', () => { + sendLogin(onLoginSuccess); + }); + + ws.on('message', (data) => { + handleMessage(Buffer.isBuffer(data) ? data : Buffer.from(data)); + }); + + ws.on('close', (code, reason) => { + console.log(`[WS] 连接关闭 (code=${code})`); + cleanup(); + }); + + ws.on('error', (err) => { + logWarn('WS', `错误: ${err.message}`); + }); +} + +function cleanup() { + if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } + pendingCallbacks.clear(); +} + +function getWs() { return ws; } + +module.exports = { + connect, cleanup, getWs, + sendMsg, sendMsgAsync, + getUserState, + networkEvents, +}; diff --git a/server/src/proto.js b/server/src/proto.js new file mode 100644 index 0000000..e0fe4c4 --- /dev/null +++ b/server/src/proto.js @@ -0,0 +1,123 @@ +/** + * Proto 加载与消息类型管理 + */ + +const protobuf = require('protobufjs'); +const path = require('path'); +const { log } = require('./utils'); + +// Proto 根对象与所有消息类型 +let root = null; +const types = {}; + +async function loadProto() { + const protoDir = path.join(__dirname, '..', 'proto'); + root = new protobuf.Root(); + await root.load([ + path.join(protoDir, 'game.proto'), + path.join(protoDir, 'userpb.proto'), + path.join(protoDir, 'plantpb.proto'), + path.join(protoDir, 'corepb.proto'), + path.join(protoDir, 'shoppb.proto'), + path.join(protoDir, 'friendpb.proto'), + path.join(protoDir, 'visitpb.proto'), + path.join(protoDir, 'notifypb.proto'), + path.join(protoDir, 'taskpb.proto'), + path.join(protoDir, 'itempb.proto'), + ], { keepCase: true }); + + // 网关 + types.GateMessage = root.lookupType('gatepb.Message'); + types.GateMeta = root.lookupType('gatepb.Meta'); + types.EventMessage = root.lookupType('gatepb.EventMessage'); + + // 用户 + types.LoginRequest = root.lookupType('gamepb.userpb.LoginRequest'); + types.LoginReply = root.lookupType('gamepb.userpb.LoginReply'); + types.HeartbeatRequest = root.lookupType('gamepb.userpb.HeartbeatRequest'); + types.HeartbeatReply = root.lookupType('gamepb.userpb.HeartbeatReply'); + types.ReportArkClickRequest = root.lookupType('gamepb.userpb.ReportArkClickRequest'); + types.ReportArkClickReply = root.lookupType('gamepb.userpb.ReportArkClickReply'); + + // 农场 + types.AllLandsRequest = root.lookupType('gamepb.plantpb.AllLandsRequest'); + types.AllLandsReply = root.lookupType('gamepb.plantpb.AllLandsReply'); + types.HarvestRequest = root.lookupType('gamepb.plantpb.HarvestRequest'); + types.HarvestReply = root.lookupType('gamepb.plantpb.HarvestReply'); + types.WaterLandRequest = root.lookupType('gamepb.plantpb.WaterLandRequest'); + types.WaterLandReply = root.lookupType('gamepb.plantpb.WaterLandReply'); + types.WeedOutRequest = root.lookupType('gamepb.plantpb.WeedOutRequest'); + types.WeedOutReply = root.lookupType('gamepb.plantpb.WeedOutReply'); + types.InsecticideRequest = root.lookupType('gamepb.plantpb.InsecticideRequest'); + types.InsecticideReply = root.lookupType('gamepb.plantpb.InsecticideReply'); + types.RemovePlantRequest = root.lookupType('gamepb.plantpb.RemovePlantRequest'); + types.RemovePlantReply = root.lookupType('gamepb.plantpb.RemovePlantReply'); + types.UnlockLandRequest = root.lookupType('gamepb.plantpb.UnlockLandRequest'); + types.UnlockLandReply = root.lookupType('gamepb.plantpb.UnlockLandReply'); + types.PutInsectsRequest = root.lookupType('gamepb.plantpb.PutInsectsRequest'); + types.PutInsectsReply = root.lookupType('gamepb.plantpb.PutInsectsReply'); + types.PutWeedsRequest = root.lookupType('gamepb.plantpb.PutWeedsRequest'); + types.PutWeedsReply = root.lookupType('gamepb.plantpb.PutWeedsReply'); + types.FertilizeRequest = root.lookupType('gamepb.plantpb.FertilizeRequest'); + types.FertilizeReply = root.lookupType('gamepb.plantpb.FertilizeReply'); + + // 背包/仓库 + types.BagRequest = root.lookupType('gamepb.itempb.BagRequest'); + types.BagReply = root.lookupType('gamepb.itempb.BagReply'); + types.SellRequest = root.lookupType('gamepb.itempb.SellRequest'); + types.SellReply = root.lookupType('gamepb.itempb.SellReply'); + types.UseRequest = root.lookupType('gamepb.itempb.UseRequest'); + types.UseReply = root.lookupType('gamepb.itempb.UseReply'); + types.BatchUseRequest = root.lookupType('gamepb.itempb.BatchUseRequest'); + types.BatchUseReply = root.lookupType('gamepb.itempb.BatchUseReply'); + types.PlantRequest = root.lookupType('gamepb.plantpb.PlantRequest'); + types.PlantReply = root.lookupType('gamepb.plantpb.PlantReply'); + + // 商店 + types.ShopProfilesRequest = root.lookupType('gamepb.shoppb.ShopProfilesRequest'); + types.ShopProfilesReply = root.lookupType('gamepb.shoppb.ShopProfilesReply'); + types.ShopInfoRequest = root.lookupType('gamepb.shoppb.ShopInfoRequest'); + types.ShopInfoReply = root.lookupType('gamepb.shoppb.ShopInfoReply'); + types.BuyGoodsRequest = root.lookupType('gamepb.shoppb.BuyGoodsRequest'); + types.BuyGoodsReply = root.lookupType('gamepb.shoppb.BuyGoodsReply'); + + // 好友 + types.GetAllFriendsRequest = root.lookupType('gamepb.friendpb.GetAllRequest'); + types.GetAllFriendsReply = root.lookupType('gamepb.friendpb.GetAllReply'); + types.GetApplicationsRequest = root.lookupType('gamepb.friendpb.GetApplicationsRequest'); + types.GetApplicationsReply = root.lookupType('gamepb.friendpb.GetApplicationsReply'); + types.AcceptFriendsRequest = root.lookupType('gamepb.friendpb.AcceptFriendsRequest'); + types.AcceptFriendsReply = root.lookupType('gamepb.friendpb.AcceptFriendsReply'); + + // 访问 + types.VisitEnterRequest = root.lookupType('gamepb.visitpb.EnterRequest'); + types.VisitEnterReply = root.lookupType('gamepb.visitpb.EnterReply'); + types.VisitLeaveRequest = root.lookupType('gamepb.visitpb.LeaveRequest'); + types.VisitLeaveReply = root.lookupType('gamepb.visitpb.LeaveReply'); + + // 任务 + types.TaskInfoRequest = root.lookupType('gamepb.taskpb.TaskInfoRequest'); + types.TaskInfoReply = root.lookupType('gamepb.taskpb.TaskInfoReply'); + types.ClaimTaskRewardRequest = root.lookupType('gamepb.taskpb.ClaimTaskRewardRequest'); + types.ClaimTaskRewardReply = root.lookupType('gamepb.taskpb.ClaimTaskRewardReply'); + types.BatchClaimTaskRewardRequest = root.lookupType('gamepb.taskpb.BatchClaimTaskRewardRequest'); + types.BatchClaimTaskRewardReply = root.lookupType('gamepb.taskpb.BatchClaimTaskRewardReply'); + + // 服务器推送通知 + types.LandsNotify = root.lookupType('gamepb.plantpb.LandsNotify'); + types.BasicNotify = root.lookupType('gamepb.userpb.BasicNotify'); + types.KickoutNotify = root.lookupType('gatepb.KickoutNotify'); + types.FriendApplicationReceivedNotify = root.lookupType('gamepb.friendpb.FriendApplicationReceivedNotify'); + types.FriendAddedNotify = root.lookupType('gamepb.friendpb.FriendAddedNotify'); + types.ItemNotify = root.lookupType('gamepb.itempb.ItemNotify'); + types.GoodsUnlockNotify = root.lookupType('gamepb.shoppb.GoodsUnlockNotify'); + types.TaskInfoNotify = root.lookupType('gamepb.taskpb.TaskInfoNotify'); + + // Proto 加载完成 +} + +function getRoot() { + return root; +} + +module.exports = { loadProto, types, getRoot }; diff --git a/server/src/qrlib/session.js b/server/src/qrlib/session.js new file mode 100644 index 0000000..0746d57 --- /dev/null +++ b/server/src/qrlib/session.js @@ -0,0 +1,314 @@ +const axios = require('axios'); +const { CookieUtils, HashUtils } = require('./utils'); + +// User Agent Definition +const ChromeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + +class QRLoginSession { + /** + * Presets for different login targets + */ + static Presets = { + vip: { + name: 'QQ会员 (VIP)', + description: 'QQ会员官网', + aid: '8000201', + daid: '18', + redirectUri: 'https://vip.qq.com/loginsuccess.html', + referrer: 'https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=8000201&style=20&s_url=https%3A%2F%2Fvip.qq.com%2Floginsuccess.html&maskOpacity=60&daid=18&target=self', + }, + qzone: { + name: 'QQ空间 (QZone)', + description: 'QQ空间网页版', + aid: '549000912', + daid: '5', + redirectUri: 'https://qzs.qzone.qq.com/qzone/v5/loginsucc.html?para=izone', + referrer: 'https://qzone.qq.com/', + }, + music: { + name: 'QQ音乐 (Music)', + description: 'QQ音乐网页版', + aid: '716027609', + daid: '383', + redirectUri: 'https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https%3A%2F%2Fy.qq.com%2F', + ptThirdAid: '100497308', + responseType: 'code', + openapi: '1010_1030', + }, + wegame: { + name: 'WeGame', + description: 'WeGame 平台', + aid: '1600001063', + daid: '733', + redirectUri: 'https://www.wegame.com.cn/middle/login/third_callback.html', + referrer: 'https://www.wegame.com.cn/', + }, + val: { + name: '瓦罗兰特 (VAL)', + description: '无畏契约官网', + aid: '716027609', + daid: '383', + redirectUri: 'https://val.qq.com/comm-htdocs/login/qc_redirect.html?parent_domain=https%3A%2F%2Fval.qq.com&isMiloSDK=1&isPc=1', + ptThirdAid: '102059301', + responseType: 'code', + openapi: '1010_1030', + }, + }; + + /** + * Request a new QR Code + * @param {string} presetKey - The key of the preset to use (vip, qzone, etc) + */ + static async requestQRCode(presetKey) { + const config = this.Presets[presetKey] || this.Presets.vip; + + const params = new URLSearchParams({ + appid: config.aid, + e: '2', + l: 'M', + s: '3', + d: '72', + v: '4', + t: String(Math.random()), + daid: config.daid, + }); + + if (config.ptThirdAid) { + params.set('pt_3rd_aid', config.ptThirdAid); + params.set('u1', 'https://graph.qq.com/oauth2.0/login_jump'); + } else { + params.set('u1', config.redirectUri); + } + + const url = `https://ssl.ptlogin2.qq.com/ptqrshow?${params.toString()}`; + + try { + const response = await axios.get(url, { + responseType: 'arraybuffer', + headers: { + 'Referer': config.referrer || `https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=${config.aid}&style=20&s_url=${encodeURIComponent(config.redirectUri)}&maskOpacity=60&daid=${config.daid}&target=self`, + 'User-Agent': ChromeUA, + } + }); + + const setCookie = response.headers['set-cookie']; + const qrsig = CookieUtils.getValue(setCookie, 'qrsig'); + const qrcodeBase64 = Buffer.from(response.data).toString('base64'); + + return { qrsig, qrcode: `data:image/png;base64,${qrcodeBase64}`, url }; + } catch (error) { + console.error('Request QRCode Error:', error); + throw error; + } + } + + /** + * Check the status of the QR Code + * @param {string} qrsig + * @param {string} presetKey + */ + static async checkStatus(qrsig, presetKey) { + const config = this.Presets[presetKey] || this.Presets.vip; + const ptqrtoken = HashUtils.hash(qrsig); + + const params = new URLSearchParams({ + ptqrtoken: String(ptqrtoken), + from_ui: '1', + aid: config.aid, + daid: config.daid, + action: `0-0-${Date.now()}`, // Added timestamp + pt_uistyle: '40', + js_ver: '21020514', + js_type: '1' + }); + + if (config.ptThirdAid) { + params.set('pt_3rd_aid', config.ptThirdAid); + params.set('u1', 'https://graph.qq.com/oauth2.0/login_jump'); + } else { + params.set('u1', config.redirectUri); + } + + const api = `https://ssl.ptlogin2.qq.com/ptqrlogin?${params.toString()}`; + + try { + const response = await axios.get(api, { + headers: { + 'Cookie': `qrsig=${qrsig}`, + 'Referer': config.referrer || 'https://xui.ptlogin2.qq.com/', + 'User-Agent': ChromeUA, + }, + }); + + const text = response.data; + // Parse response: ptuiCB('66','0','','0','二维码未失效。(3776510309)', '') + // Robust parsing using Regex to handle commas in content + const matcher = /ptuiCB\((.+)\)/; + const match = text.match(matcher); + + if (!match) { + throw new Error('Invalid response format'); + } + + // Extract arguments: 'arg1', 'arg2', ... + // This regex matches single-quoted strings: '([^']*)' + const args = []; + const argMatcher = /'([^']*)'/g; + let argMatch; + while ((argMatch = argMatcher.exec(match[1])) !== null) { + args.push(argMatch[1]); + } + + const [ret, extret, jumpUrl, redirect, msg, nickname] = args; + + return { + ret, + msg, + nickname, + jumpUrl, + cookie: response.headers['set-cookie'] // Return cookies to frontend if success + }; + + } catch (error) { + console.error('Check Status Error:', error); + throw new Error('Check status failed'); + } + } + + /** + * Get final cookies from the successful jump URL + * @param {string} jumpUrl + */ + static async getFinalCookies(jumpUrl) { + try { + // Prevent auto redirect to capture cookies + const response = await axios.get(jumpUrl, { + maxRedirects: 0, + validateStatus: status => status >= 200 && status < 400, + headers: { + 'User-Agent': ChromeUA + } + }); + + // This might return 302 Found + return response.headers['set-cookie']; + } catch (error) { + console.error("Get Final Cookies Error", error); + return []; + } + } +} + +class MiniProgramLoginSession { + static QUA = 'V1_HT5_QDT_0.70.2209190_x64_0_DEV_D'; + + /** + * Mini Program Presets + */ + static Presets = { + miniprogram: { + name: '小程序开发 (DevTools)', + description: 'QQ小程序开发者工具', + appid: '' // User provided + }, + farm: { + name: 'QQ经典农场 (Farm)', + description: 'QQ经典农场小程序', + appid: '1112386029' + } + }; + + static getHeaders() { + return { + 'qua': MiniProgramLoginSession.QUA, + 'host': 'q.qq.com', + 'accept': 'application/json', + 'content-type': 'application/json', + 'user-agent': ChromeUA + }; + } + + /** + * Request Login Code (for Mini Program DevTools) + */ + static async requestLoginCode() { + try { + const response = await axios.get('https://q.qq.com/ide/devtoolAuth/GetLoginCode', { + headers: this.getHeaders() + }); + + const { code, data } = response.data; + + if (+code !== 0) { + throw new Error('获取登录码失败'); + } + + return { + code: data.code || '', + url: `https://h5.qzone.qq.com/qqq/code/${data.code}?_proxy=1&from=ide` + }; + } catch (error) { + console.error('MP Request Login Code Error:', error); + throw error; + } + } + + /** + * Query Status for Mini Program Login + * @param {string} code + */ + static async queryStatus(code) { + try { + const response = await axios.get(`https://q.qq.com/ide/devtoolAuth/syncScanSateGetTicket?code=${code}`, { + headers: this.getHeaders() + }); + + // If response is not OK (e.g. 404/500), return Error + if (response.status !== 200) { + return { status: 'Error' }; + } + + const { code: resCode, data } = response.data; + + if (+resCode === 0) { + // data.ok: 1 = Success, 0 = Waiting/Scanning? + if (+data.ok !== 1) return { status: 'Wait' }; + // User says uin is here + return { status: 'OK', ticket: data.ticket, uin: data.uin }; + } + + if (+resCode === -10003) return { status: 'Used' }; + + return { status: 'Error', msg: `Code: ${resCode}` }; + } catch (error) { + console.error('MP Query Status Error:', error); + throw error; + } + } + + /** + * Get Auth Code (Final step for MP login) + * @param {string} ticket + * @param {string} appid + */ + static async getAuthCode(ticket, appid) { + try { + const response = await axios.post('https://q.qq.com/ide/login', { + appid: appid, + ticket: ticket + }, { + headers: this.getHeaders() + }); + + if (response.status !== 200) return ''; + + const { code } = response.data; + return code || ''; + } catch (error) { + console.error('MP Get Auth Code Error:', error); + return ''; + } + } +} + +module.exports = { QRLoginSession, MiniProgramLoginSession }; diff --git a/server/src/qrlib/utils.js b/server/src/qrlib/utils.js new file mode 100644 index 0000000..0a49c94 --- /dev/null +++ b/server/src/qrlib/utils.js @@ -0,0 +1,52 @@ +const axios = require('axios'); + +/** + * Cookie handling utilities + */ +class CookieUtils { + static parse(cookieStr) { + if (!cookieStr) return {}; + return cookieStr.split(';').reduce((acc, curr) => { + const [key, value] = curr.split('='); + if (key) acc[key.trim()] = value ? value.trim() : ''; + return acc; + }, {}); + } + + static getValue(cookies, key) { + if (!cookies) return null; + if (Array.isArray(cookies)) cookies = cookies.join('; '); + const match = cookies.match(new RegExp(`(?:^|;\\s*)${key}=([^;]*)`)); + return match ? match[1] : null; + } + + static getUin(cookies) { + const uin = this.getValue(cookies, 'wxuin') || this.getValue(cookies, 'uin') || this.getValue(cookies, 'ptui_loginuin'); + if (!uin) return null; + // Remove leading 'o' if present (common in QQ cookies like 'o123456') + return uin.replace(/^o0*/, ''); + } +} + +/** + * Hashing utilities for QQ login + */ +class HashUtils { + static hash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash += (hash << 5) + str.charCodeAt(i); + } + return 2147483647 & hash; + } + + static getGTk(pskey) { + let gtk = 5381; + for (let i = 0; i < pskey.length; i++) { + gtk += (gtk << 5) + pskey.charCodeAt(i); + } + return gtk & 0x7fffffff; + } +} + +module.exports = { CookieUtils, HashUtils }; diff --git a/server/src/routes/update.js b/server/src/routes/update.js new file mode 100644 index 0000000..f4b8677 --- /dev/null +++ b/server/src/routes/update.js @@ -0,0 +1,30 @@ +const express = require('express'); +const router = express.Router(); +const updateService = require('../core/UpdateService'); +const { log } = require('../utils'); + +// 检查更新 +router.get('/check', async (req, res) => { + try { + log('UpdateController', '收到检查更新请求'); + const result = await updateService.checkUpdate(); + res.json({ success: true, data: result }); + } catch (error) { + log('UpdateController', `检查更新出错: ${error.message}`); + res.status(500).json({ success: false, message: error.message }); + } +}); + +// 执行更新 +router.post('/execute', async (req, res) => { + try { + log('UpdateController', '收到执行更新请求'); + const result = await updateService.doUpdate(); + res.json({ success: true, data: result }); + } catch (error) { + log('UpdateController', `执行更新出错: ${error.message}`); + res.status(500).json({ success: false, message: error.message }); + } +}); + +module.exports = router; diff --git a/server/src/status.js b/server/src/status.js new file mode 100644 index 0000000..3e780e4 --- /dev/null +++ b/server/src/status.js @@ -0,0 +1,193 @@ +/** + * 状态栏 - 在终端固定位置显示用户状态 + */ + +const { getLevelExpTable, getLevelExpProgress } = require('./gameConfig'); + +// ============ 状态数据 ============ +const statusData = { + platform: 'qq', + name: '', + level: 0, + gold: 0, + exp: 0, +}; + +// ============ 状态栏高度 ============ +const STATUS_LINES = 3; // 状态栏占用行数 +const FREE_PROJECT_TIP = '本程序免费开源,GitHub: https://github.com/linguo2625469/qq-farm-bot'; + +// ============ ANSI 转义码 ============ +const ESC = '\x1b'; +const SAVE_CURSOR = `${ESC}7`; +const RESTORE_CURSOR = `${ESC}8`; +const MOVE_TO = (row, col) => `${ESC}[${row};${col}H`; +const CLEAR_LINE = `${ESC}[2K`; +const SCROLL_REGION = (top, bottom) => `${ESC}[${top};${bottom}r`; +const RESET_SCROLL = `${ESC}[r`; +const BOLD = `${ESC}[1m`; +const RESET = `${ESC}[0m`; +const DIM = `${ESC}[2m`; +const CYAN = `${ESC}[36m`; +const YELLOW = `${ESC}[33m`; +const GREEN = `${ESC}[32m`; +const MAGENTA = `${ESC}[35m`; + +// ============ 状态栏是否启用 ============ +let statusEnabled = false; +let termRows = 24; + +/** + * 初始化状态栏 + */ +function initStatusBar() { + // 检测终端是否支持 + if (!process.stdout.isTTY) { + return false; + } + + termRows = process.stdout.rows || 24; + statusEnabled = true; + + // 设置滚动区域,留出顶部状态栏空间 + process.stdout.write(SCROLL_REGION(STATUS_LINES + 1, termRows)); + // 移动光标到滚动区域 + process.stdout.write(MOVE_TO(STATUS_LINES + 1, 1)); + + // 监听终端大小变化 + process.stdout.on('resize', () => { + termRows = process.stdout.rows || 24; + process.stdout.write(SCROLL_REGION(STATUS_LINES + 1, termRows)); + renderStatusBar(); + }); + + // 初始渲染 + renderStatusBar(); + return true; +} + +/** + * 清理状态栏(退出时调用) + */ +function cleanupStatusBar() { + if (!statusEnabled) return; + statusEnabled = false; + // 重置滚动区域 + process.stdout.write(RESET_SCROLL); + // 清除状态栏 + process.stdout.write(MOVE_TO(1, 1) + CLEAR_LINE); + process.stdout.write(MOVE_TO(2, 1) + CLEAR_LINE); + process.stdout.write(MOVE_TO(3, 1) + CLEAR_LINE); +} + +/** + * 渲染状态栏 + */ +function renderStatusBar() { + if (!statusEnabled) return; + + const { platform, name, level, gold, exp } = statusData; + + // 构建状态行 + const platformStr = platform === 'wx' ? `${MAGENTA}微信${RESET}` : `${CYAN}QQ${RESET}`; + const nameStr = name ? `${BOLD}${name}${RESET}` : '未登录'; + const levelStr = `${GREEN}Lv${level}${RESET}`; + const goldStr = `${YELLOW}金币:${gold}${RESET}`; + + // 显示经验值 + let expStr = ''; + if (level > 0 && exp >= 0) { + const levelExpTable = getLevelExpTable(); + if (levelExpTable) { + // 有配置表时显示当前等级进度 + const progress = getLevelExpProgress(level, exp); + expStr = `${DIM}经验:${progress.current}/${progress.needed}${RESET}`; + } else { + // 没有配置表时只显示累计经验 + expStr = `${DIM}经验:${exp}${RESET}`; + } + } + + // 第一行:平台 | 昵称 | 等级 | 金币 | 经验 + const line1 = `${platformStr} | ${nameStr} | ${levelStr} | ${goldStr}${expStr ? ' | ' + expStr : ''}`; + + // 第二行:固定提醒 + const line2 = `${DIM}${FREE_PROJECT_TIP}${RESET}`; + + // 第三行:分隔线 + const width = process.stdout.columns || 80; + const line3 = `${DIM}${'─'.repeat(Math.min(width, 80))}${RESET}`; + + // 保存光标位置 + process.stdout.write(SAVE_CURSOR); + // 移动到第一行并清除 + process.stdout.write(MOVE_TO(1, 1) + CLEAR_LINE + line1); + // 移动到第二行并清除 + process.stdout.write(MOVE_TO(2, 1) + CLEAR_LINE + line2); + // 移动到第三行并清除 + process.stdout.write(MOVE_TO(3, 1) + CLEAR_LINE + line3); + // 恢复光标位置 + process.stdout.write(RESTORE_CURSOR); +} + +/** + * 更新状态数据并刷新显示 + */ +function updateStatus(data) { + let changed = false; + for (const key of Object.keys(data)) { + if (statusData[key] !== data[key]) { + statusData[key] = data[key]; + changed = true; + } + } + if (changed && statusEnabled) { + renderStatusBar(); + } +} + +/** + * 设置平台 + */ +function setStatusPlatform(platform) { + updateStatus({ platform }); +} + +/** + * 从登录数据更新状态 + */ +function updateStatusFromLogin(basic) { + updateStatus({ + name: basic.name || statusData.name, + level: basic.level || statusData.level, + gold: basic.gold || statusData.gold, + exp: basic.exp || statusData.exp, + }); +} + +/** + * 更新金币 + */ +function updateStatusGold(gold) { + updateStatus({ gold }); +} + +/** + * 更新等级和经验 + */ +function updateStatusLevel(level, exp) { + const data = { level }; + if (exp !== undefined) data.exp = exp; + updateStatus(data); +} + +module.exports = { + initStatusBar, + cleanupStatusBar, + updateStatus, + setStatusPlatform, + updateStatusFromLogin, + updateStatusGold, + updateStatusLevel, + statusData, +}; diff --git a/server/src/task.js b/server/src/task.js new file mode 100644 index 0000000..bbbe45a --- /dev/null +++ b/server/src/task.js @@ -0,0 +1,209 @@ +/** + * 任务系统 - 自动领取任务奖励 + */ + +const { types } = require('./proto'); +const { sendMsgAsync, networkEvents } = require('./network'); +const { toLong, toNum, log, logWarn, sleep } = require('./utils'); + +function isTaskNotCompleteError(message) { + if (!message) return false; + return message.includes('code=1008001') || message.includes('任务未完成'); +} + +function isTaskAlreadyClaimedError(message) { + if (!message) return false; + return message.includes('code=1008002') || message.includes('任务已领取'); +} + +// ============ 任务 API ============ + +async function getTaskInfo() { + const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body); + return types.TaskInfoReply.decode(replyBody); +} + +async function claimTaskReward(taskId, doShared = false) { + const body = types.ClaimTaskRewardRequest.encode(types.ClaimTaskRewardRequest.create({ + id: toLong(taskId), + do_shared: doShared, + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'ClaimTaskReward', body); + return types.ClaimTaskRewardReply.decode(replyBody); +} + +async function batchClaimTaskReward(taskIds, doShared = false) { + const body = types.BatchClaimTaskRewardRequest.encode(types.BatchClaimTaskRewardRequest.create({ + ids: taskIds.map(id => toLong(id)), + do_shared: doShared, + })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'BatchClaimTaskReward', body); + return types.BatchClaimTaskRewardReply.decode(replyBody); +} + +// ============ 任务分析 ============ + +/** + * 分析任务列表,找出可领取的任务 + */ +function analyzeTaskList(tasks) { + const claimable = []; + for (const task of tasks) { + const id = toNum(task.id); + const progress = toNum(task.progress); + const totalProgress = toNum(task.total_progress); + const isClaimed = task.is_claimed; + const isUnlocked = task.is_unlocked; + const shareMultiple = toNum(task.share_multiple); + + // 可领取条件: 已解锁 + 未领取 + 进度完成 + if (isUnlocked && !isClaimed && progress >= totalProgress && totalProgress > 0) { + claimable.push({ + id, + desc: task.desc || `任务#${id}`, + shareMultiple, + rewards: task.rewards || [], + }); + } + } + return claimable; +} + +/** + * 计算奖励摘要 + */ +function getRewardSummary(items) { + const summary = []; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + // 常见物品ID: 1=金币, 2=经验 + if (id === 1) summary.push(`金币${count}`); + else if (id === 2) summary.push(`经验${count}`); + else summary.push(`物品#${id}x${count}`); + } + return summary.join('/'); +} + +// ============ 自动领取 ============ + +/** + * 检查并领取所有可领取的任务奖励 + */ +async function checkAndClaimTasks() { + try { + const reply = await getTaskInfo(); + if (!reply.task_info) return; + + const taskInfo = reply.task_info; + const allTasks = [ + ...(taskInfo.growth_tasks || []), + ...(taskInfo.daily_tasks || []), + ...(taskInfo.tasks || []), + ]; + + const claimable = analyzeTaskList(allTasks); + if (claimable.length === 0) return; + + log('任务', `发现 ${claimable.length} 个可领取任务`); + + for (const task of claimable) { + try { + // 如果有分享翻倍,使用翻倍领取 + const useShare = task.shareMultiple > 1; + const multipleStr = useShare ? ` (${task.shareMultiple}倍)` : ''; + + const claimReply = await claimTaskReward(task.id, useShare); + const items = claimReply.items || []; + const rewardStr = items.length > 0 ? getRewardSummary(items) : '无'; + + log('任务', `领取: ${task.desc}${multipleStr} → ${rewardStr}`); + await sleep(300); + } catch (e) { + const message = e && e.message ? e.message : ''; + if (isTaskNotCompleteError(message)) { + log('任务', `任务未完成,跳过领取 #${task.id}`); + continue; + } + if (isTaskAlreadyClaimedError(message)) { + log('任务', `任务已领取,跳过领取 #${task.id}`); + continue; + } + logWarn('任务', `领取失败 #${task.id}: ${message}`); + } + } + } catch (e) { + // 静默失败 + } +} + +/** + * 处理任务状态变化推送 + */ +function onTaskInfoNotify(taskInfo) { + if (!taskInfo) return; + + const allTasks = [ + ...(taskInfo.growth_tasks || []), + ...(taskInfo.daily_tasks || []), + ...(taskInfo.tasks || []), + ]; + + const claimable = analyzeTaskList(allTasks); + if (claimable.length === 0) return; + + // 有可领取任务,延迟后自动领取 + log('任务', `有 ${claimable.length} 个任务可领取,准备自动领取...`); + setTimeout(() => claimTasksFromList(claimable), 1000); +} + +/** + * 从任务列表领取奖励 + */ +async function claimTasksFromList(claimable) { + for (const task of claimable) { + try { + const useShare = task.shareMultiple > 1; + const multipleStr = useShare ? ` (${task.shareMultiple}倍)` : ''; + + const claimReply = await claimTaskReward(task.id, useShare); + const items = claimReply.items || []; + const rewardStr = items.length > 0 ? getRewardSummary(items) : '无'; + + log('任务', `领取: ${task.desc}${multipleStr} → ${rewardStr}`); + await sleep(300); + } catch (e) { + const message = e && e.message ? e.message : ''; + if (isTaskNotCompleteError(message)) { + log('任务', `任务未完成,跳过领取 #${task.id}`); + continue; + } + if (isTaskAlreadyClaimedError(message)) { + log('任务', `任务已领取,跳过领取 #${task.id}`); + continue; + } + logWarn('任务', `领取失败 #${task.id}: ${message}`); + } + } +} + +// ============ 初始化 ============ + +function initTaskSystem() { + // 监听任务状态变化推送 + networkEvents.on('taskInfoNotify', onTaskInfoNotify); + + // 启动时检查一次任务 + setTimeout(() => checkAndClaimTasks(), 4000); +} + +function cleanupTaskSystem() { + networkEvents.off('taskInfoNotify', onTaskInfoNotify); +} + +module.exports = { + checkAndClaimTasks, + initTaskSystem, + cleanupTaskSystem, +}; diff --git a/server/src/userManager.js b/server/src/userManager.js new file mode 100644 index 0000000..0624694 --- /dev/null +++ b/server/src/userManager.js @@ -0,0 +1,573 @@ +const fs = require('fs'); +const path = require('path'); +const { log } = require('./utils'); +require('dotenv').config({ path: path.join(__dirname, '../../.env') }); + +const DATA_DIR = path.join(__dirname, '../data'); +const DATA_FILE = path.join(DATA_DIR, 'users.json'); +const SQLITE_FILE = path.join(DATA_DIR, 'users.db'); + +const isProduction = process.env.NODE_ENV === 'production'; + +let mysqlPool = null; +let sqliteDb = null; +let sqliteRun = null; +let sqliteAll = null; +let sqliteReady = null; + +if (isProduction) { + const mysql = require('mysql2/promise'); + mysqlPool = mysql.createPool({ + host: process.env.MYSQL_HOST || 'localhost', + user: process.env.MYSQL_USER || 'root', + password: process.env.MYSQL_PASSWORD || 'root', + database: process.env.MYSQL_DB || 'game_account_db', + port: process.env.MYSQL_PORT || 3306, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 + }); + + // Init Table + (async () => { + try { + const connection = await mysqlPool.getConnection(); + await connection.query(` + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255), + code TEXT, + auth_provider VARCHAR(50) DEFAULT 'local', + auth_id VARCHAR(255), + created_at BIGINT + ) + `); + connection.release(); + log('UserManager', 'MySQL initialized.'); + } catch (err) { + log('UserManager', `MySQL Init Error: ${err.message}`); + } + })(); +} + +if (!isProduction) { + const sqlite3 = require('sqlite3'); + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + sqliteDb = new sqlite3.Database(SQLITE_FILE); + sqliteRun = (sql, params = []) => new Promise((resolve, reject) => { + sqliteDb.run(sql, params, function (err) { + if (err) reject(err); + else resolve(this); + }); + }); + sqliteAll = (sql, params = []) => new Promise((resolve, reject) => { + sqliteDb.all(sql, params, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); + sqliteReady = sqliteRun(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password TEXT, + code TEXT, + auth_provider TEXT DEFAULT 'local', + auth_id TEXT, + created_at INTEGER + ) + `).then(() => { + log('UserManager', 'SQLite initialized.'); + }).catch((err) => { + log('UserManager', `SQLite Init Error: ${err.message}`); + }); +} + +class UserManager { + constructor() { + this.users = new Map(); // email -> user (Cache) + this.useMySQL = isProduction && !!mysqlPool; + this.useSQLite = !this.useMySQL && !!sqliteDb; + this.extraReady = this.initExtraTables(); + this.load(); + } + + async load() { + if (this.useMySQL) { + try { + const [rows] = await mysqlPool.query('SELECT * FROM users'); + rows.forEach(u => this.users.set(u.email, u)); + log('UserManager', `Loaded ${rows.length} users from MySQL.`); + } catch (e) { + log('UserManager', `Failed to load users from MySQL: ${e.message}`); + } + } else if (this.useSQLite) { + try { + await sqliteReady; + const rows = await sqliteAll('SELECT * FROM users'); + rows.forEach(u => this.users.set(u.email, u)); + log('UserManager', `Loaded ${rows.length} users from SQLite.`); + } catch (e) { + log('UserManager', `Failed to load users from SQLite: ${e.message}`); + } + } else { + try { + if (fs.existsSync(DATA_FILE)) { + const data = fs.readFileSync(DATA_FILE, 'utf8'); + const json = JSON.parse(data); + if (Array.isArray(json)) { + json.forEach(u => this.users.set(u.email, u)); + } + log('UserManager', `Loaded ${this.users.size} users from JSON.`); + } + } catch (e) { + log('UserManager', `Failed to load users from JSON: ${e.message}`); + } + } + } + + async save(user) { + const createdAt = user.createdAt || user.created_at || Date.now(); + if (this.useMySQL) { + try { + await mysqlPool.query( + `INSERT INTO users (email, password, code, auth_provider, auth_id, created_at) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE password = VALUES(password), code = VALUES(code), auth_provider = VALUES(auth_provider), auth_id = VALUES(auth_id)`, + [user.email, user.password, user.code, user.auth_provider || 'local', user.auth_id || null, createdAt] + ); + } catch (e) { + log('UserManager', `Failed to save user to MySQL: ${e.message}`); + } + } else if (this.useSQLite) { + try { + await sqliteReady; + await sqliteRun( + `INSERT INTO users (email, password, code, auth_provider, auth_id, created_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(email) DO UPDATE SET password = excluded.password, code = excluded.code, auth_provider = excluded.auth_provider, auth_id = excluded.auth_id`, + [user.email, user.password, user.code, user.auth_provider || 'local', user.auth_id || null, createdAt] + ); + } catch (e) { + log('UserManager', `Failed to save user to SQLite: ${e.message}`); + } + } else { + try { + const data = JSON.stringify(Array.from(this.users.values()), null, 2); + fs.writeFileSync(DATA_FILE, data, 'utf8'); + } catch (e) { + log('UserManager', `Failed to save users to JSON: ${e.message}`); + } + } + } + + async initExtraTables() { + if (this.useMySQL) { + try { + await mysqlPool.query(` + CREATE TABLE IF NOT EXISTS bot_logs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL, + tag VARCHAR(50), + msg TEXT, + type VARCHAR(20), + time BIGINT, + INDEX idx_email_time (email, time) + ) + `); + await mysqlPool.query(` + CREATE TABLE IF NOT EXISTS leaderboard_unlucky ( + email VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + count BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (email, name) + ) + `); + } catch (e) { + log('UserManager', `Failed to init extra tables: ${e.message}`); + } + return; + } + if (this.useSQLite) { + try { + await sqliteReady; + await sqliteRun(` + CREATE TABLE IF NOT EXISTS bot_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + tag TEXT, + msg TEXT, + type TEXT, + time INTEGER + ) + `); + await sqliteRun(` + CREATE TABLE IF NOT EXISTS leaderboard_unlucky ( + email TEXT NOT NULL, + name TEXT NOT NULL, + count INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (email, name) + ) + `); + } catch (e) { + log('UserManager', `Failed to init extra tables: ${e.message}`); + } + } + } + + normalizeLogMessage(msg) { + if (typeof msg === 'string') return msg; + if (msg === null || msg === undefined) return ''; + if (typeof msg === 'object') { + try { + return JSON.stringify(msg); + } catch { + return String(msg); + } + } + return String(msg); + } + + async saveBotLog(email, logEntry) { + if (!email || !logEntry) return; + await this.extraReady; + const tag = logEntry.tag || 'System'; + const msg = this.normalizeLogMessage(logEntry.msg); + const type = logEntry.type || 'info'; + const time = Number(logEntry.time) || Date.now(); + const limit = 500; + if (this.useMySQL) { + try { + await mysqlPool.query( + `INSERT INTO bot_logs (email, tag, msg, type, time) VALUES (?, ?, ?, ?, ?)`, + [email, tag, msg, type, time] + ); + await mysqlPool.query( + `DELETE FROM bot_logs WHERE email = ? AND id NOT IN ( + SELECT id FROM ( + SELECT id FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ? + ) t + )`, + [email, email, limit] + ); + } catch (e) { + log('UserManager', `Failed to save bot log: ${e.message}`); + } + return; + } + if (this.useSQLite) { + try { + await sqliteRun( + `INSERT INTO bot_logs (email, tag, msg, type, time) VALUES (?, ?, ?, ?, ?)`, + [email, tag, msg, type, time] + ); + await sqliteRun( + `DELETE FROM bot_logs WHERE email = ? AND id NOT IN ( + SELECT id FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ? + )`, + [email, email, limit] + ); + } catch (e) { + log('UserManager', `Failed to save bot log: ${e.message}`); + } + } + } + + async getBotLogs(email, limit = 200) { + if (!email) return []; + await this.extraReady; + const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 500) : 200; + if (this.useMySQL) { + try { + const [rows] = await mysqlPool.query( + `SELECT id, tag, msg, type, time FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?`, + [email, safeLimit] + ); + return rows.slice().reverse().map(row => ({ + id: String(row.id), + tag: row.tag || 'System', + msg: row.msg || '', + type: row.type || 'info', + time: Number(row.time) || Date.now() + })); + } catch (e) { + log('UserManager', `Failed to load bot logs: ${e.message}`); + return []; + } + } + if (this.useSQLite) { + try { + const rows = await sqliteAll( + `SELECT id, tag, msg, type, time FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?`, + [email, safeLimit] + ); + return rows.slice().reverse().map(row => ({ + id: String(row.id), + tag: row.tag || 'System', + msg: row.msg || '', + type: row.type || 'info', + time: Number(row.time) || Date.now() + })); + } catch (e) { + log('UserManager', `Failed to load bot logs: ${e.message}`); + return []; + } + } + return []; + } + + async getAllBotLogs(limit = 200, email) { + await this.extraReady; + const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 500) : 200; + const emailFilter = typeof email === 'string' ? email.trim() : ''; + if (this.useMySQL) { + try { + let rows; + if (emailFilter) { + [rows] = await mysqlPool.query( + `SELECT id, email, tag, msg, type, time FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?`, + [emailFilter, safeLimit] + ); + } else { + [rows] = await mysqlPool.query( + `SELECT id, email, tag, msg, type, time FROM bot_logs ORDER BY id DESC LIMIT ?`, + [safeLimit] + ); + } + return rows.slice().reverse().map(row => ({ + id: String(row.id), + email: row.email || '', + tag: row.tag || 'System', + msg: row.msg || '', + type: row.type || 'info', + time: Number(row.time) || Date.now() + })); + } catch (e) { + log('UserManager', `Failed to load all bot logs: ${e.message}`); + return []; + } + } + if (this.useSQLite) { + try { + let rows; + if (emailFilter) { + rows = await sqliteAll( + `SELECT id, email, tag, msg, type, time FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?`, + [emailFilter, safeLimit] + ); + } else { + rows = await sqliteAll( + `SELECT id, email, tag, msg, type, time FROM bot_logs ORDER BY id DESC LIMIT ?`, + [safeLimit] + ); + } + return rows.slice().reverse().map(row => ({ + id: String(row.id), + email: row.email || '', + tag: row.tag || 'System', + msg: row.msg || '', + type: row.type || 'info', + time: Number(row.time) || Date.now() + })); + } catch (e) { + log('UserManager', `Failed to load all bot logs: ${e.message}`); + return []; + } + } + return []; + } + + async saveUnlucky(email, name, count) { + if (!email || !name || !Number.isFinite(count) || count <= 0) return; + await this.extraReady; + if (this.useMySQL) { + try { + await mysqlPool.query( + `INSERT INTO leaderboard_unlucky (email, name, count) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE count = count + VALUES(count)`, + [email, name, count] + ); + } catch (e) { + log('UserManager', `Failed to save leaderboard: ${e.message}`); + } + return; + } + if (this.useSQLite) { + try { + await sqliteRun( + `INSERT INTO leaderboard_unlucky (email, name, count) + VALUES (?, ?, ?) + ON CONFLICT(email, name) DO UPDATE SET count = count + excluded.count`, + [email, name, count] + ); + } catch (e) { + log('UserManager', `Failed to save leaderboard: ${e.message}`); + } + } + } + + async loadUnluckyBoards() { + await this.extraReady; + const result = new Map(); + if (this.useMySQL) { + try { + const [rows] = await mysqlPool.query(`SELECT email, name, count FROM leaderboard_unlucky`); + for (const row of rows) { + const email = row.email; + const name = row.name; + const count = Number(row.count) || 0; + if (!email || !name || count <= 0) continue; + let board = result.get(email); + if (!board) { + board = new Map(); + result.set(email, board); + } + board.set(name, count); + } + } catch (e) { + log('UserManager', `Failed to load leaderboard: ${e.message}`); + } + return result; + } + if (this.useSQLite) { + try { + const rows = await sqliteAll(`SELECT email, name, count FROM leaderboard_unlucky`); + for (const row of rows) { + const email = row.email; + const name = row.name; + const count = Number(row.count) || 0; + if (!email || !name || count <= 0) continue; + let board = result.get(email); + if (!board) { + board = new Map(); + result.set(email, board); + } + board.set(name, count); + } + } catch (e) { + log('UserManager', `Failed to load leaderboard: ${e.message}`); + } + } + return result; + } + + async getUnluckyTop(email, limit = 10) { + if (!email) return []; + await this.extraReady; + const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 50) : 10; + if (this.useMySQL) { + try { + const [rows] = await mysqlPool.query( + `SELECT name, count FROM leaderboard_unlucky WHERE email = ? ORDER BY count DESC LIMIT ?`, + [email, safeLimit] + ); + return rows.map(row => ({ + name: row.name, + count: Number(row.count) || 0 + })); + } catch (e) { + log('UserManager', `Failed to load leaderboard: ${e.message}`); + return []; + } + } + if (this.useSQLite) { + try { + const rows = await sqliteAll( + `SELECT name, count FROM leaderboard_unlucky WHERE email = ? ORDER BY count DESC LIMIT ?`, + [email, safeLimit] + ); + return rows.map(row => ({ + name: row.name, + count: Number(row.count) || 0 + })); + } catch (e) { + log('UserManager', `Failed to load leaderboard: ${e.message}`); + return []; + } + } + return []; + } + + /** + * Get user by email + * @param {string} email + */ + getUser(email) { + return this.users.get(email); + } + + /** + * Create or update user + * @param {string} email + * @param {string} password + */ + async register(email, password, provider = 'local', authId = null) { + if (this.users.has(email) && provider === 'local') { + throw new Error('User already exists'); + } + + let user = this.users.get(email); + if (user) { + // Update existing user (e.g. linking OAuth) + if (provider !== 'local') { + user.auth_provider = provider; + user.auth_id = authId; + await this.save(user); + } + return user; + } + + user = { + email, + password, // In production, hash this! + code: '', + auth_provider: provider, + auth_id: authId, + createdAt: Date.now() + }; + this.users.set(email, user); + await this.save(user); + return user; + } + + /** + * Validate login + * @param {string} email + * @param {string} password + */ + login(email, password) { + const user = this.users.get(email); + if (!user) return null; + + // If OAuth user tries to login with password (and has no password set) + if (user.auth_provider !== 'local' && !user.password) { + return null; + } + + if (user.password !== password) { + return null; + } + return user; + } + + /** + * Update user's game code + * @param {string} email + * @param {string} code + */ + async updateCode(email, code) { + const user = this.users.get(email); + if (user) { + user.code = code; + await this.save(user); + return true; + } + return false; + } +} + +module.exports = new UserManager(); diff --git a/server/src/utils.js b/server/src/utils.js new file mode 100644 index 0000000..70b53ef --- /dev/null +++ b/server/src/utils.js @@ -0,0 +1,90 @@ +/** + * 通用工具函数 + */ + +const Long = require('long'); +const { RUNTIME_HINT_MASK, RUNTIME_HINT_DATA } = require('./config'); + +// ============ 服务器时间状态 ============ +let serverTimeMs = 0; +let localTimeAtSync = 0; + +// ============ 类型转换 ============ +function toLong(val) { + return Long.fromValue(val); +} + +function toNum(val) { + if (Long.isLong(val)) return val.toNumber(); + return val || 0; +} + +// ============ 时间相关 ============ +function now() { + return new Date().toLocaleTimeString(); +} + +/** 获取当前推算的服务器时间(秒) */ +function getServerTimeSec() { + if (!serverTimeMs) return Math.floor(Date.now() / 1000); + const elapsed = Date.now() - localTimeAtSync; + return Math.floor((serverTimeMs + elapsed) / 1000); +} + +/** 同步服务器时间 */ +function syncServerTime(ms) { + serverTimeMs = ms; + localTimeAtSync = Date.now(); +} + +/** + * 将时间戳归一化为秒级 + * 大于 1e12 认为是毫秒级,转换为秒级 + */ +function toTimeSec(val) { + const n = toNum(val); + if (n <= 0) return 0; + if (n > 1e12) return Math.floor(n / 1000); + return n; +} + +// ============ 日志 ============ +function log(tag, msg) { + console.log(`[${now()}] [${tag}] ${msg}`); +} + +function logWarn(tag, msg) { + console.log(`[${now()}] [${tag}] ⚠ ${msg}`); +} + +// ============ 异步工具 ============ +function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +let hintPrinted = false; +function decodeRuntimeHint() { + return String.fromCharCode(...RUNTIME_HINT_DATA.map(n => n ^ RUNTIME_HINT_MASK)); +} + +/** + * 输出开源声明: + * - force=true 时必定输出(用于启动) + * - 默认低频输出(用于业务流程中的隐性提示) + */ +function emitRuntimeHint(force = false) { + if (!force) { + // 约 3.3% 概率,且同一次进程最多输出 2 次 + if (Math.random() > 0.033) return; + if (hintPrinted && Math.random() > 0.2) return; + } + log('声明', decodeRuntimeHint()); + hintPrinted = true; +} + +module.exports = { + toLong, toNum, now, + getServerTimeSec, syncServerTime, toTimeSec, + log, logWarn, sleep, + emitRuntimeHint, +}; diff --git a/server/src/warehouse.js b/server/src/warehouse.js new file mode 100644 index 0000000..d455e3a --- /dev/null +++ b/server/src/warehouse.js @@ -0,0 +1,173 @@ +/** + * 仓库系统 - 自动出售果实 + * 协议说明:BagReply 使用 item_bag(ItemBag),item_bag.items 才是背包物品列表 + */ + +const { types } = require('./proto'); +const { sendMsgAsync } = require('./network'); +const { toLong, toNum, log, logWarn, emitRuntimeHint } = require('./utils'); +const { getFruitName } = require('./gameConfig'); +const seedShopData = require('../tools/seed-shop-merged-export.json'); + +// 游戏内金币和点券的物品 ID (GlobalData.GodItemId / DiamondItemId) +const GOLD_ITEM_ID = 1001; +const FRUIT_ID_SET = new Set( + ((seedShopData && seedShopData.rows) || []) + .map(row => Number(row.fruitId)) + .filter(Number.isFinite) +); + +let sellTimer = null; +let sellInterval = 60000; + +function isFruitIdBySeedData(id) { + return FRUIT_ID_SET.has(toNum(id)); +} + +/** + * 从 SellReply 中提取获得的金币数量 + * 新版 SellReply 返回 get_items (repeated Item),其中 id=1001 为金币 + */ +function extractGold(sellReply) { + if (sellReply.get_items && sellReply.get_items.length > 0) { + for (const item of sellReply.get_items) { + const id = toNum(item.id); + if (id === GOLD_ITEM_ID) { + return toNum(item.count); + } + } + return 0; + } + if (sellReply.gold !== undefined && sellReply.gold !== null) { + return toNum(sellReply.gold); + } + return 0; +} + +async function getBag() { + const body = types.BagRequest.encode(types.BagRequest.create({})).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.itempb.ItemService', 'Bag', body); + return types.BagReply.decode(replyBody); +} + +/** + * 将 item 转为 Sell 请求所需格式(id/count/uid 保留 Long 或转成 Long,与游戏一致) + */ +function toSellItem(item) { + const id = item.id != null ? toLong(item.id) : undefined; + const count = item.count != null ? toLong(item.count) : undefined; + const uid = item.uid != null ? toLong(item.uid) : undefined; + return { id, count, uid }; +} + +async function sellItems(items) { + const payload = items.map(toSellItem); + const body = types.SellRequest.encode(types.SellRequest.create({ items: payload })).finish(); + const { body: replyBody } = await sendMsgAsync('gamepb.itempb.ItemService', 'Sell', body); + return types.SellReply.decode(replyBody); +} + +/** + * 从 BagReply 取出物品列表(兼容 item_bag 与旧版 items) + */ +function getBagItems(bagReply) { + if (bagReply.item_bag && bagReply.item_bag.items && bagReply.item_bag.items.length) + return bagReply.item_bag.items; + return bagReply.items || []; +} + +async function sellAllFruits() { + try { + const bagReply = await getBag(); + const items = getBagItems(bagReply); + + const toSell = []; + const names = []; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + const uid = item.uid ? toNum(item.uid) : 0; + if (isFruitIdBySeedData(id) && count > 0) { + if (uid === 0) continue; // 跳过无效格子 + toSell.push(item); + names.push(`${getFruitName(id)}x${count}`); + } + } + + if (toSell.length === 0) return; + + const reply = await sellItems(toSell); + const totalGold = extractGold(reply); + log('仓库', `出售 ${names.join(', ')},获得 ${totalGold} 金币`); + emitRuntimeHint(false); + } catch (e) { + logWarn('仓库', `出售失败: ${e.message}`); + } +} + +async function debugSellFruits() { + try { + log('仓库', '正在检查背包...'); + const bagReply = await getBag(); + const items = getBagItems(bagReply); + log('仓库', `背包共 ${items.length} 种物品`); + + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + const isFruit = isFruitIdBySeedData(id); + if (isFruit) { + const name = getFruitName(id); + log('仓库', ` [果实] ${name}(${id}) x${count}`); + } + } + + const toSell = []; + for (const item of items) { + const id = toNum(item.id); + const count = toNum(item.count); + if (isFruitIdBySeedData(id) && count > 0) + toSell.push(item); + } + + if (toSell.length === 0) { + log('仓库', '没有果实可出售'); + return; + } + + log('仓库', `准备出售 ${toSell.length} 种果实...`); + const reply = await sellItems(toSell); + const totalGold = extractGold(reply); + log('仓库', `出售完成,共获得 ${totalGold} 金币`); + emitRuntimeHint(false); + } catch (e) { + logWarn('仓库', `调试出售失败: ${e.message}`); + console.error(e); + } +} + +function startSellLoop(interval = 60000) { + if (sellTimer) return; + sellInterval = interval; + setTimeout(() => { + sellAllFruits(); + sellTimer = setInterval(() => sellAllFruits(), sellInterval); + }, 10000); +} + +function stopSellLoop() { + if (sellTimer) { + clearInterval(sellTimer); + sellTimer = null; + } +} + +module.exports = { + getBag, + sellItems, + sellAllFruits, + debugSellFruits, + getBagItems, + startSellLoop, + stopSellLoop, +}; diff --git a/server/tools/calc-exp-yield.js b/server/tools/calc-exp-yield.js new file mode 100644 index 0000000..ad14fc7 --- /dev/null +++ b/server/tools/calc-exp-yield.js @@ -0,0 +1,474 @@ +/** + * 基于 tools/seed-shop-merged-export.json 计算经验收益率 + * + * 规则: + * 1) 每次收获经验 = exp(新版已去除铲地+1经验) + * 2) 种植速度: + * - 不施肥:2 秒种 18 块地 => 9 块/秒 + * - 普通肥:2 秒种 12 块地 => 6 块/秒 + * 3) 普通肥:直接减少一个生长阶段(按 Plant.json 的 grow_phases 取首个非0阶段时长) + * + * 用法: + * node tools/calc-exp-yield.js + * node tools/calc-exp-yield.js --lands 18 --level 27 + * node tools/calc-exp-yield.js --input tools/seed-shop-merged-export.json + * + * 运行时调用: + * const { getPlantingRecommendation } = require('../tools/calc-exp-yield'); + * const rec = getPlantingRecommendation(27, 18); + */ + +const fs = require('fs'); +const path = require('path'); + +const DEFAULT_INPUT = path.join(__dirname, 'seed-shop-merged-export.json'); +const PLANT_CONFIG_PATH = path.join(__dirname, '..', 'gameConfig', 'Plant.json'); +const DEFAULT_OUT_JSON = path.join(__dirname, 'exp-yield-result.json'); +const DEFAULT_OUT_CSV = path.join(__dirname, 'exp-yield-result.csv'); +const DEFAULT_OUT_TXT = path.join(__dirname, 'exp-yield-summary.txt'); + +const NO_FERT_PLANTS_PER_2_SEC = 18; +const NORMAL_FERT_PLANTS_PER_2_SEC = 12; +const NO_FERT_PLANT_SPEED_PER_SEC = NO_FERT_PLANTS_PER_2_SEC / 2; // 9 块/秒 +const NORMAL_FERT_PLANT_SPEED_PER_SEC = NORMAL_FERT_PLANTS_PER_2_SEC / 2; // 6 块/秒 + +function toNum(v, fallback = 0) { + const n = Number(v); + return Number.isFinite(n) ? n : fallback; +} + +function parseArgs(argv) { + const opts = { + input: DEFAULT_INPUT, + outJson: DEFAULT_OUT_JSON, + outCsv: DEFAULT_OUT_CSV, + outTxt: DEFAULT_OUT_TXT, + lands: 18, + level: null, + top: 20, + }; + + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--input' && argv[i + 1]) opts.input = argv[++i]; + else if (a === '--out-json' && argv[i + 1]) opts.outJson = argv[++i]; + else if (a === '--out-csv' && argv[i + 1]) opts.outCsv = argv[++i]; + else if (a === '--out-txt' && argv[i + 1]) opts.outTxt = argv[++i]; + else if (a === '--lands' && argv[i + 1]) opts.lands = Math.max(1, Math.floor(toNum(argv[++i], 18))); + else if (a === '--level' && argv[i + 1]) opts.level = Math.max(1, Math.floor(toNum(argv[++i], 1))); + else if (a === '--top' && argv[i + 1]) opts.top = Math.max(1, Math.floor(toNum(argv[++i], 20))); + else if (a === '--help' || a === '-h') { + printHelp(); + process.exit(0); + } + } + return opts; +} + +function printHelp() { + console.log('Usage: node tools/calc-exp-yield.js [options]'); + console.log(''); + console.log('Options:'); + console.log(' --input 输入 JSON 文件路径'); + console.log(' --lands 地块数(默认 18)'); + console.log(' --level 指定账号等级,输出该等级可用最优作物'); + console.log(' --top 摘要 Top 数量(默认 20)'); + console.log(' --out-json 输出 JSON 路径'); + console.log(' --out-csv 输出 CSV 路径'); + console.log(' --out-txt 输出 TXT 路径'); +} + +function readSeeds(inputPath) { + const text = fs.readFileSync(inputPath, 'utf8'); + const data = JSON.parse(text); + if (Array.isArray(data)) return data; + if (data && Array.isArray(data.rows)) return data.rows; + if (data && Array.isArray(data.seeds)) return data.seeds; + throw new Error('无法识别输入数据格式,需要数组或 rows/seeds 字段'); +} + +function parseGrowPhases(growPhases) { + if (!growPhases || typeof growPhases !== 'string') return []; + return growPhases + .split(';') + .map(x => x.trim()) + .filter(Boolean) + .map(seg => { + const parts = seg.split(':'); + return parts.length >= 2 ? toNum(parts[1], 0) : 0; + }) + .filter(sec => sec > 0); +} + +function loadSeedPhaseReduceMap() { + const text = fs.readFileSync(PLANT_CONFIG_PATH, 'utf8'); + const rows = JSON.parse(text); + if (!Array.isArray(rows)) { + throw new Error(`Plant 配置格式异常: ${PLANT_CONFIG_PATH}`); + } + + const map = new Map(); + for (const p of rows) { + const seedId = toNum(p.seed_id, 0); + if (seedId <= 0 || map.has(seedId)) continue; + const phases = parseGrowPhases(p.grow_phases); + if (phases.length === 0) continue; + map.set(seedId, phases[0]); // 普通肥减少一个阶段:以首个阶段时长为准 + } + return map; +} + +function calcEffectiveGrowTime(growSec, seedId, seedPhaseReduceMap) { + const reduce = toNum(seedPhaseReduceMap.get(seedId), 0); + if (reduce <= 0) return growSec; + return Math.max(1, growSec - reduce); +} + +function formatSec(sec) { + const s = Math.max(0, Math.round(sec)); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + const r = s % 60; + if (m < 60) return r > 0 ? `${m}m${r}s` : `${m}m`; + const h = Math.floor(m / 60); + const mm = m % 60; + return r > 0 ? `${h}h${mm}m${r}s` : `${h}h${mm}m`; +} + +function csvCell(v) { + const s = v == null ? '' : String(v); + if (s.includes(',') || s.includes('"') || s.includes('\n')) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function buildRows(rawSeeds, lands, seedPhaseReduceMap) { + const plantSecondsNoFert = lands / NO_FERT_PLANT_SPEED_PER_SEC; + const plantSecondsNormalFert = lands / NORMAL_FERT_PLANT_SPEED_PER_SEC; + const rows = []; + let skipped = 0; + let missingPhaseReduceCount = 0; + + for (const s of rawSeeds) { + const seedId = toNum(s.seedId || s.seed_id); + const name = s.name || `seed_${seedId}`; + const requiredLevel = toNum(s.requiredLevel || s.required_level || 1, 1); + const price = toNum(s.price, 0); + const expHarvest = toNum(s.exp, 0); + const growTimeSec = toNum(s.growTimeSec || s.growTime || s.grow_time || 0, 0); + + if (seedId <= 0 || growTimeSec <= 0) { + skipped++; + continue; + } + + const expPerCycle = expHarvest; + const reduceSec = toNum(seedPhaseReduceMap.get(seedId), 0); + if (reduceSec <= 0) missingPhaseReduceCount++; + const growTimeNormalFert = calcEffectiveGrowTime(growTimeSec, seedId, seedPhaseReduceMap); + + // 整个农场一轮 = 生长时间 + 本轮全部地块种植耗时 + const cycleSecNoFert = growTimeSec + plantSecondsNoFert; + const cycleSecNormalFert = growTimeNormalFert + plantSecondsNormalFert; + + const farmExpPerHourNoFert = (lands * expPerCycle / cycleSecNoFert) * 3600; + const farmExpPerHourNormalFert = (lands * expPerCycle / cycleSecNormalFert) * 3600; + const gainPercent = farmExpPerHourNoFert > 0 + ? ((farmExpPerHourNormalFert - farmExpPerHourNoFert) / farmExpPerHourNoFert) * 100 + : 0; + const expPerGoldSeed = price > 0 ? expPerCycle / price : 0; + + rows.push({ + seedId, + goodsId: toNum(s.goodsId || s.goods_id), + plantId: toNum(s.plantId || s.plant_id), + name, + requiredLevel, + unlocked: !!s.unlocked, + price, + expHarvest, + expPerCycle, + growTimeSec, + growTimeStr: s.growTimeStr || formatSec(growTimeSec), + normalFertReduceSec: reduceSec, + growTimeNormalFert, + growTimeNormalFertStr: formatSec(growTimeNormalFert), + cycleSecNoFert, + cycleSecNormalFert, + farmExpPerHourNoFert, + farmExpPerHourNormalFert, + farmExpPerDayNoFert: farmExpPerHourNoFert * 24, + farmExpPerDayNormalFert: farmExpPerHourNormalFert * 24, + gainPercent, + expPerGoldSeed, + fruitId: toNum(s?.fruit?.id || s.fruitId), + fruitCount: toNum(s?.fruit?.count || s.fruitCount), + }); + } + + return { rows, skipped, plantSecondsNoFert, plantSecondsNormalFert, missingPhaseReduceCount }; +} + +function pickTop(rows, key, topN) { + return [...rows] + .sort((a, b) => b[key] - a[key]) + .slice(0, topN); +} + +function buildBestByLevel(rows) { + const maxLevel = rows.reduce((m, r) => Math.max(m, r.requiredLevel), 1); + const result = []; + for (let lv = 1; lv <= maxLevel; lv++) { + // 按用户指定等级做理论可种分析,不受商店 unlocked 状态影响 + const available = rows.filter(r => r.requiredLevel <= lv); + if (available.length === 0) continue; + const bestNo = pickTop(available, 'farmExpPerHourNoFert', 1)[0]; + const bestFert = pickTop(available, 'farmExpPerHourNormalFert', 1)[0]; + result.push({ + level: lv, + bestNoFert: { + seedId: bestNo.seedId, + name: bestNo.name, + expPerHour: Number(bestNo.farmExpPerHourNoFert.toFixed(2)), + }, + bestNormalFert: { + seedId: bestFert.seedId, + name: bestFert.name, + expPerHour: Number(bestFert.farmExpPerHourNormalFert.toFixed(2)), + }, + }); + } + return result; +} + +function writeJson(outPath, payload) { + fs.writeFileSync(outPath, JSON.stringify(payload, null, 2), 'utf8'); +} + +function writeCsv(outPath, rows) { + const headers = [ + 'seedId', + 'name', + 'requiredLevel', + 'price', + 'expHarvest', + 'expPerCycle', + 'growTimeSec', + 'growTimeNormalFert', + 'cycleSecNoFert', + 'cycleSecNormalFert', + 'farmExpPerHourNoFert', + 'farmExpPerHourNormalFert', + 'farmExpPerDayNoFert', + 'farmExpPerDayNormalFert', + 'gainPercent', + 'expPerGoldSeed', + ]; + const lines = [headers.join(',')]; + for (const r of rows) { + lines.push(headers.map(h => csvCell(r[h])).join(',')); + } + fs.writeFileSync(outPath, `${lines.join('\n')}\n`, 'utf8'); +} + +function writeSummaryTxt(outPath, opts, meta, topNo, topFert, levelInfo) { + const lines = []; + lines.push('经验收益率分析结果'); + lines.push(''); + lines.push(`数据源: ${meta.input}`); + lines.push(`导出时间: ${new Date().toISOString()}`); + lines.push(`地块数: ${opts.lands}`); + lines.push(`种植速度(不施肥): ${NO_FERT_PLANTS_PER_2_SEC}块/${2}s (${NO_FERT_PLANT_SPEED_PER_SEC}块/s)`); + lines.push(`种植速度(普通肥): ${NORMAL_FERT_PLANTS_PER_2_SEC}块/${2}s (${NORMAL_FERT_PLANT_SPEED_PER_SEC}块/s)`); + lines.push(`整场种植耗时(不施肥): ${formatSec(meta.plantSecondsNoFert)}`); + lines.push(`整场种植耗时(普通肥): ${formatSec(meta.plantSecondsNormalFert)}`); + lines.push(`普通肥规则: 直接减少一个生长阶段(按 Plant.json 的首个阶段时长)`); + lines.push(`缺少阶段配置的种子数: ${meta.missingPhaseReduceCount}`); + lines.push(''); + + lines.push(`Top ${topNo.length}(不施肥,按每小时经验)`); + lines.push('排名 | 名称 | Lv需 | 生长 | 单轮经验 | 每小时经验'); + topNo.forEach((r, i) => { + lines.push( + `${String(i + 1).padStart(2)} | ${r.name} | ${r.requiredLevel} | ${r.growTimeStr} | ${r.expPerCycle} | ${r.farmExpPerHourNoFert.toFixed(2)}` + ); + }); + lines.push(''); + + lines.push(`Top ${topFert.length}(普通肥,按每小时经验)`); + lines.push('排名 | 名称 | Lv需 | 肥后生长 | 单轮经验 | 每小时经验 | 提升'); + topFert.forEach((r, i) => { + lines.push( + `${String(i + 1).padStart(2)} | ${r.name} | ${r.requiredLevel} | ${r.growTimeNormalFertStr} | ${r.expPerCycle} | ${r.farmExpPerHourNormalFert.toFixed(2)} | ${r.gainPercent.toFixed(2)}%` + ); + }); + lines.push(''); + + if (levelInfo) { + lines.push(`当前等级 Lv${levelInfo.level} 推荐`); + lines.push(`不施肥: ${levelInfo.bestNoFert.name}(seed=${levelInfo.bestNoFert.seedId}) -> ${levelInfo.bestNoFert.expPerHour.toFixed(2)} exp/h`); + lines.push(`普通肥: ${levelInfo.bestNormalFert.name}(seed=${levelInfo.bestNormalFert.seedId}) -> ${levelInfo.bestNormalFert.expPerHour.toFixed(2)} exp/h`); + lines.push(''); + } + + fs.writeFileSync(outPath, `${lines.join('\n')}\n`, 'utf8'); +} + +function analyzeExpYield(opts = {}) { + const lands = Math.max(1, Math.floor(toNum(opts.lands, 18))); + const level = opts.level == null ? null : Math.max(1, Math.floor(toNum(opts.level, 1))); + const top = Math.max(1, Math.floor(toNum(opts.top, 20))); + const input = opts.input || DEFAULT_INPUT; + const inputAbs = path.resolve(input); + const rawSeeds = readSeeds(inputAbs); + const seedPhaseReduceMap = loadSeedPhaseReduceMap(); + const { rows, skipped, plantSecondsNoFert, plantSecondsNormalFert, missingPhaseReduceCount } = buildRows(rawSeeds, lands, seedPhaseReduceMap); + + if (rows.length === 0) { + throw new Error('没有可计算的种子数据(请检查输入文件)'); + } + + const topNo = pickTop(rows, 'farmExpPerHourNoFert', top); + const topFert = pickTop(rows, 'farmExpPerHourNormalFert', top); + const bestByLevel = buildBestByLevel(rows); + + let currentLevel = null; + if (level != null) { + currentLevel = bestByLevel.find(x => x.level === level) || null; + } + + return { + generatedAt: new Date().toISOString(), + input: inputAbs, + config: { + lands, + plantSpeedPerSecNoFert: NO_FERT_PLANT_SPEED_PER_SEC, + plantSpeedPerSecNormalFert: NORMAL_FERT_PLANT_SPEED_PER_SEC, + plantSecondsNoFert, + plantSecondsNormalFert, + fertilizer: { + mode: 'minus_one_phase', + }, + rule: { + expPerCycle: 'expHarvest', + }, + }, + stats: { + rawCount: rawSeeds.length, + calculatedCount: rows.length, + skippedCount: skipped, + missingPhaseReduceCount, + }, + topNoFert: topNo.map(r => ({ + seedId: r.seedId, + name: r.name, + requiredLevel: r.requiredLevel, + expPerHour: Number(r.farmExpPerHourNoFert.toFixed(4)), + })), + topNormalFert: topFert.map(r => ({ + seedId: r.seedId, + name: r.name, + requiredLevel: r.requiredLevel, + expPerHour: Number(r.farmExpPerHourNormalFert.toFixed(4)), + gainPercent: Number(r.gainPercent.toFixed(4)), + })), + bestByLevel, + currentLevel, + rows, + }; +} + +function getPlantingRecommendation(level, lands, opts = {}) { + const safeLevel = Math.max(1, Math.floor(toNum(level, 1))); + const payload = analyzeExpYield({ + input: opts.input || DEFAULT_INPUT, + lands: lands == null ? 18 : lands, + top: opts.top || 20, + level: safeLevel, + }); + + const availableRows = payload.rows.filter(r => r.requiredLevel <= safeLevel); + const bestNoFertRow = pickTop(availableRows, 'farmExpPerHourNoFert', 1)[0] || null; + const bestNormalFertRow = pickTop(availableRows, 'farmExpPerHourNormalFert', 1)[0] || null; + + return { + level: safeLevel, + lands: payload.config.lands, + input: payload.input, + bestNoFert: bestNoFertRow ? { + seedId: bestNoFertRow.seedId, + name: bestNoFertRow.name, + requiredLevel: bestNoFertRow.requiredLevel, + expPerHour: Number(bestNoFertRow.farmExpPerHourNoFert.toFixed(4)), + } : null, + bestNormalFert: bestNormalFertRow ? { + seedId: bestNormalFertRow.seedId, + name: bestNormalFertRow.name, + requiredLevel: bestNormalFertRow.requiredLevel, + expPerHour: Number(bestNormalFertRow.farmExpPerHourNormalFert.toFixed(4)), + } : null, + candidatesNoFert: pickTop(availableRows, 'farmExpPerHourNoFert', opts.top || 20).map(r => ({ + seedId: r.seedId, + name: r.name, + requiredLevel: r.requiredLevel, + expPerHour: Number(r.farmExpPerHourNoFert.toFixed(4)), + })), + candidatesNormalFert: pickTop(availableRows, 'farmExpPerHourNormalFert', opts.top || 20).map(r => ({ + seedId: r.seedId, + name: r.name, + requiredLevel: r.requiredLevel, + expPerHour: Number(r.farmExpPerHourNormalFert.toFixed(4)), + gainPercent: Number(r.gainPercent.toFixed(4)), + })), + }; +} + +function main() { + const opts = parseArgs(process.argv.slice(2)); + const payload = analyzeExpYield(opts); + const rows = payload.rows; + const topNo = pickTop(rows, 'farmExpPerHourNoFert', opts.top); + const topFert = pickTop(rows, 'farmExpPerHourNormalFert', opts.top); + const currentLevel = payload.currentLevel; + + writeJson(path.resolve(opts.outJson), payload); + writeCsv(path.resolve(opts.outCsv), rows); + writeSummaryTxt( + path.resolve(opts.outTxt), + opts, + { + input: payload.input, + plantSecondsNoFert: payload.config.plantSecondsNoFert, + plantSecondsNormalFert: payload.config.plantSecondsNormalFert, + missingPhaseReduceCount: payload.stats.missingPhaseReduceCount, + }, + topNo, + topFert, + currentLevel + ); + + console.log(`[收益率] 计算完成,共 ${rows.length} 条(跳过 ${payload.stats.skippedCount} 条)`); + console.log(`[收益率] JSON: ${path.resolve(opts.outJson)}`); + console.log(`[收益率] CSV : ${path.resolve(opts.outCsv)}`); + console.log(`[收益率] TXT : ${path.resolve(opts.outTxt)}`); + if (currentLevel) { + console.log(`[收益率] Lv${opts.level} 最优(不施肥): ${currentLevel.bestNoFert.name} ${currentLevel.bestNoFert.expPerHour} exp/h`); + console.log(`[收益率] Lv${opts.level} 最优(普通肥): ${currentLevel.bestNormalFert.name} ${currentLevel.bestNormalFert.expPerHour} exp/h`); + } +} + +module.exports = { + analyzeExpYield, + getPlantingRecommendation, + DEFAULT_INPUT, +}; + +if (require.main === module) { + try { + main(); + } catch (e) { + console.error(`[收益率] 失败: ${e.message}`); + process.exit(1); + } +} diff --git a/server/tools/crop_list.md b/server/tools/crop_list.md new file mode 100644 index 0000000..3ea15ab --- /dev/null +++ b/server/tools/crop_list.md @@ -0,0 +1,127 @@ +# 农作物列表 (共 123 种) + +| ID | 名称 | 等级 | 季数 | 产量 | 生长(小时) | 阶段详情 | +|---|---|---|---|---|---|---| +| 1020001 | 草莓 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020002 | 白萝卜 | 1 | 1 | 5 | 0.0h | 种子:30;发芽:30;成熟:0; | +| 1020003 | 胡萝卜 | 1 | 1 | 10 | 0.0h | 种子:30;发芽:30;小叶子:30;大叶子:30;成熟:0; | +| 1020004 | 玉米 | 1 | 1 | 40 | 1.3h | 种子:960;发芽:960;小叶子:960;大叶子:960;开花:960;成熟:0; | +| 1020005 | 土豆 | 1 | 1 | 60 | 2.0h | 种子:1440;发芽:1440;小叶子:1440;大叶子:1440;初熟:1440;成熟:0; | +| 1020006 | 茄子 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020007 | 番茄 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020008 | 豌豆 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; | +| 1020009 | 辣椒 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; | +| 1020010 | 南瓜 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; | +| 1020011 | 苹果 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020013 | 葡萄 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020014 | 西瓜 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020015 | 香蕉 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; | +| 1020016 | 菠萝蜜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020018 | 桃子 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;开花:5760;小叶子:5760;大叶子:5760;成熟:0; | +| 1020019 | 橙子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; | +| 1020022 | 鳄梨 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020023 | 石榴 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; | +| 1020026 | 柚子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; | +| 1020027 | 菠萝 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020029 | 椰子 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020031 | 葫芦 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020033 | 火龙果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020034 | 樱桃 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020035 | 荔枝 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020036 | 箬竹 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; | +| 1020037 | 莲藕 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020038 | 木瓜 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020039 | 杨桃 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020041 | 红玫瑰 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; | +| 1020042 | 柠檬 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020043 | 无花果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020044 | 丝瓜 | 1 | 1 | 200 | 12.0h | 种子:8640;长枝:8640;开花:8640;小叶子:8640;大叶子:8640;结果:0; | +| 1020045 | 猕猴桃 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020047 | 甘蔗 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;幼苗:8640;分叶:8640;伸长:8640;成熟:0; | +| 1020048 | 杨梅 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020049 | 花生 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020050 | 蘑菇 | 1 | 2 | 200 | 4.0h | 种子:3600;发芽:3600;大叶子:3600;初熟:3600;成熟:0; | +| 1020051 | 红枣 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020052 | 金针菇 | 1 | 2 | 200 | 12.0h | 种子:7200;发菌:7200;出菇:7200;幼菇:10800;初熟:10800;成熟:0; | +| 1020053 | 桂圆 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020054 | 梨 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020055 | 枇杷 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020056 | 哈密瓜 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; | +| 1020057 | 芒果 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020058 | 榴莲 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020059 | 大白菜 | 1 | 1 | 20 | 0.1h | 种子:60;发芽:60;幼苗:60;成株:60;卷心:60;成熟:0; | +| 1020060 | 水稻 | 1 | 1 | 30 | 0.7h | 种子:480;幼苗:480;秧苗:480;幼穗:480;开花:480;成熟:0; | +| 1020061 | 小麦 | 1 | 1 | 40 | 1.0h | 种子:720;发芽:720;小叶子:720;大叶子:720;幼穗:720;成熟:0; | +| 1020062 | 四叶草 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; | +| 1020063 | 苦瓜 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020064 | 大葱 | 1 | 1 | 30 | 0.3h | 种子:300;发芽:300;小叶子:300;大叶子:300;成熟:0; | +| 1020065 | 大蒜 | 1 | 1 | 20 | 0.2h | 种子:120;发芽:120;幼苗:120;伸长:120;初熟:120;成熟:0; | +| 1020066 | 鲜姜 | 1 | 1 | 60 | 1.7h | 种子:1500;发芽:1500;小叶子:1500;大叶子:1500;成熟:0; | +| 1020067 | 香瓜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020068 | 冬瓜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020070 | 黄豆 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;初熟:8640;成熟:0; | +| 1020071 | 小白菜 | 1 | 1 | 80 | 2.5h | 种子:2250;发芽:2250;小叶子:2250;大叶子:2250;成熟:0; | +| 1020072 | 榛子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;开花:17280;小叶子:17280;大叶子:17280;成熟:0; | +| 1020073 | 菠菜 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; | +| 1020074 | 金桔 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020075 | 桑葚 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020076 | 山竹 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020077 | 蓝莓 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020078 | 杏子 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; | +| 1020079 | 番石榴 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020080 | 月柿 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020083 | 红毛丹 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; | +| 1020084 | 芭蕉 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0; | +| 1020085 | 番荔枝 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0; | +| 1020086 | 橄榄 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020087 | 百香果 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0; | +| 1020088 | 灯笼果 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0; | +| 1020089 | 芦荟 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; | +| 1020090 | 薄荷 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020091 | 山楂 | 1 | 1 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:14400;开花:14400;初熟:14400;成熟:0; | +| 1020095 | 栗子 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020096 | 生菜 | 1 | 1 | 80 | 3.0h | 种子:2160;发芽:2160;小叶子:2160;大叶子:2160;初熟:2160;成熟:0; | +| 1020097 | 黄瓜 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; | +| 1020098 | 花菜 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;幼苗:8640;卷心:8640;初熟:8640;成熟:0; | +| 1020099 | 油菜 | 1 | 1 | 200 | 4.0h | 种子:3600;发芽:3600;小叶子:3600;大叶子:3600;成熟:0; | +| 1020100 | 竹笋 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;幼苗:2880;伸长:2880;初熟:2880;成熟:0; | +| 1020103 | 天香百合 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; | +| 1020104 | 非洲菊 | 1 | 1 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:2400;花蕾:2400;盛开:2400;成熟:0; | +| 1020105 | 小雏菊 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; | +| 1020110 | 满天星 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0; | +| 1020116 | 曼陀罗华 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;花蕾:3600;盛开:0; | +| 1020120 | 蒲公英 | 1 | 1 | 200 | 24.0h | 种子:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:17280;成熟:0; | +| 1020126 | 曼珠沙华 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020128 | 茉莉花 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; | +| 1020135 | 火绒草 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0; | +| 1020141 | 花香根鸢尾 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;花蕾:8640;盛开:0; | +| 1020142 | 虞美人 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0; | +| 1020143 | 含羞草 | 1 | 1 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:7200;花蕾:7200;盛开:7200;成熟:0; | +| 1020145 | 向日葵 | 1 | 1 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:2400;开花:2400;初熟:2400;成熟:0; | +| 1020147 | 牵牛花 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0; | +| 1020161 | 秋菊(黄色) | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; | +| 1020162 | 秋菊(红色) | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; | +| 1020201 | 天山雪莲 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;幼株:21600;成熟:0; | +| 1020202 | 金边灵芝 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;芝蕾:4800;幼芝:7200;初熟:7200;成熟:0; | +| 1020204 | 人参 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; | +| 1020218 | 瓶子树 | 1 | 2 | 200 | 4.0h | 种子:2400;长枝:2400;小叶子:2400;大叶子:3600;初熟:3600;成树:0; | +| 1020220 | 猪笼草 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; | +| 1020221 | 天堂鸟 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; | +| 1020222 | 豹皮花 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; | +| 1020225 | 宝华玉兰 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; | +| 1020226 | 依米花 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; | +| 1020227 | 大王花 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;幼蕾:4800;含苞:7200;初放:7200;盛开:0; | +| 1020228 | 人参果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; | +| 1020229 | 何首乌 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | +| 1020235 | 金花茶 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; | +| 1020242 | 似血杜鹃 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; | +| 1020259 | 银莲花 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0; | +| 1020305 | 韭菜 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; | +| 1020306 | 芹菜 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; | +| 1020308 | 核桃 | 1 | 1 | 200 | 12.0h | 种子:10800;发芽:10800;小叶子:10800;大叶子:10800;成熟:0; | +| 1020396 | 迎春花 | 1 | 1 | 200 | 4.0h | 种子:2880;幼芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0; | +| 1020413 | 李子 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; | +| 1020442 | 睡莲 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;幼株:10800;成熟:0; | +| 1021542 | 新春红包 | 1 | 1 | 20 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; | +| 2020002 | 白萝卜 | 1 | 1 | 5 | 0.0h | 种子:1;发芽:1;成熟:0; | +| 2029998 | 哈哈南瓜 | 1 | 1 | 50 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; | diff --git a/server/tools/list_crops.js b/server/tools/list_crops.js new file mode 100644 index 0000000..0033663 --- /dev/null +++ b/server/tools/list_crops.js @@ -0,0 +1,37 @@ +const fs = require('fs'); +const path = require('path'); + +const plantPath = path.join(__dirname, '../gameConfig/Plant.json'); + +try { + const data = fs.readFileSync(plantPath, 'utf8'); + const plants = JSON.parse(data); + + let content = `# 农作物列表 (共 ${plants.length} 种)\n\n`; + content += `| ID | 名称 | 等级 | 季数 | 产量 | 生长(小时) | 阶段详情 |\n`; + content += `|---|---|---|---|---|---|---|\n`; + + plants.sort((a, b) => a.land_level_need - b.land_level_need || a.id - b.id); + + plants.forEach(p => { + let totalTime = 0; + if (p.grow_phases) { + const parts = p.grow_phases.split(';'); + parts.forEach(part => { + if (part) { + const [stage, time] = part.split(':'); + if (time) totalTime += parseInt(time); + } + }); + } + + const hours = (totalTime / 3600).toFixed(1); + content += `| ${p.id} | ${p.name} | ${p.land_level_need} | ${p.seasons} | ${p.fruit ? p.fruit.count : '?'} | ${hours}h | ${p.grow_phases} |\n`; + }); + + fs.writeFileSync(path.join(__dirname, 'crop_list.md'), content); + console.log('列表已生成: server/tools/crop_list.md'); + +} catch (err) { + console.error('读取失败:', err); +} diff --git a/server/tools/seed-shop-merged-export.json b/server/tools/seed-shop-merged-export.json new file mode 100644 index 0000000..0a9bb6e --- /dev/null +++ b/server/tools/seed-shop-merged-export.json @@ -0,0 +1,2007 @@ +{ + "exportedAt": "2026-02-11T01:50:22.599Z", + "source": "api:http://127.0.0.1:3000/api/seeds", + "count": 100, + "rows": [ + { + "seedId": 20002, + "goodsId": 15, + "plantId": 1020002, + "name": "白萝卜", + "requiredLevel": 1, + "price": 2, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1, + "expPerCycle": 2, + "growTimeSec": 60, + "growTimeStr": "1分钟", + "expPerHour": 120, + "expPerGold": 1, + "seasons": 1, + "fruitId": 40002, + "fruitCount": 5 + }, + { + "seedId": 20003, + "goodsId": 16, + "plantId": 1020003, + "name": "胡萝卜", + "requiredLevel": 2, + "price": 4, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 2, + "expPerCycle": 3, + "growTimeSec": 120, + "growTimeStr": "2分钟", + "expPerHour": 90, + "expPerGold": 0.75, + "seasons": 1, + "fruitId": 40003, + "fruitCount": 10 + }, + { + "seedId": 20059, + "goodsId": 17, + "plantId": 1020059, + "name": "大白菜", + "requiredLevel": 3, + "price": 10, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 5, + "expPerCycle": 6, + "growTimeSec": 300, + "growTimeStr": "5分钟", + "expPerHour": 72, + "expPerGold": 0.6, + "seasons": 1, + "fruitId": 40059, + "fruitCount": 20 + }, + { + "seedId": 20065, + "goodsId": 18, + "plantId": 1020065, + "name": "大蒜", + "requiredLevel": 4, + "price": 20, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 10, + "expPerCycle": 11, + "growTimeSec": 600, + "growTimeStr": "10分钟", + "expPerHour": 66, + "expPerGold": 0.55, + "seasons": 1, + "fruitId": 40065, + "fruitCount": 20 + }, + { + "seedId": 20064, + "goodsId": 115, + "plantId": 1020064, + "name": "大葱", + "requiredLevel": 5, + "price": 42, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 20, + "expPerCycle": 21, + "growTimeSec": 1200, + "growTimeStr": "20分钟", + "expPerHour": 63, + "expPerGold": 0.5, + "seasons": 1, + "fruitId": 40064, + "fruitCount": 30 + }, + { + "seedId": 20060, + "goodsId": 19, + "plantId": 1020060, + "name": "水稻", + "requiredLevel": 6, + "price": 84, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 41, + "expPerCycle": 42, + "growTimeSec": 2400, + "growTimeStr": "40分钟", + "expPerHour": 63, + "expPerGold": 0.5, + "seasons": 1, + "fruitId": 40060, + "fruitCount": 30 + }, + { + "seedId": 20061, + "goodsId": 20, + "plantId": 1020061, + "name": "小麦", + "requiredLevel": 7, + "price": 126, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 62, + "expPerCycle": 63, + "growTimeSec": 3600, + "growTimeStr": "1小时", + "expPerHour": 63, + "expPerGold": 0.5, + "seasons": 1, + "fruitId": 40061, + "fruitCount": 40 + }, + { + "seedId": 20004, + "goodsId": 21, + "plantId": 1020004, + "name": "玉米", + "requiredLevel": 8, + "price": 168, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 82, + "expPerCycle": 83, + "growTimeSec": 4800, + "growTimeStr": "1小时20分", + "expPerHour": 62.25, + "expPerGold": 0.494048, + "seasons": 1, + "fruitId": 40004, + "fruitCount": 40 + }, + { + "seedId": 20066, + "goodsId": 116, + "plantId": 1020066, + "name": "鲜姜", + "requiredLevel": 9, + "price": 223, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 106, + "expPerCycle": 107, + "growTimeSec": 6000, + "growTimeStr": "1小时40分", + "expPerHour": 64.2, + "expPerGold": 0.479821, + "seasons": 1, + "fruitId": 40066, + "fruitCount": 60 + }, + { + "seedId": 20005, + "goodsId": 22, + "plantId": 1020005, + "name": "土豆", + "requiredLevel": 10, + "price": 268, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 128, + "expPerCycle": 129, + "growTimeSec": 7200, + "growTimeStr": "2小时", + "expPerHour": 64.5, + "expPerGold": 0.481343, + "seasons": 1, + "fruitId": 40005, + "fruitCount": 60 + }, + { + "seedId": 20071, + "goodsId": 23, + "plantId": 1020071, + "name": "小白菜", + "requiredLevel": 11, + "price": 335, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 160, + "expPerCycle": 161, + "growTimeSec": 9000, + "growTimeStr": "2小时30分", + "expPerHour": 64.4, + "expPerGold": 0.480597, + "seasons": 1, + "fruitId": 40071, + "fruitCount": 80 + }, + { + "seedId": 20096, + "goodsId": 24, + "plantId": 1020096, + "name": "生菜", + "requiredLevel": 12, + "price": 402, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 192, + "expPerCycle": 193, + "growTimeSec": 10800, + "growTimeStr": "3小时", + "expPerHour": 64.3333, + "expPerGold": 0.4801, + "seasons": 1, + "fruitId": 40096, + "fruitCount": 80 + }, + { + "seedId": 20099, + "goodsId": 60, + "plantId": 1020099, + "name": "油菜", + "requiredLevel": 13, + "price": 576, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 272, + "expPerCycle": 273, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 68.25, + "expPerGold": 0.473958, + "seasons": 1, + "fruitId": 40099, + "fruitCount": 200 + }, + { + "seedId": 20006, + "goodsId": 25, + "plantId": 1020006, + "name": "茄子", + "requiredLevel": 14, + "price": 1152, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 544, + "expPerCycle": 545, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 68.125, + "expPerGold": 0.47309, + "seasons": 1, + "fruitId": 40006, + "fruitCount": 200 + }, + { + "seedId": 20051, + "goodsId": 26, + "plantId": 1020051, + "name": "红枣", + "requiredLevel": 15, + "price": 1728, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 816, + "expPerCycle": 817, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 68.0833, + "expPerGold": 0.472801, + "seasons": 1, + "fruitId": 40051, + "fruitCount": 200 + }, + { + "seedId": 20120, + "goodsId": 117, + "plantId": 1020120, + "name": "蒲公英", + "requiredLevel": 16, + "price": 3456, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1632, + "expPerCycle": 1633, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 68.0417, + "expPerGold": 0.472512, + "seasons": 1, + "fruitId": 40120, + "fruitCount": 200 + }, + { + "seedId": 20259, + "goodsId": 118, + "plantId": 1020259, + "name": "银莲花", + "requiredLevel": 17, + "price": 640, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 288, + "expPerCycle": 289, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 72.25, + "expPerGold": 0.451562, + "seasons": 1, + "fruitId": 40259, + "fruitCount": 200 + }, + { + "seedId": 20007, + "goodsId": 27, + "plantId": 1020007, + "name": "番茄", + "requiredLevel": 18, + "price": 1280, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 576, + "expPerCycle": 577, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 72.125, + "expPerGold": 0.450781, + "seasons": 1, + "fruitId": 40007, + "fruitCount": 200 + }, + { + "seedId": 20098, + "goodsId": 28, + "plantId": 1020098, + "name": "花菜", + "requiredLevel": 19, + "price": 1920, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 864, + "expPerCycle": 865, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 72.0833, + "expPerGold": 0.450521, + "seasons": 1, + "fruitId": 40098, + "fruitCount": 200 + }, + { + "seedId": 20305, + "goodsId": 61, + "plantId": 1020305, + "name": "韭菜", + "requiredLevel": 20, + "price": 3840, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1728, + "expPerCycle": 1729, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 72.0417, + "expPerGold": 0.45026, + "seasons": 1, + "fruitId": 40305, + "fruitCount": 200 + }, + { + "seedId": 20105, + "goodsId": 119, + "plantId": 1020105, + "name": "小雏菊", + "requiredLevel": 21, + "price": 704, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 304, + "expPerCycle": 305, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 76.25, + "expPerGold": 0.433239, + "seasons": 1, + "fruitId": 40105, + "fruitCount": 200 + }, + { + "seedId": 20008, + "goodsId": 29, + "plantId": 1020008, + "name": "豌豆", + "requiredLevel": 22, + "price": 1408, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 608, + "expPerCycle": 609, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 76.125, + "expPerGold": 0.432528, + "seasons": 1, + "fruitId": 40008, + "fruitCount": 200 + }, + { + "seedId": 20037, + "goodsId": 63, + "plantId": 1020037, + "name": "莲藕", + "requiredLevel": 23, + "price": 2112, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 912, + "expPerCycle": 913, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 76.0833, + "expPerGold": 0.432292, + "seasons": 1, + "fruitId": 40037, + "fruitCount": 200 + }, + { + "seedId": 20041, + "goodsId": 30, + "plantId": 1020041, + "name": "红玫瑰", + "requiredLevel": 24, + "price": 4224, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1824, + "expPerCycle": 1825, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 76.0417, + "expPerGold": 0.432055, + "seasons": 1, + "fruitId": 40041, + "fruitCount": 200 + }, + { + "seedId": 20161, + "goodsId": 120, + "plantId": 1020161, + "name": "秋菊(黄色)", + "requiredLevel": 25, + "price": 792, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 324, + "expPerCycle": 325, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 81.25, + "expPerGold": 0.410354, + "seasons": 1, + "fruitId": 40161, + "fruitCount": 200 + }, + { + "seedId": 20110, + "goodsId": 121, + "plantId": 1020110, + "name": "满天星", + "requiredLevel": 26, + "price": 1584, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 648, + "expPerCycle": 649, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 81.125, + "expPerGold": 0.409722, + "seasons": 1, + "fruitId": 40110, + "fruitCount": 200 + }, + { + "seedId": 20143, + "goodsId": 122, + "plantId": 1020143, + "name": "含羞草", + "requiredLevel": 27, + "price": 2376, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 972, + "expPerCycle": 973, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 81.0833, + "expPerGold": 0.409512, + "seasons": 1, + "fruitId": 40143, + "fruitCount": 200 + }, + { + "seedId": 20147, + "goodsId": 123, + "plantId": 1020147, + "name": "牵牛花", + "requiredLevel": 28, + "price": 4752, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1944, + "expPerCycle": 1945, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 81.0417, + "expPerGold": 0.409301, + "seasons": 1, + "fruitId": 40147, + "fruitCount": 200 + }, + { + "seedId": 20162, + "goodsId": 124, + "plantId": 1020162, + "name": "秋菊(红色)", + "requiredLevel": 29, + "price": 888, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 344, + "expPerCycle": 345, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 86.25, + "expPerGold": 0.388514, + "seasons": 1, + "fruitId": 40162, + "fruitCount": 200 + }, + { + "seedId": 20009, + "goodsId": 31, + "plantId": 1020009, + "name": "辣椒", + "requiredLevel": 30, + "price": 1776, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 688, + "expPerCycle": 689, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 86.125, + "expPerGold": 0.38795, + "seasons": 1, + "fruitId": 40009, + "fruitCount": 200 + }, + { + "seedId": 20097, + "goodsId": 32, + "plantId": 1020097, + "name": "黄瓜", + "requiredLevel": 31, + "price": 2664, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 1032, + "expPerCycle": 1033, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 86.0833, + "expPerGold": 0.387763, + "seasons": 1, + "fruitId": 40097, + "fruitCount": 200 + }, + { + "seedId": 20306, + "goodsId": 64, + "plantId": 1020306, + "name": "芹菜", + "requiredLevel": 32, + "price": 5328, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 2064, + "expPerCycle": 2065, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 86.0417, + "expPerGold": 0.387575, + "seasons": 1, + "fruitId": 40306, + "fruitCount": 200 + }, + { + "seedId": 20103, + "goodsId": 125, + "plantId": 1020103, + "name": "天香百合", + "requiredLevel": 33, + "price": 992, + "unlocked": true, + "limitCount": 0, + "boughtNum": 0, + "exp": 368, + "expPerCycle": 369, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 92.25, + "expPerGold": 0.371976, + "seasons": 1, + "fruitId": 40103, + "fruitCount": 200 + }, + { + "seedId": 20010, + "goodsId": 33, + "plantId": 1020010, + "name": "南瓜", + "requiredLevel": 34, + "price": 1984, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 736, + "expPerCycle": 737, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 92.125, + "expPerGold": 0.371472, + "seasons": 1, + "fruitId": 40010, + "fruitCount": 200 + }, + { + "seedId": 20308, + "goodsId": 34, + "plantId": 1020308, + "name": "核桃", + "requiredLevel": 35, + "price": 2976, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1104, + "expPerCycle": 1105, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 92.0833, + "expPerGold": 0.371304, + "seasons": 1, + "fruitId": 40308, + "fruitCount": 200 + }, + { + "seedId": 20091, + "goodsId": 126, + "plantId": 1020091, + "name": "山楂", + "requiredLevel": 36, + "price": 5952, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2208, + "expPerCycle": 2209, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 92.0417, + "expPerGold": 0.371136, + "seasons": 1, + "fruitId": 40091, + "fruitCount": 200 + }, + { + "seedId": 20073, + "goodsId": 127, + "plantId": 1020073, + "name": "菠菜", + "requiredLevel": 37, + "price": 1120, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 392, + "expPerCycle": 393, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 98.25, + "expPerGold": 0.350893, + "seasons": 1, + "fruitId": 40073, + "fruitCount": 200 + }, + { + "seedId": 20001, + "goodsId": 35, + "plantId": 1020001, + "name": "草莓", + "requiredLevel": 38, + "price": 2240, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 784, + "expPerCycle": 785, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 98.125, + "expPerGold": 0.350446, + "seasons": 1, + "fruitId": 40001, + "fruitCount": 200 + }, + { + "seedId": 20011, + "goodsId": 36, + "plantId": 1020011, + "name": "苹果", + "requiredLevel": 39, + "price": 3360, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1176, + "expPerCycle": 1177, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 98.0833, + "expPerGold": 0.350298, + "seasons": 1, + "fruitId": 40011, + "fruitCount": 200 + }, + { + "seedId": 20062, + "goodsId": 128, + "plantId": 1020062, + "name": "四叶草", + "requiredLevel": 40, + "price": 6720, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2352, + "expPerCycle": 2353, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 98.0417, + "expPerGold": 0.350149, + "seasons": 1, + "fruitId": 40062, + "fruitCount": 200 + }, + { + "seedId": 20104, + "goodsId": 129, + "plantId": 1020104, + "name": "非洲菊", + "requiredLevel": 41, + "price": 1248, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 420, + "expPerCycle": 421, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 105.25, + "expPerGold": 0.33734, + "seasons": 1, + "fruitId": 40104, + "fruitCount": 200 + }, + { + "seedId": 20135, + "goodsId": 130, + "plantId": 1020135, + "name": "火绒草", + "requiredLevel": 42, + "price": 2496, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 840, + "expPerCycle": 841, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 105.125, + "expPerGold": 0.336939, + "seasons": 1, + "fruitId": 40135, + "fruitCount": 200 + }, + { + "seedId": 20141, + "goodsId": 131, + "plantId": 1020141, + "name": "花香根鸢尾", + "requiredLevel": 43, + "price": 3744, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1260, + "expPerCycle": 1261, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 105.0833, + "expPerGold": 0.336806, + "seasons": 1, + "fruitId": 40141, + "fruitCount": 200 + }, + { + "seedId": 20142, + "goodsId": 132, + "plantId": 1020142, + "name": "虞美人", + "requiredLevel": 44, + "price": 7488, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2520, + "expPerCycle": 2521, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 105.0417, + "expPerGold": 0.336672, + "seasons": 1, + "fruitId": 40142, + "fruitCount": 200 + }, + { + "seedId": 20145, + "goodsId": 133, + "plantId": 1020145, + "name": "向日葵", + "requiredLevel": 45, + "price": 1400, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 448, + "expPerCycle": 449, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 112.25, + "expPerGold": 0.320714, + "seasons": 1, + "fruitId": 40145, + "fruitCount": 200 + }, + { + "seedId": 20014, + "goodsId": 37, + "plantId": 1020014, + "name": "西瓜", + "requiredLevel": 46, + "price": 2800, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 896, + "expPerCycle": 897, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 112.125, + "expPerGold": 0.320357, + "seasons": 1, + "fruitId": 40014, + "fruitCount": 200 + }, + { + "seedId": 20070, + "goodsId": 38, + "plantId": 1020070, + "name": "黄豆", + "requiredLevel": 47, + "price": 4200, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1344, + "expPerCycle": 1345, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 112.0833, + "expPerGold": 0.320238, + "seasons": 1, + "fruitId": 40070, + "fruitCount": 200 + }, + { + "seedId": 20015, + "goodsId": 39, + "plantId": 1020015, + "name": "香蕉", + "requiredLevel": 48, + "price": 8400, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2688, + "expPerCycle": 2689, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 112.0417, + "expPerGold": 0.320119, + "seasons": 1, + "fruitId": 40015, + "fruitCount": 200 + }, + { + "seedId": 20100, + "goodsId": 40, + "plantId": 1020100, + "name": "竹笋", + "requiredLevel": 49, + "price": 1560, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 476, + "expPerCycle": 477, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 119.25, + "expPerGold": 0.305769, + "seasons": 1, + "fruitId": 40100, + "fruitCount": 200 + }, + { + "seedId": 20018, + "goodsId": 41, + "plantId": 1020018, + "name": "桃子", + "requiredLevel": 50, + "price": 3120, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 952, + "expPerCycle": 953, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 119.125, + "expPerGold": 0.305449, + "seasons": 1, + "fruitId": 40018, + "fruitCount": 200 + }, + { + "seedId": 20047, + "goodsId": 42, + "plantId": 1020047, + "name": "甘蔗", + "requiredLevel": 51, + "price": 4680, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1428, + "expPerCycle": 1429, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 119.0833, + "expPerGold": 0.305342, + "seasons": 1, + "fruitId": 40047, + "fruitCount": 200 + }, + { + "seedId": 20019, + "goodsId": 43, + "plantId": 1020019, + "name": "橙子", + "requiredLevel": 52, + "price": 9360, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2856, + "expPerCycle": 2857, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 119.0417, + "expPerGold": 0.305235, + "seasons": 1, + "fruitId": 40019, + "fruitCount": 200 + }, + { + "seedId": 20128, + "goodsId": 134, + "plantId": 1020128, + "name": "茉莉花", + "requiredLevel": 53, + "price": 1728, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 508, + "expPerCycle": 509, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 127.25, + "expPerGold": 0.29456, + "seasons": 1, + "fruitId": 40128, + "fruitCount": 200 + }, + { + "seedId": 20013, + "goodsId": 44, + "plantId": 1020013, + "name": "葡萄", + "requiredLevel": 54, + "price": 3456, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1016, + "expPerCycle": 1017, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 127.125, + "expPerGold": 0.294271, + "seasons": 1, + "fruitId": 40013, + "fruitCount": 200 + }, + { + "seedId": 20044, + "goodsId": 45, + "plantId": 1020044, + "name": "丝瓜", + "requiredLevel": 55, + "price": 5184, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1524, + "expPerCycle": 1525, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 127.0833, + "expPerGold": 0.294174, + "seasons": 1, + "fruitId": 40044, + "fruitCount": 200 + }, + { + "seedId": 20072, + "goodsId": 66, + "plantId": 1020072, + "name": "榛子", + "requiredLevel": 56, + "price": 10368, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3048, + "expPerCycle": 3049, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 127.0417, + "expPerGold": 0.294078, + "seasons": 1, + "fruitId": 40072, + "fruitCount": 200 + }, + { + "seedId": 20396, + "goodsId": 46, + "plantId": 1020396, + "name": "迎春花", + "requiredLevel": 57, + "price": 1920, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 540, + "expPerCycle": 541, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 135.25, + "expPerGold": 0.281771, + "seasons": 1, + "fruitId": 40396, + "fruitCount": 200 + }, + { + "seedId": 20023, + "goodsId": 47, + "plantId": 1020023, + "name": "石榴", + "requiredLevel": 58, + "price": 3840, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1080, + "expPerCycle": 1081, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 135.125, + "expPerGold": 0.28151, + "seasons": 1, + "fruitId": 40023, + "fruitCount": 200 + }, + { + "seedId": 20095, + "goodsId": 48, + "plantId": 1020095, + "name": "栗子", + "requiredLevel": 59, + "price": 5760, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1620, + "expPerCycle": 1621, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 135.0833, + "expPerGold": 0.281424, + "seasons": 1, + "fruitId": 40095, + "fruitCount": 200 + }, + { + "seedId": 20026, + "goodsId": 49, + "plantId": 1020026, + "name": "柚子", + "requiredLevel": 60, + "price": 11520, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3240, + "expPerCycle": 3241, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 135.0417, + "expPerGold": 0.281337, + "seasons": 1, + "fruitId": 40026, + "fruitCount": 200 + }, + { + "seedId": 20050, + "goodsId": 50, + "plantId": 1020050, + "name": "蘑菇", + "requiredLevel": 61, + "price": 3168, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 429, + "expPerCycle": 430, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 107.5, + "expPerGold": 0.135732, + "seasons": 2, + "fruitId": 40050, + "fruitCount": 200 + }, + { + "seedId": 20027, + "goodsId": 51, + "plantId": 1020027, + "name": "菠萝", + "requiredLevel": 62, + "price": 6336, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 858, + "expPerCycle": 859, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 107.375, + "expPerGold": 0.135574, + "seasons": 2, + "fruitId": 40027, + "fruitCount": 200 + }, + { + "seedId": 20036, + "goodsId": 52, + "plantId": 1020036, + "name": "箬竹", + "requiredLevel": 63, + "price": 9504, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1287, + "expPerCycle": 1288, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 107.3333, + "expPerGold": 0.135522, + "seasons": 2, + "fruitId": 40036, + "fruitCount": 200 + }, + { + "seedId": 20043, + "goodsId": 68, + "plantId": 1020043, + "name": "无花果", + "requiredLevel": 64, + "price": 19008, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2574, + "expPerCycle": 2575, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 107.2917, + "expPerGold": 0.135469, + "seasons": 2, + "fruitId": 40043, + "fruitCount": 200 + }, + { + "seedId": 20029, + "goodsId": 53, + "plantId": 1020029, + "name": "椰子", + "requiredLevel": 65, + "price": 3492, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 456, + "expPerCycle": 457, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 114.25, + "expPerGold": 0.130871, + "seasons": 2, + "fruitId": 40029, + "fruitCount": 200 + }, + { + "seedId": 20049, + "goodsId": 54, + "plantId": 1020049, + "name": "花生", + "requiredLevel": 66, + "price": 6984, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 912, + "expPerCycle": 913, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 114.125, + "expPerGold": 0.130727, + "seasons": 2, + "fruitId": 40049, + "fruitCount": 200 + }, + { + "seedId": 20052, + "goodsId": 70, + "plantId": 1020052, + "name": "金针菇", + "requiredLevel": 67, + "price": 10476, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1368, + "expPerCycle": 1369, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 114.0833, + "expPerGold": 0.13068, + "seasons": 2, + "fruitId": 40052, + "fruitCount": 200 + }, + { + "seedId": 20031, + "goodsId": 55, + "plantId": 1020031, + "name": "葫芦", + "requiredLevel": 68, + "price": 20952, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2736, + "expPerCycle": 2737, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 114.0417, + "expPerGold": 0.130632, + "seasons": 2, + "fruitId": 40031, + "fruitCount": 200 + }, + { + "seedId": 20045, + "goodsId": 56, + "plantId": 1020045, + "name": "猕猴桃", + "requiredLevel": 69, + "price": 3828, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 480, + "expPerCycle": 481, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 120.25, + "expPerGold": 0.125653, + "seasons": 2, + "fruitId": 40045, + "fruitCount": 200 + }, + { + "seedId": 20054, + "goodsId": 74, + "plantId": 1020054, + "name": "梨", + "requiredLevel": 70, + "price": 7656, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 960, + "expPerCycle": 961, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 120.125, + "expPerGold": 0.125522, + "seasons": 2, + "fruitId": 40054, + "fruitCount": 200 + }, + { + "seedId": 20442, + "goodsId": 78, + "plantId": 1020442, + "name": "睡莲", + "requiredLevel": 71, + "price": 11484, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1440, + "expPerCycle": 1441, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 120.0833, + "expPerGold": 0.125479, + "seasons": 2, + "fruitId": 40442, + "fruitCount": 200 + }, + { + "seedId": 20033, + "goodsId": 83, + "plantId": 1020033, + "name": "火龙果", + "requiredLevel": 72, + "price": 22968, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2880, + "expPerCycle": 2881, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 120.0417, + "expPerGold": 0.125435, + "seasons": 2, + "fruitId": 40033, + "fruitCount": 200 + }, + { + "seedId": 20055, + "goodsId": 62, + "plantId": 1020055, + "name": "枇杷", + "requiredLevel": 73, + "price": 4176, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 510, + "expPerCycle": 511, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 127.75, + "expPerGold": 0.122366, + "seasons": 2, + "fruitId": 40055, + "fruitCount": 200 + }, + { + "seedId": 20034, + "goodsId": 65, + "plantId": 1020034, + "name": "樱桃", + "requiredLevel": 74, + "price": 8352, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1020, + "expPerCycle": 1021, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 127.625, + "expPerGold": 0.122246, + "seasons": 2, + "fruitId": 40034, + "fruitCount": 200 + }, + { + "seedId": 20413, + "goodsId": 67, + "plantId": 1020413, + "name": "李子", + "requiredLevel": 75, + "price": 12528, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1530, + "expPerCycle": 1531, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 127.5833, + "expPerGold": 0.122206, + "seasons": 2, + "fruitId": 40413, + "fruitCount": 200 + }, + { + "seedId": 20035, + "goodsId": 69, + "plantId": 1020035, + "name": "荔枝", + "requiredLevel": 76, + "price": 25056, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3060, + "expPerCycle": 3061, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 127.5417, + "expPerGold": 0.122166, + "seasons": 2, + "fruitId": 40035, + "fruitCount": 200 + }, + { + "seedId": 20067, + "goodsId": 72, + "plantId": 1020067, + "name": "香瓜", + "requiredLevel": 77, + "price": 4560, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 537, + "expPerCycle": 538, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 134.5, + "expPerGold": 0.117982, + "seasons": 2, + "fruitId": 40067, + "fruitCount": 200 + }, + { + "seedId": 20038, + "goodsId": 75, + "plantId": 1020038, + "name": "木瓜", + "requiredLevel": 78, + "price": 9120, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1074, + "expPerCycle": 1075, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 134.375, + "expPerGold": 0.117873, + "seasons": 2, + "fruitId": 40038, + "fruitCount": 200 + }, + { + "seedId": 20053, + "goodsId": 77, + "plantId": 1020053, + "name": "桂圆", + "requiredLevel": 79, + "price": 13680, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1611, + "expPerCycle": 1612, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 134.3333, + "expPerGold": 0.117836, + "seasons": 2, + "fruitId": 40053, + "fruitCount": 200 + }, + { + "seedId": 20080, + "goodsId": 80, + "plantId": 1020080, + "name": "月柿", + "requiredLevel": 80, + "price": 27360, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3222, + "expPerCycle": 3223, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 134.2917, + "expPerGold": 0.1178, + "seasons": 2, + "fruitId": 40080, + "fruitCount": 200 + }, + { + "seedId": 20039, + "goodsId": 82, + "plantId": 1020039, + "name": "杨桃", + "requiredLevel": 81, + "price": 4944, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 567, + "expPerCycle": 568, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 142, + "expPerGold": 0.114887, + "seasons": 2, + "fruitId": 40039, + "fruitCount": 200 + }, + { + "seedId": 20056, + "goodsId": 84, + "plantId": 1020056, + "name": "哈密瓜", + "requiredLevel": 82, + "price": 9888, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1134, + "expPerCycle": 1135, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 141.875, + "expPerGold": 0.114786, + "seasons": 2, + "fruitId": 40056, + "fruitCount": 200 + }, + { + "seedId": 20075, + "goodsId": 71, + "plantId": 1020075, + "name": "桑葚", + "requiredLevel": 83, + "price": 14832, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1701, + "expPerCycle": 1702, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 141.8333, + "expPerGold": 0.114752, + "seasons": 2, + "fruitId": 40075, + "fruitCount": 200 + }, + { + "seedId": 20042, + "goodsId": 73, + "plantId": 1020042, + "name": "柠檬", + "requiredLevel": 84, + "price": 29664, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3402, + "expPerCycle": 3403, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 141.7917, + "expPerGold": 0.114718, + "seasons": 2, + "fruitId": 40042, + "fruitCount": 200 + }, + { + "seedId": 20057, + "goodsId": 76, + "plantId": 1020057, + "name": "芒果", + "requiredLevel": 85, + "price": 5364, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 597, + "expPerCycle": 598, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 149.5, + "expPerGold": 0.111484, + "seasons": 2, + "fruitId": 40057, + "fruitCount": 200 + }, + { + "seedId": 20048, + "goodsId": 79, + "plantId": 1020048, + "name": "杨梅", + "requiredLevel": 86, + "price": 10728, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1194, + "expPerCycle": 1195, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 149.375, + "expPerGold": 0.111391, + "seasons": 2, + "fruitId": 40048, + "fruitCount": 200 + }, + { + "seedId": 20058, + "goodsId": 81, + "plantId": 1020058, + "name": "榴莲", + "requiredLevel": 87, + "price": 16092, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1791, + "expPerCycle": 1792, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 149.3333, + "expPerGold": 0.11136, + "seasons": 2, + "fruitId": 40058, + "fruitCount": 200 + }, + { + "seedId": 20079, + "goodsId": 85, + "plantId": 1020079, + "name": "番石榴", + "requiredLevel": 88, + "price": 32184, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3582, + "expPerCycle": 3583, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 149.2917, + "expPerGold": 0.111329, + "seasons": 2, + "fruitId": 40079, + "fruitCount": 200 + }, + { + "seedId": 20218, + "goodsId": 57, + "plantId": 1020218, + "name": "瓶子树", + "requiredLevel": 89, + "price": 5796, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 627, + "expPerCycle": 628, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 157, + "expPerGold": 0.108351, + "seasons": 2, + "fruitId": 40218, + "fruitCount": 200 + }, + { + "seedId": 20077, + "goodsId": 86, + "plantId": 1020077, + "name": "蓝莓", + "requiredLevel": 90, + "price": 11592, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1254, + "expPerCycle": 1255, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 156.875, + "expPerGold": 0.108264, + "seasons": 2, + "fruitId": 40077, + "fruitCount": 200 + }, + { + "seedId": 20220, + "goodsId": 58, + "plantId": 1020220, + "name": "猪笼草", + "requiredLevel": 91, + "price": 17388, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1881, + "expPerCycle": 1882, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 156.8333, + "expPerGold": 0.108236, + "seasons": 2, + "fruitId": 40220, + "fruitCount": 200 + }, + { + "seedId": 20076, + "goodsId": 87, + "plantId": 1020076, + "name": "山竹", + "requiredLevel": 92, + "price": 34776, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3762, + "expPerCycle": 3763, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 156.7917, + "expPerGold": 0.108207, + "seasons": 2, + "fruitId": 40076, + "fruitCount": 200 + }, + { + "seedId": 20116, + "goodsId": 59, + "plantId": 1020116, + "name": "曼陀罗华", + "requiredLevel": 93, + "price": 6240, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 660, + "expPerCycle": 661, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 165.25, + "expPerGold": 0.105929, + "seasons": 2, + "fruitId": 40116, + "fruitCount": 200 + }, + { + "seedId": 20126, + "goodsId": 88, + "plantId": 1020126, + "name": "曼珠沙华", + "requiredLevel": 94, + "price": 12480, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1320, + "expPerCycle": 1321, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 165.125, + "expPerGold": 0.105849, + "seasons": 2, + "fruitId": 40126, + "fruitCount": 200 + }, + { + "seedId": 20063, + "goodsId": 89, + "plantId": 1020063, + "name": "苦瓜", + "requiredLevel": 95, + "price": 18720, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1980, + "expPerCycle": 1981, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 165.0833, + "expPerGold": 0.105823, + "seasons": 2, + "fruitId": 40063, + "fruitCount": 200 + }, + { + "seedId": 20221, + "goodsId": 93, + "plantId": 1020221, + "name": "天堂鸟", + "requiredLevel": 96, + "price": 37440, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 3960, + "expPerCycle": 3961, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 165.0417, + "expPerGold": 0.105796, + "seasons": 2, + "fruitId": 40221, + "fruitCount": 200 + }, + { + "seedId": 20068, + "goodsId": 92, + "plantId": 1020068, + "name": "冬瓜", + "requiredLevel": 97, + "price": 6720, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 693, + "expPerCycle": 694, + "growTimeSec": 14400, + "growTimeStr": "4小时", + "expPerHour": 173.5, + "expPerGold": 0.103274, + "seasons": 2, + "fruitId": 40068, + "fruitCount": 200 + }, + { + "seedId": 20222, + "goodsId": 94, + "plantId": 1020222, + "name": "豹皮花", + "requiredLevel": 98, + "price": 13440, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 1386, + "expPerCycle": 1387, + "growTimeSec": 28800, + "growTimeStr": "8小时", + "expPerHour": 173.375, + "expPerGold": 0.103199, + "seasons": 2, + "fruitId": 40222, + "fruitCount": 200 + }, + { + "seedId": 20078, + "goodsId": 96, + "plantId": 1020078, + "name": "杏子", + "requiredLevel": 99, + "price": 20160, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 2079, + "expPerCycle": 2080, + "growTimeSec": 43200, + "growTimeStr": "12小时", + "expPerHour": 173.3333, + "expPerGold": 0.103175, + "seasons": 2, + "fruitId": 40078, + "fruitCount": 200 + }, + { + "seedId": 20074, + "goodsId": 100, + "plantId": 1020074, + "name": "金桔", + "requiredLevel": 100, + "price": 40320, + "unlocked": false, + "limitCount": 0, + "boughtNum": 0, + "exp": 4158, + "expPerCycle": 4159, + "growTimeSec": 86400, + "growTimeStr": "24小时", + "expPerHour": 173.2917, + "expPerGold": 0.10315, + "seasons": 2, + "fruitId": 40074, + "fruitCount": 200 + } + ] +} \ No newline at end of file diff --git a/server/ws-gateway.js b/server/ws-gateway.js new file mode 100644 index 0000000..8c19a19 --- /dev/null +++ b/server/ws-gateway.js @@ -0,0 +1,64 @@ +const http = require('http'); +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '.env') }); +require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); +const { Server } = require('socket.io'); +const { createClient } = require('redis'); +const { log } = require('./src/utils'); + +const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379'; +const gatewayPort = Number(process.env.WS_GATEWAY_PORT || 3001); +const corsOrigin = process.env.WS_GATEWAY_CORS_ORIGIN || '*'; + +const server = http.createServer(); +const io = new Server(server, { + cors: { + origin: corsOrigin, + methods: ["GET", "POST"] + } +}); + +const subscriber = createClient({ url: redisUrl }); +subscriber.on('error', (err) => { + log('Redis', `订阅异常: ${err.message}`); +}); + +function safeParseJson(input) { + try { + return JSON.parse(input); + } catch { + return {}; + } +} + +async function start() { + await subscriber.connect(); + + await Promise.all([ + subscriber.pSubscribe('bot-log-*', (message, channel) => { + io.emit(channel, safeParseJson(message)); + }), + subscriber.pSubscribe('bot-status-*', (message, channel) => { + io.emit(channel, safeParseJson(message)); + }), + subscriber.pSubscribe('bot-error-*', (message, channel) => { + io.emit(channel, safeParseJson(message)); + }) + ]); + + io.on('connection', (socket) => { + log('Gateway', `Client connected: ${socket.id}`); + socket.on('disconnect', () => { + log('Gateway', `Client disconnected: ${socket.id}`); + }); + }); + + server.listen(gatewayPort, () => { + log('Gateway', `WS Gateway running on http://localhost:${gatewayPort}`); + }); +} + +start().catch((err) => { + log('Gateway', `启动失败: ${err.message}`); + process.exit(1); +}); diff --git a/test b/test new file mode 100644 index 0000000..30d74d2 --- /dev/null +++ b/test @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..6bd782d --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.crop_list.md diff --git a/web/.vite/deps/_metadata.json b/web/.vite/deps/_metadata.json new file mode 100644 index 0000000..ded4d4e --- /dev/null +++ b/web/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "dde44a6e", + "configHash": "7c8b31c1", + "lockfileHash": "23759f69", + "browserHash": "3f667b4e", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/web/.vite/deps/package.json b/web/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/web/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/web/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/web/components.json b/web/components.json new file mode 100644 index 0000000..abd3aa3 --- /dev/null +++ b/web/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..d6359d4 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + 谢尔达莱群岛 + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..2605cc7 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,5329 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "socket.io-client": "^4.8.3", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.13", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.24", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", + "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/type-utils": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.55.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", + "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", + "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.55.0", + "@typescript-eslint/types": "^8.55.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", + "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", + "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", + "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", + "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", + "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.55.0", + "@typescript-eslint/tsconfig-utils": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", + "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", + "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", + "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.55.0", + "@typescript-eslint/parser": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..d290d22 --- /dev/null +++ b/web/package.json @@ -0,0 +1,45 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "socket.io-client": "^4.8.3", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.13", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.24", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..6e297cd --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, + } diff --git a/web/public/logo.png b/web/public/logo.png new file mode 100644 index 0000000..5e92acb Binary files /dev/null and b/web/public/logo.png differ diff --git a/web/public/shop_plants_organized/mapping.csv b/web/public/shop_plants_organized/mapping.csv new file mode 100644 index 0000000..8b23205 --- /dev/null +++ b/web/public/shop_plants_organized/mapping.csv @@ -0,0 +1,101 @@ +Seed ID,Seed Name,Plant ID,Plant Name,New File,Original File +20002,白萝卜种子,1020002,白萝卜,白萝卜_from_白萝卜种子_1020002.png,aa2d4772-8b90-417c-89be-c098e1cbb35c.edc47.png +20003,胡萝卜种子,1020003,胡萝卜,胡萝卜_from_胡萝卜种子_1020003.png,a823c190-2bd8-4b86-bbac-6c5cbf0c465d.3a774.png +20059,大白菜种子,1020059,大白菜,大白菜_from_大白菜种子_1020059.png,d4057a83-ecef-45fb-8988-905b56ae7ce3.b3280.png +20065,大蒜种子,1020065,大蒜,大蒜_from_大蒜种子_1020065.png,5a1d18bc-32f9-40d7-8cd3-be00b79004cc.2d769.png +20064,大葱种子,1020064,大葱,大葱_from_大葱种子_1020064.png,9bf36963-4f34-4c9d-b967-73ec64d1e924.2adf2.png +20060,水稻种子,1020060,水稻,水稻_from_水稻种子_1020060.png,e059e031-6953-402e-9d2b-88644465dc81.c5f7f.png +20061,小麦种子,1020061,小麦,小麦_from_小麦种子_1020061.png,2446c70c-20b6-4e3c-8a2f-5d64a044ded6.7abe2.png +20004,玉米种子,1020004,玉米,玉米_from_玉米种子_1020004.png,956ddaa2-27aa-4b9b-aa72-413390ecaaf5.de27e.png +20066,鲜姜种子,1020066,鲜姜,鲜姜_from_鲜姜种子_1020066.png,3a7230b2-a8b4-434a-83bf-4cb850c6b291.2dbd6.png +20005,土豆种子,1020005,土豆,土豆_from_土豆种子_1020005.png,95f857bc-ce3a-47f6-aae0-20ec6f84d21a.aad11.png +20071,小白菜种子,1020071,小白菜,小白菜_from_小白菜种子_1020071.png,07ca7a88-3983-42f9-8402-fdec001cb51c.d3538.png +20096,生菜种子,1020096,生菜,生菜_from_生菜种子_1020096.png,c7879560-b0d9-4f21-ab73-3fb4f083ddc5.9ce85.png +20099,油菜种子,1020099,油菜,油菜_from_油菜种子_1020099.png,35f65e28-3b35-4466-a040-e1b686085c23.0d18d.png +20006,茄子种子,1020006,茄子,茄子_from_茄子种子_1020006.png,2a626649-1952-41ef-8d5a-5ebbb726e479.ae717.png +20051,红枣种子,1020051,红枣,红枣_from_红枣种子_1020051.png,095fd7f0-24a0-415a-b4b8-96a8aeb07ba5.5e6bb.png +20120,蒲公英种子,1020120,蒲公英,蒲公英_from_蒲公英种子_1020120.png,8de9453f-e6ef-40ff-a678-3da9bb861bb5.462a2.png +20259,银莲花种子,1020259,银莲花,银莲花_from_银莲花种子_1020259.png,8df7c28d-ba7c-477d-a68e-39fb027e3969.d8964.png +20007,番茄种子,1020007,番茄,番茄_from_番茄种子_1020007.png,f7d5b3a4-c759-461f-bd81-79d8df59cbe2.af9b3.png +20098,花菜种子,1020098,花菜,花菜_from_花菜种子_1020098.png,f44f642e-4cd9-4c3d-bb25-269ba52d424a.1cbab.png +20305,韭菜种子,1020305,韭菜,韭菜_from_韭菜种子_1020305.png,7599bc40-a5d7-4834-8ed8-3e6eab7216ff.37c46.png +20105,小雏菊种子,1020105,小雏菊,小雏菊_from_小雏菊种子_1020105.png,b4f19058-1031-420f-ab64-feb4432c60a6.db1b3.png +20008,豌豆种子,1020008,豌豆,豌豆_from_豌豆种子_1020008.png,f55a4326-3e65-429e-a615-f5c7c867e451.154ba.png +20037,莲藕种子,1020037,莲藕,莲藕_from_莲藕种子_1020037.png,bb9c6484-1269-44f1-a203-74aa05fecc1d.1199a.png +20041,红玫瑰种子,1020041,红玫瑰,红玫瑰_from_红玫瑰种子_1020041.png,340b1098-6163-404c-97f9-86cbbc00336b.9b2a8.png +20161,黄色秋菊种子,1020161,秋菊(黄色),秋菊黄色_from_黄色秋菊种子_1020161.png,b72a754f-4e24-451d-a12a-a318f3937a05.641b4.png +20110,满天星种子,1020110,满天星,满天星_from_满天星种子_1020110.png,48593983-6831-4a94-8678-28a8c4304f54.39b33.png +20143,含羞草种子,1020143,含羞草,含羞草_from_含羞草种子_1020143.png,ee3d377e-30e5-4572-bd4d-6019cf1e6352.b468e.png +20147,牵牛花种子,1020147,牵牛花,牵牛花_from_牵牛花种子_1020147.png,54ce926b-ef79-4f1b-920b-756d8368408f.b863a.png +20162,红色秋菊种子,1020162,秋菊(红色),秋菊红色_from_红色秋菊种子_1020162.png,f3a09490-43c0-4d04-8a3a-679f9e25efe8.e0011.png +20009,辣椒种子,1020009,辣椒,辣椒_from_辣椒种子_1020009.png,7eed9dde-2174-4d6d-84e2-5fdb7276eaf3.7d364.png +20097,黄瓜种子,1020097,黄瓜,黄瓜_from_黄瓜种子_1020097.png,722e3547-d04e-4501-9aad-148af16cc8cb.bc687.png +20306,芹菜种子,1020306,芹菜,芹菜_from_芹菜种子_1020306.png,6d172f29-286f-4f58-88f7-e6776a863ef7.94097.png +20103,天香百合种子,1020103,天香百合,天香百合_from_天香百合种子_1020103.png,e5f9d964-28ee-4491-b714-ba917a9cec87.2710f.png +20010,南瓜种子,1020010,南瓜,南瓜_from_南瓜种子_1020010.png,0009ad77-fd65-48fa-af78-29c9872d8476.c388e.png +20308,核桃种子,1020308,核桃,核桃_from_核桃种子_1020308.png,b3b7045f-2b34-41ac-9e94-aaaf6083e06a.cf434.png +20091,山楂种子,1020091,山楂,山楂_from_山楂种子_1020091.png,4501b8e9-144e-497a-b282-ca1df8e17ee2.0f537.png +20073,菠菜种子,1020073,菠菜,菠菜_from_菠菜种子_1020073.png,a9429882-6c61-4d3b-83a4-38da55470b6a.22bda.png +20001,草莓种子,1020001,草莓,草莓_from_草莓种子_1020001.png,8d7c740a-e144-458d-95c3-15d95efa8bc3.17504.png +20011,苹果种子,1020011,苹果,苹果_from_苹果种子_1020011.png,5eff94d0-05ff-4e1d-a584-cfa473dcb5ca.250fc.png +20062,四叶草种子,1020062,四叶草,四叶草_from_四叶草种子_1020062.png,35aba40d-33bc-465c-8c58-f9b45bd61ca1.b4b9d.png +20104,非洲菊种子,1020104,非洲菊,非洲菊_from_非洲菊种子_1020104.png,5432d98e-84cb-4456-af35-ea2b8b194efe.cab54.png +20135,火绒草种子,1020135,火绒草,火绒草_from_火绒草种子_1020135.png,637a9c4e-fe92-4416-b913-f25ac46daab9.0fcc7.png +20141,花香根鸢尾种子,1020141,花香根鸢尾,花香根鸢尾_from_花香根鸢尾种子_1020141.png,5687ef62-528d-4faf-8380-a3d9fc14dd6f.a64a2.png +20142,虞美人种子,1020142,虞美人,虞美人_from_虞美人种子_1020142.png,74b327cb-e6db-496b-b665-3b3ceeee04f0.697d9.png +20145,向日葵种子,1020145,向日葵,向日葵_from_向日葵种子_1020145.png,fe45b6b7-3081-4541-bd1f-54bfded448ee.10c63.png +20014,西瓜种子,1020014,西瓜,西瓜_from_西瓜种子_1020014.png,74d60e9d-aee3-4fad-be3c-6c34b2c479d2.4a5b0.png +20070,黄豆种子,1020070,黄豆,黄豆_from_黄豆种子_1020070.png,de01453f-1159-490b-b312-6f9906cb8e38.426f8.png +20015,香蕉种子,1020015,香蕉,香蕉_from_香蕉种子_1020015.png,0ea242a7-5188-4296-8700-b4a2e00347b8.c925f.png +20100,竹笋种子,1020100,竹笋,竹笋_from_竹笋种子_1020100.png,c0fc1d8d-0c10-471a-83f4-14c400902a39.0beb7.png +20018,桃子种子,1020018,桃子,桃子_from_桃子种子_1020018.png,9ec730e7-4a9b-4825-bb45-046f1c5477b1.81f7f.png +20047,甘蔗种子,1020047,甘蔗,甘蔗_from_甘蔗种子_1020047.png,fabbc1bf-6622-433a-9471-8839d4a1aad3.b0296.png +20019,橙子种子,1020019,橙子,橙子_from_橙子种子_1020019.png,be59e9ef-d6b5-4a70-8b1d-b2a81851eaa2.537dc.png +20128,茉莉花种子,1020128,茉莉花,茉莉花_from_茉莉花种子_1020128.png,17d7dee5-e114-46f0-8017-506863370df9.ded8d.png +20013,葡萄种子,1020013,葡萄,葡萄_from_葡萄种子_1020013.png,4d63db88-33bc-4783-aeba-5213c0db86b9.cb70d.png +20044,丝瓜种子,1020044,丝瓜,丝瓜_from_丝瓜种子_1020044.png,5c446801-84f3-44fd-a066-9264a619ae04.e025a.png +20072,榛子种子,1020072,榛子,榛子_from_榛子种子_1020072.png,57888e00-33a6-4e1e-b7f7-b6a67b651935.576df.png +20396,迎春花种子,1020396,迎春花,迎春花_from_迎春花种子_1020396.png,32376957-bcd7-4dca-9bdc-7053da6fae7b.c0f12.png +20023,石榴种子,1020023,石榴,石榴_from_石榴种子_1020023.png,2a303167-26ac-4a76-8238-d8f5b90f2c5f.01562.png +20095,栗子种子,1020095,栗子,栗子_from_栗子种子_1020095.png,d1531233-1602-4322-9d08-25445ccd123f.e12b0.png +20026,柚子种子,1020026,柚子,柚子_from_柚子种子_1020026.png,c4a71275-ddce-4607-b9fc-93625244d256.6caf7.png +20050,蘑菇种子,1020050,蘑菇,蘑菇_from_蘑菇种子_1020050.png,09c2f94f-e865-472f-b599-5e857925ecfe.27d63.png +20027,菠萝种子,1020027,菠萝,菠萝_from_菠萝种子_1020027.png,37fed013-1f29-4686-992a-bbbf7bc79350.eadc1.png +20036,箬竹种子,1020036,箬竹,箬竹_from_箬竹种子_1020036.png,f75e1ed3-acfd-4bdf-bae5-a64459ed0347.e88b0.png +20043,无花果种子,1020043,无花果,无花果_from_无花果种子_1020043.png,68e20b28-d9a4-40cc-a3f0-8ff2d2ba2fc9.d9e75.png +20029,椰子种子,1020029,椰子,椰子_from_椰子种子_1020029.png,3a2d2a6e-02b3-4a67-ac63-847f728c1279.c7bf9.png +20049,花生种子,1020049,花生,花生_from_花生种子_1020049.png,30f01814-4b36-4838-9c93-d34a823db6a0.416fa.png +20052,金针菇种子,1020052,金针菇,金针菇_from_金针菇种子_1020052.png,aa0ddbb8-3102-4428-bad4-c8ad39dfab9e.f2ac9.png +20031,葫芦种子,1020031,葫芦,葫芦_from_葫芦种子_1020031.png,7cde3aa5-1732-4215-9b5d-e0aac6550f3f.73909.png +20045,猕猴桃种子,1020045,猕猴桃,猕猴桃_from_猕猴桃种子_1020045.png,d4e03a5a-6e87-4761-b29c-83835c5665b9.4dceb.png +20054,梨种子,1020054,梨,梨_from_梨种子_1020054.png,93760003-3cc3-4a51-abe4-963543437406.04b44.png +20442,睡莲种子,1020442,睡莲,睡莲_from_睡莲种子_1020442.png,00beea60-aa61-4283-a6f3-431605582581.4f00c.png +20033,火龙果种子,1020033,火龙果,火龙果_from_火龙果种子_1020033.png,51f55501-d2b0-4c14-929c-abfc08cef212.074ee.png +20055,枇杷种子,1020055,枇杷,枇杷_from_枇杷种子_1020055.png,af64a49f-6266-414c-810b-0e03a2edaf11.e40c4.png +20034,樱桃种子,1020034,樱桃,樱桃_from_樱桃种子_1020034.png,01f07ecf-6303-4b54-8a1a-ea64f5928855.8315d.png +20413,李子种子,1020413,李子,李子_from_李子种子_1020413.png,ddd8134a-15e5-4fbd-a7f6-a26457e638e2.e7bfc.png +20035,荔枝种子,1020035,荔枝,荔枝_from_荔枝种子_1020035.png,07232d7e-3d5e-4d48-923e-d70c2b64873b.85999.png +20067,香瓜种子,1020067,香瓜,香瓜_from_香瓜种子_1020067.png,4a30dcd3-efd2-4ff2-b406-7e81ab3f4923.281bf.png +20038,木瓜种子,1020038,木瓜,木瓜_from_木瓜种子_1020038.png,8816cd72-1ebd-4904-bd50-509a8e3798fb.32cf3.png +20053,桂圆种子,1020053,桂圆,桂圆_from_桂圆种子_1020053.png,ef610bda-bc53-46e7-a0fa-a519a8b180d0.db94d.png +20080,月柿种子,1020080,月柿,月柿_from_月柿种子_1020080.png,727dfe57-ed34-4e96-b8e2-bc856211c39f.4f667.png +20039,杨桃种子,1020039,杨桃,杨桃_from_杨桃种子_1020039.png,3be855b2-ac62-4e8f-b754-8ef8d8d1e46a.e08ca.png +20056,哈密瓜种子,1020056,哈密瓜,哈密瓜_from_哈密瓜种子_1020056.png,0e787d09-1443-4c0f-b496-e0f037983758.59eff.png +20075,桑葚种子,1020075,桑葚,桑葚_from_桑葚种子_1020075.png,e1c7e781-d74f-478d-b44e-cd37fc54294e.b2f01.png +20042,柠檬种子,1020042,柠檬,柠檬_from_柠檬种子_1020042.png,d78193cf-a7a5-40b4-ae67-d1519480503e.188c4.png +20057,芒果种子,1020057,芒果,芒果_from_芒果种子_1020057.png,44bd1002-5c47-47be-b9bc-1ae0fb9a26f2.4c8b1.png +20048,杨梅种子,1020048,杨梅,杨梅_from_杨梅种子_1020048.png,9eb47072-a3ae-4239-99b3-5d1fde9129d9.415cc.png +20058,榴莲种子,1020058,榴莲,榴莲_from_榴莲种子_1020058.png,7816ce22-6077-4175-8632-9f147402c98f.d5a93.png +20079,番石榴种子,1020079,番石榴,番石榴_from_番石榴种子_1020079.png,57fada3e-788a-4384-9ff7-b0d50b6a0e57.2fc48.png +20218,瓶子树种子,1020218,瓶子树,瓶子树_from_瓶子树种子_1020218.png,189f164a-a8d8-40ec-918f-567249467619.be91f.png +20077,蓝莓种子,1020077,蓝莓,蓝莓_from_蓝莓种子_1020077.png,80ae7506-850f-451f-9e18-57c11f62bca2.c3710.png +20220,猪笼草种子,1020220,猪笼草,猪笼草_from_猪笼草种子_1020220.png,7d2e3b7f-57e1-4491-8d55-cc485b8a548e.c5826.png +20076,山竹种子,1020076,山竹,山竹_from_山竹种子_1020076.png,8c490c1a-5085-4f5f-bf06-0a35b39e10e9.1c974.png +20116,曼陀罗华种子,1020116,曼陀罗华,曼陀罗华_from_曼陀罗华种子_1020116.png,6bb50aa4-3d41-426d-8b97-7b3829f045bf.ef4d0.png +20126,曼珠沙华种子,1020126,曼珠沙华,曼珠沙华_from_曼珠沙华种子_1020126.png,a7780718-b070-4922-953e-42e3234d0a6b.d2211.png +20063,苦瓜种子,1020063,苦瓜,苦瓜_from_苦瓜种子_1020063.png,7565aa2d-1ff4-43f9-aaa4-be0d1a937122.f5ab6.png +20221,天堂鸟种子,1020221,天堂鸟,天堂鸟_from_天堂鸟种子_1020221.png,bbeb2945-da57-4d63-bad8-78c046ffe46d.878ce.png +20068,冬瓜种子,1020068,冬瓜,冬瓜_from_冬瓜种子_1020068.png,95fe19b7-bb30-4b1a-b70f-fa4fd8e3d893.1ec42.png +20222,豹皮花种子,1020222,豹皮花,豹皮花_from_豹皮花种子_1020222.png,8df2f4e2-f8e1-44b3-a5cb-0e9f2617228d.e1dbc.png +20078,杏子种子,1020078,杏子,杏子_from_杏子种子_1020078.png,522a790b-e625-4743-bc1d-8a8d1d52738c.67da8.png +20074,金桔种子,1020074,金桔,金桔_from_金桔种子_1020074.png,b0811647-bd43-4402-a071-e60e05312943.d1674.png diff --git a/web/public/shop_plants_organized/丝瓜_from_丝瓜种子_1020044.png b/web/public/shop_plants_organized/丝瓜_from_丝瓜种子_1020044.png new file mode 100644 index 0000000..a7d99df Binary files /dev/null and b/web/public/shop_plants_organized/丝瓜_from_丝瓜种子_1020044.png differ diff --git a/web/public/shop_plants_organized/冬瓜_from_冬瓜种子_1020068.png b/web/public/shop_plants_organized/冬瓜_from_冬瓜种子_1020068.png new file mode 100644 index 0000000..cc4bf8b Binary files /dev/null and b/web/public/shop_plants_organized/冬瓜_from_冬瓜种子_1020068.png differ diff --git a/web/public/shop_plants_organized/南瓜_from_南瓜种子_1020010.png b/web/public/shop_plants_organized/南瓜_from_南瓜种子_1020010.png new file mode 100644 index 0000000..6e56fcb Binary files /dev/null and b/web/public/shop_plants_organized/南瓜_from_南瓜种子_1020010.png differ diff --git a/web/public/shop_plants_organized/向日葵_from_向日葵种子_1020145.png b/web/public/shop_plants_organized/向日葵_from_向日葵种子_1020145.png new file mode 100644 index 0000000..6b26ee4 Binary files /dev/null and b/web/public/shop_plants_organized/向日葵_from_向日葵种子_1020145.png differ diff --git a/web/public/shop_plants_organized/含羞草_from_含羞草种子_1020143.png b/web/public/shop_plants_organized/含羞草_from_含羞草种子_1020143.png new file mode 100644 index 0000000..1fcc1f9 Binary files /dev/null and b/web/public/shop_plants_organized/含羞草_from_含羞草种子_1020143.png differ diff --git a/web/public/shop_plants_organized/哈密瓜_from_哈密瓜种子_1020056.png b/web/public/shop_plants_organized/哈密瓜_from_哈密瓜种子_1020056.png new file mode 100644 index 0000000..dcb7d2f Binary files /dev/null and b/web/public/shop_plants_organized/哈密瓜_from_哈密瓜种子_1020056.png differ diff --git a/web/public/shop_plants_organized/四叶草_from_四叶草种子_1020062.png b/web/public/shop_plants_organized/四叶草_from_四叶草种子_1020062.png new file mode 100644 index 0000000..5947357 Binary files /dev/null and b/web/public/shop_plants_organized/四叶草_from_四叶草种子_1020062.png differ diff --git a/web/public/shop_plants_organized/土豆_from_土豆种子_1020005.png b/web/public/shop_plants_organized/土豆_from_土豆种子_1020005.png new file mode 100644 index 0000000..f85f77c Binary files /dev/null and b/web/public/shop_plants_organized/土豆_from_土豆种子_1020005.png differ diff --git a/web/public/shop_plants_organized/大白菜_from_大白菜种子_1020059.png b/web/public/shop_plants_organized/大白菜_from_大白菜种子_1020059.png new file mode 100644 index 0000000..f2420e5 Binary files /dev/null and b/web/public/shop_plants_organized/大白菜_from_大白菜种子_1020059.png differ diff --git a/web/public/shop_plants_organized/大葱_from_大葱种子_1020064.png b/web/public/shop_plants_organized/大葱_from_大葱种子_1020064.png new file mode 100644 index 0000000..975d431 Binary files /dev/null and b/web/public/shop_plants_organized/大葱_from_大葱种子_1020064.png differ diff --git a/web/public/shop_plants_organized/大蒜_from_大蒜种子_1020065.png b/web/public/shop_plants_organized/大蒜_from_大蒜种子_1020065.png new file mode 100644 index 0000000..36b45e2 Binary files /dev/null and b/web/public/shop_plants_organized/大蒜_from_大蒜种子_1020065.png differ diff --git a/web/public/shop_plants_organized/天堂鸟_from_天堂鸟种子_1020221.png b/web/public/shop_plants_organized/天堂鸟_from_天堂鸟种子_1020221.png new file mode 100644 index 0000000..a8b2a79 Binary files /dev/null and b/web/public/shop_plants_organized/天堂鸟_from_天堂鸟种子_1020221.png differ diff --git a/web/public/shop_plants_organized/天香百合_from_天香百合种子_1020103.png b/web/public/shop_plants_organized/天香百合_from_天香百合种子_1020103.png new file mode 100644 index 0000000..0d5beb0 Binary files /dev/null and b/web/public/shop_plants_organized/天香百合_from_天香百合种子_1020103.png differ diff --git a/web/public/shop_plants_organized/小白菜_from_小白菜种子_1020071.png b/web/public/shop_plants_organized/小白菜_from_小白菜种子_1020071.png new file mode 100644 index 0000000..d04909b Binary files /dev/null and b/web/public/shop_plants_organized/小白菜_from_小白菜种子_1020071.png differ diff --git a/web/public/shop_plants_organized/小雏菊_from_小雏菊种子_1020105.png b/web/public/shop_plants_organized/小雏菊_from_小雏菊种子_1020105.png new file mode 100644 index 0000000..203f3ce Binary files /dev/null and b/web/public/shop_plants_organized/小雏菊_from_小雏菊种子_1020105.png differ diff --git a/web/public/shop_plants_organized/小麦_from_小麦种子_1020061.png b/web/public/shop_plants_organized/小麦_from_小麦种子_1020061.png new file mode 100644 index 0000000..c2b3851 Binary files /dev/null and b/web/public/shop_plants_organized/小麦_from_小麦种子_1020061.png differ diff --git a/web/public/shop_plants_organized/山楂_from_山楂种子_1020091.png b/web/public/shop_plants_organized/山楂_from_山楂种子_1020091.png new file mode 100644 index 0000000..2c1051d Binary files /dev/null and b/web/public/shop_plants_organized/山楂_from_山楂种子_1020091.png differ diff --git a/web/public/shop_plants_organized/山竹_from_山竹种子_1020076.png b/web/public/shop_plants_organized/山竹_from_山竹种子_1020076.png new file mode 100644 index 0000000..9c2f333 Binary files /dev/null and b/web/public/shop_plants_organized/山竹_from_山竹种子_1020076.png differ diff --git a/web/public/shop_plants_organized/无花果_from_无花果种子_1020043.png b/web/public/shop_plants_organized/无花果_from_无花果种子_1020043.png new file mode 100644 index 0000000..d39e5db Binary files /dev/null and b/web/public/shop_plants_organized/无花果_from_无花果种子_1020043.png differ diff --git a/web/public/shop_plants_organized/曼珠沙华_from_曼珠沙华种子_1020126.png b/web/public/shop_plants_organized/曼珠沙华_from_曼珠沙华种子_1020126.png new file mode 100644 index 0000000..b3217e8 Binary files /dev/null and b/web/public/shop_plants_organized/曼珠沙华_from_曼珠沙华种子_1020126.png differ diff --git a/web/public/shop_plants_organized/曼陀罗华_from_曼陀罗华种子_1020116.png b/web/public/shop_plants_organized/曼陀罗华_from_曼陀罗华种子_1020116.png new file mode 100644 index 0000000..11a58a7 Binary files /dev/null and b/web/public/shop_plants_organized/曼陀罗华_from_曼陀罗华种子_1020116.png differ diff --git a/web/public/shop_plants_organized/月柿_from_月柿种子_1020080.png b/web/public/shop_plants_organized/月柿_from_月柿种子_1020080.png new file mode 100644 index 0000000..f535d9c Binary files /dev/null and b/web/public/shop_plants_organized/月柿_from_月柿种子_1020080.png differ diff --git a/web/public/shop_plants_organized/木瓜_from_木瓜种子_1020038.png b/web/public/shop_plants_organized/木瓜_from_木瓜种子_1020038.png new file mode 100644 index 0000000..b0c7d6d Binary files /dev/null and b/web/public/shop_plants_organized/木瓜_from_木瓜种子_1020038.png differ diff --git a/web/public/shop_plants_organized/李子_from_李子种子_1020413.png b/web/public/shop_plants_organized/李子_from_李子种子_1020413.png new file mode 100644 index 0000000..179009f Binary files /dev/null and b/web/public/shop_plants_organized/李子_from_李子种子_1020413.png differ diff --git a/web/public/shop_plants_organized/杏子_from_杏子种子_1020078.png b/web/public/shop_plants_organized/杏子_from_杏子种子_1020078.png new file mode 100644 index 0000000..01f98df Binary files /dev/null and b/web/public/shop_plants_organized/杏子_from_杏子种子_1020078.png differ diff --git a/web/public/shop_plants_organized/杨桃_from_杨桃种子_1020039.png b/web/public/shop_plants_organized/杨桃_from_杨桃种子_1020039.png new file mode 100644 index 0000000..418bd2b Binary files /dev/null and b/web/public/shop_plants_organized/杨桃_from_杨桃种子_1020039.png differ diff --git a/web/public/shop_plants_organized/杨梅_from_杨梅种子_1020048.png b/web/public/shop_plants_organized/杨梅_from_杨梅种子_1020048.png new file mode 100644 index 0000000..02a442b Binary files /dev/null and b/web/public/shop_plants_organized/杨梅_from_杨梅种子_1020048.png differ diff --git a/web/public/shop_plants_organized/枇杷_from_枇杷种子_1020055.png b/web/public/shop_plants_organized/枇杷_from_枇杷种子_1020055.png new file mode 100644 index 0000000..bf3cb4b Binary files /dev/null and b/web/public/shop_plants_organized/枇杷_from_枇杷种子_1020055.png differ diff --git a/web/public/shop_plants_organized/柚子_from_柚子种子_1020026.png b/web/public/shop_plants_organized/柚子_from_柚子种子_1020026.png new file mode 100644 index 0000000..dd3ba2b Binary files /dev/null and b/web/public/shop_plants_organized/柚子_from_柚子种子_1020026.png differ diff --git a/web/public/shop_plants_organized/柠檬_from_柠檬种子_1020042.png b/web/public/shop_plants_organized/柠檬_from_柠檬种子_1020042.png new file mode 100644 index 0000000..ffd2f88 Binary files /dev/null and b/web/public/shop_plants_organized/柠檬_from_柠檬种子_1020042.png differ diff --git a/web/public/shop_plants_organized/栗子_from_栗子种子_1020095.png b/web/public/shop_plants_organized/栗子_from_栗子种子_1020095.png new file mode 100644 index 0000000..b54802e Binary files /dev/null and b/web/public/shop_plants_organized/栗子_from_栗子种子_1020095.png differ diff --git a/web/public/shop_plants_organized/核桃_from_核桃种子_1020308.png b/web/public/shop_plants_organized/核桃_from_核桃种子_1020308.png new file mode 100644 index 0000000..f2f1a01 Binary files /dev/null and b/web/public/shop_plants_organized/核桃_from_核桃种子_1020308.png differ diff --git a/web/public/shop_plants_organized/桂圆_from_桂圆种子_1020053.png b/web/public/shop_plants_organized/桂圆_from_桂圆种子_1020053.png new file mode 100644 index 0000000..7d85652 Binary files /dev/null and b/web/public/shop_plants_organized/桂圆_from_桂圆种子_1020053.png differ diff --git a/web/public/shop_plants_organized/桃子_from_桃子种子_1020018.png b/web/public/shop_plants_organized/桃子_from_桃子种子_1020018.png new file mode 100644 index 0000000..e761b21 Binary files /dev/null and b/web/public/shop_plants_organized/桃子_from_桃子种子_1020018.png differ diff --git a/web/public/shop_plants_organized/桑葚_from_桑葚种子_1020075.png b/web/public/shop_plants_organized/桑葚_from_桑葚种子_1020075.png new file mode 100644 index 0000000..e9d5cbf Binary files /dev/null and b/web/public/shop_plants_organized/桑葚_from_桑葚种子_1020075.png differ diff --git a/web/public/shop_plants_organized/梨_from_梨种子_1020054.png b/web/public/shop_plants_organized/梨_from_梨种子_1020054.png new file mode 100644 index 0000000..e0a5939 Binary files /dev/null and b/web/public/shop_plants_organized/梨_from_梨种子_1020054.png differ diff --git a/web/public/shop_plants_organized/椰子_from_椰子种子_1020029.png b/web/public/shop_plants_organized/椰子_from_椰子种子_1020029.png new file mode 100644 index 0000000..e32edae Binary files /dev/null and b/web/public/shop_plants_organized/椰子_from_椰子种子_1020029.png differ diff --git a/web/public/shop_plants_organized/榛子_from_榛子种子_1020072.png b/web/public/shop_plants_organized/榛子_from_榛子种子_1020072.png new file mode 100644 index 0000000..2cfff48 Binary files /dev/null and b/web/public/shop_plants_organized/榛子_from_榛子种子_1020072.png differ diff --git a/web/public/shop_plants_organized/榴莲_from_榴莲种子_1020058.png b/web/public/shop_plants_organized/榴莲_from_榴莲种子_1020058.png new file mode 100644 index 0000000..1d09335 Binary files /dev/null and b/web/public/shop_plants_organized/榴莲_from_榴莲种子_1020058.png differ diff --git a/web/public/shop_plants_organized/樱桃_from_樱桃种子_1020034.png b/web/public/shop_plants_organized/樱桃_from_樱桃种子_1020034.png new file mode 100644 index 0000000..d441558 Binary files /dev/null and b/web/public/shop_plants_organized/樱桃_from_樱桃种子_1020034.png differ diff --git a/web/public/shop_plants_organized/橙子_from_橙子种子_1020019.png b/web/public/shop_plants_organized/橙子_from_橙子种子_1020019.png new file mode 100644 index 0000000..dfe73eb Binary files /dev/null and b/web/public/shop_plants_organized/橙子_from_橙子种子_1020019.png differ diff --git a/web/public/shop_plants_organized/水稻_from_水稻种子_1020060.png b/web/public/shop_plants_organized/水稻_from_水稻种子_1020060.png new file mode 100644 index 0000000..633011a Binary files /dev/null and b/web/public/shop_plants_organized/水稻_from_水稻种子_1020060.png differ diff --git a/web/public/shop_plants_organized/油菜_from_油菜种子_1020099.png b/web/public/shop_plants_organized/油菜_from_油菜种子_1020099.png new file mode 100644 index 0000000..3c833ea Binary files /dev/null and b/web/public/shop_plants_organized/油菜_from_油菜种子_1020099.png differ diff --git a/web/public/shop_plants_organized/满天星_from_满天星种子_1020110.png b/web/public/shop_plants_organized/满天星_from_满天星种子_1020110.png new file mode 100644 index 0000000..4c2ce2d Binary files /dev/null and b/web/public/shop_plants_organized/满天星_from_满天星种子_1020110.png differ diff --git a/web/public/shop_plants_organized/火绒草_from_火绒草种子_1020135.png b/web/public/shop_plants_organized/火绒草_from_火绒草种子_1020135.png new file mode 100644 index 0000000..44c100b Binary files /dev/null and b/web/public/shop_plants_organized/火绒草_from_火绒草种子_1020135.png differ diff --git a/web/public/shop_plants_organized/火龙果_from_火龙果种子_1020033.png b/web/public/shop_plants_organized/火龙果_from_火龙果种子_1020033.png new file mode 100644 index 0000000..cded9e1 Binary files /dev/null and b/web/public/shop_plants_organized/火龙果_from_火龙果种子_1020033.png differ diff --git a/web/public/shop_plants_organized/牵牛花_from_牵牛花种子_1020147.png b/web/public/shop_plants_organized/牵牛花_from_牵牛花种子_1020147.png new file mode 100644 index 0000000..c94f0b5 Binary files /dev/null and b/web/public/shop_plants_organized/牵牛花_from_牵牛花种子_1020147.png differ diff --git a/web/public/shop_plants_organized/猕猴桃_from_猕猴桃种子_1020045.png b/web/public/shop_plants_organized/猕猴桃_from_猕猴桃种子_1020045.png new file mode 100644 index 0000000..1307b25 Binary files /dev/null and b/web/public/shop_plants_organized/猕猴桃_from_猕猴桃种子_1020045.png differ diff --git a/web/public/shop_plants_organized/猪笼草_from_猪笼草种子_1020220.png b/web/public/shop_plants_organized/猪笼草_from_猪笼草种子_1020220.png new file mode 100644 index 0000000..aa21bd9 Binary files /dev/null and b/web/public/shop_plants_organized/猪笼草_from_猪笼草种子_1020220.png differ diff --git a/web/public/shop_plants_organized/玉米_from_玉米种子_1020004.png b/web/public/shop_plants_organized/玉米_from_玉米种子_1020004.png new file mode 100644 index 0000000..d18417b Binary files /dev/null and b/web/public/shop_plants_organized/玉米_from_玉米种子_1020004.png differ diff --git a/web/public/shop_plants_organized/瓶子树_from_瓶子树种子_1020218.png b/web/public/shop_plants_organized/瓶子树_from_瓶子树种子_1020218.png new file mode 100644 index 0000000..3051ecc Binary files /dev/null and b/web/public/shop_plants_organized/瓶子树_from_瓶子树种子_1020218.png differ diff --git a/web/public/shop_plants_organized/甘蔗_from_甘蔗种子_1020047.png b/web/public/shop_plants_organized/甘蔗_from_甘蔗种子_1020047.png new file mode 100644 index 0000000..e02b10f Binary files /dev/null and b/web/public/shop_plants_organized/甘蔗_from_甘蔗种子_1020047.png differ diff --git a/web/public/shop_plants_organized/生菜_from_生菜种子_1020096.png b/web/public/shop_plants_organized/生菜_from_生菜种子_1020096.png new file mode 100644 index 0000000..ef07c87 Binary files /dev/null and b/web/public/shop_plants_organized/生菜_from_生菜种子_1020096.png differ diff --git a/web/public/shop_plants_organized/番石榴_from_番石榴种子_1020079.png b/web/public/shop_plants_organized/番石榴_from_番石榴种子_1020079.png new file mode 100644 index 0000000..fd25a6f Binary files /dev/null and b/web/public/shop_plants_organized/番石榴_from_番石榴种子_1020079.png differ diff --git a/web/public/shop_plants_organized/番茄_from_番茄种子_1020007.png b/web/public/shop_plants_organized/番茄_from_番茄种子_1020007.png new file mode 100644 index 0000000..6ef99ad Binary files /dev/null and b/web/public/shop_plants_organized/番茄_from_番茄种子_1020007.png differ diff --git a/web/public/shop_plants_organized/白萝卜_from_白萝卜种子_1020002.png b/web/public/shop_plants_organized/白萝卜_from_白萝卜种子_1020002.png new file mode 100644 index 0000000..1e83c6a Binary files /dev/null and b/web/public/shop_plants_organized/白萝卜_from_白萝卜种子_1020002.png differ diff --git a/web/public/shop_plants_organized/睡莲_from_睡莲种子_1020442.png b/web/public/shop_plants_organized/睡莲_from_睡莲种子_1020442.png new file mode 100644 index 0000000..44a5d81 Binary files /dev/null and b/web/public/shop_plants_organized/睡莲_from_睡莲种子_1020442.png differ diff --git a/web/public/shop_plants_organized/石榴_from_石榴种子_1020023.png b/web/public/shop_plants_organized/石榴_from_石榴种子_1020023.png new file mode 100644 index 0000000..479670a Binary files /dev/null and b/web/public/shop_plants_organized/石榴_from_石榴种子_1020023.png differ diff --git a/web/public/shop_plants_organized/秋菊红色_from_红色秋菊种子_1020162.png b/web/public/shop_plants_organized/秋菊红色_from_红色秋菊种子_1020162.png new file mode 100644 index 0000000..66c9032 Binary files /dev/null and b/web/public/shop_plants_organized/秋菊红色_from_红色秋菊种子_1020162.png differ diff --git a/web/public/shop_plants_organized/秋菊黄色_from_黄色秋菊种子_1020161.png b/web/public/shop_plants_organized/秋菊黄色_from_黄色秋菊种子_1020161.png new file mode 100644 index 0000000..796d914 Binary files /dev/null and b/web/public/shop_plants_organized/秋菊黄色_from_黄色秋菊种子_1020161.png differ diff --git a/web/public/shop_plants_organized/竹笋_from_竹笋种子_1020100.png b/web/public/shop_plants_organized/竹笋_from_竹笋种子_1020100.png new file mode 100644 index 0000000..f47e055 Binary files /dev/null and b/web/public/shop_plants_organized/竹笋_from_竹笋种子_1020100.png differ diff --git a/web/public/shop_plants_organized/箬竹_from_箬竹种子_1020036.png b/web/public/shop_plants_organized/箬竹_from_箬竹种子_1020036.png new file mode 100644 index 0000000..3fc261b Binary files /dev/null and b/web/public/shop_plants_organized/箬竹_from_箬竹种子_1020036.png differ diff --git a/web/public/shop_plants_organized/红枣_from_红枣种子_1020051.png b/web/public/shop_plants_organized/红枣_from_红枣种子_1020051.png new file mode 100644 index 0000000..6ec3af1 Binary files /dev/null and b/web/public/shop_plants_organized/红枣_from_红枣种子_1020051.png differ diff --git a/web/public/shop_plants_organized/红玫瑰_from_红玫瑰种子_1020041.png b/web/public/shop_plants_organized/红玫瑰_from_红玫瑰种子_1020041.png new file mode 100644 index 0000000..238ac06 Binary files /dev/null and b/web/public/shop_plants_organized/红玫瑰_from_红玫瑰种子_1020041.png differ diff --git a/web/public/shop_plants_organized/胡萝卜_from_胡萝卜种子_1020003.png b/web/public/shop_plants_organized/胡萝卜_from_胡萝卜种子_1020003.png new file mode 100644 index 0000000..ba0e1c3 Binary files /dev/null and b/web/public/shop_plants_organized/胡萝卜_from_胡萝卜种子_1020003.png differ diff --git a/web/public/shop_plants_organized/芒果_from_芒果种子_1020057.png b/web/public/shop_plants_organized/芒果_from_芒果种子_1020057.png new file mode 100644 index 0000000..b98d415 Binary files /dev/null and b/web/public/shop_plants_organized/芒果_from_芒果种子_1020057.png differ diff --git a/web/public/shop_plants_organized/花生_from_花生种子_1020049.png b/web/public/shop_plants_organized/花生_from_花生种子_1020049.png new file mode 100644 index 0000000..787e792 Binary files /dev/null and b/web/public/shop_plants_organized/花生_from_花生种子_1020049.png differ diff --git a/web/public/shop_plants_organized/花菜_from_花菜种子_1020098.png b/web/public/shop_plants_organized/花菜_from_花菜种子_1020098.png new file mode 100644 index 0000000..8034801 Binary files /dev/null and b/web/public/shop_plants_organized/花菜_from_花菜种子_1020098.png differ diff --git a/web/public/shop_plants_organized/花香根鸢尾_from_花香根鸢尾种子_1020141.png b/web/public/shop_plants_organized/花香根鸢尾_from_花香根鸢尾种子_1020141.png new file mode 100644 index 0000000..ba9af1a Binary files /dev/null and b/web/public/shop_plants_organized/花香根鸢尾_from_花香根鸢尾种子_1020141.png differ diff --git a/web/public/shop_plants_organized/芹菜_from_芹菜种子_1020306.png b/web/public/shop_plants_organized/芹菜_from_芹菜种子_1020306.png new file mode 100644 index 0000000..a70c55a Binary files /dev/null and b/web/public/shop_plants_organized/芹菜_from_芹菜种子_1020306.png differ diff --git a/web/public/shop_plants_organized/苦瓜_from_苦瓜种子_1020063.png b/web/public/shop_plants_organized/苦瓜_from_苦瓜种子_1020063.png new file mode 100644 index 0000000..580cca6 Binary files /dev/null and b/web/public/shop_plants_organized/苦瓜_from_苦瓜种子_1020063.png differ diff --git a/web/public/shop_plants_organized/苹果_from_苹果种子_1020011.png b/web/public/shop_plants_organized/苹果_from_苹果种子_1020011.png new file mode 100644 index 0000000..03cad0b Binary files /dev/null and b/web/public/shop_plants_organized/苹果_from_苹果种子_1020011.png differ diff --git a/web/public/shop_plants_organized/茄子_from_茄子种子_1020006.png b/web/public/shop_plants_organized/茄子_from_茄子种子_1020006.png new file mode 100644 index 0000000..e93c18a Binary files /dev/null and b/web/public/shop_plants_organized/茄子_from_茄子种子_1020006.png differ diff --git a/web/public/shop_plants_organized/茉莉花_from_茉莉花种子_1020128.png b/web/public/shop_plants_organized/茉莉花_from_茉莉花种子_1020128.png new file mode 100644 index 0000000..5acb03c Binary files /dev/null and b/web/public/shop_plants_organized/茉莉花_from_茉莉花种子_1020128.png differ diff --git a/web/public/shop_plants_organized/草莓_from_草莓种子_1020001.png b/web/public/shop_plants_organized/草莓_from_草莓种子_1020001.png new file mode 100644 index 0000000..89d3d0b Binary files /dev/null and b/web/public/shop_plants_organized/草莓_from_草莓种子_1020001.png differ diff --git a/web/public/shop_plants_organized/荔枝_from_荔枝种子_1020035.png b/web/public/shop_plants_organized/荔枝_from_荔枝种子_1020035.png new file mode 100644 index 0000000..bcc11e3 Binary files /dev/null and b/web/public/shop_plants_organized/荔枝_from_荔枝种子_1020035.png differ diff --git a/web/public/shop_plants_organized/莲藕_from_莲藕种子_1020037.png b/web/public/shop_plants_organized/莲藕_from_莲藕种子_1020037.png new file mode 100644 index 0000000..41c7deb Binary files /dev/null and b/web/public/shop_plants_organized/莲藕_from_莲藕种子_1020037.png differ diff --git a/web/public/shop_plants_organized/菠菜_from_菠菜种子_1020073.png b/web/public/shop_plants_organized/菠菜_from_菠菜种子_1020073.png new file mode 100644 index 0000000..bae6064 Binary files /dev/null and b/web/public/shop_plants_organized/菠菜_from_菠菜种子_1020073.png differ diff --git a/web/public/shop_plants_organized/菠萝_from_菠萝种子_1020027.png b/web/public/shop_plants_organized/菠萝_from_菠萝种子_1020027.png new file mode 100644 index 0000000..e06db9e Binary files /dev/null and b/web/public/shop_plants_organized/菠萝_from_菠萝种子_1020027.png differ diff --git a/web/public/shop_plants_organized/葡萄_from_葡萄种子_1020013.png b/web/public/shop_plants_organized/葡萄_from_葡萄种子_1020013.png new file mode 100644 index 0000000..29bd745 Binary files /dev/null and b/web/public/shop_plants_organized/葡萄_from_葡萄种子_1020013.png differ diff --git a/web/public/shop_plants_organized/葫芦_from_葫芦种子_1020031.png b/web/public/shop_plants_organized/葫芦_from_葫芦种子_1020031.png new file mode 100644 index 0000000..f1c4e69 Binary files /dev/null and b/web/public/shop_plants_organized/葫芦_from_葫芦种子_1020031.png differ diff --git a/web/public/shop_plants_organized/蒲公英_from_蒲公英种子_1020120.png b/web/public/shop_plants_organized/蒲公英_from_蒲公英种子_1020120.png new file mode 100644 index 0000000..0a0026f Binary files /dev/null and b/web/public/shop_plants_organized/蒲公英_from_蒲公英种子_1020120.png differ diff --git a/web/public/shop_plants_organized/蓝莓_from_蓝莓种子_1020077.png b/web/public/shop_plants_organized/蓝莓_from_蓝莓种子_1020077.png new file mode 100644 index 0000000..1212e50 Binary files /dev/null and b/web/public/shop_plants_organized/蓝莓_from_蓝莓种子_1020077.png differ diff --git a/web/public/shop_plants_organized/蘑菇_from_蘑菇种子_1020050.png b/web/public/shop_plants_organized/蘑菇_from_蘑菇种子_1020050.png new file mode 100644 index 0000000..23e4a7a Binary files /dev/null and b/web/public/shop_plants_organized/蘑菇_from_蘑菇种子_1020050.png differ diff --git a/web/public/shop_plants_organized/虞美人_from_虞美人种子_1020142.png b/web/public/shop_plants_organized/虞美人_from_虞美人种子_1020142.png new file mode 100644 index 0000000..ee87ae5 Binary files /dev/null and b/web/public/shop_plants_organized/虞美人_from_虞美人种子_1020142.png differ diff --git a/web/public/shop_plants_organized/西瓜_from_西瓜种子_1020014.png b/web/public/shop_plants_organized/西瓜_from_西瓜种子_1020014.png new file mode 100644 index 0000000..4138680 Binary files /dev/null and b/web/public/shop_plants_organized/西瓜_from_西瓜种子_1020014.png differ diff --git a/web/public/shop_plants_organized/豌豆_from_豌豆种子_1020008.png b/web/public/shop_plants_organized/豌豆_from_豌豆种子_1020008.png new file mode 100644 index 0000000..95f7ab3 Binary files /dev/null and b/web/public/shop_plants_organized/豌豆_from_豌豆种子_1020008.png differ diff --git a/web/public/shop_plants_organized/豹皮花_from_豹皮花种子_1020222.png b/web/public/shop_plants_organized/豹皮花_from_豹皮花种子_1020222.png new file mode 100644 index 0000000..97a2f92 Binary files /dev/null and b/web/public/shop_plants_organized/豹皮花_from_豹皮花种子_1020222.png differ diff --git a/web/public/shop_plants_organized/辣椒_from_辣椒种子_1020009.png b/web/public/shop_plants_organized/辣椒_from_辣椒种子_1020009.png new file mode 100644 index 0000000..fa9d318 Binary files /dev/null and b/web/public/shop_plants_organized/辣椒_from_辣椒种子_1020009.png differ diff --git a/web/public/shop_plants_organized/迎春花_from_迎春花种子_1020396.png b/web/public/shop_plants_organized/迎春花_from_迎春花种子_1020396.png new file mode 100644 index 0000000..291466a Binary files /dev/null and b/web/public/shop_plants_organized/迎春花_from_迎春花种子_1020396.png differ diff --git a/web/public/shop_plants_organized/金桔_from_金桔种子_1020074.png b/web/public/shop_plants_organized/金桔_from_金桔种子_1020074.png new file mode 100644 index 0000000..62b04a3 Binary files /dev/null and b/web/public/shop_plants_organized/金桔_from_金桔种子_1020074.png differ diff --git a/web/public/shop_plants_organized/金针菇_from_金针菇种子_1020052.png b/web/public/shop_plants_organized/金针菇_from_金针菇种子_1020052.png new file mode 100644 index 0000000..ccc7afd Binary files /dev/null and b/web/public/shop_plants_organized/金针菇_from_金针菇种子_1020052.png differ diff --git a/web/public/shop_plants_organized/银莲花_from_银莲花种子_1020259.png b/web/public/shop_plants_organized/银莲花_from_银莲花种子_1020259.png new file mode 100644 index 0000000..01df145 Binary files /dev/null and b/web/public/shop_plants_organized/银莲花_from_银莲花种子_1020259.png differ diff --git a/web/public/shop_plants_organized/非洲菊_from_非洲菊种子_1020104.png b/web/public/shop_plants_organized/非洲菊_from_非洲菊种子_1020104.png new file mode 100644 index 0000000..1135d46 Binary files /dev/null and b/web/public/shop_plants_organized/非洲菊_from_非洲菊种子_1020104.png differ diff --git a/web/public/shop_plants_organized/韭菜_from_韭菜种子_1020305.png b/web/public/shop_plants_organized/韭菜_from_韭菜种子_1020305.png new file mode 100644 index 0000000..b227554 Binary files /dev/null and b/web/public/shop_plants_organized/韭菜_from_韭菜种子_1020305.png differ diff --git a/web/public/shop_plants_organized/香瓜_from_香瓜种子_1020067.png b/web/public/shop_plants_organized/香瓜_from_香瓜种子_1020067.png new file mode 100644 index 0000000..2d1e6d7 Binary files /dev/null and b/web/public/shop_plants_organized/香瓜_from_香瓜种子_1020067.png differ diff --git a/web/public/shop_plants_organized/香蕉_from_香蕉种子_1020015.png b/web/public/shop_plants_organized/香蕉_from_香蕉种子_1020015.png new file mode 100644 index 0000000..778dabb Binary files /dev/null and b/web/public/shop_plants_organized/香蕉_from_香蕉种子_1020015.png differ diff --git a/web/public/shop_plants_organized/鲜姜_from_鲜姜种子_1020066.png b/web/public/shop_plants_organized/鲜姜_from_鲜姜种子_1020066.png new file mode 100644 index 0000000..06ed9b1 Binary files /dev/null and b/web/public/shop_plants_organized/鲜姜_from_鲜姜种子_1020066.png differ diff --git a/web/public/shop_plants_organized/黄瓜_from_黄瓜种子_1020097.png b/web/public/shop_plants_organized/黄瓜_from_黄瓜种子_1020097.png new file mode 100644 index 0000000..02a89d7 Binary files /dev/null and b/web/public/shop_plants_organized/黄瓜_from_黄瓜种子_1020097.png differ diff --git a/web/public/shop_plants_organized/黄豆_from_黄豆种子_1020070.png b/web/public/shop_plants_organized/黄豆_from_黄豆种子_1020070.png new file mode 100644 index 0000000..4d645c4 Binary files /dev/null and b/web/public/shop_plants_organized/黄豆_from_黄豆种子_1020070.png differ diff --git a/web/public/verified_items/100003_化肥礼包_4_d13af7af.png b/web/public/verified_items/100003_化肥礼包_4_d13af7af.png new file mode 100644 index 0000000..b8360a9 Binary files /dev/null and b/web/public/verified_items/100003_化肥礼包_4_d13af7af.png differ diff --git a/web/public/verified_items/1001_金币_4_65088be3.png b/web/public/verified_items/1001_金币_4_65088be3.png new file mode 100644 index 0000000..e53a72c Binary files /dev/null and b/web/public/verified_items/1001_金币_4_65088be3.png differ diff --git a/web/public/verified_items/1002_点券_50_bd489f54.png b/web/public/verified_items/1002_点券_50_bd489f54.png new file mode 100644 index 0000000..2f359b5 Binary files /dev/null and b/web/public/verified_items/1002_点券_50_bd489f54.png differ diff --git a/web/public/verified_items/1004_钻石_7_ff3c1c6f.png b/web/public/verified_items/1004_钻石_7_ff3c1c6f.png new file mode 100644 index 0000000..13ea0fd Binary files /dev/null and b/web/public/verified_items/1004_钻石_7_ff3c1c6f.png differ diff --git a/web/public/verified_items/1011_普通化肥容器_12_3ad710ed.png b/web/public/verified_items/1011_普通化肥容器_12_3ad710ed.png new file mode 100644 index 0000000..5d4e433 Binary files /dev/null and b/web/public/verified_items/1011_普通化肥容器_12_3ad710ed.png differ diff --git a/web/public/verified_items/1012_有机化肥容器_49_fe1c87b7.png b/web/public/verified_items/1012_有机化肥容器_49_fe1c87b7.png new file mode 100644 index 0000000..65aca2e Binary files /dev/null and b/web/public/verified_items/1012_有机化肥容器_49_fe1c87b7.png differ diff --git a/web/public/verified_items/80001_化肥1小时_1_cb2a91eb.png b/web/public/verified_items/80001_化肥1小时_1_cb2a91eb.png new file mode 100644 index 0000000..349a109 Binary files /dev/null and b/web/public/verified_items/80001_化肥1小时_1_cb2a91eb.png differ diff --git a/web/public/verified_items/80002_化肥4小时_1_af16205d.png b/web/public/verified_items/80002_化肥4小时_1_af16205d.png new file mode 100644 index 0000000..06a0de3 Binary files /dev/null and b/web/public/verified_items/80002_化肥4小时_1_af16205d.png differ diff --git a/web/public/verified_items/80003_化肥8小时_1_c3700ffc.png b/web/public/verified_items/80003_化肥8小时_1_c3700ffc.png new file mode 100644 index 0000000..d796998 Binary files /dev/null and b/web/public/verified_items/80003_化肥8小时_1_c3700ffc.png differ diff --git a/web/public/verified_items/80004_化肥12小时_1_179fe100.png b/web/public/verified_items/80004_化肥12小时_1_179fe100.png new file mode 100644 index 0000000..ee01b46 Binary files /dev/null and b/web/public/verified_items/80004_化肥12小时_1_179fe100.png differ diff --git a/web/public/verified_items/80011_有机化肥1小时_2_8fe4411f.png b/web/public/verified_items/80011_有机化肥1小时_2_8fe4411f.png new file mode 100644 index 0000000..04b7d1e Binary files /dev/null and b/web/public/verified_items/80011_有机化肥1小时_2_8fe4411f.png differ diff --git a/web/public/verified_items/80012_有机化肥4小时_1_03357e47.png b/web/public/verified_items/80012_有机化肥4小时_1_03357e47.png new file mode 100644 index 0000000..c8757ed Binary files /dev/null and b/web/public/verified_items/80012_有机化肥4小时_1_03357e47.png differ diff --git a/web/public/verified_items/80013_有机化肥8小时_1_242b3363.png b/web/public/verified_items/80013_有机化肥8小时_1_242b3363.png new file mode 100644 index 0000000..1c16bde Binary files /dev/null and b/web/public/verified_items/80013_有机化肥8小时_1_242b3363.png differ diff --git a/web/public/verified_items/80014_有机化肥12小时_2_5b6e11c0.png b/web/public/verified_items/80014_有机化肥12小时_2_5b6e11c0.png new file mode 100644 index 0000000..3ae8366 Binary files /dev/null and b/web/public/verified_items/80014_有机化肥12小时_2_5b6e11c0.png differ diff --git a/web/public/verified_items/90001_田园犬_1.png b/web/public/verified_items/90001_田园犬_1.png new file mode 100644 index 0000000..7ea129d Binary files /dev/null and b/web/public/verified_items/90001_田园犬_1.png differ diff --git a/web/public/verified_items/90002_牧羊犬_1_0a3a47cb.png b/web/public/verified_items/90002_牧羊犬_1_0a3a47cb.png new file mode 100644 index 0000000..e7ec0bd Binary files /dev/null and b/web/public/verified_items/90002_牧羊犬_1_0a3a47cb.png differ diff --git a/web/public/verified_items/90003_斑点狗_1_ccf8c0ff.png b/web/public/verified_items/90003_斑点狗_1_ccf8c0ff.png new file mode 100644 index 0000000..5e54b20 Binary files /dev/null and b/web/public/verified_items/90003_斑点狗_1_ccf8c0ff.png differ diff --git a/web/public/verified_items/90004_1天狗粮_1_ec8e4ace.png b/web/public/verified_items/90004_1天狗粮_1_ec8e4ace.png new file mode 100644 index 0000000..04f52c4 Binary files /dev/null and b/web/public/verified_items/90004_1天狗粮_1_ec8e4ace.png differ diff --git a/web/public/verified_items/90005_3天狗粮_1_835fa5b1.png b/web/public/verified_items/90005_3天狗粮_1_835fa5b1.png new file mode 100644 index 0000000..d307615 Binary files /dev/null and b/web/public/verified_items/90005_3天狗粮_1_835fa5b1.png differ diff --git a/web/public/verified_items/90006_5天狗粮_1_a04b7acf.png b/web/public/verified_items/90006_5天狗粮_1_a04b7acf.png new file mode 100644 index 0000000..5dc661f Binary files /dev/null and b/web/public/verified_items/90006_5天狗粮_1_a04b7acf.png differ diff --git a/web/public/verified_items/index.html b/web/public/verified_items/index.html new file mode 100644 index 0000000..bda47db --- /dev/null +++ b/web/public/verified_items/index.html @@ -0,0 +1,184 @@ + + + + + + Verified Items + + + +

Verified Items (20)

+
+ +
+ 1001_金币_4_65088be3.png +
+
1001
+
1001_金币_4_65088be3.png
+
+
+ +
+ 1002_点券_50_bd489f54.png +
+
1002
+
1002_点券_50_bd489f54.png
+
+
+ +
+ 1004_钻石_7_ff3c1c6f.png +
+
1004
+
1004_钻石_7_ff3c1c6f.png
+
+
+ +
+ 1011_普通化肥容器_12_3ad710ed.png +
+
1011
+
1011_普通化肥容器_12_3ad710ed.png
+
+
+ +
+ 1012_有机化肥容器_49_fe1c87b7.png +
+
1012
+
1012_有机化肥容器_49_fe1c87b7.png
+
+
+ +
+ 80001_化肥1小时_1_cb2a91eb.png +
+
80001
+
80001_化肥1小时_1_cb2a91eb.png
+
+
+ +
+ 80002_化肥4小时_1_af16205d.png +
+
80002
+
80002_化肥4小时_1_af16205d.png
+
+
+ +
+ 80003_化肥8小时_1_c3700ffc.png +
+
80003
+
80003_化肥8小时_1_c3700ffc.png
+
+
+ +
+ 80004_化肥12小时_1_179fe100.png +
+
80004
+
80004_化肥12小时_1_179fe100.png
+
+
+ +
+ 80011_有机化肥1小时_2_8fe4411f.png +
+
80011
+
80011_有机化肥1小时_2_8fe4411f.png
+
+
+ +
+ 80012_有机化肥4小时_1_03357e47.png +
+
80012
+
80012_有机化肥4小时_1_03357e47.png
+
+
+ +
+ 80013_有机化肥8小时_1_242b3363.png +
+
80013
+
80013_有机化肥8小时_1_242b3363.png
+
+
+ +
+ 80014_有机化肥12小时_2_5b6e11c0.png +
+
80014
+
80014_有机化肥12小时_2_5b6e11c0.png
+
+
+ +
+ 90002_牧羊犬_1_0a3a47cb.png +
+
90002
+
90002_牧羊犬_1_0a3a47cb.png
+
+
+ +
+ 90003_斑点狗_1_ccf8c0ff.png +
+
90003
+
90003_斑点狗_1_ccf8c0ff.png
+
+
+ +
+ 90004_1天狗粮_1_ec8e4ace.png +
+
90004
+
90004_1天狗粮_1_ec8e4ace.png
+
+
+ +
+ 90005_3天狗粮_1_835fa5b1.png +
+
90005
+
90005_3天狗粮_1_835fa5b1.png
+
+
+ +
+ 90006_5天狗粮_1_a04b7acf.png +
+
90006
+
90006_5天狗粮_1_a04b7acf.png
+
+
+ +
+ 100003_化肥礼包_4_d13af7af.png +
+
100003
+
100003_化肥礼包_4_d13af7af.png
+
+
+ +
+ 田园犬 +
+
90001
+
田园犬
+
+
+ +
+ + + \ No newline at end of file diff --git a/web/public/verified_items/mapping.json b/web/public/verified_items/mapping.json new file mode 100644 index 0000000..495a009 --- /dev/null +++ b/web/public/verified_items/mapping.json @@ -0,0 +1,103 @@ +[ + { + "id": "1001", + "filename": "1001_金币_4_65088be3.png", + "path": "verified_items/1001_金币_4_65088be3.png" + }, + { + "id": "1002", + "filename": "1002_点券_50_bd489f54.png", + "path": "verified_items/1002_点券_50_bd489f54.png" + }, + { + "id": "1004", + "filename": "1004_钻石_7_ff3c1c6f.png", + "path": "verified_items/1004_钻石_7_ff3c1c6f.png" + }, + { + "id": "1011", + "filename": "1011_普通化肥容器_12_3ad710ed.png", + "path": "verified_items/1011_普通化肥容器_12_3ad710ed.png" + }, + { + "id": "1012", + "filename": "1012_有机化肥容器_49_fe1c87b7.png", + "path": "verified_items/1012_有机化肥容器_49_fe1c87b7.png" + }, + { + "id": "80001", + "filename": "80001_化肥1小时_1_cb2a91eb.png", + "path": "verified_items/80001_化肥1小时_1_cb2a91eb.png" + }, + { + "id": "80002", + "filename": "80002_化肥4小时_1_af16205d.png", + "path": "verified_items/80002_化肥4小时_1_af16205d.png" + }, + { + "id": "80003", + "filename": "80003_化肥8小时_1_c3700ffc.png", + "path": "verified_items/80003_化肥8小时_1_c3700ffc.png" + }, + { + "id": "80004", + "filename": "80004_化肥12小时_1_179fe100.png", + "path": "verified_items/80004_化肥12小时_1_179fe100.png" + }, + { + "id": "80011", + "filename": "80011_有机化肥1小时_2_8fe4411f.png", + "path": "verified_items/80011_有机化肥1小时_2_8fe4411f.png" + }, + { + "id": "80012", + "filename": "80012_有机化肥4小时_1_03357e47.png", + "path": "verified_items/80012_有机化肥4小时_1_03357e47.png" + }, + { + "id": "80013", + "filename": "80013_有机化肥8小时_1_242b3363.png", + "path": "verified_items/80013_有机化肥8小时_1_242b3363.png" + }, + { + "id": "80014", + "filename": "80014_有机化肥12小时_2_5b6e11c0.png", + "path": "verified_items/80014_有机化肥12小时_2_5b6e11c0.png" + }, + { + "id": "90002", + "filename": "90002_牧羊犬_1_0a3a47cb.png", + "path": "verified_items/90002_牧羊犬_1_0a3a47cb.png" + }, + { + "id": "90003", + "filename": "90003_斑点狗_1_ccf8c0ff.png", + "path": "verified_items/90003_斑点狗_1_ccf8c0ff.png" + }, + { + "id": "90004", + "filename": "90004_1天狗粮_1_ec8e4ace.png", + "path": "verified_items/90004_1天狗粮_1_ec8e4ace.png" + }, + { + "id": "90005", + "filename": "90005_3天狗粮_1_835fa5b1.png", + "path": "verified_items/90005_3天狗粮_1_835fa5b1.png" + }, + { + "id": "90006", + "filename": "90006_5天狗粮_1_a04b7acf.png", + "path": "verified_items/90006_5天狗粮_1_a04b7acf.png" + }, + { + "id": "100003", + "filename": "100003_化肥礼包_4_d13af7af.png", + "path": "verified_items/100003_化肥礼包_4_d13af7af.png" + }, + { + "id": "90001", + "filename": "90001_田园犬_1.png", + "path": "verified_items/90001_田园犬_1.png", + "name": "田园犬" + } +] \ No newline at end of file diff --git a/web/public/verified_items/土地.png b/web/public/verified_items/土地.png new file mode 100644 index 0000000..1c4af5c Binary files /dev/null and b/web/public/verified_items/土地.png differ diff --git a/web/public/vite.svg b/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.css b/web/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/web/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..2632e38 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,24 @@ +import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom" +import Login from "./pages/Login" +import Dashboard from "./pages/Dashboard" + +function App() { + return ( + +
+
+ + } /> + } /> + } /> + +
+
+ Made By ♥Karriis +
+
+
+ ) +} + +export default App diff --git a/web/src/assets/fonts/MapleMono-NF-CN-Regular.woff2 b/web/src/assets/fonts/MapleMono-NF-CN-Regular.woff2 new file mode 100644 index 0000000..1e207c6 Binary files /dev/null and b/web/src/assets/fonts/MapleMono-NF-CN-Regular.woff2 differ diff --git a/web/src/assets/logo.png b/web/src/assets/logo.png new file mode 100644 index 0000000..5e92acb Binary files /dev/null and b/web/src/assets/logo.png differ diff --git a/web/src/assets/react.svg b/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/FarmGrid.tsx b/web/src/components/FarmGrid.tsx new file mode 100644 index 0000000..bb1f983 --- /dev/null +++ b/web/src/components/FarmGrid.tsx @@ -0,0 +1,244 @@ +import { useEffect, useState } from "react" +import { Sprout, Lock, Droplets, Bug, Skull, Shovel, Flower, Ban, Hand } from "lucide-react" +import { cn } from "@/lib/utils" + +export interface LandData { + id: number + type: 'locked' | 'empty' | 'planted' + status?: 'growing' | 'mature' | 'dead' + plantName?: string + plantId?: number + phase?: number + phaseName?: string + fertilized?: boolean + needs?: { + water: boolean + weed: boolean + bug: boolean + } + canSteal?: boolean + couldUnlock?: boolean + unlockCondition?: { + needLevel: number + needGold: number + } +} + +interface FarmGridProps { + lands: LandData[] + isLoading?: boolean + onLandClick?: (land: LandData) => void + onLandUnlock?: (land: LandData) => void + selectionEnabled?: boolean + selectedLandIds?: number[] + userLevel?: number + userGold?: number + unlockingLandIds?: number[] +} + +export function FarmGrid({ lands, isLoading = false, onLandClick, onLandUnlock, selectionEnabled = false, selectedLandIds = [], userLevel = 0, userGold = 0, unlockingLandIds = [] }: FarmGridProps) { + const [plantImageMap, setPlantImageMap] = useState>({}) + + useEffect(() => { + let cancelled = false + fetch('/shop_plants_organized/mapping.csv') + .then(res => res.text()) + .then(text => { + if (cancelled) return + const lines = text.split(/\r?\n/).filter(Boolean) + if (lines.length <= 1) { + setPlantImageMap({}) + return + } + const map: Record = {} + for (let i = 1; i < lines.length; i += 1) { + const line = lines[i].trim() + if (!line) continue + const parts = line.split(',') + if (parts.length < 5) continue + const plantId = Number(parts[2]) + const fileName = parts[4] + if (!Number.isNaN(plantId) && fileName) { + map[plantId] = fileName + } + } + setPlantImageMap(map) + }) + .catch(() => { + if (!cancelled) setPlantImageMap({}) + }) + return () => { + cancelled = true + } + }, []) + + // Generate default 18 slots if lands is empty (initial state) + const displayLands = lands.length > 0 ? lands : Array.from({ length: 18 }, (_, i) => ({ + id: i + 1, + type: 'locked' as const + })) + + if (isLoading && lands.length === 0) { + return ( +
+ {Array.from({ length: 18 }).map((_, i) => ( +
+ ))} +
+ ) + } + + return ( +
+ {displayLands.map((land) => ( + onLandClick?.(land)} + onUnlock={() => onLandUnlock?.(land)} + userLevel={userLevel} + userGold={userGold} + isUnlocking={unlockingLandIds.includes(land.id)} + selected={selectionEnabled && selectedLandIds.includes(land.id)} + selectionEnabled={selectionEnabled} + /> + ))} +
+ ) +} + +function LandCard({ land, plantImageMap, onClick, onUnlock, userLevel, userGold, isUnlocking, selected, selectionEnabled }: { land: LandData, plantImageMap: Record, onClick?: () => void, onUnlock?: () => void, userLevel: number, userGold: number, isUnlocking: boolean, selected?: boolean, selectionEnabled?: boolean }) { + const isLocked = land.type === 'locked' + const isEmpty = land.type === 'empty' + const isPlanted = land.type === 'planted' + + const isDead = land.status === 'dead' + const isMature = land.status === 'mature' + const isGrowing = land.status === 'growing' + const isFertilized = land.fertilized === true + const plantImage = land.plantId ? plantImageMap[land.plantId] : undefined + const needLevel = land.unlockCondition?.needLevel ?? 0 + const needGold = land.unlockCondition?.needGold ?? 0 + const canUnlock = isLocked && (import.meta.env.DEV ? true : (!!land.couldUnlock && userLevel >= needLevel && userGold >= needGold)) + + return ( +
+ + {/* Status Badge (ID) */} + + #{land.id} + + {canUnlock && ( +
+ +
+ )} + + {/* Main Icon */} +
+ {isLocked && } + {isEmpty && } + + {isPlanted && ( + <> + {plantImage ? ( + {land.plantName + ) : ( + <> + {isDead && } + {isGrowing && } + {isMature && } + + )} + + {land.plantName || '等待中...'} + + + {`${land.phaseName || '未知'}/${isFertilized ? '已施肥' : '未施肥'}`} + + + )} +
+ + {/* Action Indicators */} + {isPlanted && !isDead && land.needs && ( +
+ {land.needs.water && ( +
+ +
+ )} + {land.needs.bug && ( +
+ +
+ )} + {land.needs.weed && ( +
+ +
+ )} +
+ )} + + {/* Steal Indicator */} + {land.canSteal && ( +
+ +
+ )} + + {/* Background decoration for mature */} + {isMature && ( +
+ )} +
+ ) +} diff --git a/web/src/components/FriendList.tsx b/web/src/components/FriendList.tsx new file mode 100644 index 0000000..3a88e04 --- /dev/null +++ b/web/src/components/FriendList.tsx @@ -0,0 +1,108 @@ +import { useCallback, useEffect, useState } from "react" +import { Users, User, Crown, RefreshCw } from "lucide-react" +import { ScrollArea } from "@/components/ui/scroll-area" +import { cn } from "@/lib/utils" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" + +interface Friend { + uin: string + userName: string + headPic: string + yellowLevel: number + exp: number + money: number +} + +export function FriendList({ email, onVisit }: { email: string, onVisit: (uid: string, name: string) => void }) { + const [friends, setFriends] = useState([]) + const [isLoading, setIsLoading] = useState(false) + + const fetchFriends = useCallback(async () => { + if (!email) return + setIsLoading(true) + try { + const res = await fetch(`/api/friends?email=${email}`) + const json = await res.json() + if (json.friends) { + setFriends(json.friends) + } + } catch (e) { + console.error(e) + } finally { + setIsLoading(false) + } + }, [email]) + + useEffect(() => { + fetchFriends() + }, [fetchFriends]) + + return ( +
+
+

+ + + + 好友列表 + + {friends.length} + +

+ +
+ + +
+ {friends.map((friend) => ( +
onVisit(friend.uin, friend.userName)} + > +
+ + + + + + + {friend.yellowLevel > 0 && ( +
+ Lv{friend.yellowLevel} +
+ )} +
+ +
+
+ {friend.userName} +
+
+ + 💰 {friend.money.toLocaleString()} + + + Exp {friend.exp} + +
+
+
+ ))} + + {friends.length === 0 && !isLoading && ( +
+ + 暂无好友数据 +
+ )} +
+
+
+ ) +} diff --git a/web/src/components/LogPanel.tsx b/web/src/components/LogPanel.tsx new file mode 100644 index 0000000..e66acc4 --- /dev/null +++ b/web/src/components/LogPanel.tsx @@ -0,0 +1,265 @@ +import { useCallback, useEffect, useRef, useState } from "react" +import { ScrollArea } from "@/components/ui/scroll-area" +import { cn } from "@/lib/utils" +import { io, Socket } from "socket.io-client" +import { Terminal, Info, AlertTriangle, CheckCircle2, Clock } from "lucide-react" + +export interface LogEntry { + id: string + tag: string + msg: string + type: 'info' | 'warn' | 'error' | 'success' + time: number +} + +interface LogPanelProps { + className?: string + email?: string +} + +interface BotLogPayload { + tag?: string + msg?: unknown + type?: LogEntry['type'] + time?: number +} + +interface BotErrorPayload { + message?: string +} + +interface BotStatusPayload { + status?: string +} + +export function LogPanel({ className, email }: LogPanelProps) { + const [logs, setLogs] = useState([]) + const [isConnected, setIsConnected] = useState(false) + const scrollRef = useRef(null) + const socketRef = useRef(null) + const retryCountRef = useRef(0) + const disconnectNotifiedRef = useRef(false) + const retryNotifiedRef = useRef(false) + + const addLog = useCallback((log: Omit) => { + setLogs(prev => [...prev.slice(-200), { ...log, id: Math.random().toString(36).slice(2, 11) }]) + }, []) + + const addSystemLog = useCallback((msg: string, type: LogEntry['type'] = 'info') => { + addLog({ tag: 'System', msg, type, time: Date.now() }) + }, [addLog]) + + useEffect(() => { + if (!email) return + const loadHistory = async () => { + try { + const res = await fetch(`/api/logs?email=${encodeURIComponent(email)}`) + if (!res.ok) return + const data = await res.json() + if (!Array.isArray(data.logs)) return + const normalized = data.logs.map((entry: LogEntry) => ({ + id: entry.id || Math.random().toString(36).slice(2, 11), + tag: entry.tag || 'System', + msg: entry.msg || '', + type: entry.type || 'info', + time: entry.time || Date.now() + })) + setLogs(normalized) + } catch { + addSystemLog('历史日志加载失败', 'warn') + } + } + loadHistory() + + // Connect to WebSocket + // In production this should be an env var, here we assume same host + const socket = io(import.meta.env.VITE_WS_GATEWAY_URL || '/', { + transports: ['websocket'], + reconnectionAttempts: 3, + reconnectionDelay: 1000, + reconnectionDelayMax: 3000 + }) + + socketRef.current = socket + + socket.on('connect', () => { + setIsConnected(true) + retryCountRef.current = 0 + disconnectNotifiedRef.current = false + retryNotifiedRef.current = false + addSystemLog('已连接到农场服务器') + }) + + socket.on('disconnect', () => { + setIsConnected(false) + if (!disconnectNotifiedRef.current) { + disconnectNotifiedRef.current = true + addSystemLog('与服务器断开连接', 'warn') + } + }) + + socket.on('reconnect_attempt', (attempt: number) => { + retryCountRef.current = attempt + if (!retryNotifiedRef.current) { + retryNotifiedRef.current = true + addSystemLog('连接断开,正在重试(最多3次)', 'warn') + } + }) + + socket.on('reconnect_failed', () => { + addSystemLog('连接失败,请重新绑定', 'error') + }) + + socket.on(`bot-log-${email}`, (data: unknown) => { + const payload = (typeof data === 'object' && data !== null ? data : {}) as BotLogPayload + // 格式化日志内容(如果它是对象) + const rawMsg = payload.msg + let message = '' + if (typeof rawMsg === 'string') { + message = rawMsg + } else if (rawMsg === null || rawMsg === undefined) { + message = '' + } else if (typeof rawMsg === 'object') { + try { + message = JSON.stringify(rawMsg) + } catch { + message = String(rawMsg) + } + } else { + message = String(rawMsg) + } + + const normalizedType: LogEntry['type'] = + payload.type === 'info' || payload.type === 'warn' || payload.type === 'error' || payload.type === 'success' + ? payload.type + : 'info' + + addLog({ + tag: payload.tag || 'System', + msg: message, + type: normalizedType, + time: payload.time || Date.now() + }) + }) + + socket.on(`bot-error-${email}`, (data: unknown) => { + const payload = (typeof data === 'object' && data !== null ? data : {}) as BotErrorPayload + addLog({ + tag: 'Error', + msg: payload.message || '未知错误', + type: 'error', + time: Date.now() + }) + }) + + socket.on(`bot-status-${email}`, (data: unknown) => { + const payload = (typeof data === 'object' && data !== null ? data : {}) as BotStatusPayload + const statusText = payload.status || 'unknown' + addSystemLog(`Bot状态更新: ${statusText}`, statusText === 'running' ? 'success' : 'warn') + }) + + return () => { + socket.off(`bot-log-${email}`) + socket.off(`bot-error-${email}`) + socket.off(`bot-status-${email}`) + socket.off('connect') + socket.off('disconnect') + socket.off('reconnect_attempt') + socket.off('reconnect_failed') + socket.disconnect() + } + }, [email, addLog, addSystemLog]) + + // Auto-scroll to bottom + useEffect(() => { + if (scrollRef.current) { + const scrollElement = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]') + if (scrollElement) { + scrollElement.scrollTop = scrollElement.scrollHeight + } + } + }, [logs]) + + return ( +
+ {/* Header */} +
+
+
+ +
+

运行日志

+
+
+ + + {isConnected ? 'Live' : 'Offline'} + +
+
+ + {/* Logs Area */} + +
+ {logs.length === 0 && ( +
+ +

等待活动...

+
+ )} + + {logs.map((log) => ( +
+ {/* Icon */} +
+ {log.type === 'info' && } + {log.type === 'success' && } + {log.type === 'warn' && } + {log.type === 'error' && } +
+ + {/* Content */} +
+
+ + {log.tag} + + + {new Date(log.time).toLocaleTimeString()} + +
+

+ {log.msg} +

+
+
+ ))} +
+
+
+ ) +} diff --git a/web/src/components/ShopPanel.tsx b/web/src/components/ShopPanel.tsx new file mode 100644 index 0000000..079b1e6 --- /dev/null +++ b/web/src/components/ShopPanel.tsx @@ -0,0 +1,274 @@ +import { useCallback, useEffect, useState } from "react" +import { RefreshCw, Coins, Search } from "lucide-react" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { cn } from "@/lib/utils" + +interface GoodsItem { + goodsId: number + itemId: number + name: string + price: number + limitCount: number + boughtNum: number + unlocked: boolean + itemCount: number +} + +export function ShopPanel({ email, refreshKey }: { email: string, refreshKey?: number }) { + const [goods, setGoods] = useState([]) + const [filteredGoods, setFilteredGoods] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [isBuying, setIsBuying] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [buyDialogOpen, setBuyDialogOpen] = useState(false) + const [selectedGoods, setSelectedGoods] = useState(null) + const [buyAmount, setBuyAmount] = useState(1) + const [message, setMessage] = useState("") + const [imageMap, setImageMap] = useState>({}) + + const fetchShop = useCallback(async () => { + if (!email) return + setIsLoading(true) + try { + const res = await fetch(`/api/shop?email=${email}`) + const json = await res.json() + if (json.goods) { + setGoods(json.goods) + setFilteredGoods(json.goods) + } + } catch (e) { + console.error(e) + } finally { + setIsLoading(false) + } + }, [email]) + + useEffect(() => { + fetchShop() + }, [fetchShop, refreshKey]) + + useEffect(() => { + if (!searchQuery) { + setFilteredGoods(goods) + } else { + const query = searchQuery.toLowerCase() + setFilteredGoods(goods.filter(g => g.name.toLowerCase().includes(query))) + } + }, [searchQuery, goods]) + + useEffect(() => { + let cancelled = false + fetch('/shop_plants_organized/mapping.csv') + .then(res => res.text()) + .then(text => { + if (cancelled) return + const lines = text.split(/\r?\n/).filter(Boolean) + if (lines.length <= 1) { + setImageMap({}) + return + } + const map: Record = {} + for (let i = 1; i < lines.length; i += 1) { + const line = lines[i].trim() + if (!line) continue + const parts = line.split(',') + if (parts.length < 5) continue + const seedId = Number(parts[0]) + const fileName = parts[4] + if (!Number.isNaN(seedId) && fileName) { + map[seedId] = fileName + } + } + setImageMap(map) + }) + .catch(() => { + if (!cancelled) setImageMap({}) + }) + return () => { + cancelled = true + } + }, []) + + const openBuyDialog = (item: GoodsItem) => { + setSelectedGoods(item) + setBuyAmount(1) + setMessage("") + setBuyDialogOpen(true) + } + + const handleBuy = async () => { + if (!selectedGoods) return + + setIsBuying(true) + setMessage("") + + try { + const res = await fetch('/api/shop/buy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + goodsId: selectedGoods.goodsId, + count: buyAmount, + price: selectedGoods.price + }) + }) + const json = await res.json() + + if (json.success) { + setMessage(`购买成功!获得 ${selectedGoods.name} x${buyAmount * selectedGoods.itemCount}`) + setTimeout(() => { + setBuyDialogOpen(false) + fetchShop() // Refresh limits/stock if any + }, 1500) + } else { + setMessage(`购买失败: ${json.error}`) + } + } catch (error) { + console.error(error) + setMessage("网络错误") + } finally { + setIsBuying(false) + } + } + + return ( +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-8" + /> +
+ +
+ + +
+ {filteredGoods.map((item) => ( +
+ {imageMap[item.itemId] && ( +
+ {item.name} +
+ )} +
+
{item.name}
+
ID: {item.itemId}
+
+ +
+ + {item.price} +
+ + + + {item.limitCount > 0 && ( +
+ 限购 {item.boughtNum}/{item.limitCount} +
+ )} +
+ ))} + {filteredGoods.length === 0 && !isLoading && ( +
+ 未找到商品 +
+ )} +
+
+ + + + + 购买 {selectedGoods?.name} + + 单价: {selectedGoods?.price} 金币 + + + +
+
+ +
{buyAmount}
+ +
+ +
+ 总价: {(selectedGoods?.price || 0) * buyAmount} 金币 +
+ + {message && ( +
+ {message} +
+ )} +
+ + + + + +
+
+
+ ) +} diff --git a/web/src/components/StatusBar.tsx b/web/src/components/StatusBar.tsx new file mode 100644 index 0000000..7c3a26c --- /dev/null +++ b/web/src/components/StatusBar.tsx @@ -0,0 +1,126 @@ +import { User, TrendingUp, Activity } from "lucide-react" +import { cn } from "@/lib/utils" + +interface StatusBarProps { + nickname?: string + level?: number + exp?: number + nextLevelExp?: number + gold?: number + tickets?: number + normalFertilizerHours?: number + organicFertilizerHours?: number + status?: 'running' | 'stopped' | 'error' + avatarUrl?: string + code?: string +} + +export function StatusBar({ + nickname = "Farmer", + level = 1, + exp = 0, + nextLevelExp = 100, + gold = 0, + tickets = 0, + normalFertilizerHours = 0, + organicFertilizerHours = 0, + status = 'stopped', + avatarUrl, + code +}: StatusBarProps) { + const expPercentage = Math.min(100, Math.max(0, (exp / nextLevelExp) * 100)) + + return ( +
+ {/* User Info */} +
+
+ {avatarUrl ? ( + Avatar + ) : ( + + )} +
+
+
+ {nickname} + {import.meta.env.DEV && code && ( + + {code} + + )} +
+
+ + + {status === 'running' ? '运行中' : '已停止'} + + + {level} 级 + +
+
+
+ + {/* Stats Group */} +
+ {/* Experience Bar */} +
+
+ 经验 + {Math.floor(expPercentage)}% +
+
+
+
+
+ +
+
+ 点券 +
+
+ 点券 + {tickets.toLocaleString()} +
+
+ +
+
+ 普通化肥 +
+
+ 普通化肥余量 + {normalFertilizerHours.toFixed(1)}h +
+
+ +
+
+ 有机化肥 +
+
+ 有机化肥余量 + {organicFertilizerHours.toFixed(1)}h +
+
+ +
+
+ 金币 +
+
+ 金币 + {gold.toLocaleString()} +
+
+
+
+ ) +} diff --git a/web/src/components/SystemMonitor.tsx b/web/src/components/SystemMonitor.tsx new file mode 100644 index 0000000..2006baa --- /dev/null +++ b/web/src/components/SystemMonitor.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react" +import { Cpu, Database, Activity, ChevronLeft, ChevronRight } from "lucide-react" + +interface SystemStats { + memory: { + rss: number + heapTotal: number + heapUsed: number + } + uptime: number + activeBots: number +} + +export function SystemMonitor() { + const [stats, setStats] = useState(null) + const [collapsed, setCollapsed] = useState(true) + + useEffect(() => { + const fetchStats = async () => { + try { + const response = await fetch('/api/system/stats') + if (response.ok) { + const data = await response.json() + setStats(data) + } + } catch (error) { + console.error('Failed to fetch system stats:', error) + } + } + + fetchStats() + const interval = setInterval(fetchStats, 5000) // Update every 5s + return () => clearInterval(interval) + }, []) + + if (!stats) { + return ( +
+ + Loading stats... +
+ ) + } + + return ( +
+ +
+
+ + 运行时间: {formatUptime(stats.uptime)} +
+
+ + 内存: {stats.memory.rss} MB (堆: {stats.memory.heapUsed} MB) +
+
+ + 挂机数量: {stats.activeBots} +
+
+
+ ) +} + +function formatUptime(seconds: number) { + const h = Math.floor(seconds / 3600) + const m = Math.floor((seconds % 3600) / 60) + const s = seconds % 60 + return `${h}h ${m}m ${s}s` +} diff --git a/web/src/components/WarehousePanel.tsx b/web/src/components/WarehousePanel.tsx new file mode 100644 index 0000000..4fc433b --- /dev/null +++ b/web/src/components/WarehousePanel.tsx @@ -0,0 +1,595 @@ +import { useCallback, useEffect, useState, type ComponentType, type SVGProps } from "react" +import { Package, Leaf, Apple, Zap, RefreshCw, CheckCircle2, Circle, Coins } from "lucide-react" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { cn } from "@/lib/utils" + +interface Item { + id: number + uid: number + name: string + count: number + type: 'seed' | 'produce' | 'other' +} + +interface WarehouseData { + seeds: Item[] + produce: Item[] + others: Item[] +} +interface Gain { + id: number + name: string + count: number +} + +export function WarehousePanel({ email, refreshKey, onRefresh }: { email: string, refreshKey?: number, onRefresh?: () => void }) { + const [data, setData] = useState({ seeds: [], produce: [], others: [] }) + const [isLoading, setIsLoading] = useState(false) + const [isSelling, setIsSelling] = useState(false) + const [selectedItems, setSelectedItems] = useState>(new Set()) // Format: "id-uid" + const [isSelectionMode, setIsSelectionMode] = useState(false) + const [activeTab, setActiveTab] = useState("seeds") + const [message, setMessage] = useState("") + const [seedImageMap, setSeedImageMap] = useState>({}) + const [nameImageMap, setNameImageMap] = useState>({}) + const [verifiedItemMap, setVerifiedItemMap] = useState>({}) + const [useDialogOpen, setUseDialogOpen] = useState(false) + const [useTargets, setUseTargets] = useState([]) + const [useQuantities, setUseQuantities] = useState>({}) + const [gainDialogOpen, setGainDialogOpen] = useState(false) + const [gains, setGains] = useState([]) + + const fetchWarehouse = useCallback(async (silent = false) => { + if (!email) return + if (!silent) setIsLoading(true) + try { + const res = await fetch(`/api/warehouse?email=${email}`) + const json = await res.json() + if (json.seeds) { + setData(json) + } + } catch (e) { + console.error(e) + } finally { + if (!silent) setIsLoading(false) + } + }, [email]) + + useEffect(() => { + fetchWarehouse() + const interval = setInterval(() => fetchWarehouse(true), 5000) + return () => clearInterval(interval) + }, [fetchWarehouse, refreshKey]) + + useEffect(() => { + let cancelled = false + fetch('/shop_plants_organized/mapping.csv') + .then(res => res.text()) + .then(text => { + if (cancelled) return + const lines = text.split(/\r?\n/).filter(Boolean) + if (lines.length <= 1) { + setSeedImageMap({}) + setNameImageMap({}) + return + } + const map: Record = {} + const nameMap: Record = {} + for (let i = 1; i < lines.length; i += 1) { + const line = lines[i].trim() + if (!line) continue + const parts = line.split(',') + if (parts.length < 5) continue + const seedId = Number(parts[0]) + const fileName = parts[4] + if (!Number.isNaN(seedId) && fileName) { + map[seedId] = fileName + } + if (fileName) { + const name = fileName.split('_from_')[0] + if (name) nameMap[name] = fileName + } + } + setSeedImageMap(map) + setNameImageMap(nameMap) + }) + .catch(() => { + if (!cancelled) { + setSeedImageMap({}) + setNameImageMap({}) + } + }) + return () => { + cancelled = true + } + }, []) + + useEffect(() => { + let cancelled = false + fetch('/verified_items/mapping.json') + .then(res => res.json()) + .then((list: Array<{ id: string, path: string }>) => { + if (cancelled || !Array.isArray(list)) return + const map: Record = {} + for (const item of list) { + const id = Number(item.id) + if (!Number.isNaN(id) && item.path) { + map[id] = item.path + } + } + setVerifiedItemMap(map) + }) + .catch(() => { + if (!cancelled) setVerifiedItemMap({}) + }) + return () => { + cancelled = true + } + }, []) + + const toggleSelection = (item: Item) => { + const key = `${item.id}-${item.uid}` + const newSet = new Set(selectedItems) + if (newSet.has(key)) { + newSet.delete(key) + } else { + newSet.add(key) + } + setSelectedItems(newSet) + } + + const collectSelectedItems = useCallback(() => { + if (selectedItems.size === 0) return [] + const allItems = [...data.seeds, ...data.produce, ...data.others] + const result: Item[] = [] + selectedItems.forEach(key => { + const [idStr, uidStr] = key.split('-') + const id = Number(idStr) + const uid = Number(uidStr) + const item = allItems.find(i => i.id === id && i.uid === uid) + if (item) result.push(item) + }) + return result + }, [data.others, data.produce, data.seeds, selectedItems]) + + const closeUseDialog = useCallback(() => { + setUseDialogOpen(false) + setUseTargets([]) + setUseQuantities({}) + }, []) + + const openUseDialogForItems = useCallback((list: Item[]) => { + if (list.length === 0) return + const nextQuantities: Record = {} + list.forEach(item => { + nextQuantities[`${item.id}-${item.uid}`] = 1 + }) + setUseTargets(list) + setUseQuantities(nextQuantities) + setUseDialogOpen(true) + }, []) + + const openUseDialog = useCallback(() => { + const list = collectSelectedItems() + openUseDialogForItems(list) + }, [collectSelectedItems, openUseDialogForItems]) + + const updateUseQuantity = useCallback((key: string, value: string, maxCount: number) => { + const parsed = Number(value) + const next = Number.isNaN(parsed) ? 1 : Math.floor(parsed) + const clamped = Math.max(1, Math.min(maxCount, next)) + setUseQuantities(prev => { + if (prev[key] === clamped) return prev + return { ...prev, [key]: clamped } + }) + }, []) + + const handleSell = async () => { + if (selectedItems.size === 0) return + + if (activeTab === 'others') { + openUseDialog() + return + } + + setIsSelling(true) + setMessage("") + + const selectedList = collectSelectedItems() + if (selectedList.length === 0) { + setIsSelling(false) + return + } + const itemsToSell: { id: number, count: number, uid: number }[] = [] + selectedList.forEach(item => { + itemsToSell.push({ id: item.id, count: item.count, uid: item.uid }) + }) + + try { + const res = await fetch('/api/warehouse/sell', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, items: itemsToSell }) + }) + const json = await res.json() + + if (json.success) { + setMessage(`出售成功!获得 ${json.gold} 金币`) + setSelectedItems(new Set()) + setIsSelectionMode(false) + fetchWarehouse() + onRefresh?.() + } else { + setMessage(`出售失败: ${json.error}`) + } + } catch (error) { + console.error(error) + setMessage("网络错误") + } finally { + setIsSelling(false) + setTimeout(() => setMessage(""), 3000) + } + } + + const handleUse = async () => { + if (useTargets.length === 0) return + + setIsSelling(true) + setMessage("") + setGains([]) + + const itemsToUse: { id: number, count: number, uid: number }[] = [] + useTargets.forEach(item => { + const key = `${item.id}-${item.uid}` + const selectedCount = useQuantities[key] ?? 1 + const count = Math.max(1, Math.min(item.count, selectedCount)) + itemsToUse.push({ id: item.id, count, uid: item.uid }) + }) + if (itemsToUse.length === 0) { + setIsSelling(false) + return + } + + try { + const res = await fetch('/api/warehouse/use', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, items: itemsToUse }) + }) + const json = await res.json() + if (json.success) { + setMessage(json.message || "使用成功") + if (Array.isArray(json.gains) && json.gains.length > 0) { + setGains(json.gains) + setGainDialogOpen(true) + } + setSelectedItems(new Set()) + setIsSelectionMode(false) + fetchWarehouse() + onRefresh?.() + closeUseDialog() + } else { + setMessage(`使用失败: ${json.error}`) + } + } catch (error) { + console.error(error) + setMessage("网络错误") + } finally { + setIsSelling(false) + setTimeout(() => setMessage(""), 3000) + } + } + + const getGainImage = (id: number, name?: string) => { + if (verifiedItemMap[id]) return `/${verifiedItemMap[id]}` + if (seedImageMap[id]) return `/shop_plants_organized/${seedImageMap[id]}` + if (name && nameImageMap[name]) return `/shop_plants_organized/${nameImageMap[name]}` + return undefined + } + + return ( +
+ { if (!open) closeUseDialog() }}> + + + 使用数量 + +
+ {useTargets.map(item => { + const key = `${item.id}-${item.uid}` + const value = useQuantities[key] ?? 1 + return ( +
+
+
{item.name}
+
拥有 {item.count}
+
+
+ updateUseQuantity(key, event.target.value, item.count)} + className="text-center" + /> +
+
+ ) + })} +
+
+ + +
+
+
+ + + + 获得物品 + +
+ {gains.map(gain => { + const imagePath = getGainImage(gain.id, gain.name) + return ( +
+ {imagePath ? ( +
+ {gain.name} +
+ ) : ( +
+ +
+ )} +
+
{gain.name}
+
x{gain.count}
+
+
+ ) + })} +
+
+ +
+
+
+
+

+ + + + 我的仓库 +

+
+ {message && ( + + {message} + + )} + + +
+
+ + { + setActiveTab(val) + setSelectedItems(new Set()) + setIsSelectionMode(false) + }}> + + + 种子 + + + 果实 + + + 道具 + + + + + + + + + + + + { + if (isSelectionMode) return + setSelectedItems(new Set()) + openUseDialogForItems([item]) + }} + seedImageMap={seedImageMap} + nameImageMap={nameImageMap} + verifiedItemMap={verifiedItemMap} + /> + + + + + {isSelectionMode && selectedItems.size > 0 && ( +
+ +
+ )} +
+ ) +} + +function ItemGrid({ + items, + color, + icon: Icon, + emptyMsg, + isSelectionMode, + selectedItems, + onToggle, + onItemClick, + seedImageMap, + nameImageMap, + verifiedItemMap +}: { + items: Item[], + color: string, + icon: ComponentType>, + emptyMsg: string, + isSelectionMode: boolean, + selectedItems: Set, + onToggle: (item: Item) => void, + onItemClick?: (item: Item) => void, + seedImageMap: Record, + nameImageMap: Record, + verifiedItemMap: Record +}) { + if (items.length === 0) { + return ( +
+ + {emptyMsg} +
+ ) + } + + const colorStyles = { + green: "bg-green-50 border-green-100 text-green-700", + red: "bg-red-50 border-red-100 text-red-700", + purple: "bg-purple-50 border-purple-100 text-purple-700", + } + + return ( +
+ {items.map(item => { + const key = `${item.id}-${item.uid}` + const isSelected = selectedItems.has(key) + const imagePath = item.type === 'seed' + ? seedImageMap[item.id] + ? `/shop_plants_organized/${seedImageMap[item.id]}` + : undefined + : item.type === 'produce' + ? nameImageMap[item.name] + ? `/shop_plants_organized/${nameImageMap[item.name]}` + : undefined + : item.type === 'other' + ? verifiedItemMap[item.id] + ? `/${verifiedItemMap[item.id]}` + : undefined + : undefined + + return ( +
{ + if (isSelectionMode) { + onToggle(item) + return + } + onItemClick?.(item) + }} + className={cn( + "relative group flex flex-col items-center p-4 rounded-2xl border transition-all duration-300", + colorStyles[color as keyof typeof colorStyles] || colorStyles.green, + isSelectionMode ? "cursor-pointer hover:shadow-md" : (onItemClick ? "cursor-pointer hover:shadow-md" : "cursor-default hover:scale-[1.02]"), + isSelected && "ring-2 ring-offset-2 ring-orange-400 bg-orange-50 border-orange-200" + )} + > + {isSelectionMode && ( +
+ {isSelected ? ( + + ) : ( + + )} +
+ )} + + {imagePath ? ( +
+ {item.name} +
+ ) : ( +
+ +
+ )} + {item.name} + x{item.count} +
+ ) + })} +
+ ) +} diff --git a/web/src/components/ui/avatar.tsx b/web/src/components/ui/avatar.tsx new file mode 100644 index 0000000..41249d7 --- /dev/null +++ b/web/src/components/ui/avatar.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Avatar.displayName = "Avatar" + +const AvatarImage = React.forwardRef< + HTMLImageElement, + React.ImgHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = "AvatarImage" + +const AvatarFallback = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AvatarFallback.displayName = "AvatarFallback" + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx new file mode 100644 index 0000000..1239422 --- /dev/null +++ b/web/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button } diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..9dbeaa0 --- /dev/null +++ b/web/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx new file mode 100644 index 0000000..5c17e74 --- /dev/null +++ b/web/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export type InputProps = React.InputHTMLAttributes + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/web/src/components/ui/scroll-area.tsx b/web/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..77bd0c4 --- /dev/null +++ b/web/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/web/src/components/ui/tabs.tsx b/web/src/components/ui/tabs.tsx new file mode 100644 index 0000000..0f4caeb --- /dev/null +++ b/web/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..3dcefa2 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,111 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@font-face { + font-family: 'MapleMono'; + src: url('./assets/fonts/MapleMono-NF-CN-Regular.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@layer base { + :root { + /* Base background - Very soft warm white */ + --background: 0 0% 100%; + /* Foreground - Soft Charcoal, not harsh black */ + --foreground: 0 0% 3.9%; + + /* Card background - White with slight transparency for glassmorphism */ + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + /* Primary - Soft Sky Blue (Pastel) */ + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + /* Secondary - Light Pink (Pastel) */ + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + /* Muted - Soft Lavender */ + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + /* Accent - Mint Green or Buttercream (used for highlights) */ + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + /* Destructive - Soft Coral/Salmon */ + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + + /* Ring - Matches Primary but slightly stronger */ + --ring: 0 0% 3.9%; + + --radius: 0.5rem; /* More rounded corners for softer feel */ --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground font-sans; + margin: 0; + } +} + + + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx new file mode 100644 index 0000000..bc43f37 --- /dev/null +++ b/web/src/pages/Dashboard.tsx @@ -0,0 +1,2339 @@ +import { useCallback, useEffect, useState, useRef } from "react" +import { useNavigate } from "react-router-dom" +import { StatusBar } from "@/components/StatusBar" +import { FarmGrid } from "@/components/FarmGrid" +import type { LandData } from "@/components/FarmGrid" +import { LogPanel } from "@/components/LogPanel" +import { ShopPanel } from "@/components/ShopPanel" +import { WarehousePanel } from "@/components/WarehousePanel" +import { SystemMonitor } from "@/components/SystemMonitor" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Cloud, Heart, Star, LogOut, QrCode, RefreshCw, Sprout, Settings, Trophy, ClipboardList, CheckCircle2, Circle, Shovel, Gift, Leaf, PawPrint, Sparkles, Package, Shield, Server, Cpu, Activity, Link } from "lucide-react" +import logo from "@/assets/logo.png" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +interface FarmStatus { + nickname: string + level: number + exp: number + nextLevelExp: number + gold: number + tickets: number + normalFertilizerHours: number + organicFertilizerHours: number + status: 'running' | 'stopped' | 'error' + avatarUrl?: string + freeMallClaimed?: boolean +} + +interface User { + email: string + hasCode: boolean + code?: string +} + +type SeedStrategy = 'default' | 'forceLowest' +type IdleStrategy = 'task' | 'exp' + +interface BotSettings { + farmIntervalSec: number + friendIntervalSec: number + seedStrategy: SeedStrategy + enableFriendOps: boolean + enableSteal: boolean + allowTicketFertilizerPurchase: boolean + enableNormalFertilize: boolean + enableOrganicFertilize: boolean + enableAutoSell: boolean + allowBuySeeds: boolean + allowRemove: boolean + idleStrategy: IdleStrategy +} + +interface UnluckyItem { + name: string + count: number +} + +interface AdminLogEntry { + id: string + email: string + tag: string + msg: string + type: string + time: number +} + +interface UpdateCommit { + sha: string + message: string + author: string + date: string +} + +interface UpdateRelease { + tag: string + name: string + date: string + body: string +} + +interface UpdateSummary { + local: { head: string; branch: string } + remote: { head: string } + updateNeeded: boolean + commits: UpdateCommit[] + releases: UpdateRelease[] + repo: string +} + +interface WorkerStats { + memory?: { + rss: number + heapTotal: number + heapUsed: number + } | null + uptime?: number + activeBots?: number +} + +interface SystemStats { + memory: { + rss: number + heapTotal: number + heapUsed: number + } + uptime: number + activeBots: number + worker?: WorkerStats | null +} + +type PaidMallCategory = 'daily' | 'organic' | 'fertilizer' | 'dog' + +interface PaidMallItem { + id: number + name: string + currency?: string + price?: number + discount?: string + items: { name: string; count: number }[] + category: PaidMallCategory +} + +const paidMallItems: PaidMallItem[] = [ + { + id: 1001, + name: "每日福利", + items: [{ name: "化肥(1小时)", count: 1 }], + category: 'daily' + }, + { + id: 1002, + name: "10小时有机化肥", + currency: "点券", + price: 42, + discount: "9.1折", + items: [ + { name: "有机化肥(1小时)", count: 2 }, + { name: "有机化肥(8小时)", count: 1 } + ], + category: 'organic' + }, + { + id: 1003, + name: "10小时化肥", + currency: "点券", + price: 34, + discount: "9.4折", + items: [ + { name: "化肥(1小时)", count: 2 }, + { name: "化肥(8小时)", count: 1 } + ], + category: 'fertilizer' + }, + { + id: 1004, + name: "20小时有机化肥", + currency: "钻石", + price: 80, + discount: "9.1折", + items: [ + { name: "有机化肥(8小时)", count: 1 }, + { name: "有机化肥(12小时)", count: 1 } + ], + category: 'organic' + }, + { + id: 1005, + name: "20小时化肥", + currency: "钻石", + price: 64, + discount: "9.4折", + items: [ + { name: "化肥(8小时)", count: 1 }, + { name: "化肥(12小时)", count: 1 } + ], + category: 'fertilizer' + }, + { + id: 1006, + name: "狗粮礼包", + currency: "点券", + price: 33, + items: [ + { name: "1天狗粮", count: 1 }, + { name: "3天狗粮", count: 1 }, + { name: "5天狗粮", count: 1 } + ], + category: 'dog' + }, + { + id: 1007, + name: "有机化肥(1小时)", + currency: "钻石", + price: 5, + items: [{ name: "有机化肥(1小时)", count: 1 }], + category: 'organic' + }, + { + id: 1008, + name: "有机化肥(4小时)", + currency: "钻石", + price: 19, + items: [{ name: "有机化肥(4小时)", count: 1 }], + category: 'organic' + }, + { + id: 1009, + name: "有机化肥(8小时)", + currency: "钻石", + price: 36, + items: [{ name: "有机化肥(8小时)", count: 1 }], + category: 'organic' + }, + { + id: 1010, + name: "有机化肥(12小时)", + currency: "钻石", + price: 52, + items: [{ name: "有机化肥(12小时)", count: 1 }], + category: 'organic' + }, + { + id: 1011, + name: "化肥(1小时)", + currency: "钻石", + price: 4, + items: [{ name: "化肥(1小时)", count: 1 }], + category: 'fertilizer' + }, + { + id: 1012, + name: "化肥(4小时)", + currency: "钻石", + price: 15, + items: [{ name: "化肥(4小时)", count: 1 }], + category: 'fertilizer' + }, + { + id: 1013, + name: "化肥(8小时)", + currency: "钻石", + price: 28, + items: [{ name: "化肥(8小时)", count: 1 }], + category: 'fertilizer' + }, + { + id: 1014, + name: "化肥(12小时)", + currency: "钻石", + price: 40, + items: [{ name: "化肥(12小时)", count: 1 }], + category: 'fertilizer' + }, + { + id: 1015, + name: "1天狗粮", + currency: "钻石", + price: 4, + items: [{ name: "1天狗粮", count: 1 }], + category: 'dog' + }, + { + id: 1016, + name: "3天狗粮", + currency: "钻石", + price: 11, + items: [{ name: "3天狗粮", count: 1 }], + category: 'dog' + }, + { + id: 1017, + name: "5天狗粮", + currency: "钻石", + price: 18, + items: [{ name: "5天狗粮", count: 1 }], + category: 'dog' + } +] + +function getPaidMallIcon(category: PaidMallCategory) { + if (category === 'daily') return + if (category === 'organic') return + if (category === 'dog') return + return +} + +interface TaskReward { + id: number + count: number +} + +type TaskCategory = 'growth' | 'daily' | 'normal' + +interface TaskItem { + id: number + desc: string + progress: number + totalProgress: number + isClaimed: boolean + isUnlocked: boolean + shareMultiple: number + rewards: TaskReward[] + category: TaskCategory +} + +function TaskPanel({ email }: { email: string }) { + const [tasks, setTasks] = useState([]) + const [loading, setLoading] = useState(false) + const [claimingId, setClaimingId] = useState(null) + const [showCompleted, setShowCompleted] = useState(false) + const [error, setError] = useState("") + const [lastClaim, setLastClaim] = useState<{ claimedCount: number; failed: number } | null>(null) + const inflightRef = useRef(false) + const claimedCacheRef = useRef>(new Map()) + const autoCompleteRef = useRef(false) + + const getTaskKey = useCallback((task: TaskItem) => { + return `${(task.desc || '').trim()}|${task.totalProgress}` + }, []) + + const mergeWithClaimedCache = useCallback((nextTasks: TaskItem[]) => { + const resultMap = new Map() + for (const task of nextTasks) { + const key = getTaskKey(task) + resultMap.set(key, task) + if (task.isClaimed) { + claimedCacheRef.current.set(key, task) + } + } + for (const [key, cached] of claimedCacheRef.current.entries()) { + const current = resultMap.get(key) + if (!current || (cached.isClaimed && !current.isClaimed)) { + resultMap.set(key, cached) + } + } + return Array.from(resultMap.values()) + }, [getTaskKey]) + + const fetchTasks = useCallback(async (withClaim: boolean) => { + if (!email) return + if (inflightRef.current) return + inflightRef.current = true + setLoading(true) + setError("") + try { + if (withClaim) { + const res = await fetch('/api/tasks/claim', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }) + if (!res.ok) throw new Error('claim failed') + const data = await res.json() + const list = Array.isArray(data.tasks) ? data.tasks : [] + setTasks(mergeWithClaimedCache(list)) + const failed = Array.isArray(data.failed) ? data.failed.length : 0 + setLastClaim({ claimedCount: Number(data.claimedCount) || 0, failed }) + } else { + const res = await fetch(`/api/tasks?email=${encodeURIComponent(email)}`) + if (!res.ok) throw new Error('fetch failed') + const data = await res.json() + const list = Array.isArray(data.tasks) ? data.tasks : [] + setTasks(mergeWithClaimedCache(list)) + } + } catch { + setError("任务获取失败") + } finally { + setLoading(false) + inflightRef.current = false + } + }, [email, mergeWithClaimedCache]) + + const claimSingle = useCallback(async (task: TaskItem) => { + if (!email) return + if (claimingId !== null) return + setClaimingId(task.id) + setError("") + try { + const res = await fetch('/api/tasks/claim-one', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, taskId: task.id, doShared: task.shareMultiple > 1 }) + }) + if (!res.ok) throw new Error('claim failed') + const data = await res.json() + const list = Array.isArray(data.tasks) ? data.tasks : [] + const claimedTask = { ...task, isClaimed: true } + claimedCacheRef.current.set(getTaskKey(claimedTask), claimedTask) + setTasks(mergeWithClaimedCache(list)) + const claimedCount = Number(data.claimedCount) || 0 + const failedCount = Number(data.failedCount) || 0 + if (claimedCount > 0 || failedCount > 0) { + setLastClaim({ claimedCount, failed: failedCount }) + } + } catch { + setError("任务完成失败") + } finally { + setClaimingId(null) + } + }, [claimingId, email, getTaskKey, mergeWithClaimedCache]) + + const autoCompleteNext = useCallback(() => { + if (autoCompleteRef.current) return + const nextTask = tasks.find((task) => { + if (task.isClaimed) return false + if (!task.isUnlocked) return false + if (task.totalProgress <= 0) return false + return task.progress >= task.totalProgress + }) + if (!nextTask) return + autoCompleteRef.current = true + claimSingle(nextTask).finally(() => { + autoCompleteRef.current = false + }) + }, [claimSingle, tasks]) + + useEffect(() => { + if (!email) return + fetchTasks(false) + const timer = setInterval(() => fetchTasks(false), 15000) + return () => clearInterval(timer) + }, [email, fetchTasks]) + + useEffect(() => { + autoCompleteNext() + }, [autoCompleteNext]) + + const completedCount = tasks.filter(task => task.isClaimed).length + const visibleTasks = showCompleted ? tasks : tasks.filter(task => !task.isClaimed) + const buckets: Record = { growth: [], daily: [], normal: [] } + for (const task of visibleTasks) { + if (task.category === 'growth' || task.category === 'daily' || task.category === 'normal') { + buckets[task.category].push(task) + } else { + buckets.normal.push(task) + } + } + + const categoryMeta: Array<{ key: TaskCategory; label: string; badge: string }> = [ + { key: 'growth', label: '成长', badge: 'bg-indigo-50 text-indigo-600 border-indigo-100' }, + { key: 'daily', label: '日常', badge: 'bg-emerald-50 text-emerald-600 border-emerald-100' }, + { key: 'normal', label: '普通', badge: 'bg-slate-50 text-slate-600 border-slate-200' } + ] + + return ( +
+
+
+
+ + 当前任务 +
+
+
+ + +
+
+ + {lastClaim && (lastClaim.claimedCount > 0 || lastClaim.failed > 0) && ( +
+ 已完成 {lastClaim.claimedCount} 个任务 {lastClaim.failed > 0 ? `,失败 ${lastClaim.failed} 个` : ''} +
+ )} + + {error && ( +
{error}
+ )} + + {visibleTasks.length === 0 && !loading ? ( +
暂无任务
+ ) : ( +
+ {categoryMeta.map(({ key, label, badge }) => { + const items = buckets[key] + if (items.length === 0) return null + return ( +
+
+ {label} + {items.length} 项 +
+
+ {items.map(task => { + const claimable = task.isUnlocked && !task.isClaimed && task.totalProgress > 0 && task.progress >= task.totalProgress + const progressText = task.totalProgress > 0 ? `${task.progress}/${task.totalProgress}` : "-" + const progressPercent = task.totalProgress > 0 ? Math.min(100, Math.round((task.progress / task.totalProgress) * 100)) : 0 + return ( +
+
+
+ {task.isClaimed ? ( + + ) : claimable ? ( + + ) : ( + + )} +
+
{task.desc || `任务#${task.id}`}
+
+ 进度 {progressText} + {task.shareMultiple > 1 ? ` · 分享翻倍 x${task.shareMultiple}` : ''} +
+
+
+
+ +
+ {task.isClaimed ? "已完成" : "进行中"} +
+
+
+
+
+
+
+ ) + })} +
+
+ ) + })} +
+ )} +
+ ) +} + +const defaultSettings: BotSettings = { + farmIntervalSec: 1, + friendIntervalSec: 10, + seedStrategy: 'default', + enableFriendOps: true, + enableSteal: true, + allowTicketFertilizerPurchase: false, + enableNormalFertilize: false, + enableOrganicFertilize: false, + enableAutoSell: true, + allowBuySeeds: true, + allowRemove: true, + idleStrategy: 'exp' +} + +export default function Dashboard() { + const navigate = useNavigate() + const [user, setUser] = useState(null) + const [farmStatus, setFarmStatus] = useState({ + nickname: "加载中...", + level: 1, + exp: 0, + nextLevelExp: 100, + gold: 0, + tickets: 0, + normalFertilizerHours: 0, + organicFertilizerHours: 0, + status: 'stopped', + freeMallClaimed: false + }) + const [warehouseRefreshKey, setWarehouseRefreshKey] = useState(0) + const [lands, setLands] = useState([]) + const [removeMode, setRemoveMode] = useState(false) + const [selectedLandIds, setSelectedLandIds] = useState([]) + const [isRemoving, setIsRemoving] = useState(false) + const [unlockingLandIds, setUnlockingLandIds] = useState([]) + const [showBindDialog, setShowBindDialog] = useState(false) + const [isStarting, setIsStarting] = useState(false) + const [settings, setSettings] = useState(defaultSettings) + const [isSavingSettings, setIsSavingSettings] = useState(false) + const [settingsHint, setSettingsHint] = useState("") + const [unluckyList, setUnluckyList] = useState([]) + const [announcementMessage, setAnnouncementMessage] = useState("") + const [announcementOpen, setAnnouncementOpen] = useState(false) + const unluckyLoadingRef = useRef(false) + const [activePage, setActivePage] = useState<'farm' | 'shop' | 'paymall' | 'settings' | 'task' | 'leaderboard' | 'update' | 'admin'>('farm') + const [navOpen, setNavOpen] = useState(false) + const [payMallDialogOpen, setPayMallDialogOpen] = useState(false) + const [payMallSelected, setPayMallSelected] = useState(null) + const [payMallCount, setPayMallCount] = useState(1) + const [payMallMessage, setPayMallMessage] = useState("") + const [unlockMessage, setUnlockMessage] = useState("") + const [payMallBuying, setPayMallBuying] = useState(false) + const isPayMallClaimed = payMallSelected?.id === 1001 && farmStatus.freeMallClaimed === true + const [isAdmin, setIsAdmin] = useState(false) + const [adminLogs, setAdminLogs] = useState([]) + const [adminLogsLoading, setAdminLogsLoading] = useState(false) + const [adminLogsError, setAdminLogsError] = useState("") + const [adminLimit, setAdminLimit] = useState(200) + const [adminFilterEmail, setAdminFilterEmail] = useState("") + const [adminStats, setAdminStats] = useState(null) + const [adminStatsLoading, setAdminStatsLoading] = useState(false) + const [adminActionMessage, setAdminActionMessage] = useState("") + const [adminAutoRefresh, setAdminAutoRefresh] = useState(true) + const [updateSummary, setUpdateSummary] = useState(null) + const [updateLoading, setUpdateLoading] = useState(false) + const [updateError, setUpdateError] = useState("") + const [updateRunning, setUpdateRunning] = useState(false) + const [updateOutput, setUpdateOutput] = useState("") + const [updateMessage, setUpdateMessage] = useState("") + + // QR Code States + const [qrCodeUrl, setQrCodeUrl] = useState("") + const [qrSig, setQrSig] = useState("") + const [qrStatus, setQrStatus] = useState("init") // init, waiting, scanned, success, expired + const [qrMsg, setQrMsg] = useState("") + + // Ref to track current status for stale closure prevention in setInterval + const statusRef = useRef(farmStatus.status) + + // Sync ref with state + useEffect(() => { + statusRef.current = farmStatus.status + }, [farmStatus.status]) + + useEffect(() => { + if (removeMode) return + if (selectedLandIds.length === 0) return + setSelectedLandIds([]) + }, [removeMode, selectedLandIds.length]) + + // Initialize user from local storage + useEffect(() => { + const storedUser = localStorage.getItem('farm_user') + if (!storedUser) { + navigate('/login') + return + } + const userData = JSON.parse(storedUser) + setUser(userData) + + // Check if user needs to bind farm + if (!userData.hasCode && !userData.code) { + setShowBindDialog(true) + } + }, [navigate]) + + const getStoredUser = useCallback(() => { + const storedUser = localStorage.getItem('farm_user') + if (!storedUser) return null + try { + const parsed = JSON.parse(storedUser) + if (parsed && typeof parsed.email === 'string') { + return parsed as User + } + } catch { + return null + } + return null + }, []) + + useEffect(() => { + if (!user) return + let cancelled = false + const seenKey = `farm_announcement_seen_${user.email || 'guest'}` + const loadAnnouncement = async () => { + try { + const res = await fetch('/api/announcement') + if (!res.ok) return + const data = await res.json() + const message = String(data?.message || '').trim() + if (!message) return + if (cancelled) return + setAnnouncementMessage(message) + if (!sessionStorage.getItem(seenKey)) { + setAnnouncementOpen(true) + sessionStorage.setItem(seenKey, '1') + } + } catch { + return + } + } + loadAnnouncement() + return () => { + cancelled = true + } + }, [user]) + + const normalizeSettings = useCallback((value: BotSettings): BotSettings => { + const farmIntervalSec = Math.max(1, Math.floor(Number(value.farmIntervalSec) || 1)) + const friendIntervalSec = Math.max(1, Math.floor(Number(value.friendIntervalSec) || 1)) + const seedStrategy: SeedStrategy = value.seedStrategy === 'forceLowest' ? 'forceLowest' : 'default' + const enableFriendOps = value.enableFriendOps !== false + const enableSteal = value.enableSteal !== false + const allowTicketFertilizerPurchase = value.allowTicketFertilizerPurchase === true + const enableNormalFertilize = value.enableNormalFertilize === true + const enableOrganicFertilize = value.enableOrganicFertilize === true + const enableAutoSell = value.enableAutoSell !== false + const allowBuySeeds = value.allowBuySeeds !== false + const allowRemove = value.allowRemove !== false + const idleStrategy: IdleStrategy = value.idleStrategy === 'task' ? 'task' : 'exp' + return { farmIntervalSec, friendIntervalSec, seedStrategy, enableFriendOps, enableSteal, allowTicketFertilizerPurchase, enableNormalFertilize, enableOrganicFertilize, enableAutoSell, allowBuySeeds, allowRemove, idleStrategy } + }, []) + + const fetchAdminStats = useCallback(async () => { + setAdminStatsLoading(true) + try { + const res = await fetch('/api/system/stats') + if (!res.ok) return + const data = await res.json() + setAdminStats(data) + } catch { + return + } finally { + setAdminStatsLoading(false) + } + }, []) + + const fetchAdminLogs = useCallback(async () => { + if (!user?.email) return + setAdminLogsLoading(true) + setAdminLogsError("") + try { + const params = new URLSearchParams() + if (adminFilterEmail.trim()) { + params.set('email', adminFilterEmail.trim()) + } + if (Number.isFinite(adminLimit) && adminLimit > 0) { + params.set('limit', String(adminLimit)) + } + const query = params.toString() + const url = query ? `/api/admin/logs?${query}` : '/api/admin/logs' + const res = await fetch(url, { + headers: { + 'x-admin-email': user.email + } + }) + const data = await res.json() + if (!res.ok) { + setAdminLogsError(String(data?.error || '加载失败')) + return + } + if (!Array.isArray(data?.logs)) { + setAdminLogs([]) + return + } + setAdminLogs(data.logs) + } catch { + setAdminLogsError("网络错误") + } finally { + setAdminLogsLoading(false) + } + }, [adminFilterEmail, adminLimit, user?.email]) + + const fetchUpdateSummary = useCallback(async () => { + if (!user?.email) return + setUpdateLoading(true) + setUpdateError("") + try { + const res = await fetch('/api/admin/update/summary', { + headers: { + 'x-admin-email': user.email + } + }) + const data = await res.json() + if (!res.ok) { + setUpdateError(String(data?.error || '加载失败')) + return + } + setUpdateSummary(data) + } catch { + setUpdateError("网络错误") + } finally { + setUpdateLoading(false) + } + }, [user?.email]) + + const handleRunUpdate = useCallback(async () => { + if (!user?.email) return + if (updateRunning) return + setUpdateRunning(true) + setUpdateMessage("") + setUpdateOutput("") + try { + const res = await fetch('/api/admin/update/run', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-admin-email': user.email + }, + body: JSON.stringify({ email: user.email }) + }) + const data = await res.json() + if (!res.ok) { + setUpdateMessage(String(data?.error || '更新失败')) + setUpdateOutput(String(data?.output || '')) + return + } + setUpdateMessage("更新完成") + setUpdateOutput(String(data?.output || '')) + fetchUpdateSummary() + } catch { + setUpdateMessage("网络错误") + } finally { + setUpdateRunning(false) + } + }, [fetchUpdateSummary, updateRunning, user?.email]) + + const handleAdminWorkerAction = useCallback(async (action: 'start' | 'stop-all' | 'shutdown') => { + if (!user?.email) return + setAdminActionMessage("") + try { + const res = await fetch(`/api/admin/worker/${action}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-admin-email': user.email + }, + body: JSON.stringify({ email: user.email }) + }) + const data = await res.json() + if (!res.ok) { + setAdminActionMessage(String(data?.error || '操作失败')) + return + } + setAdminActionMessage("操作已提交") + await fetchAdminStats() + } catch { + setAdminActionMessage("网络错误") + } + }, [fetchAdminStats, user?.email]) + + useEffect(() => { + if (!user?.email) return + let cancelled = false + const checkAdmin = async () => { + try { + const res = await fetch(`/api/admin/me?email=${encodeURIComponent(user.email)}`) + const data = await res.json() + if (cancelled) return + setIsAdmin(Boolean(data?.isAdmin)) + } catch { + if (cancelled) return + setIsAdmin(false) + } + } + checkAdmin() + return () => { + cancelled = true + } + }, [user?.email]) + + useEffect(() => { + if (!isAdmin && (activePage === 'admin' || activePage === 'update')) { + setActivePage('farm') + } + }, [activePage, isAdmin]) + + useEffect(() => { + if (!isAdmin) return + if (activePage !== 'admin') return + fetchAdminStats() + fetchAdminLogs() + if (!adminAutoRefresh) return + const interval = setInterval(() => { + fetchAdminStats() + fetchAdminLogs() + }, 5000) + return () => clearInterval(interval) + }, [activePage, adminAutoRefresh, fetchAdminLogs, fetchAdminStats, isAdmin]) + + useEffect(() => { + if (!isAdmin) return + if (activePage !== 'update') return + fetchUpdateSummary() + }, [activePage, fetchUpdateSummary, isAdmin]) + + const openPayMallDialog = useCallback((item: PaidMallItem) => { + setPayMallSelected(item) + setPayMallCount(1) + setPayMallMessage("") + setPayMallDialogOpen(true) + }, []) + + const handlePayMallBuy = useCallback(async () => { + if (!payMallSelected) return + if (!user?.email) { + setPayMallMessage("请先登录") + return + } + if (isPayMallClaimed) { + setPayMallMessage("今日已领取") + return + } + if (payMallBuying) return + setPayMallBuying(true) + setPayMallMessage("") + try { + const res = await fetch('/api/paymall/buy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: user.email, + itemId: payMallSelected.id, + count: payMallCount + }) + }) + const json = await res.json() + if (json.success) { + setPayMallMessage("购买成功") + if (payMallSelected.id === 1001) { + setFarmStatus(prev => ({ ...prev, freeMallClaimed: true })) + } + setTimeout(() => { + setPayMallDialogOpen(false) + }, 1200) + return + } + setPayMallMessage(`购买失败: ${json.error || '未知错误'}`) + } catch { + setPayMallMessage("网络错误") + } finally { + setPayMallBuying(false) + } + }, [isPayMallClaimed, payMallBuying, payMallCount, payMallSelected, user?.email]) + + useEffect(() => { + const storedSettings = localStorage.getItem('farm_settings') + if (!storedSettings) return + try { + const parsed = JSON.parse(storedSettings) + const next = normalizeSettings({ ...defaultSettings, ...parsed }) + setSettings(next) + } catch { + setSettings(defaultSettings) + } + }, [normalizeSettings]) + + const fetchStatus = useCallback(async () => { + if (!user) return + + try { + const res = await fetch(`/api/status?email=${user.email}`) + const data = await res.json() + + if (data.status === 'stopped') { + // Detect disconnect: if we were running but now stopped, prompt rebind + if (statusRef.current === 'running') { + setQrStatus('init') + setShowBindDialog(true) + } + setFarmStatus(prev => ({ ...prev, status: 'stopped' })) + } else { + setFarmStatus({ + nickname: data.user?.nickname || 'Farmer', + level: data.user?.level || 1, + exp: data.user?.exp || 0, + nextLevelExp: data.user?.nextLevelExp || 100, + gold: data.user?.gold || 0, + tickets: data.user?.tickets || 0, + normalFertilizerHours: data.user?.normalFertilizerHours || 0, + organicFertilizerHours: data.user?.organicFertilizerHours || 0, + status: 'running', + avatarUrl: data.user?.avatarUrl, + freeMallClaimed: data.user?.freeMallClaimed === true + }) + } + } catch (error) { + console.error("Failed to fetch status", error) + } + }, [user]) + + const fetchLands = useCallback(async () => { + if (!user || farmStatus.status !== 'running') return + + try { + const res = await fetch(`/api/lands?email=${user.email}`) + if (res.ok) { + const data = await res.json() + setLands(data.lands || []) + } + } catch (error) { + console.error("Failed to fetch lands", error) + } + }, [farmStatus.status, user]) + + const handleWarehouseRefresh = useCallback(() => { + setWarehouseRefreshKey(prev => prev + 1) + fetchStatus() + fetchLands() + }, [fetchLands, fetchStatus]) + + const handleLandClick = useCallback((land: LandData) => { + if (!removeMode) return + if (land.type !== 'planted') return + setSelectedLandIds(prev => { + if (prev.includes(land.id)) { + return prev.filter(id => id !== land.id) + } + return [...prev, land.id] + }) + }, [removeMode]) + + const handleRemoveSelected = useCallback(async () => { + if (!user) return + if (isRemoving) return + if (selectedLandIds.length === 0) return + setIsRemoving(true) + try { + const res = await fetch('/api/lands/remove', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: user.email, landIds: selectedLandIds }) + }) + if (!res.ok) throw new Error('remove failed') + await fetchLands() + setSelectedLandIds([]) + setRemoveMode(false) + } catch (error) { + console.error("Remove plants failed", error) + } finally { + setIsRemoving(false) + } + }, [fetchLands, isRemoving, selectedLandIds, user]) + + const handleUnlockLand = useCallback(async (land: LandData) => { + if (!user) return + if (unlockingLandIds.includes(land.id)) return + setUnlockingLandIds(prev => prev.includes(land.id) ? prev : [...prev, land.id]) + setUnlockMessage("") + try { + const res = await fetch('/api/lands/unlock', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: user.email, landId: land.id }) + }) + const data = await res.json().catch(() => null) + if (!res.ok) { + const message = data && data.error ? String(data.error) : '解锁失败' + setUnlockMessage(message) + return + } + await Promise.all([fetchLands(), fetchStatus()]) + } catch (error) { + setUnlockMessage("解锁失败") + console.error("Unlock land failed", error) + } finally { + setUnlockingLandIds(prev => prev.filter(id => id !== land.id)) + } + }, [fetchLands, fetchStatus, unlockingLandIds, user]) + + // QR Code Logic + const generateQRCode = useCallback(async () => { + setQrStatus("waiting") + setQrMsg("正在获取二维码...") + try { + const res = await fetch('/api/qr/create', { method: 'POST' }) + const data = await res.json() + if (data.success) { + setQrCodeUrl(data.qrcode) + setQrSig(data.qrsig) + setQrMsg("请使用手机QQ扫码") + } else { + setQrStatus("error") + setQrMsg("获取二维码失败") + } + } catch (error) { + console.error("Generate QR error", error) + setQrStatus("error") + setQrMsg("网络错误") + } + }, []) + + const handleBindSuccess = useCallback(async (code: string) => { + const storedUser = getStoredUser() + const email = user?.email || storedUser?.email || '' + if (!email) { + setQrMsg("登录信息未加载,请刷新页面重试") + setQrStatus('init') + return + } + if (!user && storedUser) { + setUser(storedUser) + } + setIsStarting(true) + + try { + const normalizedSettings = normalizeSettings(settings) + // Start bot + const res = await fetch('/api/bot/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, code, settings: normalizedSettings }) + }) + + const data = await res.json() + if (data.success) { + // Update local user state + const newUser = user ? { ...user, hasCode: true, code } : { email, hasCode: true, code } + setUser(newUser) + localStorage.setItem('farm_user', JSON.stringify(newUser)) + + setShowBindDialog(false) + fetchStatus() + } else { + setQrMsg(`启动失败: ${data.error}`) + } + } catch (error) { + console.error("Bind error", error) + setQrMsg("启动服务失败") + } finally { + setIsStarting(false) + } + }, [fetchStatus, getStoredUser, normalizeSettings, settings, user]) + + useEffect(() => { + let timer: ReturnType | undefined + if (showBindDialog && qrStatus === 'init') { + generateQRCode() + } + + if (showBindDialog && qrStatus === 'waiting' && qrSig) { + timer = setInterval(async () => { + try { + const res = await fetch('/api/qr/check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ qrsig: qrSig }) + }) + const data = await res.json() + + if (data.success) { + if (data.status === 'OK') { + setQrStatus('success') + setQrMsg('登录成功!正在绑定...') + clearInterval(timer) + + // Start bot with new code + await handleBindSuccess(data.code) + } else if (data.status === 'Used') { + setQrStatus('expired') + setQrMsg('二维码已失效,请刷新') + clearInterval(timer) + } + } + } catch (error) { + console.error("Check QR error", error) + } + }, 3000) + } + + return () => { + if (timer) clearInterval(timer) + } + }, [generateQRCode, handleBindSuccess, qrSig, qrStatus, showBindDialog]) + + useEffect(() => { + if (!user) return + + // Initial fetch + fetchStatus() + + // Poll status every 5 seconds + const statusTimer = setInterval(fetchStatus, 5000) + + return () => clearInterval(statusTimer) + }, [fetchStatus, user]) + + useEffect(() => { + if (farmStatus.status === 'running') { + fetchLands() + const landTimer = setInterval(fetchLands, 10000) + return () => clearInterval(landTimer) + } + }, [farmStatus.status, fetchLands]) + + const handleLogout = () => { + localStorage.removeItem('farm_user') + navigate('/login') + } + + const handleStartBot = async () => { + const storedUser = getStoredUser() + const email = user?.email || storedUser?.email || '' + if (!email) { + setQrMsg("登录信息未加载,请刷新页面重试") + return + } + if (!user && storedUser) { + setUser(storedUser) + } + setIsStarting(true) + try { + const normalizedSettings = normalizeSettings(settings) + const res = await fetch('/api/bot/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, settings: normalizedSettings }) + }) + const data = await res.json() + if (data.success) { + fetchStatus() + } else { + console.error(data.error || 'Failed to start bot') + } + } catch (e) { + console.error('Failed to start bot', e) + } finally { + setIsStarting(false) + } + } + + const handleSaveSettings = async () => { + if (isSavingSettings) return + setIsSavingSettings(true) + const normalizedSettings = normalizeSettings(settings) + setSettings(normalizedSettings) + localStorage.setItem('farm_settings', JSON.stringify(normalizedSettings)) + const storedUser = getStoredUser() + const email = user?.email || storedUser?.email || '' + let hint = "已保存,下次启动生效" + try { + if (email && farmStatus.status === 'running') { + const res = await fetch('/api/bot/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, settings: normalizedSettings }) + }) + const data = await res.json().catch(() => null) + if (res.ok && data?.success) { + hint = "已保存,已热更新" + } else { + hint = `已保存,热更新失败: ${data?.error || '未知错误'}` + } + } + } catch { + if (email && farmStatus.status === 'running') { + hint = "已保存,热更新失败" + } + } finally { + setSettingsHint(hint) + setTimeout(() => setSettingsHint(""), 2500) + setIsSavingSettings(false) + } + } + + const fetchUnlucky = useCallback(async () => { + if (!user) return + if (unluckyLoadingRef.current) return + unluckyLoadingRef.current = true + try { + const res = await fetch(`/api/leaderboard/unlucky?email=${encodeURIComponent(user.email)}`) + if (!res.ok) return + const data = await res.json() + setUnluckyList(Array.isArray(data.items) ? data.items : []) + } catch (error) { + console.error("Failed to fetch unlucky list", error) + } finally { + unluckyLoadingRef.current = false + } + }, [user]) + + useEffect(() => { + if (!user) return + fetchUnlucky() + const timer = setInterval(fetchUnlucky, 10000) + return () => clearInterval(timer) + }, [fetchUnlucky, user]) + + return ( +
+ {/* Decorative Background Elements */} +
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ Logo +
+

谢尔达莱群岛

+
+
+
+ + {navOpen && ( +
+ + + + + + + {isAdmin && ( + <> + + + + )} +
+ )} +
+ {farmStatus.status !== 'running' && user?.hasCode && ( + + )} + + +
+
+ + + {activePage === 'farm' && ( +
+
+
+
+

+ + 我的土地 +

+
+ + {removeMode && ( + + )} + +
+
+ {removeMode && ( +
+ 铲除模式已开启,点击已种植土地进行多选 +
+ )} + {unlockMessage && ( +
+ {unlockMessage} +
+ )} +
+ +
+
+ +
+
+

+ + 运行日志 +

+
+ +
+
+ +
+ )} + + {activePage === 'shop' && ( +
+
+
+

+ + 商店 +

+
+
+ +
+
+
+
+

+ + 仓库 +

+
+ +
+
+ )} + + {activePage === 'paymall' && ( +
+
+

+ + 付费商城 +

+
共 {paidMallItems.length} 件
+
+
+ {paidMallItems.map((item) => { + const isDailyFree = item.id === 1001 + const isClaimed = isDailyFree && farmStatus.freeMallClaimed === true + return ( +
+
+
+ {getPaidMallIcon(item.category)} +
+ {isClaimed ? ( + + 已领取 + + ) : item.discount && ( + + {item.discount} + + )} +
+
+
{item.name}
+
+ {item.items.map((sub) => ( + + {sub.name} x{sub.count} + + ))} +
+
+
+
+ {item.price ? `${item.price} ${item.currency || ''}` : "免费"} +
+ +
+
+ )})} +
+
+ )} + + + + + 购买 {payMallSelected?.name} + + {payMallSelected?.price ? `${payMallSelected.price} ${payMallSelected.currency || ''}` : '免费'} + + +
+
+ +
{payMallCount}
+ +
+
+ {payMallSelected?.price + ? `合计 ${payMallSelected.price * payMallCount} ${payMallSelected.currency || ''}` + : '合计 免费'} +
+ {payMallMessage && ( +
+ {payMallMessage} +
+ )} +
+
+ +
+
+
+ + {activePage === 'settings' && ( +
+
+

+ + 设置 +

+
+
+
+
+
+
自己农场巡查间隔
+
最小 1 秒
+
+ setSettings(prev => ({ ...prev, farmIntervalSec: Number(e.target.value) }))} + className="h-10 w-32 rounded-xl bg-white/70 border-white/60 focus:border-emerald-200 focus:ring-emerald-100 text-right" + /> +
+ +
+
+
好友巡查间隔
+
开启帮忙后生效
+
+ setSettings(prev => ({ ...prev, friendIntervalSec: Number(e.target.value) }))} + disabled={!settings.enableFriendOps} + className={`h-10 w-32 rounded-xl bg-white/70 border-white/60 focus:border-emerald-200 focus:ring-emerald-100 text-right ${settings.enableFriendOps ? "" : "opacity-60 cursor-not-allowed"}`} + /> +
+ +
+
+
是否帮忙(好友相关)
+
关闭后不进行好友操作
+
+ setSettings(prev => ({ ...prev, enableFriendOps: e.target.checked }))} + className="h-5 w-5 rounded border-slate-300 text-emerald-500 focus:ring-emerald-200" + /> +
+ +
+
+
是否偷菜
+
依赖帮忙开关
+
+ setSettings(prev => ({ ...prev, enableSteal: e.target.checked }))} + disabled={!settings.enableFriendOps} + className={`h-5 w-5 rounded border-slate-300 text-emerald-500 focus:ring-emerald-200 ${settings.enableFriendOps ? "" : "opacity-60 cursor-not-allowed"}`} + /> +
+ +
+
+
使用普通肥
+
自动使用普通肥料
+
+ setSettings(prev => ({ ...prev, enableNormalFertilize: e.target.checked }))} + className="h-5 w-5 rounded border-slate-300 text-emerald-500 focus:ring-emerald-200" + /> +
+ +
+
+
使用有机肥
+
自动使用有机肥料
+
+ setSettings(prev => ({ ...prev, enableOrganicFertilize: e.target.checked }))} + className="h-5 w-5 rounded border-slate-300 text-emerald-500 focus:ring-emerald-200" + /> +
+ +
+
+
允许点券购买肥料
+
默认不使用点券
+
+ setSettings(prev => ({ ...prev, allowTicketFertilizerPurchase: e.target.checked }))} + className="h-5 w-5 rounded border-slate-300 text-emerald-500 focus:ring-emerald-200" + /> +
+ +
+
+
种子策略
+
选择种植策略
+
+ +
+ +
+
+
挂机逻辑
+
任务或经验优先
+
+ +
+ +
+
+
自动出售
+
自动处理成熟作物
+
+ setSettings(prev => ({ ...prev, enableAutoSell: e.target.checked }))} + className="h-5 w-5 rounded border-slate-300 text-emerald-500 focus:ring-emerald-200" + /> +
+ +
+
+
仓库无种子自动购买
+
库存为 0 时自动购买
+
+ setSettings(prev => ({ ...prev, allowBuySeeds: e.target.checked }))} + className="h-5 w-5 rounded border-slate-300 text-emerald-500 focus:ring-emerald-200" + /> +
+ +
+
+
允许铲除
+
允许批量铲除作物,关闭后将无法正常铲除荒废的作物(默认开启)
+
+ setSettings(prev => ({ ...prev, allowRemove: e.target.checked }))} + className="h-5 w-5 rounded border-slate-300 text-emerald-500 focus:ring-emerald-200" + /> +
+ +
+
{settingsHint}
+ +
+
+ +
+
+
公告
+ 实时同步 +
+ {announcementMessage ? ( +
+ ) : ( +
暂无公告
+ )} +
+
+
+ )} + + {activePage === 'task' && ( +
+
+

+ + 任务 +

+
+ +
+ )} + + {activePage === 'leaderboard' && ( +
+
+

+ + 榜单 +

+ 已偷榜 +
+
+ {unluckyList.length === 0 ? ( +
暂无数据
+ ) : ( +
+ {unluckyList.slice(0, 10).map((item, index) => ( +
+
+
+ {index + 1} +
+ {item.name} +
+
+ + {item.count} 次 +
+
+ ))} +
+ )} +
+
+ )} + + {activePage === 'update' && ( +
+
+

+ + 更新中心 +

+
+ + 仅管理员可见 +
+
+
+
+
+
+
+ + 更新状态 +
+ +
+ {updateError && ( +
{updateError}
+ )} +
+
+ 本地版本 + {updateSummary?.local?.head ? updateSummary.local.head.slice(0, 7) : '-'} +
+
+ 远端版本 + {updateSummary?.remote?.head ? updateSummary.remote.head.slice(0, 7) : '-'} +
+
+ 当前分支 + {updateSummary?.local?.branch || '-'} +
+
+ 更新状态 + + {updateSummary?.updateNeeded ? "有更新" : "已最新"} + +
+
+ {updateSummary?.repo && ( + + + 仓库地址 + + )} +
+ +
+
+
+ + 版本更新日志 +
+
+ {updateSummary?.releases?.length ? ( +
+ {updateSummary.releases.map((release, index) => ( +
+
+
{release.name || release.tag}
+
{release.date || '-'}
+
+ {release.body ? ( +
{release.body}
+ ) : ( +
暂无描述
+ )} +
+ ))} +
+ ) : ( +
暂无更新日志
+ )} +
+ +
+
+
+ + 最近提交 +
+
+ {updateSummary?.commits?.length ? ( +
+ {updateSummary.commits.map((commit, index) => ( +
+
+
{commit.sha}
+
{commit.date || '-'}
+
+
{commit.message || '-'}
+
{commit.author || '-'}
+
+ ))} +
+ ) : ( +
暂无提交记录
+ )} +
+
+ +
+
+
+
+ + 可更新资源 +
+ {updateSummary?.updateNeeded ? "检测到更新" : "已最新"} +
+
+ {['服务端', 'Web 前端', 'Worker'].map((item) => ( +
+ {item} + + {updateSummary?.updateNeeded ? "可更新" : "已最新"} + +
+ ))} +
+
+ +
+
+
+ + 更新操作 +
+
{updateMessage}
+
+ + {updateOutput && ( +
+ {updateOutput} +
+ )} +
+
+
+
+ )} + + {activePage === 'admin' && ( +
+
+

+ + 管理员中心 +

+
+ + 仅管理员可见 +
+
+
+
+
+
+
+ + Worker 管理 +
+
{adminActionMessage}
+
+
+ + + +
+
+ +
+
+
+ + Worker 日志 +
+
+ setAdminFilterEmail(event.target.value)} + placeholder="过滤邮箱" + className="h-9 w-44 rounded-xl bg-white/70 border-white/60" + /> + setAdminLimit(Number(event.target.value) || 0)} + placeholder="数量" + className="h-9 w-24 rounded-xl bg-white/70 border-white/60" + type="number" + min={1} + max={500} + /> + + +
+
+ {adminLogsError && ( +
{adminLogsError}
+ )} + {!adminLogsError && adminLogs.length === 0 && !adminLogsLoading && ( +
暂无日志
+ )} + {adminLogs.length > 0 && ( +
+ {adminLogs.map((entry) => ( +
+
+
+ {entry.type} + {entry.tag} + {entry.email} +
+
{formatLogTime(entry.time)}
+
+
{entry.msg}
+
+ ))} +
+ )} +
+
+ +
+
+
+
+ + 系统状态 +
+ +
+
+
+ 主进程内存 + {adminStats ? `${adminStats.memory.rss} MB` : '-'} +
+
+ 主进程堆 + {adminStats ? `${adminStats.memory.heapUsed} MB` : '-'} +
+
+ 主进程运行 + {adminStats ? formatUptime(adminStats.uptime) : '-'} +
+
+
+ Worker 内存 + {adminStats?.worker?.memory ? `${adminStats.worker.memory.rss} MB` : '-'} +
+
+ Worker 堆 + {adminStats?.worker?.memory ? `${adminStats.worker.memory.heapUsed} MB` : '-'} +
+
+ Worker 运行 + {adminStats?.worker ? formatUptime(adminStats.worker.uptime || 0) : '-'} +
+
+ Worker Bot 数量 + {adminStats?.worker ? adminStats.worker.activeBots : '-'} +
+
+
+
+
+
+ )} +
+
+ + + + + 公告 +
+ +
+ +
+ +
+ + {/* Bind Farm Dialog */} + + + + 绑定 QQ 农场 + + 请使用手机 QQ 扫描下方二维码进行登录 + + + +
+ {qrStatus === 'waiting' && qrCodeUrl ? ( +
+ Scan QR Code +
+ 请扫码 +
+
+ ) : qrStatus === 'success' ? ( +
+
+ +
+ 扫码成功 +
+ ) : ( +
+ +
+ )} + +

+ {qrMsg || "等待操作..."} +

+
+
+
+ {isStarting && ( +
+
+ + 正在启动农场... +
+
+ )} +
+ ) +} + +function formatUptime(seconds: number) { + const safeSeconds = Number.isFinite(seconds) && seconds >= 0 ? Math.floor(seconds) : 0 + const h = Math.floor(safeSeconds / 3600) + const m = Math.floor((safeSeconds % 3600) / 60) + const s = safeSeconds % 60 + return `${h}h ${m}m ${s}s` +} + +function formatLogTime(time: number) { + const date = new Date(Number(time) || Date.now()) + if (Number.isNaN(date.getTime())) return '' + return date.toLocaleString('zh-CN', { hour12: false }) +} diff --git a/web/src/pages/Login.tsx b/web/src/pages/Login.tsx new file mode 100644 index 0000000..9ea5bbb --- /dev/null +++ b/web/src/pages/Login.tsx @@ -0,0 +1,238 @@ +import { useEffect, useState } from "react" +import { useNavigate } from "react-router-dom" +import { Heart, Star, Cloud, Loader2 } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import logo from "@/assets/logo.png" + +export default function Login() { + const [isRegister, setIsRegister] = useState(false) + const [showPasswordLogin, setShowPasswordLogin] = useState(false) + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState("") + const [oauthEnabled, setOauthEnabled] = useState(false) + const navigate = useNavigate() + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + const oauthEmail = params.get('email') + const oauthStatus = params.get('oauth') + if (oauthStatus === 'success' && oauthEmail) { + const loadProfile = async () => { + let hasCode = false + try { + const res = await fetch(`/api/auth/profile?email=${encodeURIComponent(oauthEmail)}`) + if (res.ok) { + const data = await res.json() + hasCode = Boolean(data?.user?.hasCode) + } + } catch { + hasCode = false + } + const user = { email: oauthEmail, auth_provider: 'authentik', hasCode } + localStorage.setItem('farm_user', JSON.stringify(user)) + localStorage.removeItem('farm_code') + navigate("/dashboard") + } + loadProfile() + return + } + if (params.get('error') === 'oauth_failed') { + setError("OAuth 登录失败,请重试") + } + }, [navigate]) + + useEffect(() => { + let cancelled = false + fetch('/api/auth/oauth/status') + .then((res) => res.json()) + .then((data) => { + if (!cancelled) setOauthEnabled(Boolean(data?.enabled)) + }) + .catch(() => { + if (!cancelled) setOauthEnabled(false) + }) + return () => { + cancelled = true + } + }, []) + + const handleOAuthLogin = () => { + window.location.href = '/api/auth/oauth/authentik'; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!email || !password) return + if (isRegister && password !== confirmPassword) { + setError("两次输入的密码不一致") + return + } + + setIsLoading(true) + setError("") + + try { + const endpoint = isRegister ? '/api/auth/register' : '/api/auth/login' + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || (isRegister ? '注册失败' : '登录失败')) + } + + // Save user info to localStorage + localStorage.setItem('farm_user', JSON.stringify(data.user)) + // Clear old code if exists + localStorage.removeItem('farm_code') + + navigate("/dashboard") + } catch (err) { + const message = err instanceof Error ? err.message : '出错了' + setError(message || '出错了') + } finally { + setIsLoading(false) + } + } + + return ( +
+ {/* Decorative Background Elements */} +
+ +
+
+ +
+
+ +
+ + +
+ + +
+ Logo +
+ + {isRegister ? '加入谢尔达莱群岛' : '谢尔达莱群岛'} + + + {isRegister ? '开启你的田园生活' : '欢迎回到你的温馨农场'}
+ 今天也要种出美好的事物哦 +
+
+ +
+ +
+ {oauthEnabled && ( + + )} + + + + {showPasswordLogin && ( +
+
+ setEmail(e.target.value)} + disabled={isLoading} + className="h-12 text-center text-lg bg-slate-50/50 border-slate-100 focus:border-blue-200 focus:ring-blue-100 rounded-2xl transition-all duration-300 hover:bg-white" + /> + setPassword(e.target.value)} + disabled={isLoading} + className="h-12 text-center text-lg bg-slate-50/50 border-slate-100 focus:border-blue-200 focus:ring-blue-100 rounded-2xl transition-all duration-300 hover:bg-white" + /> + {isRegister && ( + setConfirmPassword(e.target.value)} + disabled={isLoading} + className="h-12 text-center text-lg bg-slate-50/50 border-slate-100 focus:border-blue-200 focus:ring-blue-100 rounded-2xl transition-all duration-300 hover:bg-white animate-in slide-in-from-top-2 fade-in duration-300" + /> + )} +
+ {error && ( +
+ {error} +
+ )} + + +
+ {isRegister ? '已有账号?' : '还没有账号?'} + +
+
+ )} +
+
+ + +
+ +
+ 专为 Karriis 设计 🌸 +
+
+ ) +} diff --git a/web/tailwind.config.js b/web/tailwind.config.js new file mode 100644 index 0000000..9f5bc60 --- /dev/null +++ b/web/tailwind.config.js @@ -0,0 +1,79 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ["class"], + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + fontFamily: { + sans: [ + 'MapleMono', + 'ui-sans-serif', + 'system-ui', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ] + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + } + } + }, + plugins: [require("tailwindcss-animate")], + } diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..0a7e47d --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client", "node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Paths */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..fec8c8e --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..750ff2e --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,26 @@ +import path from "path" +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + '/socket.io': { + target: 'http://localhost:3001', + changeOrigin: true, + ws: true, + } + } + } +})