first commit
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -41,3 +41,4 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
/src/generated
|
||||
.zzap-session.json
|
||||
|
37
README.md
37
README.md
@ -9,16 +9,17 @@
|
||||
- ✅ GraphQL API
|
||||
- ✅ Современный UI с Shadcn/ui
|
||||
- ✅ Работа с базой данных через Prisma ORM
|
||||
- ✅ Скрипт «AI Gateway» (AI SDK)
|
||||
- 🔄 Интеграция с S3 для файлов (в разработке)
|
||||
|
||||
## Технологии
|
||||
|
||||
- **Frontend**: Next.js 15, React 19, TypeScript
|
||||
- **UI**: Tailwind CSS, Shadcn/ui, Radix UI
|
||||
- **Backend**: GraphQL (Apollo Server)
|
||||
- **Database**: PostgreSQL + Prisma ORM
|
||||
- **Forms**: React Hook Form + Zod validation
|
||||
- **Storage**: AWS S3 (планируется)
|
||||
- Frontend: Next.js 15, React 19, TypeScript
|
||||
- UI: Tailwind CSS, Shadcn/ui, Radix UI
|
||||
- Backend: GraphQL (Apollo Server)
|
||||
- Database: PostgreSQL + Prisma ORM
|
||||
- Forms: React Hook Form + Zod validation
|
||||
- Storage: AWS S3 (планируется)
|
||||
|
||||
## Установка и настройка
|
||||
|
||||
@ -65,7 +66,23 @@ npx prisma generate
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Откройте [http://localhost:3000](http://localhost:3000) в браузере.
|
||||
Откройте http://localhost:3000 в браузере.
|
||||
|
||||
### 5. AI Gateway (скрипт)
|
||||
|
||||
Для запуска скрипта AI Gateway нужен ключ в `.env`:
|
||||
|
||||
```env
|
||||
AI_GATEWAY_API_KEY="your_api_key_here"
|
||||
```
|
||||
|
||||
Запуск примера (TypeScript выполняется через `tsx`):
|
||||
|
||||
```bash
|
||||
npm run ai:gateway
|
||||
```
|
||||
|
||||
Скрипт: `scripts/gateway.ts` — использует `ai` SDK и печатает стрим-ответ, usage и причину завершения.
|
||||
|
||||
## Первый запуск
|
||||
|
||||
@ -137,10 +154,10 @@ npx prisma migrate dev --name [migration-name]
|
||||
## Тестирование
|
||||
|
||||
### Тест S3 хранилища
|
||||
Откройте [http://localhost:3000/test-s3](http://localhost:3000/test-s3) для тестирования загрузки файлов в S3.
|
||||
Откройте http://localhost:3000/test-s3 для тестирования загрузки файлов в S3.
|
||||
|
||||
### Тест GraphQL API
|
||||
Откройте [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql) для проверки GraphQL API.
|
||||
Откройте http://localhost:3000/api/graphql для проверки GraphQL API.
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
@ -149,4 +166,4 @@ npx prisma migrate dev --name [migration-name]
|
||||
- [ ] Добавить управление контентом
|
||||
- [ ] Создать систему ролей и разрешений
|
||||
- [ ] Добавить логирование и мониторинг
|
||||
# protekauto-cms
|
||||
# protek
|
||||
|
1222
package-lock.json
generated
1222
package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "protekauto-cms",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^1.3.24",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@apollo/experimental-nextjs-app-support": "^0.12.2",
|
||||
"@apollo/server": "^4.12.2",
|
||||
@ -23,6 +24,7 @@
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
@ -37,6 +39,7 @@
|
||||
"@types/pg": "^8.15.4",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"ai": "^3.4.33",
|
||||
"axios": "^1.10.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@ -81,10 +84,207 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"tsx": "^4.19.2",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/openai": {
|
||||
"version": "1.3.24",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.24.tgz",
|
||||
"integrity": "sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.1.3",
|
||||
"@ai-sdk/provider-utils": "2.2.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/openai/node_modules/@ai-sdk/provider": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz",
|
||||
"integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"json-schema": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/openai/node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz",
|
||||
"integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.1.3",
|
||||
"nanoid": "^3.3.8",
|
||||
"secure-json-parse": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider": {
|
||||
"version": "0.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
|
||||
"integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"json-schema": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
|
||||
"integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "0.0.26",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"nanoid": "^3.3.7",
|
||||
"secure-json-parse": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react": {
|
||||
"version": "0.0.70",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-0.0.70.tgz",
|
||||
"integrity": "sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"swr": "^2.2.5",
|
||||
"throttleit": "2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/solid": {
|
||||
"version": "0.0.54",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/solid/-/solid-0.0.54.tgz",
|
||||
"integrity": "sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.7.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"solid-js": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/svelte": {
|
||||
"version": "0.0.57",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/svelte/-/svelte-0.0.57.tgz",
|
||||
"integrity": "sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"sswr": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/ui-utils": {
|
||||
"version": "0.0.50",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-0.0.50.tgz",
|
||||
"integrity": "sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "0.0.26",
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"json-schema": "^0.4.0",
|
||||
"secure-json-parse": "^2.7.0",
|
||||
"zod-to-json-schema": "^3.23.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/vue": {
|
||||
"version": "0.0.59",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.59.tgz",
|
||||
"integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"swrv": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
@ -1408,6 +1608,16 @@
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
@ -1417,6 +1627,22 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
@ -1426,6 +1652,20 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
|
||||
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||
@ -1457,6 +1697,448 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
||||
"integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
|
||||
"integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
|
||||
"integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
|
||||
"integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
|
||||
"integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
|
||||
"integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
|
||||
"integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
|
||||
"integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
|
||||
"integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
|
||||
"integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
@ -2283,6 +2965,17 @@
|
||||
"@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==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"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",
|
||||
@ -2546,6 +3239,15 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@ -3322,6 +4024,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz",
|
||||
"integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@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.4",
|
||||
"@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-select": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
|
||||
@ -4400,6 +5133,16 @@
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
||||
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@ -4717,11 +5460,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/diff-match-patch": {
|
||||
"version": "1.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
|
||||
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/express": {
|
||||
@ -5465,6 +6213,115 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
|
||||
"integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@vue/shared": "3.5.18",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz",
|
||||
"integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.18",
|
||||
"@vue/shared": "3.5.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz",
|
||||
"integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@vue/compiler-core": "3.5.18",
|
||||
"@vue/compiler-dom": "3.5.18",
|
||||
"@vue/compiler-ssr": "3.5.18",
|
||||
"@vue/shared": "3.5.18",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.17",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz",
|
||||
"integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.18",
|
||||
"@vue/shared": "3.5.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz",
|
||||
"integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz",
|
||||
"integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.18",
|
||||
"@vue/shared": "3.5.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz",
|
||||
"integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.18",
|
||||
"@vue/runtime-core": "3.5.18",
|
||||
"@vue/shared": "3.5.18",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz",
|
||||
"integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.18",
|
||||
"@vue/shared": "3.5.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz",
|
||||
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@whatwg-node/promise-helpers": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz",
|
||||
@ -5569,7 +6426,6 @@
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@ -5606,6 +6462,54 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ai": {
|
||||
"version": "3.4.33",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-3.4.33.tgz",
|
||||
"integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "0.0.26",
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/react": "0.0.70",
|
||||
"@ai-sdk/solid": "0.0.54",
|
||||
"@ai-sdk/svelte": "0.0.57",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"@ai-sdk/vue": "0.0.59",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"eventsource-parser": "1.1.2",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsondiffpatch": "0.6.0",
|
||||
"secure-json-parse": "^2.7.0",
|
||||
"zod-to-json-schema": "^3.23.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openai": "^4.42.0",
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"sswr": "^2.1.0",
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openai": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"sswr": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@ -5669,7 +6573,6 @@
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -5938,7 +6841,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -6875,6 +7777,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@ -6912,6 +7823,12 @@
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
@ -7057,6 +7974,19 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
@ -7260,6 +8190,48 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
|
||||
"integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.9",
|
||||
"@esbuild/android-arm": "0.25.9",
|
||||
"@esbuild/android-arm64": "0.25.9",
|
||||
"@esbuild/android-x64": "0.25.9",
|
||||
"@esbuild/darwin-arm64": "0.25.9",
|
||||
"@esbuild/darwin-x64": "0.25.9",
|
||||
"@esbuild/freebsd-arm64": "0.25.9",
|
||||
"@esbuild/freebsd-x64": "0.25.9",
|
||||
"@esbuild/linux-arm": "0.25.9",
|
||||
"@esbuild/linux-arm64": "0.25.9",
|
||||
"@esbuild/linux-ia32": "0.25.9",
|
||||
"@esbuild/linux-loong64": "0.25.9",
|
||||
"@esbuild/linux-mips64el": "0.25.9",
|
||||
"@esbuild/linux-ppc64": "0.25.9",
|
||||
"@esbuild/linux-riscv64": "0.25.9",
|
||||
"@esbuild/linux-s390x": "0.25.9",
|
||||
"@esbuild/linux-x64": "0.25.9",
|
||||
"@esbuild/netbsd-arm64": "0.25.9",
|
||||
"@esbuild/netbsd-x64": "0.25.9",
|
||||
"@esbuild/openbsd-arm64": "0.25.9",
|
||||
"@esbuild/openbsd-x64": "0.25.9",
|
||||
"@esbuild/openharmony-arm64": "0.25.9",
|
||||
"@esbuild/sunos-x64": "0.25.9",
|
||||
"@esbuild/win32-arm64": "0.25.9",
|
||||
"@esbuild/win32-ia32": "0.25.9",
|
||||
"@esbuild/win32-x64": "0.25.9"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
@ -7671,6 +8643,13 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esm-env": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||
@ -7715,6 +8694,16 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
|
||||
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
}
|
||||
},
|
||||
"node_modules/esrecurse": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||
@ -7737,6 +8726,13 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
@ -7763,6 +8759,15 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
|
||||
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/exponential-backoff": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
|
||||
@ -8188,6 +9193,21 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@ -9034,6 +10054,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@ -9294,6 +10324,12 @@
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@ -9321,6 +10357,35 @@
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsondiffpatch": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
|
||||
"integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"chalk": "^5.3.0",
|
||||
"diff-match-patch": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"jsondiffpatch": "bin/jsondiffpatch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsondiffpatch/node_modules/chalk": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
|
||||
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
@ -9695,6 +10760,13 @@
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-character": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@ -11682,6 +12754,12 @@
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@ -12094,6 +13172,18 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sswr": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.2.0.tgz",
|
||||
"integrity": "sha512-clTszLPZkmycALTHD1mXGU+mOtA/MIoLgS1KGTTzFNVm9rytQVykgRaP+z1zl572cz0bTqj4rFVoC2N+IGK4Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"swrev": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stable-hash": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||
@ -12457,6 +13547,32 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.38.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.1.tgz",
|
||||
"integrity": "sha512-fO6CLDfJYWHgfo6lQwkQU2vhCiHc2MBl6s3vEhK+sSZru17YL4R5s1v14ndRpqKAIkq8nCz6MTk1yZbESZWeyQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"acorn": "^8.12.1",
|
||||
"aria-query": "^5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.1.0",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
"zimmerframe": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
@ -12467,6 +13583,34 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
|
||||
"integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swrev": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz",
|
||||
"integrity": "sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/swrv": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/swrv/-/swrv-1.1.0.tgz",
|
||||
"integrity": "sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.2.26 < 4"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-observable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
@ -12570,6 +13714,18 @@
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/throttleit": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
||||
"integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
@ -12690,6 +13846,26 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.4.tgz",
|
||||
"integrity": "sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ttf2woff2": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ttf2woff2/-/ttf2woff2-8.0.0.tgz",
|
||||
@ -13073,6 +14249,28 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz",
|
||||
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.18",
|
||||
"@vue/compiler-sfc": "3.5.18",
|
||||
"@vue/runtime-dom": "3.5.18",
|
||||
"@vue/server-renderer": "3.5.18",
|
||||
"@vue/shared": "3.5.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@ -13476,6 +14674,13 @@
|
||||
"zen-observable": "0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/zimmerframe": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
||||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.56",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.56.tgz",
|
||||
@ -13484,6 +14689,15 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.6",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
||||
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
"build": "prisma generate && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"ai:gateway": "tsx scripts/gateway.ts",
|
||||
"test-db": "node scripts/test-db.mjs",
|
||||
"db:push": "prisma db push",
|
||||
"db:generate": "prisma generate",
|
||||
@ -18,6 +19,7 @@
|
||||
"update:env": "bash scripts/update-env.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^1.3.24",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@apollo/experimental-nextjs-app-support": "^0.12.2",
|
||||
"@apollo/server": "^4.12.2",
|
||||
@ -33,6 +35,7 @@
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
@ -47,6 +50,7 @@
|
||||
"@types/pg": "^8.15.4",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"ai": "^3.4.33",
|
||||
"axios": "^1.10.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@ -91,6 +95,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"tsx": "^4.19.2",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
@ -418,6 +418,23 @@ model PartsSearchHistory {
|
||||
@@map("parts_search_history")
|
||||
}
|
||||
|
||||
// История запросов ZZAP со скриншотами
|
||||
model ZzapRequest {
|
||||
id String @id @default(cuid())
|
||||
provider String @default("zzap")
|
||||
article String
|
||||
statsUrl String?
|
||||
imageUrl String?
|
||||
ok Boolean @default(false)
|
||||
selector String?
|
||||
logs Json?
|
||||
requestedBy String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([article])
|
||||
@@map("zzap_requests")
|
||||
}
|
||||
|
||||
model ClientDeliveryAddress {
|
||||
id String @id @default(cuid())
|
||||
clientId String
|
||||
|
20
scripts/gateway.ts
Normal file
20
scripts/gateway.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { streamText } from 'ai'
|
||||
import 'dotenv/config'
|
||||
|
||||
async function main() {
|
||||
const result = streamText({
|
||||
model: 'openai/gpt-4.1',
|
||||
prompt: 'Invent a new holiday and describe its traditions.',
|
||||
})
|
||||
|
||||
for await (const textPart of result.textStream) {
|
||||
process.stdout.write(textPart)
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('Token usage:', await result.usage)
|
||||
console.log('Finish reason:', await result.finishReason)
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
6
src/app/admin/page.tsx
Normal file
6
src/app/admin/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function AdminRedirect() {
|
||||
redirect('/dashboard')
|
||||
}
|
||||
|
6
src/app/admin/zzap/page.tsx
Normal file
6
src/app/admin/zzap/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function AdminZzapRedirect() {
|
||||
redirect('/dashboard/zzap')
|
||||
}
|
||||
|
79
src/app/api/ai/chat/route.ts
Normal file
79
src/app/api/ai/chat/route.ts
Normal file
@ -0,0 +1,79 @@
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { messages } = await req.json();
|
||||
|
||||
const response = await fetch('https://gateway.ai.cloudflare.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.AI_GATEWAY_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) return;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6);
|
||||
if (data === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const content = parsed.choices?.[0]?.delta?.content;
|
||||
if (content) {
|
||||
controller.enqueue(encoder.encode(content));
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('AI Chat Error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Произошла ошибка при обработке запроса' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
71
src/app/api/zzap/history/route.ts
Normal file
71
src/app/api/zzap/history/route.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10) || 1)
|
||||
const pageSize = Math.min(100, Math.max(1, parseInt(searchParams.get('pageSize') || '20', 10) || 20))
|
||||
const q = (searchParams.get('q') || '').trim()
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
let items: any[] = []
|
||||
let total = 0
|
||||
if ((prisma as any).zzapRequest?.findMany) {
|
||||
const where = q ? { article: { contains: q, mode: 'insensitive' as const } } : undefined
|
||||
const [i, t] = await Promise.all([
|
||||
(prisma as any).zzapRequest.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
select: {
|
||||
id: true,
|
||||
provider: true,
|
||||
article: true,
|
||||
statsUrl: true,
|
||||
imageUrl: true,
|
||||
ok: true,
|
||||
selector: true,
|
||||
logs: true,
|
||||
createdAt: true,
|
||||
}
|
||||
}),
|
||||
(prisma as any).zzapRequest.count({ where })
|
||||
])
|
||||
items = i
|
||||
total = t
|
||||
} else {
|
||||
// Fallback via raw SQL if client not regenerated yet (Postgres quoted camelCase)
|
||||
let where = 'WHERE 1=1'
|
||||
if (q) {
|
||||
const esc = q.replace(/['\\]/g, (m) => ({"'":"''","\\":"\\\\"}[m] as string)).replace(/[%_]/g, (m) => '\\' + m)
|
||||
where += ` AND "article" ILIKE '%${esc}%' ESCAPE '\\'`
|
||||
}
|
||||
const rows = await prisma.$queryRawUnsafe<any[]>(
|
||||
`SELECT id, provider, article, "statsUrl" as "statsUrl", "imageUrl" as "imageUrl", ok, selector, logs, "createdAt" as "createdAt"
|
||||
FROM "zzap_requests"
|
||||
${where}
|
||||
ORDER BY "createdAt" DESC
|
||||
LIMIT ${pageSize} OFFSET ${skip}`
|
||||
)
|
||||
const cnt = await prisma.$queryRawUnsafe<any[]>(
|
||||
`SELECT COUNT(*)::int as count FROM "zzap_requests" ${where}`
|
||||
)
|
||||
items = rows
|
||||
total = cnt?.[0]?.count || 0
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ items, total, page, pageSize }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
||||
})
|
||||
} catch (err: any) {
|
||||
return new Response(JSON.stringify({ error: String(err?.message || err || 'Unknown error') }), {
|
||||
status: 500,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8' }
|
||||
})
|
||||
}
|
||||
}
|
666
src/app/api/zzap/screenshot/route.ts
Normal file
666
src/app/api/zzap/screenshot/route.ts
Normal file
@ -0,0 +1,666 @@
|
||||
import type { NextRequest } from 'next/server'
|
||||
import puppeteer from 'puppeteer'
|
||||
import { uploadBuffer } from '@/lib/s3'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const ZZAP_BASE = process.env.ZZAP_BASE || 'https://www.zzap.ru'
|
||||
const ZZAP_TIMEOUT_MS = Number(process.env.ZZAP_TIMEOUT_MS || 30000)
|
||||
const COOKIE_FILE = process.env.ZZAP_COOKIE_FILE || path.join(process.cwd(), '.zzap-session.json')
|
||||
const COOKIE_TTL_MIN = Number(process.env.ZZAP_SESSION_TTL_MINUTES || 180)
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function waitForAnySelector(page: any, selectors: string[], timeoutMs = 10000) {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
for (const sel of selectors) {
|
||||
try { if (await page.$(sel)) return sel } catch {}
|
||||
}
|
||||
await sleep(300)
|
||||
}
|
||||
return null as string | null
|
||||
}
|
||||
|
||||
async function setInputValue(page: any, selector: string, value: string) {
|
||||
const exists = await page.$(selector)
|
||||
if (!exists) return false
|
||||
try {
|
||||
await page.evaluate((sel, val) => {
|
||||
const el = document.querySelector(sel) as HTMLInputElement | null
|
||||
if (!el) return
|
||||
el.focus()
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
el.blur()
|
||||
}, selector, value)
|
||||
return true
|
||||
} catch {
|
||||
try {
|
||||
await page.click(selector, { clickCount: 3 })
|
||||
await page.type(selector, value, { delay: 20 })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function clickByText(page: any, text: string) {
|
||||
const handle = await page.evaluateHandle((t: string) => {
|
||||
const target = t.toLowerCase()
|
||||
const candidates = Array.from(document.querySelectorAll('button, a, input[type="submit"], span, div')) as HTMLElement[]
|
||||
for (const el of candidates) {
|
||||
const txt = (el.innerText || el.textContent || '').trim().toLowerCase()
|
||||
if (!txt) continue
|
||||
if (txt.includes(target)) return el
|
||||
}
|
||||
return null
|
||||
}, text)
|
||||
try {
|
||||
const el = (handle as any).asElement?.()
|
||||
if (el) {
|
||||
await el.click()
|
||||
return true
|
||||
}
|
||||
} catch {}
|
||||
try { await (handle as any).dispose?.() } catch {}
|
||||
return false
|
||||
}
|
||||
|
||||
async function findLargestElementHandle(page: any, selectors: string[]) {
|
||||
for (const sel of selectors) {
|
||||
const handles = await page.$$(sel)
|
||||
if (handles.length) {
|
||||
let best = handles[0]
|
||||
let bestArea = 0
|
||||
for (const h of handles) {
|
||||
const box = await h.boundingBox()
|
||||
const area = box ? box.width * box.height : 0
|
||||
if (area > bestArea) {
|
||||
best = h
|
||||
bestArea = area
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function persistHistorySafely(
|
||||
data: { article: string; statsUrl: string | null; imageUrl?: string; ok: boolean; selector?: string | null; logs?: any },
|
||||
log: (m: string) => void
|
||||
) {
|
||||
try {
|
||||
if ((prisma as any).zzapRequest?.create) {
|
||||
await (prisma as any).zzapRequest.create({
|
||||
data: {
|
||||
provider: 'zzap',
|
||||
article: data.article,
|
||||
statsUrl: data.statsUrl || undefined,
|
||||
imageUrl: data.imageUrl || undefined,
|
||||
ok: data.ok,
|
||||
selector: data.selector || undefined,
|
||||
logs: data.logs ?? undefined
|
||||
}
|
||||
})
|
||||
log('DB: request persisted')
|
||||
} else {
|
||||
const esc = (v: any) => (v == null ? 'NULL' : `'${String(v).replace(/'/g, "''")}'`)
|
||||
const logsJson = data.logs ? `'${JSON.stringify(data.logs).replace(/'/g, "''")}'::jsonb` : 'NULL'
|
||||
const sql = `INSERT INTO "zzap_requests" ("provider","article","statsUrl","imageUrl","ok","selector","logs") VALUES ('zzap', ${esc(data.article)}, ${esc(data.statsUrl)}, ${esc(data.imageUrl)}, ${data.ok ? 'true' : 'false'}, ${esc(data.selector)}, ${logsJson})`
|
||||
await prisma.$executeRawUnsafe(sql)
|
||||
log('DB: request persisted (raw)')
|
||||
}
|
||||
} catch (e: any) {
|
||||
log(`DB error: ${e?.message || e}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreSession(page: any, log: (m: string) => void) {
|
||||
try {
|
||||
if (!fs.existsSync(COOKIE_FILE)) return false
|
||||
const raw = fs.readFileSync(COOKIE_FILE, 'utf-8')
|
||||
const data = JSON.parse(raw) as { cookies: any[]; savedAt: number }
|
||||
if (!data?.cookies?.length || !data?.savedAt) return false
|
||||
const ageMin = (Date.now() - data.savedAt) / 60000
|
||||
if (ageMin > COOKIE_TTL_MIN) { log(`Session expired: ${ageMin.toFixed(1)}min > ${COOKIE_TTL_MIN}min`); return false }
|
||||
await page.setCookie(...data.cookies)
|
||||
log('Session cookies restored')
|
||||
return true
|
||||
} catch (e) {
|
||||
log(`Restore session error: ${String((e as any)?.message || e)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSession(page: any, log: (m: string) => void) {
|
||||
try {
|
||||
const cookies = await page.cookies()
|
||||
const payload = { cookies, savedAt: Date.now() }
|
||||
fs.writeFileSync(COOKIE_FILE, JSON.stringify(payload))
|
||||
log(`Session cookies saved (${cookies.length})`)
|
||||
} catch (e) {
|
||||
log(`Save session error: ${String((e as any)?.message || e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const article = searchParams.get('article')?.trim()
|
||||
const explicitSelector = searchParams.get('selector')?.trim()
|
||||
const debug = searchParams.get('debug') === '1'
|
||||
if (!article) {
|
||||
return new Response(JSON.stringify({ error: 'Не передан артикул ?article=' }), { status: 400, headers: { 'content-type': 'application/json; charset=utf-8' } })
|
||||
}
|
||||
|
||||
const email = process.env.ZZAP_EMAIL
|
||||
const password = process.env.ZZAP_PASSWORD
|
||||
if (!email || !password) {
|
||||
return new Response(JSON.stringify({ error: 'Отсутствуют ZZAP_EMAIL/ZZAP_PASSWORD в .env' }), { status: 500, headers: { 'content-type': 'application/json; charset=utf-8' } })
|
||||
}
|
||||
|
||||
const logs: string[] = []
|
||||
const log = (m: string) => { logs.push(m) }
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
})
|
||||
const page = await browser.newPage()
|
||||
let workPage: any = page
|
||||
await page.setViewport({ width: 1440, height: 900 })
|
||||
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36')
|
||||
await page.setExtraHTTPHeaders({ 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7' })
|
||||
try { page.setDefaultNavigationTimeout?.(ZZAP_TIMEOUT_MS); page.setDefaultTimeout?.(ZZAP_TIMEOUT_MS) } catch {}
|
||||
|
||||
// 0) Try restore session first, verify, otherwise login
|
||||
let loggedInEarly = false
|
||||
await restoreSession(page, log)
|
||||
try {
|
||||
await page.goto(ZZAP_BASE, { waitUntil: 'domcontentloaded', timeout: 60000 })
|
||||
const byDom = await page.evaluate(() => {
|
||||
const byId = !!document.querySelector('#ctl00_lnkLogout')
|
||||
const byText = Array.from(document.querySelectorAll('a')).some(a => /выход|выйти|logout|logoff/i.test((a.textContent||'').trim()))
|
||||
return byId || byText
|
||||
}).catch(() => false)
|
||||
loggedInEarly = Boolean(byDom)
|
||||
log(`Restored session check: loggedIn=${loggedInEarly}`)
|
||||
} catch {}
|
||||
|
||||
// 0b) Explicit login if not logged yet
|
||||
try {
|
||||
if (!loggedInEarly) {
|
||||
await page.goto(`${ZZAP_BASE}/user/logon.aspx`, { waitUntil: 'domcontentloaded', timeout: Math.min(20000, ZZAP_TIMEOUT_MS) })
|
||||
log(`Open login: ${page.url()}`)
|
||||
} else {
|
||||
log('Skip login: already authenticated')
|
||||
}
|
||||
|
||||
// DevExpress (ZZap) stable selectors by id suffix / full name
|
||||
const devxEmail = 'input[id$="AddrEmail1TextBox_I"]'
|
||||
const devxPass = 'input[id$="PasswordTextBox_I"]'
|
||||
const devxEmailName = 'input[name="ctl00$BodyPlace$LogonFormCallbackPanel$LogonFormLayout$AddrEmail1TextBox"]'
|
||||
const devxPassName = 'input[name="ctl00$BodyPlace$LogonFormCallbackPanel$LogonFormLayout$PasswordTextBox"]'
|
||||
const devxEmailFull = '#ctl00_BodyPlace_LogonFormCallbackPanel_LogonFormLayout_AddrEmail1TextBox_I'
|
||||
const devxPassFull = '#ctl00_BodyPlace_LogonFormCallbackPanel_LogonFormLayout_PasswordTextBox_I'
|
||||
|
||||
// Prefer known DevExpress IDs first, then legacy ASP.NET, then generic
|
||||
const userSelPriority = [devxEmailFull, devxEmail, devxEmailName, '#ctl00_ContentPlaceHolder1_Login1_UserName', '#ctl00_ContentPlaceHolder1_tbLogin', 'input[name*="login" i]', 'input[type="email"]', 'input[name*="email" i]']
|
||||
const passSelPriority = [devxPassFull, devxPass, devxPassName, '#ctl00_ContentPlaceHolder1_Login1_Password', '#ctl00_ContentPlaceHolder1_tbPass', 'input[type="password"]', 'input[name*="pass" i]']
|
||||
const submitSelPriority = ['#ctl00_ContentPlaceHolder1_Login1_LoginButton', '#ctl00_ContentPlaceHolder1_btnLogin', 'button[type="submit" i]', 'input[type="submit" i]']
|
||||
|
||||
// Wait a moment for anti-bot/DevExpress to initialize
|
||||
if (!loggedInEarly) await sleep(2000)
|
||||
|
||||
// Wait for any input to appear explicitly
|
||||
const appeared = await waitForAnySelector(page, [devxEmailFull, devxPassFull, devxEmail, devxPass, devxEmailName, devxPassName], 10000)
|
||||
if (!appeared) log('Login inputs still not present after wait')
|
||||
|
||||
// Try waiting explicitly for DevExpress fields
|
||||
// Resolve selectors to strings (avoid handle click issues)
|
||||
const emailSelectors = [devxEmailFull, devxEmail, devxEmailName, '#ctl00_ContentPlaceHolder1_Login1_UserName', '#ctl00_ContentPlaceHolder1_tbLogin', 'input[name*="login" i]', 'input[type="email"]', 'input[name*="email" i]']
|
||||
const passSelectors = [devxPassFull, devxPass, devxPassName, '#ctl00_ContentPlaceHolder1_Login1_Password', '#ctl00_ContentPlaceHolder1_tbPass', 'input[type="password"]', 'input[name*="pass" i]']
|
||||
|
||||
let emailSelUsed: string | null = null
|
||||
for (const sel of emailSelectors) { if (await setInputValue(page, sel, email)) { emailSelUsed = sel; break } }
|
||||
// No XPath fallback to avoid $x in older runtimes
|
||||
|
||||
let passSelUsed: string | null = null
|
||||
for (const sel of passSelectors) { if (await setInputValue(page, sel, password)) { passSelUsed = sel; break } }
|
||||
// No XPath fallback to avoid $x in older runtimes
|
||||
|
||||
if (!loggedInEarly && emailSelUsed && passSelUsed) {
|
||||
log(`Login using emailSel=${emailSelUsed}, passSel=${passSelUsed}`)
|
||||
|
||||
// Helper to detect login without relying only on nav
|
||||
const checkLoggedIn = async () => {
|
||||
const url = page.url()
|
||||
if (!/logon\.aspx/i.test(url)) return true
|
||||
const byDom = await page.evaluate(() => {
|
||||
const byId = !!document.querySelector('#ctl00_lnkLogout')
|
||||
const byText = Array.from(document.querySelectorAll('a')).some(a => /выход|выйти|logout|logoff/i.test((a.textContent||'').trim()))
|
||||
return byId || byText
|
||||
}).catch(() => false)
|
||||
return byDom
|
||||
}
|
||||
|
||||
const waitStep = async (label: string) => {
|
||||
await sleep(1500)
|
||||
const ok = await checkLoggedIn()
|
||||
log(`${label} -> loggedIn=${ok}, url=${page.url()}`)
|
||||
return ok
|
||||
}
|
||||
|
||||
let loggedIn = false
|
||||
|
||||
// 1) Press Enter on password
|
||||
try { await page.focus(passSelUsed); await page.keyboard.press('Enter') ; } catch {}
|
||||
loggedIn = await waitStep('After Enter')
|
||||
|
||||
// 2) Click submit via known selectors
|
||||
if (!loggedIn) {
|
||||
for (const sel of submitSelPriority) {
|
||||
try {
|
||||
const el = await page.$(sel)
|
||||
if (el) {
|
||||
await el.click().catch(() => {})
|
||||
if (await waitStep(`After click ${sel}`)) { loggedIn = true; break }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Click any descendant with text "Войти" inside login panel
|
||||
if (!loggedIn) {
|
||||
try {
|
||||
const did = await page.evaluate(() => {
|
||||
const root = document.querySelector('#ctl00_BodyPlace_LogonFormCallbackPanel') || document.body
|
||||
if (!root) return false
|
||||
const nodes = Array.from(root.querySelectorAll('button, a, span, div, input[type="submit"]')) as HTMLElement[]
|
||||
const lc = 'войти'
|
||||
for (const el of nodes) {
|
||||
const txt = (el.innerText || el.textContent || '').trim().toLowerCase()
|
||||
if (!txt) continue
|
||||
if (txt.includes(lc)) { (el as HTMLElement).click(); return true }
|
||||
}
|
||||
return false
|
||||
})
|
||||
if (did) loggedIn = await waitStep('After panel text click')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 4) Try submitting the form directly
|
||||
if (!loggedIn) {
|
||||
try { await page.evaluate(() => { (document.querySelector('form') as HTMLFormElement | null)?.submit() }) } catch {}
|
||||
loggedIn = await waitStep('After form.submit()')
|
||||
}
|
||||
|
||||
log(`Login success=${loggedIn}`)
|
||||
if (loggedIn) { await saveSession(page, log) }
|
||||
} else {
|
||||
if (!loggedInEarly) log('Login inputs not found; continuing')
|
||||
}
|
||||
|
||||
const cookies = await page.cookies()
|
||||
log(`Cookies: ${cookies.map(c=>c.name).join(',')}`)
|
||||
} catch (e: any) {
|
||||
log(`Login step error: ${e?.message || e}`)
|
||||
}
|
||||
|
||||
// 1) Open homepage
|
||||
await page.goto(ZZAP_BASE, { waitUntil: 'domcontentloaded', timeout: Math.min(15000, ZZAP_TIMEOUT_MS) })
|
||||
log(`Open: ${page.url()}`)
|
||||
|
||||
// Cookie banners common accept
|
||||
await clickByText(page, 'Соглас').catch(() => {})
|
||||
await clickByText(page, 'Принять').catch(() => {})
|
||||
await clickByText(page, 'Хорошо').catch(() => {})
|
||||
|
||||
// 2) Try to open login
|
||||
const loginCandidates = ['a[href*="login" i]', 'a[href*="signin" i]', 'button[name="login" i]']
|
||||
let openedLogin = false
|
||||
for (const sel of loginCandidates) {
|
||||
const el = await page.$(sel)
|
||||
if (el) {
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}),
|
||||
el.click()
|
||||
])
|
||||
openedLogin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!openedLogin) {
|
||||
await clickByText(page, 'войти').catch(() => {})
|
||||
}
|
||||
log(`Login page: ${page.url()}`)
|
||||
|
||||
// 3) Fill credentials
|
||||
// Try DevExpress selectors first on whatever login UI is visible
|
||||
const emailSel = ['#ctl00_BodyPlace_LogonFormCallbackPanel_LogonFormLayout_AddrEmail1TextBox_I', 'input[id$="AddrEmail1TextBox_I"]', 'input[name="ctl00$BodyPlace$LogonFormCallbackPanel$LogonFormLayout$AddrEmail1TextBox"]', 'input[type="email"]', 'input[name="email" i]', 'input[name*="login" i]']
|
||||
const passSel = ['#ctl00_BodyPlace_LogonFormCallbackPanel_LogonFormLayout_PasswordTextBox_I', 'input[id$="PasswordTextBox_I"]', 'input[name="ctl00$BodyPlace$LogonFormCallbackPanel$LogonFormLayout$PasswordTextBox"]', 'input[type="password"]', 'input[name="password" i]']
|
||||
let emailInput = null
|
||||
for (const sel of emailSel) { emailInput = await page.$(sel); if (emailInput) break }
|
||||
let passInput = null
|
||||
for (const sel of passSel) { passInput = await page.$(sel); if (passInput) break }
|
||||
|
||||
if (emailInput && passInput) {
|
||||
await emailInput.click({ clickCount: 3 }).catch(() => {})
|
||||
await emailInput.type(email, { delay: 20 })
|
||||
await passInput.type(password, { delay: 20 })
|
||||
const submitSel = ['button[type="submit" i]', 'input[type="submit" i]']
|
||||
let clicked = false
|
||||
for (const sel of submitSel) {
|
||||
const el = await page.$(sel)
|
||||
if (el) {
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 60000 }).catch(() => {}),
|
||||
el.click()
|
||||
])
|
||||
clicked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!clicked) {
|
||||
await clickByText(page, 'войти').catch(() => {})
|
||||
await sleep(1500)
|
||||
}
|
||||
}
|
||||
log(`After login: ${page.url()}`)
|
||||
|
||||
// 4) Navigate to search by article (try a few patterns)
|
||||
const searchUrls = [
|
||||
`${ZZAP_BASE}/public/search.aspx#rawdata=${encodeURIComponent(article)}`,
|
||||
`${ZZAP_BASE}/search/?article=${encodeURIComponent(article)}`,
|
||||
`${ZZAP_BASE}/search?article=${encodeURIComponent(article)}`,
|
||||
`${ZZAP_BASE}/search?txt=${encodeURIComponent(article)}`,
|
||||
`${ZZAP_BASE}/search?query=${encodeURIComponent(article)}`,
|
||||
`${ZZAP_BASE}/catalog/?q=${encodeURIComponent(article)}`
|
||||
]
|
||||
|
||||
let reached = false
|
||||
for (const url of searchUrls) {
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: Math.min(15000, ZZAP_TIMEOUT_MS) })
|
||||
log(`Search try: ${url} -> ${page.url()}`)
|
||||
reached = true
|
||||
break
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!reached) {
|
||||
// fallback: try search input on homepage
|
||||
await page.goto(ZZAP_BASE, { waitUntil: 'domcontentloaded', timeout: Math.min(15000, ZZAP_TIMEOUT_MS) })
|
||||
const inputCandidates = ['input[type="search"]', 'input[name*="search" i]', 'input[placeholder*="артик" i]']
|
||||
let searchInput = null
|
||||
for (const sel of inputCandidates) { searchInput = await page.$(sel); if (searchInput) break }
|
||||
if (!searchInput) throw new Error('Не найдено поле поиска')
|
||||
await searchInput.type(article, { delay: 30 })
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: Math.min(15000, ZZAP_TIMEOUT_MS) }).catch(() => {}),
|
||||
page.keyboard.press('Enter')
|
||||
])
|
||||
}
|
||||
log(`Search results: ${page.url()}`)
|
||||
|
||||
// 4b) Try to open stats via explicit anchor present in the grid
|
||||
try {
|
||||
const statLinkSel = 'a[id$="_StatHyperlink"], a[onclick*="statpartpricehistory.aspx" i]'
|
||||
const statLink = await page.waitForSelector(statLinkSel, { timeout: Math.min(8000, ZZAP_TIMEOUT_MS) }).catch(() => null)
|
||||
if (statLink) {
|
||||
const rel = await statLink.evaluate((el: HTMLAnchorElement) => {
|
||||
const href = el.getAttribute('href') || ''
|
||||
const onclick = el.getAttribute('onclick') || ''
|
||||
const rx = /['"]([^'\"]*statpartpricehistory\.aspx[^'\"]*)['"]/i
|
||||
const m = onclick.match(rx)
|
||||
const candidate = m ? m[1] : (href && href.includes('statpartpricehistory') ? href : null)
|
||||
return candidate ? candidate.replace(/&/g, '&') : null
|
||||
})
|
||||
if (rel) {
|
||||
const targetUrl = rel.startsWith('http') ? rel : `${ZZAP_BASE}${rel}`
|
||||
const statsPage = await browser.newPage()
|
||||
await statsPage.setViewport({ width: 1440, height: 900 })
|
||||
await statsPage.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36')
|
||||
await statsPage.setExtraHTTPHeaders({ 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7' })
|
||||
const cookies = await page.cookies()
|
||||
await statsPage.setCookie(...cookies)
|
||||
await statsPage.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: Math.min(15000, ZZAP_TIMEOUT_MS) })
|
||||
workPage = statsPage
|
||||
log(`Stats page opened via anchor onclick: ${targetUrl}`)
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// If we already opened stats page directly, skip modal logic
|
||||
const onStatsPage = () => /\/user\/statpartpricehistory\.aspx/i.test(workPage.url?.() || '')
|
||||
let statsOpened = onStatsPage()
|
||||
if (statsOpened) {
|
||||
log(`Stats page already open: ${workPage.url?.()}`)
|
||||
}
|
||||
// 5) Open statistics (open modal, extract iframe src, load it as page) or new page
|
||||
|
||||
// Attempt 0: scrape any statpartpricehistory URL from page HTML (onclick/inline)
|
||||
try {
|
||||
const rel = await page.evaluate(() => {
|
||||
const html = document.documentElement?.outerHTML || ''
|
||||
const m = html.match(/(https?:\/\/[^"'<> ]+)?(\/user\/statpartpricehistory\.aspx[^"'<> ]*)/i)
|
||||
if (!m) return null
|
||||
const full = m[1] ? `${m[1]}${m[2]}` : m[2]
|
||||
return full.replace(/&/g, '&')
|
||||
})
|
||||
if (rel) {
|
||||
const targetUrl = rel.startsWith('http') ? rel : `${ZZAP_BASE}${rel}`
|
||||
const statsPage = await browser.newPage()
|
||||
await statsPage.setViewport({ width: 1440, height: 900 })
|
||||
await statsPage.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36')
|
||||
await statsPage.setExtraHTTPHeaders({ 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7' })
|
||||
const cookies = await page.cookies()
|
||||
await statsPage.setCookie(...cookies)
|
||||
await statsPage.goto(targetUrl, { waitUntil: 'networkidle0', timeout: 60000 })
|
||||
workPage = statsPage
|
||||
statsOpened = true
|
||||
log(`Stats page opened via HTML scrape: ${targetUrl}`)
|
||||
}
|
||||
} catch {}
|
||||
if (!statsOpened) {
|
||||
await clickByText(page, 'статистика').catch(() => {})
|
||||
try {
|
||||
const clickedByTitle = await page.evaluate(() => {
|
||||
const cand = document.querySelector('[title*="статист" i], [alt*="статист" i]') as HTMLElement | null
|
||||
if (cand) { cand.click(); return true }
|
||||
return false
|
||||
})
|
||||
if (clickedByTitle) { log('Clicked stats by title/alt attribute') }
|
||||
} catch {}
|
||||
await sleep(500)
|
||||
}
|
||||
// Look for iframe anywhere, not only inside DevExpress container
|
||||
let modalIframeHandle = statsOpened ? null : await page.waitForSelector('iframe[src*="statpartpricehistory.aspx"]', { timeout: Math.min(4000, ZZAP_TIMEOUT_MS) }).catch(() => null)
|
||||
if (!modalIframeHandle) {
|
||||
// one more short wait and retry
|
||||
if (!statsOpened) {
|
||||
await sleep(300)
|
||||
modalIframeHandle = await page.waitForSelector('iframe[src*="statpartpricehistory.aspx"]', { timeout: Math.min(2500, ZZAP_TIMEOUT_MS) }).catch(() => null)
|
||||
}
|
||||
}
|
||||
if (modalIframeHandle) {
|
||||
statsOpened = true
|
||||
try {
|
||||
const src: string | null = await modalIframeHandle.evaluate((el: HTMLIFrameElement) => el.getAttribute('src'))
|
||||
if (src) {
|
||||
const targetUrl = src.startsWith('http') ? src : `${ZZAP_BASE}${src}`
|
||||
const statsPage = await browser.newPage()
|
||||
await statsPage.setViewport({ width: 1440, height: 900 })
|
||||
await statsPage.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36')
|
||||
await statsPage.setExtraHTTPHeaders({ 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7' })
|
||||
// Reuse session cookies
|
||||
const cookies = await page.cookies()
|
||||
await statsPage.setCookie(...cookies)
|
||||
await statsPage.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: Math.min(15000, ZZAP_TIMEOUT_MS) })
|
||||
workPage = statsPage
|
||||
log(`Stats page opened via iframe src: ${targetUrl}`)
|
||||
// We won't use the iframe anymore
|
||||
modalIframeHandle = null
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (!statsOpened || workPage === page) {
|
||||
// Fallback: direct link on page to statpartpricehistory
|
||||
try {
|
||||
const rel = await page.evaluate(() => {
|
||||
const a = document.querySelector('a[href*="statpartpricehistory.aspx"]') as HTMLAnchorElement | null
|
||||
return a?.getAttribute('href') || null
|
||||
})
|
||||
if (rel) {
|
||||
const targetUrl = rel.startsWith('http') ? rel : `${ZZAP_BASE}${rel}`
|
||||
const statsPage = await browser.newPage()
|
||||
await statsPage.setViewport({ width: 1440, height: 900 })
|
||||
await statsPage.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36')
|
||||
await statsPage.setExtraHTTPHeaders({ 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7' })
|
||||
const cookies = await page.cookies()
|
||||
await statsPage.setCookie(...cookies)
|
||||
await statsPage.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: Math.min(15000, ZZAP_TIMEOUT_MS) })
|
||||
workPage = statsPage
|
||||
statsOpened = true
|
||||
log(`Stats page opened via anchor href: ${targetUrl}`)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (!statsOpened || workPage === page) {
|
||||
// Fallback: explicit selectors that might open new page
|
||||
const statsSelectors = ['a[href*="statpartpricehistory" i]', 'a[href*="stat" i]', 'button[href*="stat" i]']
|
||||
for (const sel of statsSelectors) {
|
||||
const el = await page.$(sel)
|
||||
if (el) {
|
||||
const targetCreated = new Promise<any>((resolve) => {
|
||||
const handler = async (target: any) => {
|
||||
const newPage = await target.page().catch(() => null)
|
||||
if (newPage) {
|
||||
browser.off('targetcreated', handler)
|
||||
resolve(newPage)
|
||||
}
|
||||
}
|
||||
browser.on('targetcreated', handler)
|
||||
})
|
||||
await el.click().catch(() => {})
|
||||
const maybeNewPage: any = await Promise.race([
|
||||
targetCreated,
|
||||
(async () => { await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: Math.min(8000, ZZAP_TIMEOUT_MS) }).catch(() => {}) ; return null })()
|
||||
])
|
||||
if (maybeNewPage) {
|
||||
workPage = maybeNewPage
|
||||
await sleep(1200)
|
||||
}
|
||||
statsOpened = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
log(`Stats page: ${workPage.url?.() || page.url()}`)
|
||||
|
||||
// Early bailout: if we couldn't navigate away from search page quickly, return its screenshot
|
||||
try {
|
||||
const urlNow = workPage.url?.() || page.url()
|
||||
if (/\/public\/search\.aspx/i.test(urlNow)) {
|
||||
log('Bailout: still on search page, returning current page screenshot')
|
||||
const buf = (await page.screenshot({ fullPage: true, type: 'png' })) as Buffer
|
||||
// Save to S3 + DB history
|
||||
try {
|
||||
const key = `zzap/${encodeURIComponent(article)}/${Date.now()}-search.png`
|
||||
const up = await uploadBuffer(buf, key, 'image/png')
|
||||
logs.push(`Uploaded to S3: ${up.url}`)
|
||||
await persistHistorySafely({ article, statsUrl: urlNow, imageUrl: up.url, ok: false, selector: null, logs }, log)
|
||||
} catch (e: any) {
|
||||
logs.push(`Persist error (bailout): ${e?.message || e}`)
|
||||
}
|
||||
await browser.close().catch(() => {})
|
||||
if (debug) {
|
||||
return new Response(JSON.stringify({ ok: true, url: urlNow, foundSelector: null, logs }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8' }
|
||||
})
|
||||
}
|
||||
return new Response(buf, { status: 200, headers: { 'content-type': 'image/png', 'cache-control': 'no-store' } })
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// 6) Wait a bit for charts to render (short)
|
||||
if (onStatsPage()) {
|
||||
try { await (workPage.waitForSelector?.('.highcharts-container', { timeout: Math.min(3000, ZZAP_TIMEOUT_MS) })) } catch {}
|
||||
await sleep(300)
|
||||
}
|
||||
|
||||
// 7) Capture element screenshot
|
||||
let handle = null as any
|
||||
// No modal any more – we open stats full page; try in current/new page context
|
||||
if (explicitSelector) {
|
||||
handle = await workPage.$(explicitSelector)
|
||||
}
|
||||
if (!handle) {
|
||||
handle = await findLargestElementHandle(workPage, [
|
||||
'.highcharts-container',
|
||||
'canvas',
|
||||
'svg',
|
||||
'[id*="chart" i]',
|
||||
'[class*="chart" i]'
|
||||
])
|
||||
}
|
||||
|
||||
let imageBuffer: Buffer
|
||||
let foundSelector: string | null = null
|
||||
if (!onStatsPage() && handle) {
|
||||
try { await handle.evaluate((el: any) => el.scrollIntoView({ behavior: 'instant', block: 'center' })) } catch {}
|
||||
await sleep(300)
|
||||
foundSelector = explicitSelector || 'auto'
|
||||
imageBuffer = (await handle.screenshot({ type: 'png' })) as Buffer
|
||||
} else {
|
||||
// Full-page screenshot (stats page often has 3 charts; capture all)
|
||||
imageBuffer = (await workPage.screenshot({ fullPage: true, type: 'png' })) as Buffer
|
||||
}
|
||||
|
||||
// Upload to S3 and persist history
|
||||
let uploadedUrl: string | undefined
|
||||
try {
|
||||
const key = `zzap/${encodeURIComponent(article)}/${Date.now()}.png`
|
||||
const up = await uploadBuffer(imageBuffer, key, 'image/png')
|
||||
uploadedUrl = up.url
|
||||
logs.push(`Uploaded to S3: ${up.url}`)
|
||||
} catch (e: any) {
|
||||
logs.push(`S3 upload error: ${e?.message || e}`)
|
||||
}
|
||||
await persistHistorySafely({ article, statsUrl: workPage.url?.() || page.url(), imageUrl: uploadedUrl, ok: true, selector: foundSelector, logs }, log)
|
||||
|
||||
await browser.close().catch(() => {})
|
||||
|
||||
if (debug) {
|
||||
return new Response(JSON.stringify({ ok: true, url: workPage.url?.() || page.url(), foundSelector, imageUrl: uploadedUrl, logs }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8' }
|
||||
})
|
||||
}
|
||||
|
||||
return new Response(imageBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'image/png',
|
||||
'cache-control': 'no-store'
|
||||
}
|
||||
})
|
||||
} catch (err: any) {
|
||||
const errorBody = { error: String(err?.message || err || 'Unknown error'), logs }
|
||||
return new Response(JSON.stringify(errorBody), {
|
||||
status: 500,
|
||||
headers: { 'content-type': 'application/json; charset=utf-8' }
|
||||
})
|
||||
}
|
||||
}
|
125
src/app/dashboard/ai/page.tsx
Normal file
125
src/app/dashboard/ai/page.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useChat } from 'ai/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Bot, Send, User } from 'lucide-react';
|
||||
|
||||
export default function AIChat() {
|
||||
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
|
||||
api: '/api/ai/chat',
|
||||
});
|
||||
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollAreaRef.current) {
|
||||
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl mx-auto py-6">
|
||||
<Card className="h-[calc(100vh-8rem)] flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Bot className="h-5 w-5" />
|
||||
Чат с ИИ
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Задайте любой вопрос искусственному интеллекту
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-full pr-4" ref={scrollAreaRef}>
|
||||
{messages.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<Bot className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">Добро пожаловать!</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Начните разговор с ИИ, задав свой первый вопрос
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex gap-3 ${
|
||||
message.role === 'user' ? 'justify-end' : 'justify-start'
|
||||
}`}
|
||||
>
|
||||
{message.role === 'assistant' && (
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback>
|
||||
<Bot className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`rounded-lg px-4 py-2 max-w-[80%] ${
|
||||
message.role === 'user'
|
||||
? 'bg-primary text-primary-foreground ml-auto'
|
||||
: 'bg-muted text-foreground'
|
||||
}`}
|
||||
>
|
||||
<div className="whitespace-pre-wrap text-sm">
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.role === 'user' && (
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback>
|
||||
<User className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex gap-3 justify-start">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback>
|
||||
<Bot className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="bg-muted rounded-lg px-4 py-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-pulse" />
|
||||
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-pulse delay-100" />
|
||||
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-pulse delay-200" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<form onSubmit={handleSubmit} className="flex gap-2 w-full">
|
||||
<Input
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Введите ваш вопрос..."
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="submit" disabled={isLoading || !input.trim()} size="icon">
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</form>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
182
src/app/dashboard/zzap/page.tsx
Normal file
182
src/app/dashboard/zzap/page.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { format } from 'date-fns'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
export default function ZzapStatsPage() {
|
||||
const [article, setArticle] = useState('')
|
||||
const [selector, setSelector] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [imgSrc, setImgSrc] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [debug, setDebug] = useState(false)
|
||||
const [debugInfo, setDebugInfo] = useState<any>(null)
|
||||
const [history, setHistory] = useState<any[]>([])
|
||||
const [historyLoading, setHistoryLoading] = useState(false)
|
||||
const [historyError, setHistoryError] = useState<string | null>(null)
|
||||
const [historyPage, setHistoryPage] = useState(1)
|
||||
const [historyPageSize] = useState(20)
|
||||
const [historyTotal, setHistoryTotal] = useState(0)
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const loadHistory = useCallback(async () => {
|
||||
try {
|
||||
setHistoryLoading(true)
|
||||
setHistoryError(null)
|
||||
const params = new URLSearchParams()
|
||||
params.set('page', String(historyPage))
|
||||
params.set('pageSize', String(historyPageSize))
|
||||
if (query.trim()) params.set('q', query.trim())
|
||||
const res = await fetch(`/api/zzap/history?${params.toString()}`, { cache: 'no-store' })
|
||||
if (!res.ok) throw new Error(`History error ${res.status}`)
|
||||
const data = await res.json()
|
||||
setHistory(data.items || [])
|
||||
setHistoryTotal(data.total || 0)
|
||||
} catch (e: any) {
|
||||
setHistoryError(e?.message || 'Не удалось получить историю')
|
||||
} finally {
|
||||
setHistoryLoading(false)
|
||||
}
|
||||
}, [historyPage, historyPageSize, query])
|
||||
|
||||
// initial load
|
||||
useEffect(() => { loadHistory() }, [loadHistory])
|
||||
|
||||
const onSubmit = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setImgSrc(null)
|
||||
try {
|
||||
const params = new URLSearchParams({ article })
|
||||
if (selector) params.set('selector', selector)
|
||||
if (debug) params.set('debug', '1')
|
||||
const res = await fetch(`/api/zzap/screenshot?${params.toString()}`)
|
||||
const ct = res.headers.get('content-type') || ''
|
||||
if (res.ok && ct.includes('image/png')) {
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
setImgSrc(url)
|
||||
setDebugInfo(null)
|
||||
loadHistory()
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
if (debug) {
|
||||
setDebugInfo(data)
|
||||
throw new Error(data?.error || `Ошибка ${res.status}`)
|
||||
}
|
||||
throw new Error(data?.error || `Ошибка ${res.status}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Не удалось получить скриншот')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [article, selector, debug])
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Card className="max-w-3xl">
|
||||
<CardHeader>
|
||||
<CardTitle>ZZAP: скриншот графика статистики</CardTitle>
|
||||
<CardDescription>
|
||||
Введите артикул, сервис авторизуется на zzap.ru и вернёт PNG скриншот графика.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<form onSubmit={onSubmit} className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="grid gap-2 sm:col-span-1">
|
||||
<Label htmlFor="article">Артикул</Label>
|
||||
<Input id="article" value={article} onChange={(e) => setArticle(e.target.value)} placeholder="например, 06A145710P" required />
|
||||
</div>
|
||||
<div className="grid gap-2 sm:col-span-1">
|
||||
<Label htmlFor="selector">CSS селектор (опционально)</Label>
|
||||
<Input id="selector" value={selector} onChange={(e) => setSelector(e.target.value)} placeholder="например, .chart-container" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:col-span-2">
|
||||
<input id="debug" type="checkbox" checked={debug} onChange={(e) => setDebug(e.target.checked)} />
|
||||
<Label htmlFor="debug">Режим отладки (вернуть детали вместо PNG)</Label>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Button type="submit" disabled={loading || !article}>
|
||||
{loading ? 'Получаю…' : 'Получить скрин графика'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
{imgSrc && (
|
||||
<div className="mt-2">
|
||||
<h3 className="mb-2 font-medium">Результат</h3>
|
||||
<img src={imgSrc} alt="Скриншот графика ZZAP" className="max-w-full border rounded-md" />
|
||||
</div>
|
||||
)}
|
||||
{debugInfo && (
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs bg-muted p-3 rounded-md overflow-auto max-h-[50vh]">
|
||||
{JSON.stringify(debugInfo, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>История запросов</CardTitle>
|
||||
<CardDescription>Последние 20 запросов ZZAP. Кликните по ссылке, чтобы открыть изображение.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Input placeholder="Поиск по артикулу" value={query} onChange={(e) => { setQuery(e.target.value); setHistoryPage(1); }} className="max-w-xs" />
|
||||
<Button variant="outline" onClick={() => { setHistoryPage(1); loadHistory() }} disabled={historyLoading}>Найти</Button>
|
||||
</div>
|
||||
{historyError && <p className="text-sm text-red-600">{historyError}</p>}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Дата</TableHead>
|
||||
<TableHead>Артикул</TableHead>
|
||||
<TableHead>Статус</TableHead>
|
||||
<TableHead>URL статистики</TableHead>
|
||||
<TableHead>Изображение</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{historyLoading && (
|
||||
<TableRow><TableCell colSpan={5}>Загрузка…</TableCell></TableRow>
|
||||
)}
|
||||
{!historyLoading && history.length === 0 && (
|
||||
<TableRow><TableCell colSpan={5}>Пусто</TableCell></TableRow>
|
||||
)}
|
||||
{history.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.createdAt ? format(new Date(item.createdAt), 'dd.MM.yyyy HH:mm') : '—'}</TableCell>
|
||||
<TableCell>{item.article}</TableCell>
|
||||
<TableCell>{item.ok ? 'OK' : 'ERR'}</TableCell>
|
||||
<TableCell className="max-w-[280px] truncate">
|
||||
{item.statsUrl ? <a className="text-blue-600 underline" href={item.statsUrl} target="_blank" rel="noreferrer">{item.statsUrl}</a> : '—'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.imageUrl ? <a className="text-blue-600 underline" href={item.imageUrl} target="_blank" rel="noreferrer">Открыть</a> : '—'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<div className="text-sm text-muted-foreground">Всего: {historyTotal}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setHistoryPage((p) => Math.max(1, p - 1))} disabled={historyPage === 1 || historyLoading}>Назад</Button>
|
||||
<div className="text-sm">Стр. {historyPage} / {Math.max(1, Math.ceil(historyTotal / historyPageSize))}</div>
|
||||
<Button variant="outline" size="sm" onClick={() => setHistoryPage((p) => p + 1)} disabled={historyPage >= Math.ceil(historyTotal / historyPageSize) || historyLoading}>Вперёд</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
48
src/components/ui/scroll-area.tsx
Normal file
48
src/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
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<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
@ -16,7 +16,9 @@ import {
|
||||
Palette,
|
||||
Star,
|
||||
Image,
|
||||
Shield
|
||||
BarChart3,
|
||||
Shield,
|
||||
Bot
|
||||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useAuth } from '@/components/providers/AuthProvider'
|
||||
@ -31,6 +33,11 @@ const navigationItems = [
|
||||
href: '/dashboard',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
title: 'ZZAP статистика',
|
||||
href: '/dashboard/zzap',
|
||||
icon: BarChart3,
|
||||
},
|
||||
{
|
||||
title: 'Каталог',
|
||||
href: '/dashboard/catalog',
|
||||
@ -41,6 +48,11 @@ const navigationItems = [
|
||||
href: '/dashboard/kraja',
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
title: 'Чат с ИИ',
|
||||
href: '/dashboard/ai',
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
title: 'Навигация сайта',
|
||||
href: '/dashboard/navigation',
|
||||
|
@ -17,11 +17,16 @@ export const createToken = (payload: JWTPayload): string => {
|
||||
|
||||
// Верификация JWT токена
|
||||
export const verifyToken = (token: string): JWTPayload | null => {
|
||||
// Быстрый фильтр: клиентские токены и не-JWT (без двух точек) не проверяем
|
||||
if (!token || token.startsWith('client_') || token.split('.').length !== 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload
|
||||
return decoded
|
||||
} catch (error) {
|
||||
console.error('Ошибка верификации токена:', error)
|
||||
} catch (_error) {
|
||||
// Токен выглядел как JWT, но не прошёл проверку — тихо возвращаем null без шума в логах
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -2412,7 +2412,7 @@ export const resolvers = {
|
||||
console.log(`🌐 Найдено ${externalOffers.length} предложений в AutoEuro`)
|
||||
console.log('📦 Первые 3 внешних предложения:', externalOffers.slice(0, 3))
|
||||
|
||||
// 3. Поиск в PartsIndex для получения дополнительных характеристик и изображений
|
||||
// 3. Поиск в PartsIndex для получения дополнительных характеристик и изображений (может быть отключён)
|
||||
let partsIndexData: any = null
|
||||
|
||||
try {
|
||||
@ -2421,11 +2421,14 @@ export const resolvers = {
|
||||
brand: cleanBrand
|
||||
})
|
||||
|
||||
// Используем прямой поиск по артикулу и бренду
|
||||
// Используем прямой поиск по артикулу и бренду, только если сервис включён
|
||||
const partsIndexEnabled = (process.env.PARTSINDEX_ENABLED === 'true') || false
|
||||
if (partsIndexEnabled) {
|
||||
partsIndexData = await partsIndexService.searchEntityByCode(
|
||||
cleanArticleNumber,
|
||||
cleanBrand
|
||||
)
|
||||
}
|
||||
|
||||
if (partsIndexData) {
|
||||
console.log('✅ GraphQL Resolver - найден товар в PartsIndex:', {
|
||||
@ -3969,10 +3972,13 @@ export const resolvers = {
|
||||
// Если нет изображений и есть артикул и бренд, пытаемся получить из PartsIndex
|
||||
if (product.article && product.brand) {
|
||||
try {
|
||||
const partsIndexEntity = await partsIndexService.searchEntityByCode(
|
||||
const partsIndexEnabled = (process.env.PARTSINDEX_ENABLED === 'true') || false
|
||||
const partsIndexEntity = partsIndexEnabled
|
||||
? await partsIndexService.searchEntityByCode(
|
||||
product.article,
|
||||
product.brand
|
||||
)
|
||||
: null
|
||||
|
||||
if (partsIndexEntity && partsIndexEntity.images && partsIndexEntity.images.length > 0) {
|
||||
// Создаем временные изображения для отображения (не сохраняем в БД)
|
||||
@ -4058,10 +4064,13 @@ export const resolvers = {
|
||||
// Если нет изображений и есть артикул и бренд, пытаемся получить из PartsIndex
|
||||
if (product.article && product.brand) {
|
||||
try {
|
||||
const partsIndexEntity = await partsIndexService.searchEntityByCode(
|
||||
const partsIndexEnabled = (process.env.PARTSINDEX_ENABLED === 'true') || false
|
||||
const partsIndexEntity = partsIndexEnabled
|
||||
? await partsIndexService.searchEntityByCode(
|
||||
product.article,
|
||||
product.brand
|
||||
)
|
||||
: null
|
||||
|
||||
if (partsIndexEntity && partsIndexEntity.images && partsIndexEntity.images.length > 0) {
|
||||
// Создаем временные изображения для отображения (не сохраняем в БД)
|
||||
@ -4153,10 +4162,13 @@ export const resolvers = {
|
||||
// Если нет изображений и есть артикул и бренд, пытаемся получить из PartsIndex
|
||||
if (product.article && product.brand) {
|
||||
try {
|
||||
const partsIndexEntity = await partsIndexService.searchEntityByCode(
|
||||
const partsIndexEnabled = (process.env.PARTSINDEX_ENABLED === 'true') || false
|
||||
const partsIndexEntity = partsIndexEnabled
|
||||
? await partsIndexService.searchEntityByCode(
|
||||
product.article,
|
||||
product.brand
|
||||
)
|
||||
: null
|
||||
|
||||
if (partsIndexEntity && partsIndexEntity.images && partsIndexEntity.images.length > 0) {
|
||||
// Создаем временные изображения для отображения (не сохраняем в БД)
|
||||
|
@ -1,363 +1,46 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import { Pool } from 'pg'
|
||||
|
||||
class PartsDatabase {
|
||||
private pool: Pool
|
||||
|
||||
// Temporary no-op implementation to avoid requiring 'pg' when parts index is disabled.
|
||||
class NoopPartsDatabase {
|
||||
constructor() {
|
||||
const connectionString = process.env.DATABASE_URL
|
||||
if (!connectionString) {
|
||||
throw new Error('DATABASE_URL environment variable is not set')
|
||||
console.warn('Parts DB disabled: using no-op implementation')
|
||||
}
|
||||
|
||||
this.pool = new Pool({
|
||||
connectionString,
|
||||
max: 10, // Reduce concurrent connections to avoid overwhelming the DB
|
||||
idleTimeoutMillis: 60000, // 1 minute
|
||||
connectionTimeoutMillis: 10000, // 10 seconds
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: 10000,
|
||||
})
|
||||
|
||||
console.log('🔌 Parts Database connection initialized (using main DATABASE_URL)')
|
||||
async createCategoryTable() {
|
||||
return
|
||||
}
|
||||
|
||||
// Create table for a specific category
|
||||
async createCategoryTable(categoryId: string, categoryName: string, categoryType: 'partsindex' | 'partsapi'): Promise<void> {
|
||||
const tableName = this.getCategoryTableName(categoryId, categoryType)
|
||||
|
||||
try {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS "${tableName}" (
|
||||
id SERIAL PRIMARY KEY,
|
||||
external_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(500) NOT NULL,
|
||||
brand VARCHAR(255),
|
||||
article VARCHAR(255),
|
||||
description TEXT,
|
||||
image_url VARCHAR(500),
|
||||
price DECIMAL(10,2),
|
||||
category_id VARCHAR(255) NOT NULL,
|
||||
category_name VARCHAR(500) NOT NULL,
|
||||
category_type VARCHAR(20) NOT NULL,
|
||||
group_id VARCHAR(255),
|
||||
group_name VARCHAR(500),
|
||||
raw_data JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS "idx_${tableName}_external_id" ON "${tableName}" (external_id);
|
||||
CREATE INDEX IF NOT EXISTS "idx_${tableName}_category_id" ON "${tableName}" (category_id);
|
||||
CREATE INDEX IF NOT EXISTS "idx_${tableName}_brand" ON "${tableName}" (brand);
|
||||
CREATE INDEX IF NOT EXISTS "idx_${tableName}_article" ON "${tableName}" (article);
|
||||
CREATE INDEX IF NOT EXISTS "idx_${tableName}_created_at" ON "${tableName}" (created_at);
|
||||
|
||||
-- Create trigger to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_${tableName}_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS trigger_update_${tableName}_timestamp ON "${tableName}";
|
||||
CREATE TRIGGER trigger_update_${tableName}_timestamp
|
||||
BEFORE UPDATE ON "${tableName}"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_${tableName}_timestamp();
|
||||
`
|
||||
|
||||
await this.pool.query(query)
|
||||
console.log(`✅ Created table ${tableName} for category: ${categoryName}`)
|
||||
} catch (error) {
|
||||
console.error(`❌ Error creating table ${tableName}:`, error)
|
||||
throw error
|
||||
}
|
||||
async insertProducts(_categoryId, _categoryName, _categoryType, products) {
|
||||
console.warn(`Parts DB noop: insertProducts called for ${products?.length || 0} items`)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Insert or update products in category table
|
||||
async insertProducts(
|
||||
categoryId: string,
|
||||
categoryName: string,
|
||||
categoryType: 'partsindex' | 'partsapi',
|
||||
products: any[],
|
||||
groupId?: string,
|
||||
groupName?: string
|
||||
): Promise<number> {
|
||||
const tableName = this.getCategoryTableName(categoryId, categoryType)
|
||||
|
||||
console.log(`🔄 Starting to insert ${products.length} products into ${tableName}`)
|
||||
|
||||
// First ensure table exists
|
||||
await this.createCategoryTable(categoryId, categoryName, categoryType)
|
||||
|
||||
let insertedCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
// Process in smaller batches to reduce connection pressure
|
||||
const batchSize = 50
|
||||
const batches: any[][] = []
|
||||
for (let i = 0; i < products.length; i += batchSize) {
|
||||
batches.push(products.slice(i, i + batchSize))
|
||||
}
|
||||
|
||||
console.log(`📦 Processing ${products.length} products in ${batches.length} batches of ${batchSize}`)
|
||||
|
||||
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
||||
const batch = batches[batchIndex]
|
||||
console.log(`🔄 Processing batch ${batchIndex + 1}/${batches.length} (${batch.length} products)`)
|
||||
|
||||
for (let i = 0; i < batch.length; i++) {
|
||||
const globalIndex = batchIndex * batchSize + i
|
||||
const product = batch[i]
|
||||
|
||||
// Retry logic with exponential backoff
|
||||
let retryAttempts = 0
|
||||
const maxRetries = 3
|
||||
let success = false
|
||||
|
||||
while (retryAttempts < maxRetries && !success) {
|
||||
try {
|
||||
const values = this.prepareProductData(product, categoryId, categoryName, categoryType, groupId, groupName)
|
||||
|
||||
const query = `
|
||||
INSERT INTO "${tableName}" (
|
||||
external_id, name, brand, article, description, image_url, price,
|
||||
category_id, category_name, category_type, group_id, group_name, raw_data
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
ON CONFLICT (external_id)
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
brand = EXCLUDED.brand,
|
||||
article = EXCLUDED.article,
|
||||
description = EXCLUDED.description,
|
||||
image_url = EXCLUDED.image_url,
|
||||
price = EXCLUDED.price,
|
||||
group_id = EXCLUDED.group_id,
|
||||
group_name = EXCLUDED.group_name,
|
||||
raw_data = EXCLUDED.raw_data,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`
|
||||
|
||||
await this.pool.query(query, values)
|
||||
insertedCount++
|
||||
success = true
|
||||
|
||||
// Log progress every 100 insertions
|
||||
if (insertedCount % 100 === 0) {
|
||||
console.log(`📊 Progress: ${insertedCount}/${products.length} products inserted into ${tableName}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
retryAttempts++
|
||||
|
||||
// Check if it's a network/connection error that might be retryable
|
||||
if (error.code === 'ENOTFOUND' || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
|
||||
if (retryAttempts < maxRetries) {
|
||||
const delayMs = Math.pow(2, retryAttempts) * 1000 // Exponential backoff: 2s, 4s, 8s
|
||||
console.log(`🔄 Retry ${retryAttempts}/${maxRetries} for product ${globalIndex + 1} after ${delayMs}ms delay`)
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If max retries exceeded or non-retryable error
|
||||
errorCount++
|
||||
console.error(`❌ Error inserting product ${globalIndex + 1}/${products.length} into ${tableName} (after ${retryAttempts} retries):`, error)
|
||||
console.error(`❌ Product data:`, product)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between batches to allow DB to recover
|
||||
if (batchIndex < batches.length - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Insertion complete for ${tableName}:`)
|
||||
console.log(` - Successfully inserted/updated: ${insertedCount} products`)
|
||||
console.log(` - Errors: ${errorCount} products`)
|
||||
console.log(` - Total processed: ${products.length} products`)
|
||||
|
||||
return insertedCount
|
||||
}
|
||||
|
||||
// Get products from category table
|
||||
async getProducts(
|
||||
categoryId: string,
|
||||
categoryType: 'partsindex' | 'partsapi',
|
||||
options: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
search?: string
|
||||
} = {}
|
||||
): Promise<{ products: any[], total: number }> {
|
||||
const tableName = this.getCategoryTableName(categoryId, categoryType)
|
||||
const { limit = 50, offset = 0, search } = options
|
||||
|
||||
try {
|
||||
let whereClause = ''
|
||||
let searchParams: any[] = []
|
||||
|
||||
if (search) {
|
||||
whereClause = 'WHERE (name ILIKE $1 OR brand ILIKE $1 OR article ILIKE $1 OR description ILIKE $1)'
|
||||
searchParams = [`%${search}%`]
|
||||
}
|
||||
|
||||
// Count total
|
||||
const countQuery = `SELECT COUNT(*) FROM "${tableName}" ${whereClause}`
|
||||
const countResult = await this.pool.query(countQuery, searchParams)
|
||||
const total = parseInt(countResult.rows[0].count)
|
||||
|
||||
// Get products
|
||||
const dataQuery = `
|
||||
SELECT * FROM "${tableName}"
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $${searchParams.length + 1} OFFSET $${searchParams.length + 2}
|
||||
`
|
||||
const dataResult = await this.pool.query(dataQuery, [...searchParams, limit, offset])
|
||||
|
||||
return {
|
||||
products: dataResult.rows,
|
||||
total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error getting products from ${tableName}:`, error)
|
||||
async getProducts(_categoryId, _categoryType, _options = {}) {
|
||||
return { products: [], total: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// Get all category tables
|
||||
async getCategoryTables(): Promise<{ tableName: string, categoryId: string, categoryType: string, recordCount: number }[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
tablename,
|
||||
schemaname
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
AND (tablename LIKE 'category_partsindex_%' OR tablename LIKE 'category_partsapi_%')
|
||||
ORDER BY tablename
|
||||
`
|
||||
|
||||
const result = await this.pool.query(query)
|
||||
const tables: { tableName: string, categoryId: string, categoryType: string, recordCount: number }[] = []
|
||||
|
||||
for (const row of result.rows) {
|
||||
const tableName = row.tablename
|
||||
|
||||
// Parse category info from table name
|
||||
const [, categoryType, categoryId] = tableName.split('_')
|
||||
|
||||
// Get record count
|
||||
const countQuery = `SELECT COUNT(*) FROM "${tableName}"`
|
||||
const countResult = await this.pool.query(countQuery)
|
||||
const recordCount = parseInt(countResult.rows[0].count)
|
||||
|
||||
tables.push({
|
||||
tableName,
|
||||
categoryId,
|
||||
categoryType,
|
||||
recordCount
|
||||
})
|
||||
}
|
||||
|
||||
return tables
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting category tables:', error)
|
||||
async getCategoryTables() {
|
||||
return []
|
||||
}
|
||||
|
||||
async deleteCategoryTable() {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete category table
|
||||
async deleteCategoryTable(categoryId: string, categoryType: 'partsindex' | 'partsapi'): Promise<void> {
|
||||
const tableName = this.getCategoryTableName(categoryId, categoryType)
|
||||
|
||||
try {
|
||||
await this.pool.query(`DROP TABLE IF EXISTS "${tableName}" CASCADE`)
|
||||
console.log(`✅ Deleted table ${tableName}`)
|
||||
} catch (error) {
|
||||
console.error(`❌ Error deleting table ${tableName}:`, error)
|
||||
throw error
|
||||
}
|
||||
async testConnection() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Helper method to generate table name
|
||||
private getCategoryTableName(categoryId: string, categoryType: 'partsindex' | 'partsapi'): string {
|
||||
// Sanitize category ID for use in table name
|
||||
const sanitizedId = categoryId.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()
|
||||
async close() {
|
||||
return
|
||||
}
|
||||
|
||||
// Keep signature compatibility with previous helper
|
||||
getCategoryTableName(categoryId, categoryType) {
|
||||
const sanitizedId = String(categoryId || '').replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()
|
||||
return `category_${categoryType}_${sanitizedId}`
|
||||
}
|
||||
|
||||
// Helper method to prepare product data
|
||||
private prepareProductData(
|
||||
product: any,
|
||||
categoryId: string,
|
||||
categoryName: string,
|
||||
categoryType: 'partsindex' | 'partsapi',
|
||||
groupId?: string,
|
||||
groupName?: string
|
||||
): any[] {
|
||||
if (categoryType === 'partsindex') {
|
||||
return [
|
||||
product.id || product.external_id || `${Date.now()}_${Math.random()}`,
|
||||
product.name || '',
|
||||
product.brand || '',
|
||||
product.article || '',
|
||||
product.description || '',
|
||||
product.image || '',
|
||||
product.price ? parseFloat(product.price) : null,
|
||||
categoryId,
|
||||
categoryName,
|
||||
categoryType,
|
||||
groupId || null,
|
||||
groupName || null,
|
||||
JSON.stringify(product)
|
||||
]
|
||||
} else {
|
||||
// PartsAPI
|
||||
return [
|
||||
product.artId || `${Date.now()}_${Math.random()}`,
|
||||
product.artArticleNr || '',
|
||||
product.artSupBrand || '',
|
||||
product.artArticleNr || '',
|
||||
product.productGroup || '',
|
||||
'', // no image for PartsAPI
|
||||
null, // no price for PartsAPI
|
||||
categoryId,
|
||||
categoryName,
|
||||
categoryType,
|
||||
groupId || null,
|
||||
groupName || null,
|
||||
JSON.stringify(product)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Test database connection
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
await this.pool.query('SELECT 1')
|
||||
console.log('✅ Parts database connection test successful')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ Parts database connection test failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Close connection pool
|
||||
async close(): Promise<void> {
|
||||
await this.pool.end()
|
||||
console.log('🔌 Parts database connection closed')
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const partsDb = new PartsDatabase()
|
||||
export const partsDb = new NoopPartsDatabase()
|
||||
|
@ -155,8 +155,10 @@ interface CacheEntry<T> {
|
||||
}
|
||||
|
||||
class PartsIndexService {
|
||||
private baseURL = process.env.PARTSAPI_URL+"/v1" || 'https://api.parts-index.com/v1';
|
||||
private apiKey = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
|
||||
private baseHost = process.env.PARTSAPI_URL || 'https://api.parts-index.com';
|
||||
private baseURL = `${this.baseHost}/v1`;
|
||||
private apiKey = process.env.PARTSAPI_KEY || 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
|
||||
private enabled = Boolean(process.env.PARTSAPI_URL) || process.env.PARTSINDEX_ENABLED === 'true';
|
||||
|
||||
// Простой in-memory кэш
|
||||
private cache = new Map<string, CacheEntry<any>>();
|
||||
@ -232,6 +234,10 @@ class PartsIndexService {
|
||||
|
||||
// Получить список каталогов
|
||||
async getCatalogs(lang: 'ru' | 'en' = 'ru'): Promise<PartsIndexCatalog[]> {
|
||||
if (!this.enabled) {
|
||||
// Disabled: return empty to avoid external calls during local dev
|
||||
return [];
|
||||
}
|
||||
const cacheKey = `catalogs_${lang}`;
|
||||
|
||||
// Проверяем кэш
|
||||
@ -271,6 +277,9 @@ class PartsIndexService {
|
||||
|
||||
// Получить группы каталога
|
||||
async getCatalogGroups(catalogId: string, lang: 'ru' | 'en' = 'ru'): Promise<PartsIndexGroup[]> {
|
||||
if (!this.enabled) {
|
||||
return [];
|
||||
}
|
||||
const cacheKey = `groups_${catalogId}_${lang}`;
|
||||
|
||||
// Проверяем кэш
|
||||
@ -352,6 +361,9 @@ class PartsIndexService {
|
||||
} = options;
|
||||
|
||||
try {
|
||||
if (!this.enabled) {
|
||||
return [];
|
||||
}
|
||||
console.log('🔍 PartsIndex запрос ВСЕХ товаров каталога:', {
|
||||
catalogId,
|
||||
groupId,
|
||||
@ -434,6 +446,10 @@ class PartsIndexService {
|
||||
params
|
||||
} = options;
|
||||
|
||||
if (!this.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Создаем ключ кэша на основе всех параметров
|
||||
const cacheKey = `entities_${catalogId}_${groupId}_${lang}_${limit}_${page}_${q || 'no-query'}_${engineId || 'no-engine'}_${generationId || 'no-generation'}_${JSON.stringify(params || {})}`;
|
||||
|
||||
@ -670,6 +686,9 @@ class PartsIndexService {
|
||||
lang: 'ru' | 'en' = 'ru'
|
||||
): Promise<PartsIndexEntityDetail | null> {
|
||||
try {
|
||||
if (!this.enabled) {
|
||||
return null;
|
||||
}
|
||||
console.log('🔍 PartsIndex запрос детали товара:', { catalogId, entityId, lang });
|
||||
|
||||
const response = await axios.get(`${this.baseURL}/catalogs/${catalogId}/entities/${entityId}`, {
|
||||
@ -716,6 +735,9 @@ class PartsIndexService {
|
||||
lang: 'ru' | 'en' = 'ru'
|
||||
): Promise<PartsIndexEntityDetail | null> {
|
||||
try {
|
||||
if (!this.enabled) {
|
||||
return null;
|
||||
}
|
||||
console.log('🔍 PartsIndex прямой поиск по артикулу:', { code, brand, lang });
|
||||
|
||||
const params: any = {
|
||||
@ -768,6 +790,9 @@ class PartsIndexService {
|
||||
lang: 'ru' | 'en' = 'ru'
|
||||
): Promise<PartsIndexEntity | null> {
|
||||
try {
|
||||
if (!this.enabled) {
|
||||
return null;
|
||||
}
|
||||
console.log('🔍 PartsIndex поиск товара в конкретной категории:', {
|
||||
catalogId,
|
||||
groupId,
|
||||
|
Reference in New Issue
Block a user