From 413c147109ddb02971296c4143df3a2099ee9c6a Mon Sep 17 00:00:00 2001 From: xiongxiao Date: Fri, 26 Dec 2025 18:13:15 +0800 Subject: [PATCH] update --- .claude/settings.local.json | 10 + .env.example | 1 + .gitignore | 12 +- bun.config.ts | 5 + drizzle.config.ts | 19 + package.json | 46 +- pnpm-lock.yaml | 1510 ++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + readme.md | 16 + src/app.ts | 49 + src/db/cache.ts | 53 + src/db/schema.ts | 24 + src/index.ts | 18 + src/modules/cache.ts | 6 + src/playwright/core.ts | 187 ++++ src/playwright/index.ts | 184 +--- src/routes/browser/index.ts | 2 + src/routes/browser/page.ts | 16 + src/routes/browser/pane-manager.ts | 67 ++ src/routes/index.ts | 2 + src/routes/xhs/index.ts | 1 + src/routes/xhs/search-notes.ts | 205 ++++ src/routes/xhs/user.ts | 0 src/test/browser.ts | 13 + src/test/cmd.ts | 7 + src/test/common.ts | 34 + src/test/db/add.ts | 34 + src/test/pane.ts | 47 + src/test/xhs/index.ts | 18 + start-browser.ts => start-browser.js | 30 +- tsconfig.json | 26 + typings/{note.ts => note.d.ts} | 10 +- 32 files changed, 2449 insertions(+), 205 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .env.example create mode 100644 bun.config.ts create mode 100644 drizzle.config.ts create mode 100644 pnpm-workspace.yaml create mode 100644 readme.md create mode 100644 src/app.ts create mode 100644 src/db/cache.ts create mode 100644 src/db/schema.ts create mode 100644 src/index.ts create mode 100644 src/modules/cache.ts create mode 100644 src/playwright/core.ts create mode 100644 src/routes/browser/index.ts create mode 100644 src/routes/browser/page.ts create mode 100644 src/routes/browser/pane-manager.ts create mode 100644 src/routes/index.ts create mode 100644 src/routes/xhs/index.ts create mode 100644 src/routes/xhs/search-notes.ts create mode 100644 src/routes/xhs/user.ts create mode 100644 src/test/browser.ts create mode 100644 src/test/cmd.ts create mode 100644 src/test/common.ts create mode 100644 src/test/db/add.ts create mode 100644 src/test/pane.ts create mode 100644 src/test/xhs/index.ts rename start-browser.ts => start-browser.js (66%) create mode 100644 tsconfig.json rename typings/{note.ts => note.d.ts} (92%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..69c7f53 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm drizzle-kit generate:*)", + "Bash(pnpm drizzle-kit push:*)", + "Bash(pnpm rebuild:*)", + "Bash(tsx:*)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8167d07 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATABASE_URL=storage/browser-helper/data.sqlite3 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4a2603f..aed26f0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,14 @@ node_modules browser-context -cache \ No newline at end of file +cache + +.env +!.env*example + +storage +# storage/browser-helper/data.db + +dist + +pack-dist \ No newline at end of file diff --git a/bun.config.ts b/bun.config.ts new file mode 100644 index 0000000..10dfcc4 --- /dev/null +++ b/bun.config.ts @@ -0,0 +1,5 @@ +import { buildWithBun } from '@kevisual/code-builder' + +buildWithBun({ + external: ['playwright', 'better-sqlite3'] +}) \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..6664fbd --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,19 @@ +import type { Config } from 'drizzle-kit'; +import 'dotenv/config'; +const url = process.env.DATABASE_URL || 'storage/browser-helper/data.sqlite3'; +// Ensure the directory exists +import fs from "node:fs"; +import path from "node:path"; +const dir = path.dirname(url); +if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); +} + +export default { + schema: './src/db/schema.ts', + out: './storage/browser-helper/drizzle', + dialect: 'sqlite', + dbCredentials: { + url: process.env.DATABASE_URL || 'storage/browser-helper/data.sqlite3', + }, +} satisfies Config; diff --git a/package.json b/package.json index c31e2bd..fdaae1f 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,35 @@ { - "name": "xhs-helper", + "name": "@kevisual/browser-helper", "version": "0.0.1", "description": "", "main": "index.js", - "types": "typings/note.ts", + "types": "typings/note.d.ts", + "basename": "/root/browser-helper", + "app": { + "type": "pm2-system-app", + "entry": "./app.js", + "runtime": [ + "client" + ] + }, "scripts": { "start": "tsx src/playwright/index.ts", - "init:base": "npx playwright install", - "browser": "pm2 start start-browser.ts --name xhs-helper-browser --interpreter=tsx" + "dev": "tsx watch src/index.ts", + "init:browser": "npx playwright install", + "build": "code-builder build", + "browser": "pm2 start start-browser.js --name browser-helper", + "cmd": "tsx src/test/cmd.ts ", + "init": "pnpm run init:pnpm && pnpm run init:db && pnpm run init:browser", + "init:pnpm": "pnpm approve-builds", + "init:db": "npx drizzle-kit push", + "studio": "npx drizzle-kit studio", + "drizzle:migrate": "npx drizzle-kit migrate", + "drizzle:push": "npx drizzle-kit push" }, "keywords": [], "files": [ "typings", + "dist", "src", "start-browser.ts" ], @@ -20,9 +38,25 @@ "packageManager": "pnpm@10.26.0", "type": "module", "dependencies": { - "playwright": "^1.57.0" + "playwright": "^1.57.0", + "better-sqlite3": "^12.5.0" }, "devDependencies": { - "@types/node": "^25.0.3" + "@kevisual/code-builder": "^0.0.2", + "@kevisual/context": "^0.0.4", + "@kevisual/router": "^0.0.49", + "@kevisual/types": "^0.0.10", + "@kevisual/use-config": "^1.0.21", + "@types/better-sqlite3": "^7.6.13", + "@types/bun": "^1.3.5", + "@types/node": "^25.0.3", + "commander": "^14.0.2", + "dotenv": "^17.2.3", + "drizzle-kit": "^0.31.8", + "drizzle-orm": "^0.45.1", + "es-toolkit": "^1.43.0", + "eventemitter3": "^5.0.1", + "lru-cache": "^11.2.4", + "window-size": "^1.1.1" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a210a90..30304cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,24 +8,754 @@ importers: .: dependencies: + better-sqlite3: + specifier: ^12.5.0 + version: 12.5.0 playwright: specifier: ^1.57.0 version: 1.57.0 devDependencies: + '@kevisual/code-builder': + specifier: ^0.0.2 + version: 0.0.2(dotenv@17.2.3) + '@kevisual/context': + specifier: ^0.0.4 + version: 0.0.4 + '@kevisual/router': + specifier: ^0.0.49 + version: 0.0.49 + '@kevisual/types': + specifier: ^0.0.10 + version: 0.0.10 + '@kevisual/use-config': + specifier: ^1.0.21 + version: 1.0.21(dotenv@17.2.3) + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@types/bun': + specifier: ^1.3.5 + version: 1.3.5 '@types/node': specifier: ^25.0.3 version: 25.0.3 + commander: + specifier: ^14.0.2 + version: 14.0.2 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + drizzle-kit: + specifier: ^0.31.8 + version: 0.31.8 + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(bun-types@1.3.5) + es-toolkit: + specifier: ^1.43.0 + version: 1.43.0 + eventemitter3: + specifier: ^5.0.1 + version: 5.0.1 + lru-cache: + specifier: ^11.2.4 + version: 11.2.4 + window-size: + specifier: ^1.1.1 + version: 1.1.1 packages: + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@kevisual/code-builder@0.0.2': + resolution: {integrity: sha512-SJAYfPqoK/TsjFj1JyUrr7Ac+Q5PjpXIK3BupVH9XJLVqy5ADgnHEuHDcYb0dRsea5KJ+k2XTPchCEIImRo4yw==} + hasBin: true + + '@kevisual/context@0.0.4': + resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==} + + '@kevisual/load@0.0.6': + resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==} + + '@kevisual/router@0.0.49': + resolution: {integrity: sha512-2HXuOnnWdRfkO0LyqolWU9cvWHGXi8FV3OqEvWgfO+f7wx8GT8T6Bb8dCzdldDaAxve1dgLBavtdmnHyCkp+1Q==} + + '@kevisual/types@0.0.10': + resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==} + + '@kevisual/use-config@1.0.21': + resolution: {integrity: sha512-czgy4+tBDBJI6QTnKh2PCwswET6ZpZ4ZqBE/SPkkOivEtlrcPzLs5elwMLZ3goD1XMD4VB3yjumb5WuW/8H8MA==} + peerDependencies: + dotenv: ^17 + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@peculiar/asn1-cms@2.6.0': + resolution: {integrity: sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==} + + '@peculiar/asn1-csr@2.6.0': + resolution: {integrity: sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==} + + '@peculiar/asn1-ecc@2.6.0': + resolution: {integrity: sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==} + + '@peculiar/asn1-pfx@2.6.0': + resolution: {integrity: sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==} + + '@peculiar/asn1-pkcs8@2.6.0': + resolution: {integrity: sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==} + + '@peculiar/asn1-pkcs9@2.6.0': + resolution: {integrity: sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==} + + '@peculiar/asn1-rsa@2.6.0': + resolution: {integrity: sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==} + + '@peculiar/asn1-schema@2.6.0': + resolution: {integrity: sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==} + + '@peculiar/asn1-x509-attr@2.6.0': + resolution: {integrity: sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==} + + '@peculiar/asn1-x509@2.6.0': + resolution: {integrity: sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==} + + '@peculiar/x509@1.14.2': + resolution: {integrity: sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==} + engines: {node: '>=22.0.0'} + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/bun@1.3.5': + resolution: {integrity: sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==} + '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + asn1js@3.0.7: + resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} + engines: {node: '>=12.0.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@12.5.0: + resolution: {integrity: sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bun-types@1.3.5: + resolution: {integrity: sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw==} + + bytestreamjs@2.0.1: + resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} + engines: {node: '>=6.0.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + drizzle-kit@0.31.8: + resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==} + hasBin: true + + drizzle-orm@0.45.1: + resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-toolkit@1.43.0: + resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.85.0: + resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} + engines: {node: '>=10'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + pkijs@3.3.3: + resolution: {integrity: sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==} + engines: {node: '>=16.0.0'} + playwright-core@1.57.0: resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} engines: {node: '>=18'} @@ -36,18 +766,665 @@ packages: engines: {node: '>=18'} hasBin: true + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.5: + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + selfsigned@5.4.0: + resolution: {integrity: sha512-Yn8qZOOJv+NhcGY19iC+ngW6hlUCNpvWEkrKllXNhmkLgR9fcErm8EqZ/wev7/tiwjKC9qj17Fa/PtBNzb6q8g==} + engines: {node: '>=15.6.0'} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsyringe@4.10.0: + resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} + engines: {node: '>= 6.0.0'} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + window-size@1.1.1: + resolution: {integrity: sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==} + engines: {node: '>= 0.10.0'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + snapshots: + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@kevisual/code-builder@0.0.2(dotenv@17.2.3)': + dependencies: + '@kevisual/use-config': 1.0.21(dotenv@17.2.3) + nanoid: 5.1.6 + transitivePeerDependencies: + - dotenv + + '@kevisual/context@0.0.4': {} + + '@kevisual/load@0.0.6': + dependencies: + eventemitter3: 5.0.1 + + '@kevisual/router@0.0.49': + dependencies: + path-to-regexp: 8.3.0 + selfsigned: 5.4.0 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@kevisual/types@0.0.10': {} + + '@kevisual/use-config@1.0.21(dotenv@17.2.3)': + dependencies: + '@kevisual/load': 0.0.6 + dotenv: 17.2.3 + + '@noble/hashes@1.4.0': {} + + '@peculiar/asn1-cms@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/asn1-x509-attr': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-csr@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-pfx@2.6.0': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-pkcs8': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs8@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs9@2.6.0': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-pfx': 2.6.0 + '@peculiar/asn1-pkcs8': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/asn1-x509-attr': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.6.0': + dependencies: + asn1js: 3.0.7 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/asn1-x509-attr@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + asn1js: 3.0.7 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/x509@1.14.2': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-csr': 2.6.0 + '@peculiar/asn1-ecc': 2.6.0 + '@peculiar/asn1-pkcs9': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + pvtsutils: 1.3.6 + reflect-metadata: 0.2.2 + tslib: 2.8.1 + tsyringe: 4.10.0 + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 25.0.3 + + '@types/bun@1.3.5': + dependencies: + bun-types: 1.3.5 + '@types/node@25.0.3': dependencies: undici-types: 7.16.0 + asn1js@3.0.7: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + + base64-js@1.5.1: {} + + better-sqlite3@12.5.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bun-types@1.3.5: + dependencies: + '@types/node': 25.0.3 + + bytestreamjs@2.0.1: {} + + chownr@1.1.4: {} + + commander@14.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.3 + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dotenv@17.2.3: {} + + drizzle-kit@0.31.8: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(bun-types@1.3.5): + optionalDependencies: + '@types/better-sqlite3': 7.6.13 + better-sqlite3: 12.5.0 + bun-types: 1.3.5 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-toolkit@1.43.0: {} + + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventemitter3@5.0.1: {} + + expand-template@2.0.3: {} + + file-uri-to-path@1.0.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + fsevents@2.3.2: optional: true + function-bind@1.1.2: {} + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-from-package@0.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-accessor-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-buffer@1.1.6: {} + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-descriptor@1.0.3: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + lru-cache@11.2.4: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-response@3.1.0: {} + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + ms@2.1.3: {} + + nanoid@5.1.6: {} + + napi-build-utils@2.0.0: {} + + node-abi@3.85.0: + dependencies: + semver: 7.7.3 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + path-to-regexp@8.3.0: {} + + pkijs@3.3.3: + dependencies: + '@noble/hashes': 1.4.0 + asn1js: 3.0.7 + bytestreamjs: 2.0.1 + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + playwright-core@1.57.0: {} playwright@1.57.0: @@ -56,4 +1433,137 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.85.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.5: {} + + range-parser@1.2.1: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + reflect-metadata@0.2.2: {} + + resolve-pkg-maps@1.0.0: {} + + safe-buffer@5.2.1: {} + + selfsigned@5.4.0: + dependencies: + '@peculiar/x509': 1.14.2 + pkijs: 3.3.3 + + semver@7.7.3: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + statuses@2.0.2: {} + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-json-comments@2.0.1: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + toidentifier@1.0.1: {} + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsyringe@4.10.0: + dependencies: + tslib: 1.14.1 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + undici-types@7.16.0: {} + + util-deprecate@1.0.2: {} + + window-size@1.1.1: + dependencies: + define-property: 1.0.0 + is-number: 3.0.0 + + wrappy@1.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..e4a4b5b --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - better-sqlite3 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..4d742d5 --- /dev/null +++ b/readme.md @@ -0,0 +1,16 @@ +# 浏览器自动化助手 + +实现功能,浏览了页面,自动把想要的数据,存储到数据库中,方便后续分析和使用。 + +## 初始化 + +```bash +pnpm install +pnpm run init +``` + +## 启动studio + +```bash +pnpm run studio +``` \ No newline at end of file diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..be35b86 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,49 @@ +import { App } from '@kevisual/router' +import { useConfigKey } from '@kevisual/context' +import { useConfig } from '@kevisual/use-config' +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import { Core } from './playwright/core.ts'; +import * as schema from './db/schema.ts'; + +export const config = useConfig() + +export const app = useConfigKey('app', () => new App({ + +})) +app.router.createRouteList(); + +export const db = useConfigKey('db', () => { + const sqlite = new Database(config.DATABASE_URL || 'storage/browser-helper/data.sqlite3'); + sqlite.pragma('journal_mode = WAL'); + const db = drizzle({ client: sqlite, schema }); + return db; +}) + +export const core = useConfigKey('core', () => new Core({ + listeners: [ + { + path: "search/notes", + response: async (page) => { + console.log('处理搜索笔记响应'); + console.log(page.url); + try { + const data = JSON.parse(page.text); + if (data.code === 0) { + const notes = data.data?.items || []; + console.log(`搜索到 ${notes.length} 条笔记`); + app.run({ + path: 'xhs', + key: 'save-search-notes', + payload: { + data: notes, + } + }) + } + } catch (error) { + console.error('解析搜索笔记响应失败:', error); + } + } + } + ] +})); diff --git a/src/db/cache.ts b/src/db/cache.ts new file mode 100644 index 0000000..a7e7b17 --- /dev/null +++ b/src/db/cache.ts @@ -0,0 +1,53 @@ +import { eq, and } from 'drizzle-orm'; +import { db } from '../app.ts'; +import { cache } from './schema.ts'; + +export const cacheStore = { + set: (key: string, value: unknown, days = 7) => { + const expireAt = Math.floor(Date.now() / 1000) + days * 86400; + return db.insert(cache) + .values({ + key, + value: JSON.stringify(value), + expireAt, + createdAt: Math.floor(Date.now() / 1000), + }) + .onConflictDoUpdate({ + target: cache.key, + set: { value: JSON.stringify(value), expireAt }, + }); + }, + + get: (key: string): T | null => { + const now = Math.floor(Date.now() / 1000); + const row = db.select() + .from(cache) + .where(and(eq(cache.key, key), eq(cache.expireAt, now))) + .get() as { value: string } | undefined; + return row ? JSON.parse(row.value) as T : null; + }, + + has: (key: string): boolean => { + const now = Math.floor(Date.now() / 1000); + const row = db.select() + .from(cache) + .where(and(eq(cache.key, key), eq(cache.expireAt, now))) + .get(); + return !!row; + }, + + delete: (key: string) => { + return db.delete(cache).where(eq(cache.key, key)); + }, + + // 定期清理过期数据 + cleanExpired: () => { + const now = Math.floor(Date.now() / 1000); + return db.delete(cache).where(eq(cache.expireAt, now)); + }, +}; + +// 每5小时清理一次过期数据 +setInterval(() => { + cacheStore.cleanExpired(); +}, 5 * 1000 * 60 * 60); diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..ab93301 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,24 @@ +import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; + +export const cache = sqliteTable('cache', { + key: text('key').primaryKey(), + value: text('value').notNull(), + expireAt: integer('expire_at').notNull(), + createdAt: integer('created_at').notNull(), +}); + +export const xhsNote = sqliteTable('xhs_note', { + id: text('id').primaryKey(), + content: text('content').notNull(), + title: text('title'), + description: text('description'), + tags: text('tags').notNull(), + noteUrl: text('note_url'), + status: text('status'), + authorUrl: text('author_url'), + cover: text('cover'), + syncStatus: integer('sync_status').notNull(), + syncAt: integer('sync_at').notNull(), + createdAt: integer('created_at').notNull(), + updatedAt: integer('updated_at').notNull(), +}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4d636ef --- /dev/null +++ b/src/index.ts @@ -0,0 +1,18 @@ +import { app } from './app.ts' +export { app, db } from './app.ts'; +export { Core } from './playwright/core.ts'; +import './routes/index.ts'; +// 如果是直接运行,则启动应用 + +app.route({ + id: 'auth' +}).define(async (ctx) => { + // token authentication + +}).addTo(app); +if (import.meta.main) { + console.log('Starting application...'); + app.listen(52000, () => { + console.log('Application is running on http://localhost:52000'); + }) +} \ No newline at end of file diff --git a/src/modules/cache.ts b/src/modules/cache.ts new file mode 100644 index 0000000..e080dfc --- /dev/null +++ b/src/modules/cache.ts @@ -0,0 +1,6 @@ +import { LRUCache } from 'lru-cache' + +export const sessionCache = new LRUCache({ + max: 500, + ttl: 1000 * 60 * 60, // 1 hour +}); \ No newline at end of file diff --git a/src/playwright/core.ts b/src/playwright/core.ts new file mode 100644 index 0000000..f716308 --- /dev/null +++ b/src/playwright/core.ts @@ -0,0 +1,187 @@ +import { chromium, Page, BrowserContext, Browser, CDPSession, Request } from 'playwright'; +import { execSync } from 'node:child_process'; +import { EventEmitter } from 'eventemitter3' +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +type RequestObject = { + url: string; + path: string; + request: Request +} +type ResponseObject = { + url: string; + path: string; + text: string; +} +export type Listener = { + path: string; + request?: (data: RequestObject) => void; + response?: (data: ResponseObject) => void; + type?: 'request' | 'response' | 'both'; +} + +export class Core { + browserContext: BrowserContext | null = null; + browser: Browser | null = null; + page: Page | null = null; + debugPort = 9223; + status: 'disconnected' | 'connecting' | 'connected' | 'failed' = 'disconnected'; + emitter = new EventEmitter(); + listeners: Listener[] = []; + recordReady: boolean = false; + data: T | null = null; + constructor(opts?: { debugPort?: number, listeners?: Listener[] }) { + if (opts?.debugPort) { + this.debugPort = opts.debugPort; + } + if (opts?.listeners) { + this.listeners = opts.listeners; + } + } + async init() { + const debugPort = this.debugPort; + try { + const stdout = execSync(`netstat -ano | findstr :${debugPort}`); + console.log(`端口 ${debugPort} 已在监听:\n${stdout}`); + const browser = await chromium.connectOverCDP(`http://127.0.0.1:${debugPort}`); + console.log('成功连接到 Chrome CDP!'); + this.browser = browser; + this.browserContext = browser.contexts()[0]; + this.handleRequest(this.browserContext); + this.page = this.browserContext.pages()[0] || await this.browserContext.newPage(); + this.emitter.emit('connected'); + return; + } catch (error: any) { + throw new Error(`无法连接到 Chrome CDP,端口 ${debugPort} 可能未正确启动: ${(error as Error).message.slice(0, 100)}`); + } + } + async connect() { + if (this.status === 'connected') { + return this + } + if (this.status === 'connecting') { + return new Promise((resolve) => { + const timer = setTimeout(() => { + this.emitter.removeAllListeners('connected'); + resolve(false) + }, 60 * 5 * 1000); + this.emitter.once('connected', () => { + clearInterval(timer) + resolve(true) + }); + + }); + } + if (this.status === 'disconnected' || this.status === 'failed') { + this.status = 'connecting'; + for (let i = 0; i < 10; i++) { + try { + await this.init(); + this.status = 'connected' + return true; + } catch (e) { + console.log(`尝试 ${i + 1}/10 连接失败: ${(e as Error).message.slice(0, 100)}`); + await sleep(2000); + } + } + } + this.status = 'failed'; + return false; + } + async getBrowser() { + if (this.browser) { + return this.browser; + } + const connected = await this.connect(); + if (connected) { + return this.browser!; + } + throw new Error('无法连接到浏览器实例'); + } + async getPage() { + if (this.page) { + return this.page; + } + const connected = await this.connect(); + if (connected) { + return this.page!; + } + throw new Error('无法连接到浏览器实例'); + } + async setReady(ready: boolean = true) { + if (this.recordReady !== ready) { + this.recordReady = ready; + } + } + async setData(data?: any) { + if (!data) { + this.data = null; + return + } + this.data = data; + } + async handleRequest(context: BrowserContext) { + context.on('request', request => { + const url = request.url(); + for (let listener of this.listeners) { + const type = listener.type || 'both'; + if (type === 'response') continue; + const data = { url, path: listener.path, request }; + if (url.includes(listener.path)) { + this.emitter.emit(listener.path, data); + } + if (listener.request && typeof listener.request === 'function') { + listener.request(data); + } + } + }); + + context.on('response', async response => { + const url = response.url(); + const recordReady = this.recordReady; + + for (let listener of this.listeners) { + const type = listener.type || 'both'; + if (type === 'request') continue; + if (url.includes(listener.path)) { + if (!recordReady) { + console.log('记录未就绪,跳过响应处理'); + return + } + try { + const status = response.status(); + const contentType = response.headers()['content-type'] || ''; + if (status >= 400) { + console.log(`请求 ${url} 返回错误状态码: ${status}`); + this.emitter.emit('error', { url, status, error: '错误状态码' }); + return; + } + const isText = contentType.includes('application/json') || contentType.includes('text/'); + if (!isText) { + console.log(`请求 ${url} 返回非文本内容类型: ${contentType}`); + this.emitter.emit('error', { url, status, error: '非文本内容' }); + return; + } + const responseBody = await response.text(); + const data: ResponseObject = { url, path: listener.path, text: responseBody }; + this.emitter.emit(listener.path, data); + if (listener.response && typeof listener.response === 'function') { + listener.response(data); + } + } catch (e) { + console.log('无法读取响应内容'); + } + } + } + }); + } + close() { + if (this.browser) { + this.browser.close(); + this.browser = null; + this.browserContext = null; + this.page = null; + this.status = 'disconnected'; + } + } +} \ No newline at end of file diff --git a/src/playwright/index.ts b/src/playwright/index.ts index aa1e6c0..ea20da9 100644 --- a/src/playwright/index.ts +++ b/src/playwright/index.ts @@ -1,183 +1 @@ -import { chromium, Page, BrowserContext } from 'playwright'; -import path from 'node:path'; -import { exec } from 'node:child_process'; -import { promisify } from 'node:util'; -import fs from 'node:fs'; - -const execAsync = promisify(exec); -export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -export const main = async () => { - const debugPort = 9223; - // 等待并检查端口是否监听 - for (let i = 0; i < 15; i++) { - await new Promise(resolve => setTimeout(resolve, 2000)); - - try { - // 检查端口是否被监听 - const { stdout } = await execAsync(`netstat -ano | findstr :${debugPort}`); - console.log(`端口 ${debugPort} 已在监听:\n${stdout}`); - - // 尝试连接 - const browser = await chromium.connectOverCDP(`http://127.0.0.1:${debugPort}`); - console.log('成功连接到 Chrome CDP!'); - - // 获取已有的 context - const context = browser.contexts()[0]; - - // 获取已有的页面或创建新页面 - const page = context.pages()[0] || await context.newPage(); - - // console.log('Navigating to xiaohongshu.com'); - // await page.goto('https://www.xiaohongshu.com/search_result?keyword=%25E5%25A4%259A%25E7%25BB%25B4%25E8%25A1%25A8%25E6%25A0%25BC&type=51'); - // console.log('当前页面标题:', await page.title()); - // 点击筛选按钮的示例 - // await page.click('text=筛选'); - - // PAIU - await listenFetchRequests(context); - await sleep(2000); - // 关闭浏览器连接,不关闭实际浏览器 - await hoverPickerExample(page, { keyword: '多维表格' }); - // 等待更长时间让请求有机会发生 - console.log('等待 API 请求...'); - await sleep(5000); - // 清理所有路由,避免 TargetClosedError - await context.unrouteAll({ behavior: 'ignoreErrors' }); - // 获取 - await browser.close(); - return; - } catch (error: any) { - console.log(`尝试 ${i + 1}/15: ${(error as Error).message.slice(0, 100)}`); - } - } - - throw new Error(`无法连接到 Chrome CDP,端口 ${debugPort} 可能未正确启动`); -} - -main(); - -type HoverPickerOptions = { - keyword?: string; - pushTime?: '一天内' | '一周内' | '半年内'; - sort?: '综合' | '最新' | '最多点赞' | '最多评论'; - searchRange?: '不限' | '已看过' | '未看过' | '已关注'; - distance?: '不限' | '同城' | '附近'; - scrollTimes?: number; -} -const hoverPickerExample = async (page: Page, opts?: HoverPickerOptions) => { - const { pushTime = '一天内', sort = '最新', keyword = '', scrollTimes = 5 } = opts || {}; - if (keyword) { - // class为 input-box - const inputBox = await page.$('.input-box'); - const input = await inputBox?.$('input'); - if (input) { - //先获取当前内容,如果一样就不输入 - const currentValue = await input.inputValue(); - if (currentValue === keyword) { - console.log('关键词已存在,无需输入'); - } else { - await input.fill(keyword); - await input.press('Enter'); - console.log(`已输入关键词: ${keyword}`); - await sleep(3000); // 等待搜索结果加载 - } - } - } - // 查找筛选按钮并保持 hover 状态 - let filterButton = await page.$('text=筛选'); - if (filterButton) { - await filterButton.hover(); - console.log('鼠标悬停在筛选按钮上'); - // 在保持 hover 的情况下,等待筛选面板出现并点击元素 - const filterClassPanel = await page.$('.filter-panel'); - if (filterClassPanel) { - console.log('筛选面板已打开'); - - // 点击最新选项 - const latestOption = await filterClassPanel.$(`text=${sort}`); - if (latestOption) { - await latestOption.click(); - console.log(`已选择${sort}选项`); - } - - // 点击一周内选项 - const oneWeekOption = await filterClassPanel.$(`text=${pushTime}`); - if (oneWeekOption) { - await oneWeekOption.click(); - console.log(`已选择${pushTime}选项`); - } - if (opts?.distance && opts.distance !== '不限') { - const distanceOption = await filterClassPanel.$(`text=${opts.distance}`); - if (distanceOption) { - await distanceOption.click(); - console.log(`已选择${opts.distance}选项`); - } - } - if (opts?.searchRange && opts.searchRange !== '不限') { - const rangeOption = await filterClassPanel.$(`text=${opts.searchRange}`); - if (rangeOption) { - await rangeOption.click(); - console.log(`已选择${opts.searchRange}选项`); - } - } - // 点击收起按钮 - const shouquButton = await filterClassPanel.$('text=收起'); - if (shouquButton) { - await shouquButton.click(); - console.log('已点击收起按钮'); - } - } - } - // 将鼠标移到页面外,移除 hover 状态 - await page.mouse.move(0, 0); - console.log('已移除 hover 状态'); - // 自动滚动页面5次以触发更多请求 - for (let i = 0; i < scrollTimes; i++) { - await page.evaluate(() => { - window.scrollBy({ - top: window.innerHeight, - left: 0, - behavior: 'smooth' - }); - }); - console.log(`已滚动页面 ${i + 1} 次`); - // 判断是否滚动到底部 - const isBottom = await page.evaluate(() => { - return (window.innerHeight + window.scrollY) >= document.body.scrollHeight; - }); - if (isBottom) { - console.log('已到达页面底部,停止滚动'); - break; - } - await sleep(2000); // 等待2秒以加载新内容 - } -} - -const listenFetchRequests = async (context: BrowserContext) => { - // 监听访问 https://edith.xiaohongshu.com/api/sns/web/v1/search/notes - // 返回对应的结果 - 使用 context 级别的路由,刷新后不会丢失 - await context.route('https://edith.xiaohongshu.com/api/sns/web/v1/search/notes*', async (route) => { - const request = route.request(); - console.log('捕获到请求:', request.url()); - await route.continue(); - }); - - // 使用 response 事件来获取响应内容 - context.on('response', async (response) => { - const url = response.url(); - // 打印所有 edith.xiaohongshu.com 的 API 响应 - if (url.includes('edith.xiaohongshu.com/api/')) { - console.log('收到 API 响应:', url); - console.log('状态:', response.status()); - if (url.includes('search/notes')) { - try { - const responseBody = await response.text(); - fs.writeFileSync(path.join(process.cwd(), 'cache', Date.now().toString() + '.json'), responseBody, 'utf-8'); - } catch (e) { - console.log('无法读取响应内容'); - } - } - - } - }); -} \ No newline at end of file +export * from './core.ts' \ No newline at end of file diff --git a/src/routes/browser/index.ts b/src/routes/browser/index.ts new file mode 100644 index 0000000..a34baff --- /dev/null +++ b/src/routes/browser/index.ts @@ -0,0 +1,2 @@ +import './pane-manager.ts'; +import './page.ts'; \ No newline at end of file diff --git a/src/routes/browser/page.ts b/src/routes/browser/page.ts new file mode 100644 index 0000000..fb271f9 --- /dev/null +++ b/src/routes/browser/page.ts @@ -0,0 +1,16 @@ +import { app, core } from "../../app.ts"; + + +app.route({ + path: 'page', + key: 'go', + middleware: ['auth'] +}).define(async (ctx) => { + const url = ctx.query?.url as string; + if (!url) { + ctx.throw!(400, '缺少 url 参数'); + } + const page = await core.getPage(); + await page.goto(url); + ctx.body = { success: true, message: `已导航到 ${url}` }; +}).addTo(app); \ No newline at end of file diff --git a/src/routes/browser/pane-manager.ts b/src/routes/browser/pane-manager.ts new file mode 100644 index 0000000..e1f9f3e --- /dev/null +++ b/src/routes/browser/pane-manager.ts @@ -0,0 +1,67 @@ +import { app, core } from "../../app.ts"; + +/** + * 浏览器窗口边界信息 + */ +export interface Bounds { + /** + * 窗口距离屏幕左边缘的偏移量(像素) + */ + left?: number; + /** + * 窗口距离屏幕上边缘的偏移量(像素) + */ + top?: number; + /** + * 窗口宽度(像素) + */ + width?: number; + /** + * 窗口高度(像素) + */ + height?: number; + /** + * 窗口状态,默认为 normal + */ + windowState?: "normal" | "minimized" | "maximized" | "fullscreen"; +} +const desc = `窗口管理,参数为窗口位置和大小信息 +参数示例: +left: 窗口左上角横坐标 +top: 窗口左上角纵坐标 +width: 窗口宽度 +height: 窗口高度 +windowState: 窗口状态,可选值有 normal(正常), minimized(最小化), maximized(最大化), fullscreen(全屏) +`; + +app.route({ + path: 'window', + key: 'manager', + description: desc, + middleware: ['auth'] +}).define(async (ctx) => { + const bounds = ctx.query as Bounds || {}; + const { left, top, width, height, windowState } = bounds; + if (!left && !top && !width && !height && !windowState) { + bounds.windowState = 'normal' + } + const page = await core.getPage(); + const cdp = await page.context().newCDPSession(page); + try { + const { windowId } = await cdp.send('Browser.getWindowForTarget'); + const page2 = { left: -1920, top: 0 } + const full = { windowState: 'fullscreen' } + // 设定传入的窗口参数 + await cdp.send('Browser.setWindowBounds', { + windowId, + bounds: bounds + }); + ctx.body = { success: true, message: '窗口已调整' }; + } catch (error) { + ctx.throw!(500, '调整窗口失败: ' + (error as Error).message); + } finally { + // 关闭session + await cdp.detach(); + } +}).addTo(app); + diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..2dd0872 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,2 @@ +import './browser/index.ts'; +import './xhs/index.ts'; diff --git a/src/routes/xhs/index.ts b/src/routes/xhs/index.ts new file mode 100644 index 0000000..c12704d --- /dev/null +++ b/src/routes/xhs/index.ts @@ -0,0 +1 @@ +import './search-notes.ts'; \ No newline at end of file diff --git a/src/routes/xhs/search-notes.ts b/src/routes/xhs/search-notes.ts new file mode 100644 index 0000000..32c408f --- /dev/null +++ b/src/routes/xhs/search-notes.ts @@ -0,0 +1,205 @@ +import { xhsNote } from '@/db/schema.ts'; +import { app, core, db } from '../../app.ts'; +import { sql } from 'drizzle-orm'; +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +import { Page } from 'playwright'; +import { Core } from '@/playwright/core.ts'; +import { sessionCache } from '@/modules/cache.ts'; +type HoverPickerOptions = { + keyword?: string; + pushTime?: '一天内' | '一周内' | '半年内'; + sort?: '综合' | '最新' | '最多点赞' | '最多评论'; + searchRange?: '不限' | '已看过' | '未看过' | '已关注'; + distance?: '不限' | '同城' | '附近'; + scrollTimes?: number; + core?: Core +} +const hoverPickerExample = async (page: Page, opts?: HoverPickerOptions) => { + const { pushTime = '一天内', sort = '最新', keyword = '', scrollTimes = 5 } = opts || {}; + core.setReady(false) + if (keyword) { + // class为 input-box + const inputBox = await page.$('.input-box'); + const input = await inputBox?.$('input'); + if (input) { + //先获取当前内容,如果一样就不输入 + const currentValue = await input.inputValue(); + if (currentValue === keyword) { + console.log('关键词已存在,无需输入'); + } else { + await input.fill(keyword); + await input.press('Enter'); + console.log(`已输入关键词: ${keyword}`); + await sleep(3000); // 等待搜索结果加载 + } + } + } + // 查找筛选按钮并保持 hover 状态 + let filterButton = await page.$('text=筛选'); + if (filterButton) { + await filterButton.hover(); + console.log('鼠标悬停在筛选按钮上'); + // 在保持 hover 的情况下,等待筛选面板出现并点击元素 + const filterClassPanel = await page.$('.filter-panel'); + if (filterClassPanel) { + console.log('筛选面板已打开'); + + // 点击最新选项 + const latestOption = await filterClassPanel.$(`text=${sort}`); + if (latestOption) { + await latestOption.click(); + console.log(`已选择${sort}选项`); + } + // 点击一周内选项 + const oneWeekOption = await filterClassPanel.$(`text=${pushTime}`); + if (oneWeekOption) { + await oneWeekOption.click(); + console.log(`已选择${pushTime}选项`); + } + core.setReady(true) + if (opts?.distance && opts.distance !== '不限') { + const distanceOption = await filterClassPanel.$(`text=${opts.distance}`); + if (distanceOption) { + await distanceOption.click(); + console.log(`已选择${opts.distance}选项`); + } + } + if (opts?.searchRange && opts.searchRange !== '不限') { + const rangeOption = await filterClassPanel.$(`text=${opts.searchRange}`); + if (rangeOption) { + await rangeOption.click(); + console.log(`已选择${opts.searchRange}选项`); + } + } + // 点击收起按钮 + const shouquButton = await filterClassPanel.$('text=收起'); + if (shouquButton) { + await shouquButton.click(); + console.log('已点击收起按钮'); + } + } + } + // 将鼠标移到页面外,移除 hover 状态 + await page.mouse.move(0, 0); + console.log('已移除 hover 状态'); + // 自动滚动页面5次以触发更多请求 + for (let i = 0; i < scrollTimes; i++) { + await page.evaluate(() => { + window.scrollBy({ + top: window.innerHeight, + left: 0, + behavior: 'smooth' + }); + }); + console.log(`已滚动页面 ${i + 1} 次`); + // 判断是否滚动到底部 + const isBottom = await page.evaluate(() => { + return (window.innerHeight + window.scrollY) >= document.body.scrollHeight; + }); + if (isBottom) { + console.log('已到达页面底部,停止滚动'); + break; + } + await sleep(2000); // 等待2秒以加载新内容 + } +} + +const searchNoteDesc = ` +根据关键词搜索小红书笔记。 +参数说明: +- keyword: 搜索关键词 +- pushTime: 笔记发布时间筛选(一天内、一周内、半年内) +- sort: 排序方式(综合、最新、最多点赞、最多评论) +- distance: 距离筛选(不限、同城、附近) +- searchRange: 搜索范围(不限、已看过、未看过、已关注) +- scrollTimes: 页面自动滚动次数,默认5次 + +其中只有keyword是必填项。 +`; +app.route({ + path: 'xhs', + key: 'search-notes', + description: searchNoteDesc, + middleware: ['auth'] +}).define( + async (ctx) => { + const query = ctx.query!; + const page = await core.getPage(); + // 获取浏览器的当前URL + const currentURL = page.url(); + if (!currentURL.includes('www.xiaohongshu.com/search_result')) { + const url = new URL('https://www.xiaohongshu.com/search_result'); + url.searchParams.set('keyword', query.keyword as string || ''); + await page.goto(url.toString()); + console.log(`导航到搜索页面: ${url.toString()}`); + await sleep(3000); // 等待页面加载 + } + const keyword = query.keyword as string; + // 存储关键词到 core 的 data 中,供响应处理使用 + sessionCache.set('xhs-search-keyword', keyword); + await hoverPickerExample(page, { + keyword: query.keyword as string, + pushTime: (query.pushTime as '一天内' | '一周内' | '半年内') || '一天内', + sort: (query.sort as '综合' | '最新' | '最多点赞' | '最多评论') || '最新', + distance: (query.distance as '不限' | '同城' | '附近') || '不限', + searchRange: (query.searchRange as '不限' | '已看过' | '未看过' | '已关注') || '不限', + scrollTimes: query.scrollTimes ?? 5, + }); + ctx.body = { success: true, message: '操作完成' }; + }).addTo(app); + + +app.route({ + path: 'xhs', + key: 'save-search-notes', + description: '保存搜索笔记结果', + middleware: ['auth'] +}).define(async (ctx) => { + const data = ctx.query!.data as XHS.SearchNote[]; + try { + const getNoteUrl = (note: XHS.SearchNote) => { + const id = note.id; + const secToken = note.xsec_token; + return `https://www.xiaohongshu.com/explore/${id}?xsec_token=${secToken}&xsec_source=pc_feed` + } + const getUserUrl = (note: XHS.SearchNote) => { + const user = note.note_card?.user; + const id = user?.user_id; + const secToken = user?.xsec_token; + if (user) { + return `https://www.xiaohongshu.com/user/profile/${id}?xsec_token=${secToken}&xsec_source=pc_note` + } + return `` + } + const getCover = (note: XHS.SearchNote) => { + const cover = note.note_card?.cover + return cover?.url_default || '' + } + const keyword = sessionCache.get('xhs-search-keyword'); + const notes = data.filter(note => note.model_type === 'note').map(note => ({ + id: note.id, + content: JSON.stringify(note), + description: keyword || '', + title: note.note_card?.display_title || '', + authorUrl: getUserUrl(note), + tags: '', + syncStatus: 0, + noteUrl: getNoteUrl(note), + cover: getCover(note), + syncAt: 0, + createdAt: Date.now(), + updatedAt: Date.now(), + })); + await db.insert(xhsNote).values(notes).onConflictDoUpdate({ + target: xhsNote.id, + set: { + content: sql`excluded.content`, + updatedAt: Date.now(), + }, + }).execute(); + console.log(`已保存 ${data.length} 条搜索笔记结果`); + } catch (error) { + console.error('保存搜索笔记结果时出错:', error); + } + ctx.body = { success: true, message: '保存搜索笔记结果功能已实现' }; +}).addTo(app); \ No newline at end of file diff --git a/src/routes/xhs/user.ts b/src/routes/xhs/user.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/test/browser.ts b/src/test/browser.ts new file mode 100644 index 0000000..1ce45c2 --- /dev/null +++ b/src/test/browser.ts @@ -0,0 +1,13 @@ +import { Command } from "commander"; +import { core, program, app, showMore } from './common.ts' + +program.addCommand(new Command('browser').action(async () => { + const res = await app.run({ + path: 'page', + key: 'go', + payload: { + url: 'https://www.xiaohongshu.com/explore' + } + }) + console.log(showMore(res)); +})); \ No newline at end of file diff --git a/src/test/cmd.ts b/src/test/cmd.ts new file mode 100644 index 0000000..b6014d5 --- /dev/null +++ b/src/test/cmd.ts @@ -0,0 +1,7 @@ +import { program } from './common.ts' +import './pane.ts' +import './browser.ts' +import './db/add.ts' +import './xhs/index.ts' + +program.parse(process.argv); \ No newline at end of file diff --git a/src/test/common.ts b/src/test/common.ts new file mode 100644 index 0000000..dcd92e5 --- /dev/null +++ b/src/test/common.ts @@ -0,0 +1,34 @@ +import util from 'node:util'; +export * from '../index.ts'; +import { Core } from '../index.ts'; +export const core = new Core(); +export const showMore = (data: any) => { + return util.inspect(data, { depth: null, colors: true }); +} +export const sleep = async (ms: number) => { + return new Promise(resolve => setTimeout(resolve, ms)); +} +export const exit = async (time: number = 0) => { + await sleep(time * 1000); + process.exit(0); +} +// const main = async () => { +// const connected = await core.connect(); +// if (!connected) { +// console.log('无法连接到浏览器'); +// return; +// } +// const page = core.page!; +// await page.goto('https://www.xiaohongshu.com/explore'); +// console.log('已打开小红书首页'); +// } + +// main() + +import { program } from 'commander'; + +program + .name('xhs-helper') + .description('CLI to manage xhs-helper tasks') + .version('0.1.0'); +export { program }; \ No newline at end of file diff --git a/src/test/db/add.ts b/src/test/db/add.ts new file mode 100644 index 0000000..e12cb66 --- /dev/null +++ b/src/test/db/add.ts @@ -0,0 +1,34 @@ +import { cache } from './../../db/schema.ts'; +import { Command } from 'commander'; +import { program, db } from '../common.ts'; + +program + .addCommand(new Command('db:add') + .description('Add initial data to the database') + .action(async () => { + const result = await db.select().from(cache).limit(1).all(); // Ensure DB is initialized + if (result.length > 0) { + console.log('Database already has data. No action taken.'); + const [a] = result; + console.log(a); + return; + } + + await db.insert(cache).values([ + { + key: 'example_key_1', + value: JSON.stringify({ data: 'example_value_1' }), + expireAt: Math.floor(Date.now() / 1000) + 7 * 86400, + createdAt: Math.floor(Date.now() / 1000), + }, + { + key: 'example_key_2', + value: JSON.stringify({ data: 'example_value_2' }), + expireAt: Math.floor(Date.now() / 1000) + 7 * 86400, + createdAt: Math.floor(Date.now() / 1000), + }, + ]); + + console.log('Initial data added to the database.'); + }) + ); \ No newline at end of file diff --git a/src/test/pane.ts b/src/test/pane.ts new file mode 100644 index 0000000..f7907a9 --- /dev/null +++ b/src/test/pane.ts @@ -0,0 +1,47 @@ +import { Command } from "commander"; +import { core, program, exit } from './common.ts' +// import size from 'window-size' +const main = async () => { + const connected = await core.connect(); + if (!connected) { + console.log('无法连接到浏览器实例,程序退出'); + process.exit(1); + } + const page = core.page!; + + // 设置页面视口大小 + const browser = core.browser!; + const cdp = await page.context().newCDPSession(page); + const { windowId } = await cdp.send('Browser.getWindowForTarget'); + // 第一个窗口(左侧) + // await cdp.send('Browser.setWindowBounds', { + // windowId, + // bounds: { left: -1920, top: 0 } + // }); + // 如何获取屏幕宽度 + // const { width, height } = size.get(); + // console.log(`屏幕宽度: ${width}, 高度: ${height}`); + // await page.goto('https://www.xiaohongshu.com/'); + await cdp.send('Browser.setWindowBounds', { + windowId, + bounds: { + windowState: 'maximized' + } + }); + // 开启全屏 + + // const session = await page.context().newCDPSession(page); + // await session.send('Emulation.setDeviceMetricsOverride', { + // width: 1920, + // height: 1080, + // deviceScaleFactor: 1, + // mobile: false, + // }); + exit(0) +} + +export const paneCommand = new Command('pane').action(async () => { + await main(); +}); + +program.addCommand(paneCommand); \ No newline at end of file diff --git a/src/test/xhs/index.ts b/src/test/xhs/index.ts new file mode 100644 index 0000000..bc61230 --- /dev/null +++ b/src/test/xhs/index.ts @@ -0,0 +1,18 @@ +import { program, app, showMore } from '../common.ts' + + +program + .command('xhs:run') + .description('运行小红书浏览器辅助') + .action(async () => { + const res = await app.run({ + path: 'xhs', + key: 'search-notes', + payload: { + keyword: '多维表格', + scrollTimes: 1, + } + }) + console.log(showMore(res)); + }); + diff --git a/start-browser.ts b/start-browser.js similarity index 66% rename from start-browser.ts rename to start-browser.js index 96ad733..6ba950c 100644 --- a/start-browser.ts +++ b/start-browser.js @@ -1,4 +1,3 @@ -import { chromium } from 'playwright'; import { spawn } from 'node:child_process'; import path from 'node:path'; @@ -15,21 +14,32 @@ export const main = async () => { // console.log('注意:需要手动登录账号和安装插件'); // 启动 Chrome(带远程调试端口) - const chromeProcess = spawn(executablePath, [ - `--remote-debugging-port=${debugPort}`, - `--user-data-dir=${userDataDir}`, - ], { - detached: false, - stdio: 'inherit', - }); + const chromeProcess = spawn( + executablePath, + [ + `--remote-debugging-port=${debugPort}`, + `--user-data-dir=${userDataDir}`, + // '--kiosk', // 全屏模式,无修改边框 + ], + { + windowsHide: true, // 隐藏 CMD 窗口 + detached: false, + stdio: ['ignore', 'ignore', 'ignore'], + }, + ); chromeProcess.on('error', (err) => { console.error('Chrome 启动失败:', err); + // 需要重新启动 }); chromeProcess.on('exit', (code, signal) => { console.log(`Chrome 进程退出,代码: ${code}, 信号: ${signal}`); + if (code === 0) { + // 重启 + main(); + } }); -} +}; -main(); \ No newline at end of file +main(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4755a65 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "@kevisual/types/json/backend.json", + "compilerOptions": { + "module": "NodeNext", + "target": "esnext", + "baseUrl": ".", + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@kevisual/types/index.d.ts", + "./typings" + ], + "paths": { + "@/*": [ + "src/*" + ], + "@agent/*": [ + "agent/*" + ] + }, + }, + "include": [ + "typings/**/*.d.ts", + "src/**/*", + "agent/**/*", + ], +} \ No newline at end of file diff --git a/typings/note.ts b/typings/note.d.ts similarity index 92% rename from typings/note.ts rename to typings/note.d.ts index ba295ef..ddffe01 100644 --- a/typings/note.ts +++ b/typings/note.d.ts @@ -1,4 +1,4 @@ -export namespace XHS { +declare namespace XHS { /** 笔记用户信息 */ export interface NoteUser { /** 昵称 */ @@ -86,11 +86,11 @@ export namespace XHS { } /** 笔记 */ - export interface Note { + export interface SearchNote { /** 笔记ID */ id: string; /** 模型类型(如note笔记) */ - model_type: string; + model_type: 'note' | 'hot_query' | string; /** 笔记卡片 */ note_card: NoteCard; /** 安全令牌 */ @@ -98,8 +98,8 @@ export namespace XHS { } } -export namespace XHS { - export interface ResultList { +declare namespace XHS { + export interface ResultList { hasMore: boolean; items: T[]; }