From d9385bc8fec8e617332c1f9ff32d9f3eb2a56ff0 Mon Sep 17 00:00:00 2001 From: Han-che Wang <60660116+Carnage1999@users.noreply.github.com> Date: Fri, 13 Mar 2026 09:11:12 +0800 Subject: [PATCH 01/15] chore: upgrade to typescript 5 and migrate runtime scripts --- Dockerfile | 12 +- README.md | 4 +- jest.config.js | 2 +- jest.setup.js | 5 - jest.setup.ts | 3 + package.json | 27 +- pnpm-lock.yaml | 464 +++++++++--------- proxy.worker.js => proxy.worker.ts | 140 +++--- ...vert-changelog.js => convert-changelog.ts} | 161 +++--- scripts/generate-manifest.js | 63 --- scripts/generate-manifest.ts | 65 +++ .../{init-postgres.js => init-postgres.ts} | 45 +- scripts/{init-sqlite.js => init-sqlite.ts} | 35 +- scripts/reset-sqlite.ts | 15 + server.js => server.ts | 360 +++++++------- server/watch-room-standalone-server.js | 65 --- server/watch-room-standalone-server.ts | 59 +++ src/lib/changelog.ts | 2 +- src/lib/m3u8-downloader.ts | 4 +- src/types/watch-room.ts | 2 +- start.js | 91 ---- start.ts | 76 +++ tsconfig.json | 4 +- 23 files changed, 837 insertions(+), 867 deletions(-) delete mode 100644 jest.setup.js create mode 100644 jest.setup.ts rename proxy.worker.js => proxy.worker.ts (67%) rename scripts/{convert-changelog.js => convert-changelog.ts} (53%) delete mode 100644 scripts/generate-manifest.js create mode 100644 scripts/generate-manifest.ts rename scripts/{init-postgres.js => init-postgres.ts} (68%) rename scripts/{init-sqlite.js => init-sqlite.ts} (61%) create mode 100644 scripts/reset-sqlite.ts rename server.js => server.ts (62%) delete mode 100644 server/watch-room-standalone-server.js create mode 100644 server/watch-room-standalone-server.ts delete mode 100644 start.js create mode 100644 start.ts diff --git a/Dockerfile b/Dockerfile index 2c646d927..dcced5640 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,10 +50,10 @@ ENV DOCKER_ENV=true COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ # 从构建器中复制 scripts 目录 COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts -# 从构建器中复制 start.js -COPY --from=builder --chown=nextjs:nodejs /app/start.js ./start.js -# 从构建器中复制自定义 server.js(包含 Socket.IO 支持) -COPY --from=builder --chown=nextjs:nodejs /app/server.js ./server.js +# 从构建器中复制 TypeScript 启动入口 +COPY --from=builder --chown=nextjs:nodejs /app/start.ts ./start.ts +# 从构建器中复制自定义 server.ts(包含 Socket.IO 支持) +COPY --from=builder --chown=nextjs:nodejs /app/server.ts ./server.ts # 从构建器中复制 public 和 .next/static 目录 COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static @@ -66,5 +66,5 @@ USER nextjs EXPOSE 3000 -# 使用自定义启动脚本,先预加载配置再启动服务器 -CMD ["node", "start.js"] +# 使用自定义 TypeScript 启动脚本,先预加载配置再启动服务器 +CMD ["./node_modules/.bin/tsx", "start.ts"] diff --git a/README.md b/README.md index 977a9edb5..76a4235be 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ![Next.js](https://img.shields.io/badge/Next.js-14-000?logo=nextdotjs) ![TailwindCSS](https://img.shields.io/badge/TailwindCSS-3-38bdf8?logo=tailwindcss) -![TypeScript](https://img.shields.io/badge/TypeScript-4.x-3178c6?logo=typescript) +![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6?logo=typescript) ![License](https://img.shields.io/badge/License-MIT-green) ![Docker Ready](https://img.shields.io/badge/Docker-ready-blue?logo=docker) @@ -79,7 +79,7 @@ | --------- | ------------------------------------------------------------ | | 前端框架 | [Next.js 14](https://nextjs.org/) · App Router | | UI & 样式 | [Tailwind CSS 3](https://tailwindcss.com/) | -| 语言 | TypeScript 4 | +| 语言 | TypeScript 5 | | 播放器 | [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) · [HLS.js](https://github.com/video-dev/hls.js/) | | 代码质量 | ESLint · Prettier · Jest | | 部署 | Docker | diff --git a/jest.config.js b/jest.config.js index 10886cb18..5b374d33d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,7 +9,7 @@ const createJestConfig = nextJest({ // Add any custom config to be passed to Jest const customJestConfig = { // Add more setup options before each test is run - setupFilesAfterEnv: ['/jest.setup.js'], + setupFilesAfterEnv: ['/jest.setup.ts'], // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work moduleDirectories: ['node_modules', '/'], diff --git a/jest.setup.js b/jest.setup.js deleted file mode 100644 index 3f1e9e10e..000000000 --- a/jest.setup.js +++ /dev/null @@ -1,5 +0,0 @@ -import '@testing-library/jest-dom/extend-expect'; - -// Allow router mocks. -// eslint-disable-next-line no-undef -jest.mock('next/router', () => require('next-router-mock')); diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 000000000..46ca8eb19 --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1,3 @@ +import '@testing-library/jest-dom/extend-expect'; + +jest.mock('next/router', () => require('next-router-mock')); \ No newline at end of file diff --git a/package.json b/package.json index 961eb179a..081c5a326 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "pnpm gen:manifest && node server.js", + "dev": "pnpm gen:manifest && tsx server.ts", "build": "pnpm gen:manifest && next build", - "build:cloudflare": "BUILD_TARGET=cloudflare pnpm gen:manifest && npx @opennextjs/cloudflare build", - "start": "NODE_ENV=production node server.js", + "build:cloudflare": "cross-env BUILD_TARGET=cloudflare pnpm gen:manifest && npx @opennextjs/cloudflare build", + "start": "tsx start.ts", "preview:cloudflare": "wrangler dev", "deploy:cloudflare": "wrangler deploy", "lint": "next lint", @@ -17,13 +17,13 @@ "test": "jest", "format": "prettier -w .", "format:check": "prettier -c .", - "gen:manifest": "node scripts/generate-manifest.js", + "gen:manifest": "tsx scripts/generate-manifest.ts", "postbuild": "echo 'Build completed - sitemap generation disabled'", "prepare": "husky install", - "watch-room:server": "node server/watch-room-standalone-server.js --port 3001 --auth YOUR_SECRET_KEY", - "init:sqlite": "node scripts/init-sqlite.js", - "init:postgres": "node scripts/init-postgres.js", - "db:reset": "rm -f .data/moontv.db .data/moontv.db-shm .data/moontv.db-wal && node scripts/init-sqlite.js" + "watch-room:server": "tsx server/watch-room-standalone-server.ts --port 3001 --auth YOUR_SECRET_KEY", + "init:sqlite": "tsx scripts/init-sqlite.ts", + "init:postgres": "tsx scripts/init-postgres.ts", + "db:reset": "tsx scripts/reset-sqlite.ts" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -74,6 +74,7 @@ "socket.io-client": "^4.8.1", "swiper": "^11.2.8", "tailwind-merge": "^2.6.0", + "tsx": "^4.19.2", "vidstack": "^0.6.15", "xml2js": "^0.6.2", "zod": "^3.24.1" @@ -99,14 +100,15 @@ "@types/react-window": "^2.0.0", "@types/testing-library__jest-dom": "^5.14.9", "@types/xml2js": "^0.4.14", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", "autoprefixer": "^10.4.20", + "cross-env": "^7.0.3", "eslint": "^8.57.1", "eslint-config-next": "^14.2.23", "eslint-config-prettier": "^8.10.0", "eslint-plugin-simple-import-sort": "^7.0.0", - "eslint-plugin-unused-imports": "^2.0.0", + "eslint-plugin-unused-imports": "^4.4.1", "husky": "^7.0.4", "jest": "^27.5.1", "lint-staged": "^12.5.0", @@ -115,8 +117,9 @@ "prettier": "^2.8.8", "prettier-plugin-tailwindcss": "^0.5.0", "tailwindcss": "^3.4.17", - "typescript": "^4.9.5", + "typescript": "^5.9.3", "vercel": "^50.4.10", + "workbox-build": "6.6.0", "webpack-obfuscator": "^3.5.1" }, "lint-staged": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 341d08282..df4411362 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: tailwind-merge: specifier: ^2.6.0 version: 2.6.0 + tsx: + specifier: ^4.19.2 + version: 4.21.0 vidstack: specifier: ^0.6.15 version: 0.6.15 @@ -173,7 +176,7 @@ importers: version: 1.15.1(next@14.2.35(@babel/core@7.28.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(wrangler@4.60.0(bufferutil@4.1.0)) '@svgr/webpack': specifier: ^8.1.0 - version: 8.1.0(typescript@4.9.5) + version: 8.1.0(typescript@5.9.3) '@tailwindcss/forms': specifier: ^0.5.10 version: 0.5.11(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) @@ -223,20 +226,23 @@ importers: specifier: ^0.4.14 version: 0.4.14 '@typescript-eslint/eslint-plugin': - specifier: ^5.62.0 - version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) + specifier: ^8.0.0 + version: 8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^5.62.0 - version: 5.62.0(eslint@8.57.1)(typescript@4.9.5) + specifier: ^8.0.0 + version: 8.57.0(eslint@8.57.1)(typescript@5.9.3) autoprefixer: specifier: ^10.4.20 version: 10.4.23(postcss@8.5.6) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^8.57.1 version: 8.57.1 eslint-config-next: specifier: ^14.2.23 - version: 14.2.35(eslint@8.57.1)(typescript@4.9.5) + version: 14.2.35(eslint@8.57.1)(typescript@5.9.3) eslint-config-prettier: specifier: ^8.10.0 version: 8.10.2(eslint@8.57.1) @@ -244,14 +250,14 @@ importers: specifier: ^7.0.0 version: 7.0.0(eslint@8.57.1) eslint-plugin-unused-imports: - specifier: ^2.0.0 - version: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) + specifier: ^4.4.1 + version: 4.4.1(@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) husky: specifier: ^7.0.4 version: 7.0.4 jest: specifier: ^27.5.1 - version: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)) + version: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)) lint-staged: specifier: ^12.5.0 version: 12.5.0(enquirer@2.4.1) @@ -271,14 +277,17 @@ importers: specifier: ^3.4.17 version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) typescript: - specifier: ^4.9.5 - version: 4.9.5 + specifier: ^5.9.3 + version: 5.9.3 vercel: specifier: ^50.4.10 - version: 50.4.10(rollup@2.79.2)(typescript@4.9.5) + version: 50.4.10(rollup@2.79.2)(typescript@5.9.3) webpack-obfuscator: specifier: ^3.5.1 version: 3.6.0(javascript-obfuscator@5.1.0)(webpack@5.104.1) + workbox-build: + specifier: 6.6.0 + version: 6.6.0(@types/babel__core@7.20.5) packages: @@ -312,28 +321,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.40.0': resolution: {integrity: sha512-MS9qalLRjUnF2PCzuTKTvCMVSORYHxxe3Qa0+SSaVULsXRBmuy5C/b1FeWwMFnwNnC0uie3VDet31Zujwi8q6A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.40.0': resolution: {integrity: sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.40.0': resolution: {integrity: sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.40.0': resolution: {integrity: sha512-9SqmnQqd4zTEUk6yx0TuW2ycZZs2+e569O/R0QnhSiQNpgwiJCYOe/yPS0BC9HkiaozQm6jjAcasWpFtz/dp+w==} @@ -2005,105 +2010,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -2311,28 +2300,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@14.2.33': resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@14.2.33': resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@14.2.33': resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@14.2.33': resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} @@ -2522,28 +2507,24 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': resolution: {integrity: sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': resolution: {integrity: sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': resolution: {integrity: sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': resolution: {integrity: sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==} @@ -3328,9 +3309,6 @@ packages: '@types/resolve@1.17.1': resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -3361,63 +3339,64 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@5.62.0': - resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/eslint-plugin@8.57.0': + resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser': ^8.57.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@5.62.0': - resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/parser@8.57.0': + resolution: {integrity: sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@5.62.0': - resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/project-service@8.57.0': + resolution: {integrity: sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@5.62.0': - resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/scope-manager@8.57.0': + resolution: {integrity: sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.57.0': + resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@5.62.0': - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/type-utils@8.57.0': + resolution: {integrity: sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@5.62.0': - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/types@8.57.0': + resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.57.0': + resolution: {integrity: sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@5.62.0': - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/utils@8.57.0': + resolution: {integrity: sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@5.62.0': - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/visitor-keys@8.57.0': + resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -3461,49 +3440,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -4050,6 +4021,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} @@ -4100,6 +4075,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -4465,6 +4444,11 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -4969,20 +4953,15 @@ packages: peerDependencies: eslint: '>=5.0.0' - eslint-plugin-unused-imports@2.0.0: - resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-plugin-unused-imports@4.4.1: + resolution: {integrity: sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==} peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 - eslint: ^8.0.0 + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^10.0.0 || ^9.0.0 || ^8.0.0 peerDependenciesMeta: '@typescript-eslint/eslint-plugin': optional: true - eslint-rule-composer@0.3.0: - resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} - engines: {node: '>=4.0.0'} - eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -5003,6 +4982,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5514,6 +5497,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -6529,6 +6516,10 @@ packages: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -6643,9 +6634,6 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true - natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -8092,6 +8080,12 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -8141,12 +8135,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsutils@3.21.0: - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -11644,7 +11632,7 @@ snapshots: jest-util: 27.5.1 slash: 3.0.0 - '@jest/core@27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5))': + '@jest/core@27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3))': dependencies: '@jest/console': 27.5.1 '@jest/reporters': 27.5.1 @@ -11658,7 +11646,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 27.5.1 - jest-config: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)) + jest-config: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)) jest-haste-map: 27.5.1 jest-message-util: 27.5.1 jest-regex-util: 27.5.1 @@ -12864,12 +12852,12 @@ snapshots: '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.5) '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.5) - '@svgr/core@8.1.0(typescript@4.9.5)': + '@svgr/core@8.1.0(typescript@5.9.3)': dependencies: '@babel/core': 7.28.5 '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@4.9.5) + cosmiconfig: 8.3.6(typescript@5.9.3) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -12880,35 +12868,35 @@ snapshots: '@babel/types': 7.28.5 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@4.9.5))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': dependencies: '@babel/core': 7.28.5 '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) - '@svgr/core': 8.1.0(typescript@4.9.5) + '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@4.9.5))(typescript@4.9.5)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)': dependencies: - '@svgr/core': 8.1.0(typescript@4.9.5) - cosmiconfig: 8.3.6(typescript@4.9.5) + '@svgr/core': 8.1.0(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@5.9.3) deepmerge: 4.3.1 svgo: 3.3.2 transitivePeerDependencies: - typescript - '@svgr/webpack@8.1.0(typescript@4.9.5)': + '@svgr/webpack@8.1.0(typescript@5.9.3)': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.28.5) '@babel/preset-env': 7.28.5(@babel/core@7.28.5) '@babel/preset-react': 7.28.5(@babel/core@7.28.5) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) - '@svgr/core': 8.1.0(typescript@4.9.5) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@4.9.5)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@4.9.5))(typescript@4.9.5) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) transitivePeerDependencies: - supports-color - typescript @@ -13184,8 +13172,6 @@ snapshots: dependencies: '@types/node': 24.0.3 - '@types/semver@7.7.1': {} - '@types/stack-utils@2.0.3': {} '@types/testing-library__jest-dom@5.14.9': @@ -13214,89 +13200,96 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)': + '@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) - '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) - debug: 4.4.3(supports-color@9.4.0) + '@typescript-eslint/parser': 8.57.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.0 eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare-lite: 1.4.0 - semver: 7.7.3 - tsutils: 3.21.0(typescript@4.9.5) - optionalDependencies: - typescript: 4.9.5 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5)': + '@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.0 debug: 4.4.3(supports-color@9.4.0) eslint: 8.57.1 - optionalDependencies: - typescript: 4.9.5 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.57.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + debug: 4.4.3(supports-color@9.4.0) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@5.62.0': + '@typescript-eslint/scope-manager@8.57.0': dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 - '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@4.9.5)': + '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) - '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.57.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@8.57.1)(typescript@5.9.3) debug: 4.4.3(supports-color@9.4.0) eslint: 8.57.1 - tsutils: 3.21.0(typescript@4.9.5) - optionalDependencies: - typescript: 4.9.5 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@5.62.0': {} + '@typescript-eslint/types@8.57.0': {} - '@typescript-eslint/typescript-estree@5.62.0(typescript@4.9.5)': + '@typescript-eslint/typescript-estree@8.57.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 + '@typescript-eslint/project-service': 8.57.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 debug: 4.4.3(supports-color@9.4.0) - globby: 11.1.0 - is-glob: 4.0.3 + minimatch: 10.2.4 semver: 7.7.3 - tsutils: 3.21.0(typescript@4.9.5) - optionalDependencies: - typescript: 4.9.5 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@4.9.5)': + '@typescript-eslint/utils@8.57.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.7.1 - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) eslint: 8.57.1 - eslint-scope: 5.1.1 - semver: 7.7.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@5.62.0': + '@typescript-eslint/visitor-keys@8.57.0': dependencies: - '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.57.0 + eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} @@ -13363,9 +13356,9 @@ snapshots: dependencies: uncrypto: 0.1.3 - '@vercel/backends@0.0.23(rollup@2.79.2)(typescript@4.9.5)': + '@vercel/backends@0.0.23(rollup@2.79.2)(typescript@5.9.3)': dependencies: - '@vercel/cervel': 0.0.10(typescript@4.9.5) + '@vercel/cervel': 0.0.10(typescript@5.9.3) '@vercel/introspection': 0.0.10 '@vercel/nft': 1.1.1(rollup@2.79.2) '@vercel/static-config': 3.1.2 @@ -13386,13 +13379,13 @@ snapshots: '@vercel/build-utils@13.2.15': {} - '@vercel/cervel@0.0.10(typescript@4.9.5)': + '@vercel/cervel@0.0.10(typescript@5.9.3)': dependencies: execa: 3.2.0 rolldown: 1.0.0-beta.59 srvx: 0.8.9 tsx: 4.21.0 - typescript: 4.9.5 + typescript: 5.9.3 '@vercel/detect-agent@1.0.0': {} @@ -13409,9 +13402,9 @@ snapshots: '@vercel/error-utils@2.0.3': {} - '@vercel/express@0.1.33(rollup@2.79.2)(typescript@4.9.5)': + '@vercel/express@0.1.33(rollup@2.79.2)(typescript@5.9.3)': dependencies: - '@vercel/cervel': 0.0.10(typescript@4.9.5) + '@vercel/cervel': 0.0.10(typescript@5.9.3) '@vercel/nft': 1.1.1(rollup@2.79.2) '@vercel/node': 5.5.27(rollup@2.79.2) '@vercel/static-config': 3.1.2 @@ -14129,6 +14122,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base-x@5.0.1: {} base64-js@1.5.1: {} @@ -14185,6 +14180,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -14533,17 +14532,21 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@8.3.6(typescript@4.9.5): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 4.9.5 + typescript: 5.9.3 create-require@1.1.1: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -15062,21 +15065,21 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@14.2.35(eslint@8.57.1)(typescript@4.9.5): + eslint-config-next@14.2.35(eslint@8.57.1)(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 14.2.35 '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) optionalDependencies: - typescript: 4.9.5 + typescript: 5.9.3 transitivePeerDependencies: - eslint-import-resolver-webpack - eslint-plugin-import-x @@ -15105,22 +15108,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/parser': 8.57.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -15131,7 +15134,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15143,7 +15146,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/parser': 8.57.0(eslint@8.57.1)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -15198,14 +15201,11 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-unused-imports@2.0.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-rule-composer: 0.3.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) - - eslint-rule-composer@0.3.0: {} + '@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) eslint-scope@5.1.1: dependencies: @@ -15226,6 +15226,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@8.57.1: dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) @@ -15862,6 +15864,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -16207,16 +16211,16 @@ snapshots: transitivePeerDependencies: - supports-color - jest-cli@27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)): + jest-cli@27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)): dependencies: - '@jest/core': 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)) + '@jest/core': 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)) '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.2.0 - jest-config: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)) + jest-config: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)) jest-util: 27.5.1 jest-validate: 27.5.1 prompts: 2.4.2 @@ -16228,7 +16232,7 @@ snapshots: - ts-node - utf-8-validate - jest-config@27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)): + jest-config@27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 27.5.1 @@ -16255,7 +16259,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - ts-node: 10.9.2(@types/node@24.0.3)(typescript@4.9.5) + ts-node: 10.9.2(@types/node@24.0.3)(typescript@5.9.3) transitivePeerDependencies: - bufferutil - canvas @@ -16573,11 +16577,11 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)): + jest@27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)): dependencies: - '@jest/core': 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)) + '@jest/core': 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)) import-local: 3.2.0 - jest-cli: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5)) + jest-cli: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3)) transitivePeerDependencies: - bufferutil - canvas @@ -17431,6 +17435,10 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.0 + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -17529,8 +17537,6 @@ snapshots: napi-postinstall@0.3.4: {} - natural-compare-lite@1.4.0: {} - natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -19078,6 +19084,10 @@ snapshots: trough@2.2.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-interface-checker@0.1.13: {} ts-morph@12.0.0: @@ -19121,6 +19131,25 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + ts-node@10.9.2(@types/node@24.0.3)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.0.3 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + ts-toolbelt@6.15.5: {} ts-tqdm@0.8.6: {} @@ -19136,11 +19165,6 @@ snapshots: tslib@2.8.1: {} - tsutils@3.21.0(typescript@4.9.5): - dependencies: - tslib: 1.14.1 - typescript: 4.9.5 - tsx@4.21.0: dependencies: esbuild: 0.27.0 @@ -19426,14 +19450,14 @@ snapshots: vary@1.1.2: {} - vercel@50.4.10(rollup@2.79.2)(typescript@4.9.5): + vercel@50.4.10(rollup@2.79.2)(typescript@5.9.3): dependencies: - '@vercel/backends': 0.0.23(rollup@2.79.2)(typescript@4.9.5) + '@vercel/backends': 0.0.23(rollup@2.79.2)(typescript@5.9.3) '@vercel/blob': 1.0.2 '@vercel/build-utils': 13.2.15 '@vercel/detect-agent': 1.0.0 '@vercel/elysia': 0.1.26(rollup@2.79.2) - '@vercel/express': 0.1.33(rollup@2.79.2)(typescript@4.9.5) + '@vercel/express': 0.1.33(rollup@2.79.2)(typescript@5.9.3) '@vercel/fastify': 0.1.29(rollup@2.79.2) '@vercel/fun': 1.2.1 '@vercel/go': 3.3.4 diff --git a/proxy.worker.js b/proxy.worker.ts similarity index 67% rename from proxy.worker.js rename to proxy.worker.ts index b31c97fdf..eb6baa060 100644 --- a/proxy.worker.js +++ b/proxy.worker.ts @@ -1,14 +1,21 @@ /* eslint-disable */ -addEventListener('fetch', (event) => { - event.respondWith(handleRequest(event.request)); -}); +type WorkerFetchEvent = Event & { + request: Request; + respondWith(response: Response | Promise): void; +}; -async function handleRequest(request) { +const onFetch: EventListener = (event) => { + const fetchEvent = event as WorkerFetchEvent; + fetchEvent.respondWith(handleRequest(fetchEvent.request)); +}; + +addEventListener('fetch', onFetch); + +async function handleRequest(request: Request): Promise { try { const url = new URL(request.url); - // 如果访问根目录,返回HTML if (url.pathname === '/') { return new Response(getRootHtml(), { headers: { @@ -17,142 +24,113 @@ async function handleRequest(request) { }); } - // 从请求路径中提取目标 URL - let actualUrlStr = decodeURIComponent(url.pathname.replace('/', '')); - - // 判断用户输入的 URL 是否带有协议 - actualUrlStr = ensureProtocol(actualUrlStr, url.protocol); - - // 保留查询参数 - actualUrlStr += url.search; - - // 创建新 Headers 对象,排除以 'cf-' 开头的请求头 - const newHeaders = filterHeaders( - request.headers, - (name) => !name.startsWith('cf-') - ); + let actualUrl = decodeURIComponent(url.pathname.replace('/', '')); + actualUrl = ensureProtocol(actualUrl, url.protocol); + actualUrl += url.search; - // 创建一个新的请求以访问目标 URL - const modifiedRequest = new Request(actualUrlStr, { + const newHeaders = filterHeaders(request.headers, (name) => !name.startsWith('cf-')); + const modifiedRequest = new Request(actualUrl, { headers: newHeaders, method: request.method, body: request.body, redirect: 'manual', }); - // 发起对目标 URL 的请求 const response = await fetch(modifiedRequest); - let body = response.body; + let body: BodyInit | ReadableStream | null = response.body; - // 处理重定向 if ([301, 302, 303, 307, 308].includes(response.status)) { - body = response.body; - // 创建新的 Response 对象以修改 Location 头部 return handleRedirect(response, body); - } else if (response.headers.get('Content-Type')?.includes('text/html')) { - body = await handleHtmlContent( - response, - url.protocol, - url.host, - actualUrlStr - ); } - // 创建修改后的响应对象 + if (response.headers.get('Content-Type')?.includes('text/html')) { + body = await handleHtmlContent(response, url.protocol, url.host, actualUrl); + } + const modifiedResponse = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers, }); - // 添加禁用缓存的头部 setNoCacheHeaders(modifiedResponse.headers); - - // 添加 CORS 头部,允许跨域访问 setCorsHeaders(modifiedResponse.headers); return modifiedResponse; } catch (error) { - // 如果请求目标地址时出现错误,返回带有错误消息的响应和状态码 500(服务器错误) - return jsonResponse( - { - error: error.message, - }, - 500 - ); + const message = error instanceof Error ? error.message : String(error); + return jsonResponse({ error: message }, 500); } } -// 确保 URL 带有协议 -function ensureProtocol(url, defaultProtocol) { - return url.startsWith('http://') || url.startsWith('https://') - ? url - : defaultProtocol + '//' + url; +function ensureProtocol(url: string, defaultProtocol: string): string { + return url.startsWith('http://') || url.startsWith('https://') ? url : `${defaultProtocol}//${url}`; } -// 处理重定向 -function handleRedirect(response, body) { - const location = new URL(response.headers.get('location')); +function handleRedirect( + response: Response, + body: BodyInit | ReadableStream | null +): Response { + const locationHeader = response.headers.get('location'); + if (!locationHeader) { + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } + + const location = new URL(locationHeader); const modifiedLocation = `/${encodeURIComponent(location.toString())}`; + const headers = new Headers(response.headers); + headers.set('Location', modifiedLocation); + return new Response(body, { status: response.status, statusText: response.statusText, - headers: { - ...response.headers, - Location: modifiedLocation, - }, + headers, }); } -// 处理 HTML 内容中的相对路径 -async function handleHtmlContent(response, protocol, host, actualUrlStr) { +async function handleHtmlContent( + response: Response, + protocol: string, + host: string, + actualUrl: string +): Promise { const originalText = await response.text(); - const regex = new RegExp('((href|src|action)=["\'])/(?!/)', 'g'); - let modifiedText = replaceRelativePaths( - originalText, - protocol, - host, - new URL(actualUrlStr).origin - ); - - return modifiedText; + return replaceRelativePaths(originalText, protocol, host, new URL(actualUrl).origin); } -// 替换 HTML 内容中的相对路径 -function replaceRelativePaths(text, protocol, host, origin) { +function replaceRelativePaths(text: string, protocol: string, host: string, origin: string): string { const regex = new RegExp('((href|src|action)=["\'])/(?!/)', 'g'); return text.replace(regex, `$1${protocol}//${host}/${origin}/`); } -// 返回 JSON 格式的响应 -function jsonResponse(data, status) { +function jsonResponse(data: Record, status: number): Response { return new Response(JSON.stringify(data), { - status: status, + status, headers: { 'Content-Type': 'application/json; charset=utf-8', }, }); } -// 过滤请求头 -function filterHeaders(headers, filterFunc) { +function filterHeaders(headers: Headers, filterFunc: (name: string) => boolean): Headers { return new Headers([...headers].filter(([name]) => filterFunc(name))); } -// 设置禁用缓存的头部 -function setNoCacheHeaders(headers) { +function setNoCacheHeaders(headers: Headers): void { headers.set('Cache-Control', 'no-store'); } -// 设置 CORS 头部 -function setCorsHeaders(headers) { +function setCorsHeaders(headers: Headers): void { headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); headers.set('Access-Control-Allow-Headers', '*'); } -// 返回根目录的 HTML -function getRootHtml() { +function getRootHtml(): string { return ` @@ -237,4 +215,4 @@ function getRootHtml() { `; -} +} \ No newline at end of file diff --git a/scripts/convert-changelog.js b/scripts/convert-changelog.ts similarity index 53% rename from scripts/convert-changelog.js rename to scripts/convert-changelog.ts index c88c8f827..8890d7fe8 100644 --- a/scripts/convert-changelog.js +++ b/scripts/convert-changelog.ts @@ -1,24 +1,36 @@ -#!/usr/bin / env node +#!/usr/bin/env node -/* eslint-disable */ +import fs from 'node:fs'; +import path from 'node:path'; -const fs = require('fs'); -const path = require('path'); +interface ParsedVersion { + version: string; + date: string; + added: string[]; + changed: string[]; + fixed: string[]; + content: string[]; +} -function parseChangelog(content) { +interface OutputVersion { + version: string; + date: string; + added: string[]; + changed: string[]; + fixed: string[]; +} + +function parseChangelog(content: string): { versions: OutputVersion[] } { const lines = content.split('\n'); - const versions = []; - let currentVersion = null; - let currentSection = null; + const versions: ParsedVersion[] = []; + let currentVersion: ParsedVersion | null = null; + let currentSection: 'added' | 'changed' | 'fixed' | null = null; let inVersionContent = false; for (const line of lines) { const trimmedLine = line.trim(); + const versionMatch = trimmedLine.match(/^## \[([\d.]+)\] - (\d{4}-\d{2}-\d{2})$/); - // 匹配版本行: ## [X.Y.Z] - YYYY-MM-DD - const versionMatch = trimmedLine.match( - /^## \[([\d.]+)\] - (\d{4}-\d{2}-\d{2})$/ - ); if (versionMatch) { if (currentVersion) { versions.push(currentVersion); @@ -30,78 +42,70 @@ function parseChangelog(content) { added: [], changed: [], fixed: [], - content: [], // 用于存储原始内容,当没有分类时使用 + content: [], }; currentSection = null; inVersionContent = true; continue; } - // 如果遇到下一个版本或到达文件末尾,停止处理当前版本 - if (inVersionContent && currentVersion) { - // 匹配章节标题 - if (trimmedLine === '### Added') { - currentSection = 'added'; - continue; - } else if (trimmedLine === '### Changed') { - currentSection = 'changed'; - continue; - } else if (trimmedLine === '### Fixed') { - currentSection = 'fixed'; - continue; - } + if (!inVersionContent || !currentVersion) { + continue; + } - // 匹配条目: - 内容 - if (trimmedLine.startsWith('- ') && currentSection) { - const entry = trimmedLine.substring(2); - currentVersion[currentSection].push(entry); - } else if ( - trimmedLine && - !trimmedLine.startsWith('#') && - !trimmedLine.startsWith('###') - ) { - currentVersion.content.push(trimmedLine); - } + if (trimmedLine === '### Added') { + currentSection = 'added'; + continue; + } + + if (trimmedLine === '### Changed') { + currentSection = 'changed'; + continue; + } + + if (trimmedLine === '### Fixed') { + currentSection = 'fixed'; + continue; + } + + if (trimmedLine.startsWith('- ') && currentSection) { + currentVersion[currentSection].push(trimmedLine.substring(2)); + continue; + } + + if (trimmedLine && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('###')) { + currentVersion.content.push(trimmedLine); } } - // 添加最后一个版本 if (currentVersion) { versions.push(currentVersion); } - // 后处理:如果某个版本没有分类内容,但有 content,则将 content 放到 changed 中 - versions.forEach((version) => { - const hasCategories = - version.added.length > 0 || - version.changed.length > 0 || - version.fixed.length > 0; - if (!hasCategories && version.content.length > 0) { - version.changed = version.content; - } - // 清理 content 字段 - delete version.content; + const normalizedVersions = versions.map((version) => { + const hasCategories = version.added.length > 0 || version.changed.length > 0 || version.fixed.length > 0; + return { + version: version.version, + date: version.date, + added: version.added, + changed: hasCategories ? version.changed : version.content, + fixed: version.fixed, + }; }); - return { versions }; + return { versions: normalizedVersions }; } -function generateTypeScript(changelogData) { +function generateTypeScript(changelogData: { versions: OutputVersion[] }): string { const entries = changelogData.versions .map((version) => { - const addedEntries = version.added - .map((entry) => ` "${entry}"`) - .join(',\n'); - const changedEntries = version.changed - .map((entry) => ` "${entry}"`) - .join(',\n'); - const fixedEntries = version.fixed - .map((entry) => ` "${entry}"`) - .join(',\n'); + const addedEntries = version.added.map((entry) => ` ${JSON.stringify(entry)}`).join(',\n'); + const changedEntries = version.changed.map((entry) => ` ${JSON.stringify(entry)}`).join(',\n'); + const fixedEntries = version.fixed.map((entry) => ` ${JSON.stringify(entry)}`).join(',\n'); return ` { - version: "${version.version}", - date: "${version.date}", + version: ${JSON.stringify(version.version)}, + date: ${JSON.stringify(version.date)}, added: [ ${addedEntries || ' // 无新增内容'} ], @@ -115,7 +119,7 @@ ${fixedEntries || ' // 无修复内容'} }) .join(',\n'); - return `// 此文件由 scripts/convert-changelog.js 自动生成 + return `// 此文件由 scripts/convert-changelog.ts 自动生成 // 请勿手动编辑 export interface ChangelogEntry { @@ -134,23 +138,22 @@ export default changelog; `; } -function updateVersionFile(version) { +function updateVersionFile(version: string): void { const versionTxtPath = path.join(process.cwd(), 'VERSION.txt'); try { fs.writeFileSync(versionTxtPath, version, 'utf8'); console.log(`✅ 已更新 VERSION.txt: ${version}`); } catch (error) { - console.error(`❌ 无法更新 VERSION.txt:`, error.message); + const message = error instanceof Error ? error.message : String(error); + console.error('❌ 无法更新 VERSION.txt:', message); process.exit(1); } } -function updateVersionTs(version) { +function updateVersionTs(version: string): void { const versionTsPath = path.join(process.cwd(), 'src/lib/version.ts'); try { - let content = fs.readFileSync(versionTsPath, 'utf8'); - - // 替换 CURRENT_VERSION 常量 + const content = fs.readFileSync(versionTsPath, 'utf8'); const updatedContent = content.replace( /const CURRENT_VERSION = ['"`][^'"`]+['"`];/, `const CURRENT_VERSION = '${version}';` @@ -159,12 +162,13 @@ function updateVersionTs(version) { fs.writeFileSync(versionTsPath, updatedContent, 'utf8'); console.log(`✅ 已更新 version.ts: ${version}`); } catch (error) { - console.error(`❌ 无法更新 version.ts:`, error.message); + const message = error instanceof Error ? error.message : String(error); + console.error('❌ 无法更新 version.ts:', message); process.exit(1); } } -function main() { +function main(): void { try { const changelogPath = path.join(process.cwd(), 'CHANGELOG'); const outputPath = path.join(process.cwd(), 'src/lib/changelog.ts'); @@ -180,14 +184,12 @@ function main() { process.exit(1); } - // 获取最新版本号(CHANGELOG中的第一个版本) const latestVersion = changelogData.versions[0].version; console.log(`🔢 最新版本: ${latestVersion}`); console.log('正在生成 TypeScript 文件...'); const tsContent = generateTypeScript(changelogData); - // 确保输出目录存在 const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); @@ -195,22 +197,17 @@ function main() { fs.writeFileSync(outputPath, tsContent, 'utf-8'); - // 检查是否在 GitHub Actions 环境中运行 - const isGitHubActions = process.env.GITHUB_ACTIONS === 'true'; - - if (isGitHubActions) { - // 在 GitHub Actions 中,更新版本文件 + if (process.env.GITHUB_ACTIONS === 'true') { console.log('正在更新版本文件...'); updateVersionFile(latestVersion); updateVersionTs(latestVersion); } else { - // 在本地运行时,只提示但不更新版本文件 console.log('🔧 本地运行模式:跳过版本文件更新'); console.log('💡 版本文件更新将在 git tag 触发的 release 工作流中完成'); } console.log(`✅ 成功生成 ${outputPath}`); - console.log(`📊 版本统计:`); + console.log('📊 版本统计:'); changelogData.versions.forEach((version) => { console.log( ` ${version.version} (${version.date}): +${version.added.length} ~${version.changed.length} !${version.fixed.length}` @@ -224,6 +221,4 @@ function main() { } } -if (require.main === module) { - main(); -} +main(); \ No newline at end of file diff --git a/scripts/generate-manifest.js b/scripts/generate-manifest.js deleted file mode 100644 index 7484f6bc3..000000000 --- a/scripts/generate-manifest.js +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable */ -// 根据 NEXT_PUBLIC_SITE_NAME 动态生成 manifest.json - -const fs = require('fs'); -const path = require('path'); - -// 获取项目根目录 -const projectRoot = path.resolve(__dirname, '..'); -const publicDir = path.join(projectRoot, 'public'); -const manifestPath = path.join(publicDir, 'manifest.json'); - -// 从环境变量获取站点名称 -const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTVPlus'; - -// manifest.json 模板 -const manifestTemplate = { - name: siteName, - short_name: siteName, - description: '影视聚合', - start_url: '/', - scope: '/', - display: 'standalone', - background_color: '#000000', - 'apple-mobile-web-app-capable': 'yes', - 'apple-mobile-web-app-status-bar-style': 'black', - icons: [ - { - src: '/icons/icon-192x192.png', - sizes: '192x192', - type: 'image/png', - }, - { - src: '/icons/icon-256x256.png', - sizes: '256x256', - type: 'image/png', - }, - { - src: '/icons/icon-384x384.png', - sizes: '384x384', - type: 'image/png', - }, - { - src: '/icons/icon-512x512.png', - sizes: '512x512', - type: 'image/png', - }, - ], -}; - -try { - // 确保 public 目录存在 - if (!fs.existsSync(publicDir)) { - fs.mkdirSync(publicDir, { recursive: true }); - } - - // 写入 manifest.json - fs.writeFileSync(manifestPath, JSON.stringify(manifestTemplate, null, 2)); - console.log(`✅ Generated manifest.json with site name: ${siteName}`); -} catch (error) { - console.error('❌ Error generating manifest.json:', error); - process.exit(1); -} diff --git a/scripts/generate-manifest.ts b/scripts/generate-manifest.ts new file mode 100644 index 000000000..515086763 --- /dev/null +++ b/scripts/generate-manifest.ts @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +interface ManifestIcon { + src: string; + sizes: string; + type: string; +} + +interface WebManifest { + name: string; + short_name: string; + description: string; + start_url: string; + scope: string; + display: 'standalone'; + background_color: string; + 'apple-mobile-web-app-capable': 'yes'; + 'apple-mobile-web-app-status-bar-style': 'black'; + icons: ManifestIcon[]; +} + +export function generateManifest(): void { + const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + const publicDir = path.join(projectRoot, 'public'); + const manifestPath = path.join(publicDir, 'manifest.json'); + const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTVPlus'; + + const manifestTemplate: WebManifest = { + name: siteName, + short_name: siteName, + description: '影视聚合', + start_url: '/', + scope: '/', + display: 'standalone', + background_color: '#000000', + 'apple-mobile-web-app-capable': 'yes', + 'apple-mobile-web-app-status-bar-style': 'black', + icons: [ + { src: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' }, + { src: '/icons/icon-256x256.png', sizes: '256x256', type: 'image/png' }, + { src: '/icons/icon-384x384.png', sizes: '384x384', type: 'image/png' }, + { src: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' }, + ], + }; + + try { + if (!fs.existsSync(publicDir)) { + fs.mkdirSync(publicDir, { recursive: true }); + } + + fs.writeFileSync(manifestPath, JSON.stringify(manifestTemplate, null, 2)); + console.log(`✅ Generated manifest.json with site name: ${siteName}`); + } catch (error) { + console.error('❌ Error generating manifest.json:', error); + process.exit(1); + } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + generateManifest(); +} \ No newline at end of file diff --git a/scripts/init-postgres.js b/scripts/init-postgres.ts similarity index 68% rename from scripts/init-postgres.js rename to scripts/init-postgres.ts index b52a71a34..93f424fcb 100644 --- a/scripts/init-postgres.js +++ b/scripts/init-postgres.ts @@ -1,34 +1,25 @@ -/** - * Vercel Postgres 数据库初始化脚本 - * - * 创建数据库表结构并初始化默认管理员用户 - */ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; -const { sql } = require('@vercel/postgres'); -const crypto = require('crypto'); +import { sql } from '@vercel/postgres'; -// SHA-256 加密密码 -function hashPassword(password) { +function hashPassword(password: string): string { return crypto.createHash('sha256').update(password).digest('hex'); } console.log('📦 Initializing Vercel Postgres database...'); -// 读取迁移脚本 -const fs = require('fs'); -const path = require('path'); - -// 获取所有迁移文件 -const migrationsDir = path.join(__dirname, '../migrations/postgres'); +const migrationsDir = path.join(process.cwd(), 'migrations/postgres'); if (!fs.existsSync(migrationsDir)) { console.error('❌ Migrations directory not found:', migrationsDir); process.exit(1); } -// 读取并排序所有 .sql 文件 -const migrationFiles = fs.readdirSync(migrationsDir) - .filter(file => file.endsWith('.sql')) - .sort(); // 按文件名排序,确保按顺序执行 +const migrationFiles = fs + .readdirSync(migrationsDir) + .filter((fileName) => fileName.endsWith('.sql')) + .sort(); if (migrationFiles.length === 0) { console.error('❌ No migration files found in:', migrationsDir); @@ -37,9 +28,8 @@ if (migrationFiles.length === 0) { console.log(`📄 Found ${migrationFiles.length} migration file(s):`, migrationFiles.join(', ')); -async function init() { +async function main(): Promise { try { - // 执行所有迁移脚本 console.log('🔧 Running database migrations...'); for (const migrationFile of migrationFiles) { @@ -47,12 +37,10 @@ async function init() { console.log(` ⏳ Executing ${migrationFile}...`); const schemaSql = fs.readFileSync(sqlPath, 'utf8'); - - // 将 SQL 脚本按语句分割并逐个执行 const statements = schemaSql .split(';') - .map(s => s.trim()) - .filter(s => s.length > 0); + .map((statement) => statement.trim()) + .filter((statement) => statement.length > 0); for (const statement of statements) { await sql.query(statement); @@ -63,7 +51,6 @@ async function init() { console.log('✅ All migrations completed successfully!'); - // 创建默认管理员用户 const username = process.env.USERNAME || 'admin'; const password = process.env.PASSWORD || '123456789'; const passwordHash = hashPassword(password); @@ -83,10 +70,10 @@ async function init() { console.log('1. Set NEXT_PUBLIC_STORAGE_TYPE=postgres in .env'); console.log('2. Set POSTGRES_URL environment variable'); console.log('3. Run: npm run dev'); - } catch (err) { - console.error('❌ Initialization failed:', err); + } catch (error) { + console.error('❌ Initialization failed:', error); process.exit(1); } } -init(); +void main(); \ No newline at end of file diff --git a/scripts/init-sqlite.js b/scripts/init-sqlite.ts similarity index 61% rename from scripts/init-sqlite.js rename to scripts/init-sqlite.ts index 2d1d5ac69..e2d55ccde 100644 --- a/scripts/init-sqlite.js +++ b/scripts/init-sqlite.ts @@ -1,54 +1,49 @@ -const Database = require('better-sqlite3'); -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; -// SHA-256 加密密码(与 Redis 保持一致) -function hashPassword(password) { +import Database from 'better-sqlite3'; + +function hashPassword(password: string): string { return crypto.createHash('sha256').update(password).digest('hex'); } -// 确保 .data 目录存在 -const dataDir = path.join(__dirname, '../.data'); +const dataDir = path.join(process.cwd(), '.data'); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } -// 创建数据库 const dbPath = path.join(dataDir, 'moontv.db'); const db = new Database(dbPath); console.log('📦 Initializing SQLite database for development...'); console.log('📍 Database location:', dbPath); -// 读取迁移脚本 -const migrationPath = path.join(__dirname, '../migrations/001_initial_schema.sql'); +const migrationPath = path.join(process.cwd(), 'migrations/001_initial_schema.sql'); if (!fs.existsSync(migrationPath)) { console.error('❌ Migration file not found:', migrationPath); process.exit(1); } -const sql = fs.readFileSync(migrationPath, 'utf8'); +const migrationSql = fs.readFileSync(migrationPath, 'utf8'); -// 执行迁移 try { - db.exec(sql); + db.exec(migrationSql); console.log('✅ Database schema created successfully!'); - // 创建默认管理员用户(可选) const username = process.env.USERNAME || 'admin'; const password = process.env.PASSWORD || '123456789'; const passwordHash = hashPassword(password); - const stmt = db.prepare(` + const statement = db.prepare(` INSERT OR IGNORE INTO users (username, password_hash, role, created_at, playrecord_migrated, favorite_migrated, skip_migrated) VALUES (?, ?, 'owner', ?, 1, 1, 1) `); - stmt.run(username, passwordHash, Date.now()); + statement.run(username, passwordHash, Date.now()); console.log(`✅ Default admin user created: ${username}`); -} catch (err) { - console.error('❌ Migration failed:', err); +} catch (error) { + console.error('❌ Migration failed:', error); process.exit(1); } finally { db.close(); @@ -59,4 +54,4 @@ console.log('🎉 SQLite database initialized successfully!'); console.log(''); console.log('Next steps:'); console.log('1. Set NEXT_PUBLIC_STORAGE_TYPE=d1 in .env'); -console.log('2. Run: npm run dev'); +console.log('2. Run: npm run dev'); \ No newline at end of file diff --git a/scripts/reset-sqlite.ts b/scripts/reset-sqlite.ts new file mode 100644 index 000000000..f11798908 --- /dev/null +++ b/scripts/reset-sqlite.ts @@ -0,0 +1,15 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const filesToDelete = ['moontv.db', 'moontv.db-shm', 'moontv.db-wal']; +const dataDir = path.join(process.cwd(), '.data'); + +for (const fileName of filesToDelete) { + const filePath = path.join(dataDir, fileName); + if (fs.existsSync(filePath)) { + fs.rmSync(filePath); + console.log(`🗑 Deleted ${filePath}`); + } +} + +await import('./init-sqlite'); \ No newline at end of file diff --git a/server.js b/server.ts similarity index 62% rename from server.js rename to server.ts index ebf7e94a6..ed199e30d 100644 --- a/server.js +++ b/server.ts @@ -1,22 +1,34 @@ -// Next.js 自定义服务器 + Socket.IO -const { createServer } = require('http'); -const { parse } = require('url'); -const next = require('next'); -const { Server } = require('socket.io'); +import { createServer } from 'node:http'; +import next from 'next'; +import { parse } from 'node:url'; + +import { Server, type Socket } from 'socket.io'; + +import type { + ChatMessage, + ClientToServerEvents, + Member, + Room, + RoomMemberInfo, + ServerToClientEvents, + WatchRoomConfig, +} from './src/types/watch-room'; + +type TypedSocket = Socket; +type TypedIoServer = Server; const dev = process.env.NODE_ENV !== 'production'; const hostname = process.env.HOSTNAME || '0.0.0.0'; -const port = parseInt(process.env.PORT || '3000', 10); +const port = Number.parseInt(process.env.PORT || '3000', 10); const app = next({ dev, hostname, port }); const handle = app.getRequestHandler(); -// 读取观影室配置的辅助函数 -async function getWatchRoomConfig() { - // 观影室配置现在统一从环境变量读取 - const config = { +async function getWatchRoomConfig(): Promise { + const serverType = process.env.WATCH_ROOM_SERVER_TYPE === 'external' ? 'external' : 'internal'; + const config: WatchRoomConfig = { enabled: process.env.WATCH_ROOM_ENABLED === 'true', - serverType: (process.env.WATCH_ROOM_SERVER_TYPE || 'internal'), + serverType, externalServerUrl: process.env.WATCH_ROOM_EXTERNAL_SERVER_URL, externalServerAuth: process.env.WATCH_ROOM_EXTERNAL_SERVER_AUTH, }; @@ -25,31 +37,29 @@ async function getWatchRoomConfig() { return config; } -// 观影室服务器类 class WatchRoomServer { - constructor(io) { - this.io = io; - this.rooms = new Map(); - this.members = new Map(); - this.socketToRoom = new Map(); - this.roomDeletionTimers = new Map(); // 房间延迟删除定时器 - this.cleanupInterval = null; + private rooms = new Map(); + private members = new Map>(); + private socketToRoom = new Map(); + private roomDeletionTimers = new Map(); + private cleanupInterval: NodeJS.Timeout | null = null; + + constructor(private readonly io: TypedIoServer) { this.setupEventHandlers(); this.startCleanupTimer(); } - setupEventHandlers() { - this.io.on('connection', (socket) => { + private setupEventHandlers(): void { + this.io.on('connection', (socket: TypedSocket) => { console.log(`[WatchRoom] Client connected: ${socket.id}`); - // 创建房间 socket.on('room:create', (data, callback) => { try { const roomId = this.generateRoomId(); const userId = socket.id; - const ownerToken = this.generateRoomId(); // 生成房主令牌 + const ownerToken = this.generateRoomId(); - const room = { + const room: Room = { id: roomId, name: data.name, description: data.description, @@ -57,14 +67,14 @@ class WatchRoomServer { isPublic: data.isPublic, ownerId: userId, ownerName: data.userName, - ownerToken: ownerToken, // 保存房主令牌 + ownerToken, memberCount: 1, currentState: null, createdAt: Date.now(), lastOwnerHeartbeat: Date.now(), }; - const member = { + const member: Member = { id: userId, name: data.userName, isOwner: true, @@ -90,42 +100,41 @@ class WatchRoomServer { } }); - // 加入房间 socket.on('room:join', (data, callback) => { try { const room = this.rooms.get(data.roomId); if (!room) { - return callback({ success: false, error: '房间不存在' }); + callback({ success: false, error: '房间不存在' }); + return; } if (room.password && room.password !== data.password) { - return callback({ success: false, error: '密码错误' }); + callback({ success: false, error: '密码错误' }); + return; } const userId = socket.id; let isOwner = false; - // 检查是否是房主重连(通过 ownerToken 验证) if (data.ownerToken && data.ownerToken === room.ownerToken) { isOwner = true; - // 更新房主的 socket.id room.ownerId = userId; room.lastOwnerHeartbeat = Date.now(); this.rooms.set(data.roomId, room); console.log(`[WatchRoom] Owner ${data.userName} reconnected to room ${data.roomId}`); } - // 取消房间的删除定时器(如果有人重连) - if (this.roomDeletionTimers.has(data.roomId)) { + const deletionTimer = this.roomDeletionTimers.get(data.roomId); + if (deletionTimer) { console.log(`[WatchRoom] Cancelling deletion timer for room ${data.roomId}`); - clearTimeout(this.roomDeletionTimers.get(data.roomId)); + clearTimeout(deletionTimer); this.roomDeletionTimers.delete(data.roomId); } - const member = { + const member: Member = { id: userId, name: data.userName, - isOwner: isOwner, + isOwner, lastHeartbeat: Date.now(), }; @@ -140,7 +149,7 @@ class WatchRoomServer { roomId: data.roomId, userId, userName: data.userName, - isOwner: isOwner, + isOwner, }); socket.join(data.roomId); @@ -156,18 +165,15 @@ class WatchRoomServer { } }); - // 离开房间 socket.on('room:leave', () => { this.handleLeaveRoom(socket); }); - // 获取房间列表 socket.on('room:list', (callback) => { const publicRooms = Array.from(this.rooms.values()).filter((room) => room.isPublic); callback(publicRooms); }); - // 播放状态更新(任何成员都可以触发同步) socket.on('play:update', (state) => { console.log(`[WatchRoom] Received play:update from ${socket.id}:`, state); const roomInfo = this.socketToRoom.get(socket.id); @@ -177,17 +183,17 @@ class WatchRoomServer { } const room = this.rooms.get(roomInfo.roomId); - if (room) { - room.currentState = state; - this.rooms.set(roomInfo.roomId, room); - console.log(`[WatchRoom] Broadcasting play:update to room ${roomInfo.roomId} from ${roomInfo.userName}`); - socket.to(roomInfo.roomId).emit('play:update', state); - } else { + if (!room) { console.log('[WatchRoom] Room not found for play:update'); + return; } + + room.currentState = state; + this.rooms.set(roomInfo.roomId, room); + console.log(`[WatchRoom] Broadcasting play:update to room ${roomInfo.roomId} from ${roomInfo.userName}`); + socket.to(roomInfo.roomId).emit('play:update', state); }); - // 播放进度跳转 socket.on('play:seek', (currentTime) => { console.log(`[WatchRoom] Received play:seek from ${socket.id}:`, currentTime); const roomInfo = this.socketToRoom.get(socket.id); @@ -195,11 +201,11 @@ class WatchRoomServer { console.log('[WatchRoom] No room info for socket, ignoring play:seek'); return; } + console.log(`[WatchRoom] Broadcasting play:seek to room ${roomInfo.roomId}`); socket.to(roomInfo.roomId).emit('play:seek', currentTime); }); - // 播放 socket.on('play:play', () => { console.log(`[WatchRoom] Received play:play from ${socket.id}`); const roomInfo = this.socketToRoom.get(socket.id); @@ -207,11 +213,11 @@ class WatchRoomServer { console.log('[WatchRoom] No room info for socket, ignoring play:play'); return; } + console.log(`[WatchRoom] Broadcasting play:play to room ${roomInfo.roomId}`); socket.to(roomInfo.roomId).emit('play:play'); }); - // 暂停 socket.on('play:pause', () => { console.log(`[WatchRoom] Received play:pause from ${socket.id}`); const roomInfo = this.socketToRoom.get(socket.id); @@ -219,11 +225,11 @@ class WatchRoomServer { console.log('[WatchRoom] No room info for socket, ignoring play:pause'); return; } + console.log(`[WatchRoom] Broadcasting play:pause to room ${roomInfo.roomId}`); socket.to(roomInfo.roomId).emit('play:pause'); }); - // 切换视频/集数 socket.on('play:change', (state) => { console.log(`[WatchRoom] Received play:change from ${socket.id}:`, state); const roomInfo = this.socketToRoom.get(socket.id); @@ -231,41 +237,47 @@ class WatchRoomServer { console.log('[WatchRoom] No room info for socket, ignoring play:change'); return; } + if (!roomInfo.isOwner) { console.log('[WatchRoom] User is not owner, ignoring play:change'); return; } const room = this.rooms.get(roomInfo.roomId); - if (room) { - room.currentState = state; - this.rooms.set(roomInfo.roomId, room); - console.log(`[WatchRoom] Broadcasting play:change to room ${roomInfo.roomId}`); - socket.to(roomInfo.roomId).emit('play:change', state); - } else { + if (!room) { console.log('[WatchRoom] Room not found for play:change'); + return; } + + room.currentState = state; + this.rooms.set(roomInfo.roomId, room); + console.log(`[WatchRoom] Broadcasting play:change to room ${roomInfo.roomId}`); + socket.to(roomInfo.roomId).emit('play:change', state); }); - // 切换直播频道 socket.on('live:change', (state) => { const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo || !roomInfo.isOwner) return; + if (!roomInfo || !roomInfo.isOwner) { + return; + } const room = this.rooms.get(roomInfo.roomId); - if (room) { - room.currentState = state; - this.rooms.set(roomInfo.roomId, room); - socket.to(roomInfo.roomId).emit('live:change', state); + if (!room) { + return; } + + room.currentState = state; + this.rooms.set(roomInfo.roomId, room); + socket.to(roomInfo.roomId).emit('live:change', state); }); - // 聊天消息 socket.on('chat:message', (data) => { const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo) return; + if (!roomInfo) { + return; + } - const message = { + const message: ChatMessage = { id: this.generateMessageId(), userId: roomInfo.userId, userName: roomInfo.userName, @@ -277,10 +289,12 @@ class WatchRoomServer { this.io.to(roomInfo.roomId).emit('chat:message', message); }); - // WebRTC 信令 socket.on('voice:offer', (data) => { const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo) return; + if (!roomInfo) { + return; + } + this.io.to(data.targetUserId).emit('voice:offer', { userId: socket.id, offer: data.offer, @@ -289,7 +303,10 @@ class WatchRoomServer { socket.on('voice:answer', (data) => { const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo) return; + if (!roomInfo) { + return; + } + this.io.to(data.targetUserId).emit('voice:answer', { userId: socket.id, answer: data.answer, @@ -298,19 +315,22 @@ class WatchRoomServer { socket.on('voice:ice', (data) => { const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo) return; + if (!roomInfo) { + return; + } + this.io.to(data.targetUserId).emit('voice:ice', { userId: socket.id, candidate: data.candidate, }); }); - // 语音聊天 - 服务器中转音频数据 socket.on('voice:audio-chunk', (data) => { const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo) return; + if (!roomInfo) { + return; + } - // 将音频数据转发给房间内的其他成员 socket.to(roomInfo.roomId).emit('voice:audio-chunk', { userId: socket.id, audioData: data.audioData, @@ -318,11 +338,9 @@ class WatchRoomServer { }); }); - // 心跳 socket.on('heartbeat', () => { const roomInfo = this.socketToRoom.get(socket.id); - // 如果用户在房间中,更新心跳时间 if (roomInfo) { const roomMembers = this.members.get(roomInfo.roomId); const member = roomMembers?.get(roomInfo.userId); @@ -340,11 +358,9 @@ class WatchRoomServer { } } - // 无论是否在房间中,都响应心跳包(pong) socket.emit('heartbeat:pong', { timestamp: Date.now() }); }); - // 断开连接 socket.on('disconnect', () => { console.log(`[WatchRoom] Client disconnected: ${socket.id}`); this.handleLeaveRoom(socket); @@ -352,9 +368,11 @@ class WatchRoomServer { }); } - handleLeaveRoom(socket) { + private handleLeaveRoom(socket: TypedSocket): void { const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo) return; + if (!roomInfo) { + return; + } const { roomId, userId, isOwner } = roomInfo; const room = this.rooms.get(roomId); @@ -370,44 +388,34 @@ class WatchRoomServer { socket.to(roomId).emit('room:member-left', userId); - // 如果是房主主动离开,解散房间并踢出所有成员 if (isOwner) { console.log(`[WatchRoom] Owner actively left room ${roomId}, disbanding room`); - - // 通知所有成员房间被解散 socket.to(roomId).emit('room:deleted', { reason: 'owner_left' }); - // 强制所有成员离开房间 - const members = Array.from(roomMembers.keys()); - members.forEach(memberId => { + for (const memberId of roomMembers.keys()) { this.socketToRoom.delete(memberId); - }); + } - // 立即删除房间(跳过通知,因为上面已经发送了) this.deleteRoom(roomId, true); - // 清除可能存在的删除定时器 - if (this.roomDeletionTimers.has(roomId)) { - clearTimeout(this.roomDeletionTimers.get(roomId)); + const deletionTimer = this.roomDeletionTimers.get(roomId); + if (deletionTimer) { + clearTimeout(deletionTimer); this.roomDeletionTimers.delete(roomId); } - } else { - // 普通成员离开,房间为空时延迟删除 - if (roomMembers.size === 0) { - console.log(`[WatchRoom] Room ${roomId} is now empty, will delete in 30 seconds if no one rejoins`); - - const deletionTimer = setTimeout(() => { - // 再次检查房间是否仍然为空 - const currentRoomMembers = this.members.get(roomId); - if (currentRoomMembers && currentRoomMembers.size === 0) { - console.log(`[WatchRoom] Room ${roomId} deletion timer expired, deleting room`); - this.deleteRoom(roomId); - this.roomDeletionTimers.delete(roomId); - } - }, 30000); // 30秒后删除 + } else if (roomMembers.size === 0) { + console.log(`[WatchRoom] Room ${roomId} is now empty, will delete in 30 seconds if no one rejoins`); + + const deletionTimer = setTimeout(() => { + const currentRoomMembers = this.members.get(roomId); + if (currentRoomMembers && currentRoomMembers.size === 0) { + console.log(`[WatchRoom] Room ${roomId} deletion timer expired, deleting room`); + this.deleteRoom(roomId); + this.roomDeletionTimers.delete(roomId); + } + }, 30000); - this.roomDeletionTimers.set(roomId, deletionTimer); - } + this.roomDeletionTimers.set(roomId, deletionTimer); } } @@ -415,10 +423,9 @@ class WatchRoomServer { this.socketToRoom.delete(socket.id); } - deleteRoom(roomId, skipNotify = false) { + private deleteRoom(roomId: string, skipNotify = false): void { console.log(`[WatchRoom] Deleting room ${roomId}`); - // 如果不跳过通知,则发送 room:deleted 事件 if (!skipNotify) { this.io.to(roomId).emit('room:deleted'); } @@ -427,47 +434,43 @@ class WatchRoomServer { this.members.delete(roomId); } - startCleanupTimer() { + private startCleanupTimer(): void { this.cleanupInterval = setInterval(() => { const now = Date.now(); - const deleteTimeout = 5 * 60 * 1000; // 5分钟 - 删除房间 - const clearStateTimeout = 30 * 1000; // 30秒 - 清除播放状态 + const deleteTimeout = 5 * 60 * 1000; + const clearStateTimeout = 30 * 1000; for (const [roomId, room] of this.rooms.entries()) { const timeSinceHeartbeat = now - room.lastOwnerHeartbeat; - // 如果房主心跳超过30秒,清除播放状态 if (timeSinceHeartbeat > clearStateTimeout && room.currentState !== null) { console.log(`[WatchRoom] Room ${roomId} owner inactive for 30s, clearing play state`); room.currentState = null; this.rooms.set(roomId, room); - // 通知房间内所有成员状态已清除 this.io.to(roomId).emit('state:cleared'); } - // 检查房主是否超时5分钟 - 删除房间 if (timeSinceHeartbeat > deleteTimeout) { console.log(`[WatchRoom] Room ${roomId} owner timeout, deleting...`); this.deleteRoom(roomId); } } - }, 10000); // 每10秒检查一次,确保更及时的清理 + }, 10000); } - generateRoomId() { + private generateRoomId(): string { return Math.random().toString(36).substring(2, 8).toUpperCase(); } - generateMessageId() { + private generateMessageId(): string { return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } - destroy() { + public destroy(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } - // 清理所有房间删除定时器 for (const timer of this.roomDeletionTimers.values()) { clearTimeout(timer); } @@ -475,80 +478,69 @@ class WatchRoomServer { } } -app.prepare().then(async () => { - const httpServer = createServer(async (req, res) => { - try { - const parsedUrl = parse(req.url, true); - await handle(req, res, parsedUrl); - } catch (err) { - console.error('Error occurred handling', req.url, err); - res.statusCode = 500; - res.end('Internal server error'); - } - }); +void app + .prepare() + .then(async () => { + const httpServer = createServer(async (req, res) => { + try { + const parsedUrl = parse(req.url || '/', true); + await handle(req, res, parsedUrl); + } catch (error) { + console.error('Error occurred handling', req.url, error); + res.statusCode = 500; + res.end('Internal server error'); + } + }); - // 读取观影室配置 - const watchRoomConfig = await getWatchRoomConfig(); - console.log('[WatchRoom] Config:', watchRoomConfig); + const watchRoomConfig = await getWatchRoomConfig(); + console.log('[WatchRoom] Config:', watchRoomConfig); - let watchRoomServer = null; + let watchRoomServer: WatchRoomServer | null = null; - // 只在启用观影室且使用内部服务器时初始化 Socket.IO - if (watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal') { - console.log('[WatchRoom] Initializing Socket.IO server...'); + if (watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal') { + console.log('[WatchRoom] Initializing Socket.IO server...'); - // 初始化 Socket.IO - const io = new Server(httpServer, { - path: '/socket.io', - cors: { - origin: '*', - methods: ['GET', 'POST'], - }, - }); + const io = new Server(httpServer, { + path: '/socket.io', + cors: { + origin: '*', + methods: ['GET', 'POST'], + }, + }); - // 初始化观影室服务器 - watchRoomServer = new WatchRoomServer(io); - console.log('[WatchRoom] Socket.IO server initialized'); - } else { - if (!watchRoomConfig.enabled) { + watchRoomServer = new WatchRoomServer(io); + console.log('[WatchRoom] Socket.IO server initialized'); + } else if (!watchRoomConfig.enabled) { console.log('[WatchRoom] Watch room is disabled'); } else if (watchRoomConfig.serverType === 'external') { console.log('[WatchRoom] Using external watch room server'); } - } - - httpServer - .once('error', (err) => { - console.error(err); - process.exit(1); - }) - .listen(port, () => { - console.log(`> Ready on http://${hostname}:${port}`); - if (watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal') { - console.log(`> Socket.IO ready on ws://${hostname}:${port}`); - } - }); - // 优雅关闭 - process.on('SIGINT', () => { - console.log('\n[Server] Shutting down...'); - if (watchRoomServer) { - watchRoomServer.destroy(); - } - httpServer.close(() => { - console.log('[Server] Server closed'); - process.exit(0); - }); - }); + httpServer + .once('error', (error) => { + console.error(error); + process.exit(1); + }) + .listen(port, () => { + console.log(`> Ready on http://${hostname}:${port}`); + if (watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal') { + console.log(`> Socket.IO ready on ws://${hostname}:${port}`); + } + }); - process.on('SIGTERM', () => { - console.log('\n[Server] Shutting down...'); - if (watchRoomServer) { - watchRoomServer.destroy(); - } - httpServer.close(() => { - console.log('[Server] Server closed'); - process.exit(0); - }); - }); -}); + const shutdown = (): void => { + console.log('\n[Server] Shutting down...'); + watchRoomServer?.destroy(); + httpServer.close(() => { + console.log('[Server] Server closed'); + process.exit(0); + }); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + }) + .catch((error) => { + console.error('[Server] Failed to start:', error); + process.exit(1); + }); \ No newline at end of file diff --git a/server/watch-room-standalone-server.js b/server/watch-room-standalone-server.js deleted file mode 100644 index c56d0b03b..000000000 --- a/server/watch-room-standalone-server.js +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env node - -// 独立的观影室服务器 -// 使用方式: node watch-room-standalone-server.js --port 3001 --auth YOUR_SECRET_KEY - -import { createServer } from 'http'; -import { Server } from 'socket.io'; -import { WatchRoomServer } from '../lib/watch-room-server'; - -const args = process.argv.slice(2); -const port = parseInt(args[args.indexOf('--port') + 1] || '3001'); -const authKey = args[args.indexOf('--auth') + 1] || ''; - -if (!authKey) { - console.error('Error: --auth parameter is required'); - console.log('Usage: node watch-room-standalone-server.js --port 3001 --auth YOUR_SECRET_KEY'); - process.exit(1); -} - -const httpServer = createServer(); - -const io = new Server(httpServer, { - cors: { - origin: '*', - methods: ['GET', 'POST'], - credentials: true, - }, - // 添加鉴权中间件 - allowRequest: (req, callback) => { - const auth = req.headers.authorization; - if (auth === `Bearer ${authKey}`) { - callback(null, true); - } else { - console.log('[WatchRoom] Unauthorized connection attempt'); - callback('Unauthorized', false); - } - }, -}); - -// 初始化观影室服务器 -const watchRoomServer = new WatchRoomServer(io); - -httpServer.listen(port, () => { - console.log(`[WatchRoom] Standalone server running on port ${port}`); - console.log(`[WatchRoom] Auth key: ${authKey.substring(0, 8)}...`); -}); - -// 优雅关闭 -process.on('SIGINT', () => { - console.log('\n[WatchRoom] Shutting down...'); - watchRoomServer.destroy(); - httpServer.close(() => { - console.log('[WatchRoom] Server closed'); - process.exit(0); - }); -}); - -process.on('SIGTERM', () => { - console.log('\n[WatchRoom] Shutting down...'); - watchRoomServer.destroy(); - httpServer.close(() => { - console.log('[WatchRoom] Server closed'); - process.exit(0); - }); -}); diff --git a/server/watch-room-standalone-server.ts b/server/watch-room-standalone-server.ts new file mode 100644 index 000000000..d48feda1d --- /dev/null +++ b/server/watch-room-standalone-server.ts @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +import { createServer } from 'node:http'; + +import { Server } from 'socket.io'; + +import { WatchRoomServer } from '../src/lib/watch-room-server'; +import type { ClientToServerEvents, ServerToClientEvents } from '../src/types/watch-room'; + +const args = process.argv.slice(2); +const portIndex = args.indexOf('--port'); +const authIndex = args.indexOf('--auth'); +const port = Number.parseInt(portIndex >= 0 ? args[portIndex + 1] || '3001' : '3001', 10); +const authKey = authIndex >= 0 ? args[authIndex + 1] || '' : ''; + +if (!authKey) { + console.error('Error: --auth parameter is required'); + console.log('Usage: tsx server/watch-room-standalone-server.ts --port 3001 --auth YOUR_SECRET_KEY'); + process.exit(1); +} + +const httpServer = createServer(); + +const io = new Server(httpServer, { + cors: { + origin: '*', + methods: ['GET', 'POST'], + credentials: true, + }, + allowRequest: (req, callback) => { + const auth = req.headers.authorization; + if (auth === `Bearer ${authKey}`) { + callback(null, true); + return; + } + + console.log('[WatchRoom] Unauthorized connection attempt'); + callback('Unauthorized', false); + }, +}); + +const watchRoomServer = new WatchRoomServer(io); + +httpServer.listen(port, () => { + console.log(`[WatchRoom] Standalone server running on port ${port}`); + console.log(`[WatchRoom] Auth key: ${authKey.substring(0, 8)}...`); +}); + +const shutdown = (): void => { + console.log('\n[WatchRoom] Shutting down...'); + watchRoomServer.destroy(); + httpServer.close(() => { + console.log('[WatchRoom] Server closed'); + process.exit(0); + }); +}; + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); \ No newline at end of file diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index feb008fee..2c7b50117 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -1,4 +1,4 @@ -// 此文件由 scripts/convert-changelog.js 自动生成 +// 此文件由 scripts/convert-changelog.ts 自动生成 // 请勿手动编辑 export interface ChangelogEntry { diff --git a/src/lib/m3u8-downloader.ts b/src/lib/m3u8-downloader.ts index f965f4349..1e628bcca 100644 --- a/src/lib/m3u8-downloader.ts +++ b/src/lib/m3u8-downloader.ts @@ -705,7 +705,9 @@ export class M3U8Downloader { } try { - return task.aesConf.decryption.decrypt(data, 0, iv.buffer, true); + const ivBuffer = new ArrayBuffer(iv.byteLength); + new Uint8Array(ivBuffer).set(iv); + return task.aesConf.decryption.decrypt(data, 0, ivBuffer, true); } catch (error) { console.error('AES 解密失败:', error); return data; diff --git a/src/types/watch-room.ts b/src/types/watch-room.ts index 6bbf1ca50..9e8a9116d 100644 --- a/src/types/watch-room.ts +++ b/src/types/watch-room.ts @@ -66,7 +66,7 @@ export interface ServerToClientEvents { 'room:list': (rooms: Room[]) => void; 'room:member-joined': (member: Member) => void; 'room:member-left': (userId: string) => void; - 'room:deleted': () => void; + 'room:deleted': (data?: { reason?: 'owner_left' }) => void; 'play:update': (state: PlayState) => void; 'play:seek': (currentTime: number) => void; 'play:play': () => void; diff --git a/start.js b/start.js deleted file mode 100644 index 0e46be536..000000000 --- a/start.js +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env node - -/* eslint-disable no-console,@typescript-eslint/no-var-requires */ -const http = require('http'); -const path = require('path'); - -// 调用 generate-manifest.js 生成 manifest.json -function generateManifest() { - console.log('Generating manifest.json for Docker deployment...'); - - try { - const generateManifestScript = path.join( - __dirname, - 'scripts', - 'generate-manifest.js' - ); - require(generateManifestScript); - } catch (error) { - console.error('❌ Error calling generate-manifest.js:', error); - throw error; - } -} - -generateManifest(); - -// 直接在当前进程中启动 standalone Server(`server.js`) -require('./server.js'); - -// 每 1 秒轮询一次,直到请求成功 -const TARGET_URL = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000 - }/login`; - -const intervalId = setInterval(() => { - console.log(`Fetching ${TARGET_URL} ...`); - - const req = http.get(TARGET_URL, (res) => { - // 当返回 2xx 状态码时认为成功,然后停止轮询 - if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { - console.log('Server is up, stop polling.'); - clearInterval(intervalId); - - setTimeout(() => { - // 服务器启动后,立即执行一次 cron 任务 - executeCronJob(); - }, 3000); - - // 然后设置每小时执行一次 cron 任务 - setInterval(() => { - executeCronJob(); - }, 60 * 60 * 1000); // 每小时执行一次 - } - }); - - req.setTimeout(2000, () => { - req.destroy(); - }); -}, 1000); - -// 执行 cron 任务的函数 -function executeCronJob() { - const cronPassword = process.env.CRON_PASSWORD || 'mtvpls'; - const cronUrl = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000 - }/api/cron/${cronPassword}`; - - console.log(`Executing cron job: ${cronUrl}`); - - const req = http.get(cronUrl, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { - console.log('Cron job executed successfully:', data); - } else { - console.error('Cron job failed:', res.statusCode, data); - } - }); - }); - - req.on('error', (err) => { - console.error('Error executing cron job:', err); - }); - - req.setTimeout(30000, () => { - console.error('Cron job timeout'); - req.destroy(); - }); -} diff --git a/start.ts b/start.ts new file mode 100644 index 000000000..fea419f0b --- /dev/null +++ b/start.ts @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +import http from 'node:http'; + +import { generateManifest } from './scripts/generate-manifest'; + +if (!process.env.NODE_ENV) { + Object.assign(process.env, { NODE_ENV: 'production' }); +} + +function executeCronJob(): void { + const cronPassword = process.env.CRON_PASSWORD || 'mtvpls'; + const hostname = process.env.HOSTNAME || 'localhost'; + const port = process.env.PORT || '3000'; + const cronUrl = `http://${hostname}:${port}/api/cron/${cronPassword}`; + + console.log(`Executing cron job: ${cronUrl}`); + + const req = http.get(cronUrl, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + console.log('Cron job executed successfully:', data); + return; + } + + console.error('Cron job failed:', res.statusCode, data); + }); + }); + + req.on('error', (error) => { + console.error('Error executing cron job:', error); + }); + + req.setTimeout(30000, () => { + console.error('Cron job timeout'); + req.destroy(); + }); +} + +generateManifest(); +await import('./server'); + +const hostname = process.env.HOSTNAME || 'localhost'; +const port = process.env.PORT || '3000'; +const targetUrl = `http://${hostname}:${port}/login`; + +const intervalId = setInterval(() => { + console.log(`Fetching ${targetUrl} ...`); + + const req = http.get(targetUrl, (res) => { + if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) { + return; + } + + console.log('Server is up, stop polling.'); + clearInterval(intervalId); + + setTimeout(() => { + executeCronJob(); + }, 3000); + + setInterval(() => { + executeCronJob(); + }, 60 * 60 * 1000); + }); + + req.setTimeout(2000, () => { + req.destroy(); + }); +}, 1000); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index ebd30a0c9..5a69071b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, + "allowJs": false, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, From aedcb9641370b901a3a72833340b242dc8f5dd1a Mon Sep 17 00:00:00 2001 From: Han-che Wang <60660116+Carnage1999@users.noreply.github.com> Date: Fri, 13 Mar 2026 09:34:53 +0800 Subject: [PATCH 02/15] chore: refresh eslint and prettier tooling --- .eslintrc.js | 6 + .github/ISSUE_TEMPLATE/bug.yaml | 4 +- .github/ISSUE_TEMPLATE/question.yml | 2 +- ...37\350\203\275\350\257\267\346\261\202.md" | 4 +- .github/workflows/cloudflare-deploy.yml | 4 +- .github/workflows/docker-image-dev.yml | 8 +- .github/workflows/docker-image.yml | 4 +- README.md | 122 +- VERCEL_DEPLOYMENT.md | 29 +- jest.setup.ts | 2 +- next.config.js | 29 +- package.json | 16 +- pnpm-lock.yaml | 55 +- proxy.worker.ts | 41 +- scripts/convert-changelog.ts | 37 +- scripts/generate-manifest.ts | 12 +- scripts/init-postgres.ts | 7 +- scripts/init-sqlite.ts | 7 +- scripts/reset-sqlite.ts | 2 +- server.ts | 125 +- server/watch-room-standalone-server.ts | 16 +- src/app/admin/page.tsx | 2973 ++++++---- src/app/api/acg/acgrip/route.ts | 30 +- src/app/api/acg/dmhy/route.ts | 16 +- src/app/api/acg/download/route.ts | 32 +- src/app/api/acg/mikan/route.ts | 36 +- src/app/api/ad-filter/route.ts | 2 +- src/app/api/admin/ai/route.ts | 39 +- .../anime-subscription/[id]/check/route.ts | 4 +- .../admin/anime-subscription/[id]/route.ts | 10 +- src/app/api/admin/anime-subscription/route.ts | 7 +- .../admin/anime-subscription/toggle/route.ts | 4 +- src/app/api/admin/category/route.ts | 26 +- src/app/api/admin/config/route.ts | 16 +- src/app/api/admin/config_file/route.ts | 10 +- .../admin/config_subscription/fetch/route.ts | 12 +- .../api/admin/data_migration/export/route.ts | 102 +- .../api/admin/data_migration/import/route.ts | 153 +- .../admin/data_migration/progress/route.ts | 7 +- src/app/api/admin/email/route.ts | 38 +- src/app/api/admin/emby/export/route.ts | 9 +- src/app/api/admin/emby/import/route.ts | 13 +- src/app/api/admin/emby/route.ts | 25 +- src/app/api/admin/live/refresh/route.ts | 4 +- src/app/api/admin/live/route.ts | 24 +- src/app/api/admin/migrate-users/route.ts | 10 +- src/app/api/admin/music/route.ts | 24 +- src/app/api/admin/oidc-discover/route.ts | 22 +- src/app/api/admin/openlist/route.ts | 25 +- src/app/api/admin/reload/route.ts | 6 +- src/app/api/admin/reset/route.ts | 6 +- src/app/api/admin/site/route.ts | 54 +- src/app/api/admin/source/route.ts | 101 +- src/app/api/admin/source/validate/route.ts | 38 +- src/app/api/admin/theme/route.ts | 18 +- src/app/api/admin/user/route.ts | 141 +- src/app/api/admin/users/route.ts | 8 +- src/app/api/admin/web-live/route.ts | 21 +- src/app/api/admin/xiaoya/route.ts | 6 +- src/app/api/ai/chat/route.ts | 121 +- src/app/api/auth/oidc/callback/route.ts | 57 +- .../api/auth/oidc/complete-register/route.ts | 74 +- src/app/api/auth/oidc/login/route.ts | 12 +- src/app/api/auth/oidc/session-info/route.ts | 20 +- src/app/api/auth/refresh/route.ts | 5 +- src/app/api/change-password/route.ts | 19 +- src/app/api/cms-proxy/route.ts | 190 +- src/app/api/cron/[password]/route.ts | 141 +- src/app/api/danmaku-filter/route.ts | 7 +- src/app/api/danmaku/comment/route.ts | 10 +- src/app/api/danmaku/episodes/route.ts | 4 +- src/app/api/danmaku/match/route.ts | 4 +- src/app/api/danmaku/search/route.ts | 4 +- src/app/api/detail/route.ts | 95 +- src/app/api/douban-comments/route.ts | 17 +- src/app/api/douban-recommendations/route.ts | 9 +- src/app/api/douban/categories/route.ts | 10 +- src/app/api/douban/detail/route.ts | 7 +- src/app/api/douban/recommends/route.ts | 7 +- src/app/api/douban/route.ts | 12 +- src/app/api/douban/search/route.ts | 2 +- src/app/api/duanju/recommends/route.ts | 94 +- src/app/api/duanju/sources/route.ts | 4 +- src/app/api/emby/cms-proxy/[token]/route.ts | 34 +- src/app/api/emby/detail/route.ts | 20 +- .../api/emby/image/[token]/[itemId]/route.ts | 29 +- src/app/api/emby/list/route.ts | 11 +- .../api/emby/play/[token]/[filename]/route.ts | 14 +- src/app/api/emby/sources/route.ts | 4 +- src/app/api/emby/views/route.ts | 3 +- src/app/api/favorites/route.ts | 18 +- src/app/api/image-proxy/route.ts | 6 +- src/app/api/live/channels/route.ts | 7 +- src/app/api/live/epg/download/route.ts | 15 +- src/app/api/live/epg/route.ts | 18 +- src/app/api/live/precheck/route.ts | 12 +- src/app/api/live/sources/route.ts | 13 +- src/app/api/login/route.ts | 76 +- src/app/api/movie-requests/[id]/route.ts | 12 +- src/app/api/movie-requests/route.ts | 30 +- src/app/api/music/audio-proxy/route.ts | 20 +- src/app/api/music/playlists/route.ts | 25 +- src/app/api/music/playlists/songs/route.ts | 37 +- src/app/api/music/playrecords/route.ts | 33 +- src/app/api/music/proxy/route.ts | 29 +- src/app/api/music/route.ts | 230 +- src/app/api/notifications/route.ts | 8 +- .../[episodeIndex]/[...file]/route.ts | 22 +- src/app/api/offline-download/local/route.ts | 11 +- src/app/api/offline-download/route.ts | 65 +- src/app/api/openlist/check/route.ts | 9 +- .../api/openlist/cms-proxy/[token]/route.ts | 142 +- src/app/api/openlist/correct/route.ts | 17 +- src/app/api/openlist/delete/route.ts | 24 +- src/app/api/openlist/detail/route.ts | 46 +- src/app/api/openlist/list/route.ts | 55 +- src/app/api/openlist/play/[token]/route.ts | 16 +- src/app/api/openlist/play/route.ts | 51 +- src/app/api/openlist/refresh-video/route.ts | 9 +- src/app/api/openlist/refresh/route.ts | 9 +- src/app/api/openlist/scan-progress/route.ts | 2 +- src/app/api/pansou/search/route.ts | 14 +- src/app/api/playrecords/route.ts | 32 +- src/app/api/proxy-m3u8/route.ts | 64 +- src/app/api/proxy/key/route.ts | 13 +- src/app/api/proxy/logo/route.ts | 6 +- src/app/api/proxy/m3u8/route.ts | 66 +- src/app/api/proxy/segment/route.ts | 69 +- src/app/api/proxy/vod/key/route.ts | 34 +- src/app/api/proxy/vod/m3u8/route.ts | 90 +- src/app/api/proxy/vod/segment/route.ts | 82 +- src/app/api/register/route.ts | 75 +- src/app/api/search/one/route.ts | 10 +- src/app/api/search/route.ts | 56 +- src/app/api/search/suggestions/route.ts | 28 +- src/app/api/search/ws/route.ts | 231 +- src/app/api/searchhistory/route.ts | 8 +- src/app/api/server-config/route.ts | 15 +- src/app/api/skipconfigs/route.ts | 6 +- src/app/api/source-detail/route.ts | 159 +- src/app/api/source-search/categories/route.ts | 12 +- src/app/api/source-search/search/route.ts | 12 +- src/app/api/source-search/sources/route.ts | 2 +- src/app/api/source-search/videos/route.ts | 14 +- src/app/api/tmdb-details/route.ts | 35 +- src/app/api/tmdb-recommendations/route.ts | 49 +- src/app/api/tmdb/credits/route.ts | 13 +- src/app/api/tmdb/detail/route.ts | 20 +- src/app/api/tmdb/episodes/route.ts | 11 +- src/app/api/tmdb/search/route.ts | 10 +- src/app/api/tmdb/seasons/route.ts | 13 +- src/app/api/tmdb/trending/route.ts | 82 +- src/app/api/tmdb/upcoming/route.ts | 12 +- src/app/api/tvbox/config/route.ts | 9 +- src/app/api/tvbox/subscribe/route.ts | 77 +- src/app/api/user/email-settings/route.ts | 14 +- src/app/api/user/tvbox-token/reset/route.ts | 7 +- src/app/api/user/tvbox-token/route.ts | 7 +- src/app/api/video-proxy/route.ts | 6 +- .../api/web-live/proxy/[filename]/route.ts | 31 +- src/app/api/web-live/sources/route.ts | 4 +- src/app/api/web-live/stream/route.ts | 88 +- src/app/api/xiaoya/browse/route.ts | 40 +- src/app/api/xiaoya/play/route.ts | 45 +- src/app/api/xiaoya/search/route.ts | 16 +- src/app/douban/page.tsx | 42 +- src/app/globals.css | 27 +- src/app/layout.tsx | 31 +- src/app/live/page.tsx | 800 ++- src/app/login/page.tsx | 112 +- src/app/movie-request/page.tsx | 236 +- src/app/music/page.tsx | 2881 ++++++---- src/app/oidc-register/page.tsx | 9 +- src/app/page.tsx | 132 +- src/app/play/page.tsx | 5062 ++++++++++------- src/app/private-library/page.tsx | 474 +- src/app/register/page.tsx | 94 +- src/app/search/page.tsx | 602 +- src/app/source-search/page.tsx | 123 +- src/app/watch-room/page.tsx | 552 +- src/app/web-live/page.tsx | 209 +- src/components/AIChatPanel.tsx | 86 +- src/components/AcgSearch.tsx | 23 +- src/components/AddToPlaylistModal.tsx | 139 +- src/components/AnimeSubscriptionComponent.tsx | 143 +- src/components/BannerCarousel.tsx | 173 +- src/components/CapsuleSwitch.tsx | 4 +- src/components/ConfirmDialog.tsx | 7 +- src/components/ContinueWatching.tsx | 315 +- src/components/CorrectDialog.tsx | 758 +-- src/components/CustomHeatmap.tsx | 4 +- src/components/DanmakuFilterSettings.tsx | 138 +- src/components/DanmakuPanel.tsx | 346 +- src/components/DataMigration.tsx | 231 +- src/components/DetailPanel.tsx | 960 ++-- src/components/DoubanComments.tsx | 126 +- src/components/DoubanCustomSelector.tsx | 25 +- src/components/DoubanRecommendations.tsx | 23 +- src/components/DoubanSelector.tsx | 67 +- src/components/DownloadBubble.tsx | 20 +- src/components/DownloadEpisodeSelector.tsx | 42 +- src/components/DownloadManagementPanel.tsx | 280 +- src/components/DownloadPanel.tsx | 120 +- src/components/EpgScrollableRow.tsx | 503 +- src/components/EpisodeFilterSettings.tsx | 139 +- src/components/EpisodeSelector.tsx | 344 +- src/components/FavoritesPanel.tsx | 112 +- src/components/GlobalErrorIndicator.tsx | 2 +- src/components/HttpWarningDialog.tsx | 29 +- src/components/ImageViewer.tsx | 18 +- src/components/LyricsPiPWindow.tsx | 65 +- src/components/MobileActionSheet.tsx | 99 +- src/components/MobileBottomNav.tsx | 21 +- src/components/MultiLevelSelector.tsx | 67 +- src/components/NotificationPanel.tsx | 12 +- src/components/OfflineDownloadPanel.tsx | 126 +- src/components/PageLayout.tsx | 10 +- src/components/PansouSearch.tsx | 63 +- src/components/SearchResultFilter.tsx | 162 +- src/components/SearchSuggestions.tsx | 8 +- src/components/Sidebar.tsx | 49 +- src/components/SmartRecommendations.tsx | 21 +- src/components/Toast.tsx | 27 +- src/components/TokenRefreshManager.tsx | 66 +- src/components/TopProgressBar.tsx | 19 +- src/components/UpdateNotification.tsx | 7 +- src/components/UserMenu.tsx | 693 ++- src/components/VersionPanel.tsx | 38 +- src/components/VideoCard.tsx | 2834 ++++----- src/components/VirtualScrollableRow.tsx | 39 +- src/components/WatchRoomProvider.tsx | 63 +- src/components/WeekdaySelector.tsx | 5 +- .../watch-room/ChatFloatingWindow.tsx | 519 +- src/contexts/DownloadContext.tsx | 747 +-- src/hooks/useEnableComments.ts | 4 +- src/hooks/useLiveSync.ts | 20 +- src/hooks/useLongPress.ts | 17 +- src/hooks/usePlaySync.ts | 162 +- src/hooks/useVoiceChat.ts | 677 ++- src/hooks/useWatchRoom.ts | 205 +- src/hooks/useWebLiveSync.ts | 33 +- src/lib/admin.types.ts | 2 +- src/lib/aes-decryptor.ts | 86 +- src/lib/ai-orchestrator.ts | 129 +- src/lib/anime-subscription.ts | 60 +- src/lib/auth.ts | 35 +- src/lib/changelog.ts | 1253 ++-- src/lib/config.ts | 186 +- src/lib/crypto.ts | 5 +- src/lib/d1.db.ts | 522 +- src/lib/danmaku/api.ts | 53 +- src/lib/danmaku/cache.ts | 33 +- src/lib/danmaku/selection-memory.ts | 21 +- src/lib/data-migration-progress.ts | 24 +- src/lib/db.client.ts | 238 +- src/lib/db.ts | 140 +- src/lib/douban-anti-crawler.ts | 19 +- src/lib/douban.client.ts | 50 +- src/lib/download-db.ts | 72 +- src/lib/downstream.ts | 37 +- src/lib/email.service.ts | 8 +- src/lib/email.templates.ts | 14 +- src/lib/emby-cache.ts | 15 +- src/lib/emby-manager.ts | 98 +- src/lib/emby-token.ts | 5 +- src/lib/emby.client.ts | 143 +- src/lib/fetchVideoDetail.ts | 7 +- src/lib/kvrocks.db.ts | 8 +- src/lib/live.ts | 22 +- src/lib/lock.ts | 3 +- src/lib/m3u8-downloader.ts | 261 +- src/lib/middleware-auth.ts | 12 +- src/lib/music-song-cache.ts | 24 +- src/lib/nfo-parser.ts | 10 +- src/lib/offline-downloader.ts | 126 +- src/lib/openlist-cache.ts | 8 +- src/lib/openlist-refresh.ts | 91 +- src/lib/openlist.client.ts | 60 +- src/lib/pansou.client.ts | 8 +- src/lib/postgres-adapter.ts | 11 +- src/lib/postgres.db.ts | 526 +- src/lib/redis-adapter.ts | 35 +- src/lib/redis-base.db.ts | 577 +- src/lib/redis.db.ts | 8 +- src/lib/refresh-token.ts | 34 +- src/lib/scan-task.ts | 7 +- src/lib/search-cache.ts | 20 +- src/lib/season-parser.ts | 12 +- src/lib/special-sources-detail.ts | 78 +- src/lib/time.ts | 4 +- src/lib/tmdb.client.ts | 157 +- src/lib/tmdb.search.ts | 47 +- src/lib/token-config.ts | 6 +- src/lib/types.ts | 33 +- src/lib/upstash.db.ts | 6 +- src/lib/user-cache.ts | 4 +- src/lib/utils.ts | 71 +- src/lib/version_check.ts | 4 +- src/lib/video-parser.ts | 6 +- src/lib/watch-room-server.ts | 29 +- src/lib/watch-room-socket.ts | 27 +- src/lib/xiaoya-metadata.ts | 142 +- src/lib/xiaoya.client.ts | 28 +- src/middleware.ts | 33 +- src/styles/globals.css | 30 +- src/types/file-system-access.d.ts | 10 +- src/types/opencc-js.d.ts | 4 +- src/types/watch-room.ts | 93 +- start.ts | 11 +- 309 files changed, 22882 insertions(+), 14898 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 241cdcbe5..1b4abc02e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,13 @@ module.exports = { rules: { 'no-unused-vars': 'off', 'no-console': 'warn', + 'no-case-declarations': 'off', + 'no-constant-condition': 'warn', '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-unused-expressions': 'off', 'react/no-unescaped-entities': 'off', 'react/display-name': 'off', diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 2522f45de..b3c36a8b9 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -1,6 +1,6 @@ name: Bug 报告 🐛 description: 汇报一个BUG -labels: [ "bug" ] +labels: ['bug'] assignees: - Mr-Quin body: @@ -40,7 +40,7 @@ body: - type: markdown attributes: - value: "## 运行环境" + value: '## 运行环境' - type: dropdown attributes: diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 148e17d9a..e8ec8fdbe 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -1,7 +1,7 @@ name: 项目提问 description: 对项目功能有疑问 title: '[提问]' -labels: ["question"] +labels: ['question'] body: - type: checkboxes attributes: diff --git "a/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.md" "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.md" index 8789618d5..aa3484f9c 100644 --- "a/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.md" +++ "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.md" @@ -3,18 +3,16 @@ name: 功能请求 about: 有什么想法都可以提 title: '' labels: enhancement - --- **功能说明** 简单描述需要的功能 - **使用场景** 说明为什么需要这个功能 - **其他内容** 其他可供参考的内容,如: + - 截图 - 其他有类似功能的项目 diff --git a/.github/workflows/cloudflare-deploy.yml b/.github/workflows/cloudflare-deploy.yml index 138d750f2..4f028dc58 100644 --- a/.github/workflows/cloudflare-deploy.yml +++ b/.github/workflows/cloudflare-deploy.yml @@ -140,7 +140,7 @@ jobs: append_var WATCH_ROOM_EXTERNAL_SERVER_AUTH "${{ secrets.WATCH_ROOM_EXTERNAL_SERVER_AUTH }}" append_var WATCH_ROOM_EXTERNAL_SERVER_URL "${{ secrets.WATCH_ROOM_EXTERNAL_SERVER_URL }}" append_var WATCH_ROOM_SERVER_TYPE "${{ secrets.WATCH_ROOM_SERVER_TYPE }}" - + echo "Updated wrangler.toml:" cat wrangler.toml @@ -185,4 +185,4 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: deploy packageManager: pnpm - wranglerVersion: "4.60.0" + wranglerVersion: '4.60.0' diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index 2024ca627..f8dff66b0 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -11,7 +11,7 @@ on: type: string push: # 监听 dev 分支的推送 - branches: [ dev ] + branches: [dev] # 取消了 pull_request 触发,因为它通常不应该触发构建和推送镜像的操作 # 如果确实需要,可以重新加回来 # pull_request: @@ -65,10 +65,10 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - + # 这个步骤不再需要,因为我们直接用 github.repository_owner # - name: Set lowercase repository owner ... - + # 更改:使用动态镜像名和更强大的标签逻辑 - name: Extract metadata id: meta @@ -161,7 +161,7 @@ jobs: # 更改:使用从 meta 生成的所有标签来创建 manifest # 将逗号分隔的标签列表转换为 ' -t -t ...' 的格式 FORMATTED_TAGS=$(echo "${{ steps.meta.outputs.tags }}" | sed 's/,/ -t /g') - + docker buildx imagetools create -t $FORMATTED_TAGS \ $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 362db7984..3c65a4d83 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -9,9 +9,9 @@ on: default: 'latest' type: string push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + branches: [main, master] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/README.md b/README.md index 76a4235be..ae535528f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ - --- ## 🎉 相对原版新增内容 @@ -52,7 +51,6 @@ 项目截图 - ### 请不要在 B站、小红书、微信公众号、抖音、今日头条或其他中国大陆社交平台发布视频或文章宣传本项目,不授权任何“科技周刊/月刊”类项目或站点收录本项目。 ## 🗺 目录 @@ -71,18 +69,16 @@ - [License](#license) - [致谢](#致谢) - - ## 技术栈 -| 分类 | 主要依赖 | -| --------- | ------------------------------------------------------------ | -| 前端框架 | [Next.js 14](https://nextjs.org/) · App Router | -| UI & 样式 | [Tailwind CSS 3](https://tailwindcss.com/) | -| 语言 | TypeScript 5 | +| 分类 | 主要依赖 | +| --------- | ----------------------------------------------------------------------------------------------------- | +| 前端框架 | [Next.js 14](https://nextjs.org/) · App Router | +| UI & 样式 | [Tailwind CSS 3](https://tailwindcss.com/) | +| 语言 | TypeScript 5 | | 播放器 | [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) · [HLS.js](https://github.com/video-dev/hls.js/) | -| 代码质量 | ESLint · Prettier · Jest | -| 部署 | Docker | +| 代码质量 | ESLint · Prettier · Jest | +| 部署 | Docker | ## 部署 @@ -94,8 +90,6 @@ [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/SCHCAY/deploy) - - ### Cloudflare Workers 部署(通过 GitHub Actions) Cloudflare Workers 提供免费的边缘计算服务,通过 GitHub Actions 可以实现自动化部署。 @@ -155,7 +149,7 @@ Cloudflare Workers 提供免费的边缘计算服务,通过 GitHub Actions 可 on: push: branches: - - main # 或你的主分支名称 + - main # 或你的主分支名称 workflow_dispatch: ``` @@ -334,51 +328,51 @@ dockge/komodo 等 docker compose UI 也有自动更新功能 ## 环境变量 -| 变量 | 说明 | 可选值 | 默认值 | -| ---------------------------------------- | ------------------------------------------------------------ | --------------------------- | ------------------------------------------------------------ | -| USERNAME | 站长账号 | 任意字符串 | 无默认,必填字段 | -| PASSWORD | 站长密码 | 任意字符串 | 无默认,必填字段 | -| CRON_PASSWORD | 定时任务 API 访问密码(用于保护 /api/cron 端点) | 任意字符串 | mtvpls | -| CRON_WAIT_FOR_COMPLETION | 定时任务接口是否等待任务完全结束后再返回响应(true 时返回 200,false 时立即返回 202) | true/false | false | -| CRON_USER_BATCH_SIZE | 定时任务用户批处理大小(控制并发处理的用户数量,影响播放记录和收藏更新任务的并发性能) | 正整数 | 3 | -| SITE_BASE | 站点 url | 形如 https://example.com | 空 | -| NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV | -| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 | -| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | redis、kvrocks、upstash、d1 | 无默认,必填字段 | -| KVROCKS_URL | kvrocks 连接 url | 连接 url | 空 | -| REDIS_URL | redis 连接 url | 连接 url | 空 | -| UPSTASH_URL | upstash redis 连接 url | 连接 url | 空 | -| UPSTASH_TOKEN | upstash redis 连接 token | 连接 token | 空 | -| NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 | -| NEXT_PUBLIC_DOUBAN_PROXY_TYPE | 豆瓣数据源请求方式 | 见下方 | direct | -| NEXT_PUBLIC_DOUBAN_PROXY | 自定义豆瓣数据代理 URL | url prefix | (空) | -| NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE | 豆瓣图片代理类型 | 见下方 | direct | -| NEXT_PUBLIC_DOUBAN_IMAGE_PROXY | 自定义豆瓣图片代理 URL | url prefix | (空) | -| NEXT_PUBLIC_DISABLE_YELLOW_FILTER | 关闭色情内容过滤 | true/false | false | -| NEXT_PUBLIC_FLUID_SEARCH | 是否开启搜索接口流式输出 | true/ false | true | -| NEXT_PUBLIC_PROXY_M3U8_TOKEN | M3U8 代理 API 鉴权 Token(外部播放器跳转时的鉴权token,不填为无鉴权) | 任意字符串 | (空) | -| NEXT_PUBLIC_DANMAKU_CACHE_EXPIRE_MINUTES | 弹幕缓存失效时间(分钟数,设为 0 时不缓存) | 0 或正整数 | 4320(3天) | -| ENABLE_TVBOX_SUBSCRIBE | 是否启用 TVBOX 订阅功能 | true/false | false | -| TVBOX_SUBSCRIBE_TOKEN | TVBOX 订阅 API 访问 Token,如启用TVBOX功能必须设置该项 | 任意字符串 | (空) | -| TVBOX_BLOCKED_SOURCES | TVBOX 订阅屏蔽源列表(多个源用逗号分隔,匹配视频源的 key) | 逗号分隔的源 key | (空) | -| WATCH_ROOM_ENABLED | 是否启用观影室功能(vercel部署不支持该功能,可使用外部服务器) | true/false | false | -| WATCH_ROOM_SERVER_TYPE | 观影室服务器类型 | internal/external | internal | -| WATCH_ROOM_EXTERNAL_SERVER_URL | 外部观影室服务器地址(当 SERVER_TYPE 为 external 时必填) | WebSocket URL | (空) | -| WATCH_ROOM_EXTERNAL_SERVER_AUTH | 外部观影室服务器认证令牌(当 SERVER_TYPE 为 external 时必填) | 任意字符串 | (空) | -| NEXT_PUBLIC_VOICE_CHAT_STRATEGY | 观影室语音聊天策略 | webrtc-fallback/server-only | webrtc-fallback | -| NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD | 是否启用服务器离线下载功能(开启后也仅管理员和站长可用) | true/false | false | -| OFFLINE_DOWNLOAD_DIR | 离线下载文件存储目录 | 任意有效路径 | /data | -| VIDEOINFO_CACHE_MINUTES | 私人影库视频信息在内存中的缓存时长(分钟) | 正整数 | 1440(1天) | -| NEXT_PUBLIC_ENABLE_SOURCE_SEARCH | 是否开启源站寻片功能 | true/false | true | -| MAX_PLAY_RECORDS_PER_USER | 单个用户播放记录清理阈值(超过此数量将自动清理旧记录) | 正整数 | 100 | -| INIT_CONFIG | 初始配置(JSON 格式,包含 api_site、custom_category、lives 等) | JSON 字符串 | (空) | -| CONFIG_SUBSCRIPTION_URL | 配置订阅 URL(Base58 编码的配置文件地址,优先级高于 INIT_CONFIG) | URL | (空) | -| TMDB_API_KEY | TMDB API 密钥 | 任意字符串 | (空) | -| TMDB_PROXY | TMDB 代理地址 | URL | (空) | -| TMDB_REVERSE_PROXY | TMDB 反向代理地址 | URL | (空) | -| DANMAKU_API_BASE | 弹幕 API 地址 | URL | http://localhost:9321 | -| DANMAKU_API_TOKEN | 弹幕 API Token | 任意字符串 | 87654321 | -| DATA_MIGRATION_CHUNK_SIZE | 数据迁移批处理大小(控制导入导出时每批处理的用户数量和数据条数) | 正整数 | 10 | +| 变量 | 说明 | 可选值 | 默认值 | +| ---------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| USERNAME | 站长账号 | 任意字符串 | 无默认,必填字段 | +| PASSWORD | 站长密码 | 任意字符串 | 无默认,必填字段 | +| CRON_PASSWORD | 定时任务 API 访问密码(用于保护 /api/cron 端点) | 任意字符串 | mtvpls | +| CRON_WAIT_FOR_COMPLETION | 定时任务接口是否等待任务完全结束后再返回响应(true 时返回 200,false 时立即返回 202) | true/false | false | +| CRON_USER_BATCH_SIZE | 定时任务用户批处理大小(控制并发处理的用户数量,影响播放记录和收藏更新任务的并发性能) | 正整数 | 3 | +| SITE_BASE | 站点 url | 形如 https://example.com | 空 | +| NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV | +| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 | +| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | redis、kvrocks、upstash、d1 | 无默认,必填字段 | +| KVROCKS_URL | kvrocks 连接 url | 连接 url | 空 | +| REDIS_URL | redis 连接 url | 连接 url | 空 | +| UPSTASH_URL | upstash redis 连接 url | 连接 url | 空 | +| UPSTASH_TOKEN | upstash redis 连接 token | 连接 token | 空 | +| NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 | +| NEXT_PUBLIC_DOUBAN_PROXY_TYPE | 豆瓣数据源请求方式 | 见下方 | direct | +| NEXT_PUBLIC_DOUBAN_PROXY | 自定义豆瓣数据代理 URL | url prefix | (空) | +| NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE | 豆瓣图片代理类型 | 见下方 | direct | +| NEXT_PUBLIC_DOUBAN_IMAGE_PROXY | 自定义豆瓣图片代理 URL | url prefix | (空) | +| NEXT_PUBLIC_DISABLE_YELLOW_FILTER | 关闭色情内容过滤 | true/false | false | +| NEXT_PUBLIC_FLUID_SEARCH | 是否开启搜索接口流式输出 | true/ false | true | +| NEXT_PUBLIC_PROXY_M3U8_TOKEN | M3U8 代理 API 鉴权 Token(外部播放器跳转时的鉴权token,不填为无鉴权) | 任意字符串 | (空) | +| NEXT_PUBLIC_DANMAKU_CACHE_EXPIRE_MINUTES | 弹幕缓存失效时间(分钟数,设为 0 时不缓存) | 0 或正整数 | 4320(3天) | +| ENABLE_TVBOX_SUBSCRIBE | 是否启用 TVBOX 订阅功能 | true/false | false | +| TVBOX_SUBSCRIBE_TOKEN | TVBOX 订阅 API 访问 Token,如启用TVBOX功能必须设置该项 | 任意字符串 | (空) | +| TVBOX_BLOCKED_SOURCES | TVBOX 订阅屏蔽源列表(多个源用逗号分隔,匹配视频源的 key) | 逗号分隔的源 key | (空) | +| WATCH_ROOM_ENABLED | 是否启用观影室功能(vercel部署不支持该功能,可使用外部服务器) | true/false | false | +| WATCH_ROOM_SERVER_TYPE | 观影室服务器类型 | internal/external | internal | +| WATCH_ROOM_EXTERNAL_SERVER_URL | 外部观影室服务器地址(当 SERVER_TYPE 为 external 时必填) | WebSocket URL | (空) | +| WATCH_ROOM_EXTERNAL_SERVER_AUTH | 外部观影室服务器认证令牌(当 SERVER_TYPE 为 external 时必填) | 任意字符串 | (空) | +| NEXT_PUBLIC_VOICE_CHAT_STRATEGY | 观影室语音聊天策略 | webrtc-fallback/server-only | webrtc-fallback | +| NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD | 是否启用服务器离线下载功能(开启后也仅管理员和站长可用) | true/false | false | +| OFFLINE_DOWNLOAD_DIR | 离线下载文件存储目录 | 任意有效路径 | /data | +| VIDEOINFO_CACHE_MINUTES | 私人影库视频信息在内存中的缓存时长(分钟) | 正整数 | 1440(1天) | +| NEXT_PUBLIC_ENABLE_SOURCE_SEARCH | 是否开启源站寻片功能 | true/false | true | +| MAX_PLAY_RECORDS_PER_USER | 单个用户播放记录清理阈值(超过此数量将自动清理旧记录) | 正整数 | 100 | +| INIT_CONFIG | 初始配置(JSON 格式,包含 api_site、custom_category、lives 等) | JSON 字符串 | (空) | +| CONFIG_SUBSCRIPTION_URL | 配置订阅 URL(Base58 编码的配置文件地址,优先级高于 INIT_CONFIG) | URL | (空) | +| TMDB_API_KEY | TMDB API 密钥 | 任意字符串 | (空) | +| TMDB_PROXY | TMDB 代理地址 | URL | (空) | +| TMDB_REVERSE_PROXY | TMDB 反向代理地址 | URL | (空) | +| DANMAKU_API_BASE | 弹幕 API 地址 | URL | http://localhost:9321 | +| DANMAKU_API_TOKEN | 弹幕 API Token | 任意字符串 | 87654321 | +| DATA_MIGRATION_CHUNK_SIZE | 数据迁移批处理大小(控制导入导出时每批处理的用户数量和数据条数) | 正整数 | 10 | NEXT_PUBLIC_DOUBAN_PROXY_TYPE 选项解释: @@ -423,8 +417,6 @@ NEXT_PUBLIC_VOICE_CHAT_STRATEGY 选项解释: 3. 重启应用即可使用外部观影室服务器 - - ## 弹幕后端部署 要使用弹幕功能,需要额外部署弹幕 API 后端服务。 @@ -435,16 +427,10 @@ NEXT_PUBLIC_VOICE_CHAT_STRATEGY 选项解释: 2. 建议配置SOURCE_ORDER或PLATFORM_ORDER环境变量,默认弹幕源很少 3. 在管理面板设置后端地址 - - - -## 超分功能说明 +## 超分功能说明 超分功能需要浏览器支持webgpu并且你的浏览器环境不能是http(如非要在http中使用,需要在浏览器端设置允许不安全的内容) - - - ## AndroidTV 使用 目前该项目可以配合 [OrionTV](https://github.com/zimplexing/OrionTV) 在 Android TV 上使用,可以直接作为 OrionTV 后端 diff --git a/VERCEL_DEPLOYMENT.md b/VERCEL_DEPLOYMENT.md index 293067d77..52b0fce89 100644 --- a/VERCEL_DEPLOYMENT.md +++ b/VERCEL_DEPLOYMENT.md @@ -8,16 +8,17 @@ ### 必需的环境变量 -| 变量名 | 说明 | 示例值 | -|--------|------|--------| -| `NEXT_PUBLIC_STORAGE_TYPE` | 存储类型 | `postgres` | -| `POSTGRES_URL` | Vercel Postgres 连接字符串 | `postgres://...` | -| `USERNAME` | 管理员用户名 | `admin` | -| `PASSWORD` | 管理员密码 | `your_password` | +| 变量名 | 说明 | 示例值 | +| -------------------------- | -------------------------- | ---------------- | +| `NEXT_PUBLIC_STORAGE_TYPE` | 存储类型 | `postgres` | +| `POSTGRES_URL` | Vercel Postgres 连接字符串 | `postgres://...` | +| `USERNAME` | 管理员用户名 | `admin` | +| `PASSWORD` | 管理员密码 | `your_password` | ### Vercel Postgres 连接字符串 Vercel Postgres 会自动提供以下环境变量: + - `POSTGRES_URL` - 完整连接字符串 - `POSTGRES_PRISMA_URL` - Prisma 兼容连接字符串 - `POSTGRES_URL_NON_POOLING` - 无连接池连接字符串 @@ -68,14 +69,14 @@ vercel --prod ## 存储类型对比 -| 存储类型 | 部署平台 | 数据持久化 | 说明 | -|---------|---------|-----------|------| -| `localstorage` | 任意 | ❌ 浏览器本地 | 仅用于测试 | -| `d1` | Cloudflare | ✅ | Cloudflare D1 数据库 | -| `postgres` | Vercel | ✅ | Vercel Postgres 数据库 | -| `redis` | 自建服务器 | ✅ | Redis 数据库 | -| `upstash` | Vercel | ✅ | Upstash Redis | -| `kvrocks` | 自建服务器 | ✅ | Kvrocks 数据库 | +| 存储类型 | 部署平台 | 数据持久化 | 说明 | +| -------------- | ---------- | ------------- | ---------------------- | +| `localstorage` | 任意 | ❌ 浏览器本地 | 仅用于测试 | +| `d1` | Cloudflare | ✅ | Cloudflare D1 数据库 | +| `postgres` | Vercel | ✅ | Vercel Postgres 数据库 | +| `redis` | 自建服务器 | ✅ | Redis 数据库 | +| `upstash` | Vercel | ✅ | Upstash Redis | +| `kvrocks` | 自建服务器 | ✅ | Kvrocks 数据库 | ## 数据迁移 diff --git a/jest.setup.ts b/jest.setup.ts index 46ca8eb19..7d568f9d8 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,3 +1,3 @@ import '@testing-library/jest-dom/extend-expect'; -jest.mock('next/router', () => require('next-router-mock')); \ No newline at end of file +jest.mock('next/router', () => require('next-router-mock')); diff --git a/next.config.js b/next.config.js index d2f1279d7..656bbe391 100644 --- a/next.config.js +++ b/next.config.js @@ -2,7 +2,10 @@ /* eslint-disable @typescript-eslint/no-var-requires */ // 检测是否为 Cloudflare Pages 构建 -const isCloudflare = process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare'; +const isCloudflare = + process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare'; +const isLintRun = + process.argv.includes('lint') || process.env.NEXT_DISABLE_PWA === '1'; const nextConfig = { // Cloudflare Pages 不支持 standalone,使用默认输出 @@ -38,7 +41,7 @@ const nextConfig = { webpack(config, { isServer }) { // Grab the existing rule that handles SVG imports const fileLoaderRule = config.module.rules.find((rule) => - rule.test?.test?.('.svg') + rule.test?.test?.('.svg'), ); config.module.rules.push( @@ -58,7 +61,7 @@ const nextConfig = { dimensions: false, titleProp: true, }, - } + }, ); // Modify the file loader rule to ignore *.svg, since we have it handled now. @@ -77,7 +80,7 @@ const nextConfig = { config.externals.push({ 'better-sqlite3': 'commonjs better-sqlite3', '@vercel/postgres': 'commonjs @vercel/postgres', - 'pg': 'commonjs pg', + pg: 'commonjs pg', }); config.resolve.alias = { @@ -94,11 +97,15 @@ const nextConfig = { }, }; -const withPWA = require('next-pwa')({ - dest: 'public', - disable: process.env.NODE_ENV === 'development', - register: true, - skipWaiting: true, -}); +if (isLintRun) { + module.exports = nextConfig; +} else { + const withPWA = require('next-pwa')({ + dest: 'public', + disable: process.env.NODE_ENV === 'development', + register: true, + skipWaiting: true, + }); -module.exports = withPWA(nextConfig); + module.exports = withPWA(nextConfig); +} diff --git a/package.json b/package.json index 081c5a326..e5b9034da 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "start": "tsx start.ts", "preview:cloudflare": "wrangler dev", "deploy:cloudflare": "wrangler deploy", - "lint": "next lint", - "lint:fix": "eslint src --fix && pnpm format", - "lint:strict": "eslint --max-warnings=0 src", + "lint": "cross-env NEXT_DISABLE_PWA=1 next lint --quiet", + "lint:fix": "cross-env NEXT_DISABLE_PWA=1 eslint src --fix && pnpm format", + "lint:strict": "cross-env NEXT_DISABLE_PWA=1 eslint --max-warnings=0 src", "typecheck": "tsc --noEmit --incremental false", "test:watch": "jest --watch", "test": "jest", @@ -106,7 +106,7 @@ "cross-env": "^7.0.3", "eslint": "^8.57.1", "eslint-config-next": "^14.2.23", - "eslint-config-prettier": "^8.10.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-unused-imports": "^4.4.1", "husky": "^7.0.4", @@ -114,8 +114,8 @@ "lint-staged": "^12.5.0", "next-router-mock": "^0.9.0", "postcss": "^8.5.1", - "prettier": "^2.8.8", - "prettier-plugin-tailwindcss": "^0.5.0", + "prettier": "^3.8.1", + "prettier-plugin-tailwindcss": "^0.7.1", "tailwindcss": "^3.4.17", "typescript": "^5.9.3", "vercel": "^50.4.10", @@ -124,7 +124,7 @@ }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": [ - "eslint --max-warnings=0", + "eslint --fix --quiet", "prettier -w" ], "**/*.{json,css,scss,md,webmanifest}": [ @@ -132,4 +132,4 @@ ] }, "packageManager": "pnpm@10.14.0" -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df4411362..628a0ef57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,8 +244,8 @@ importers: specifier: ^14.2.23 version: 14.2.35(eslint@8.57.1)(typescript@5.9.3) eslint-config-prettier: - specifier: ^8.10.0 - version: 8.10.2(eslint@8.57.1) + specifier: ^10.1.8 + version: 10.1.8(eslint@8.57.1) eslint-plugin-simple-import-sort: specifier: ^7.0.0 version: 7.0.0(eslint@8.57.1) @@ -268,11 +268,11 @@ importers: specifier: ^8.5.1 version: 8.5.6 prettier: - specifier: ^2.8.8 - version: 2.8.8 + specifier: ^3.8.1 + version: 3.8.1 prettier-plugin-tailwindcss: - specifier: ^0.5.0 - version: 0.5.14(prettier@2.8.8) + specifier: ^0.7.1 + version: 0.7.2(prettier@3.8.1) tailwindcss: specifier: ^3.4.17 version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) @@ -4877,8 +4877,8 @@ packages: typescript: optional: true - eslint-config-prettier@8.10.2: - resolution: {integrity: sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -7141,61 +7141,64 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-tailwindcss@0.5.14: - resolution: {integrity: sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q==} - engines: {node: '>=14.21.3'} + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' '@prettier/plugin-pug': '*' '@shopify/prettier-plugin-liquid': '*' '@trivago/prettier-plugin-sort-imports': '*' - '@zackad/prettier-plugin-twig-melody': '*' + '@zackad/prettier-plugin-twig': '*' prettier: ^3.0 prettier-plugin-astro: '*' prettier-plugin-css-order: '*' - prettier-plugin-import-sort: '*' prettier-plugin-jsdoc: '*' prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' prettier-plugin-organize-attributes: '*' prettier-plugin-organize-imports: '*' prettier-plugin-sort-imports: '*' - prettier-plugin-style-order: '*' prettier-plugin-svelte: '*' peerDependenciesMeta: '@ianvs/prettier-plugin-sort-imports': optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true '@prettier/plugin-pug': optional: true '@shopify/prettier-plugin-liquid': optional: true '@trivago/prettier-plugin-sort-imports': optional: true - '@zackad/prettier-plugin-twig-melody': + '@zackad/prettier-plugin-twig': optional: true prettier-plugin-astro: optional: true prettier-plugin-css-order: optional: true - prettier-plugin-import-sort: - optional: true prettier-plugin-jsdoc: optional: true prettier-plugin-marko: optional: true + prettier-plugin-multiline-arrays: + optional: true prettier-plugin-organize-attributes: optional: true prettier-plugin-organize-imports: optional: true prettier-plugin-sort-imports: optional: true - prettier-plugin-style-order: - optional: true prettier-plugin-svelte: optional: true - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} hasBin: true pretty-bytes@5.6.0: @@ -15085,7 +15088,7 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-prettier@8.10.2(eslint@8.57.1): + eslint-config-prettier@10.1.8(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -18013,11 +18016,11 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-tailwindcss@0.5.14(prettier@2.8.8): + prettier-plugin-tailwindcss@0.7.2(prettier@3.8.1): dependencies: - prettier: 2.8.8 + prettier: 3.8.1 - prettier@2.8.8: {} + prettier@3.8.1: {} pretty-bytes@5.6.0: {} diff --git a/proxy.worker.ts b/proxy.worker.ts index eb6baa060..85ee70ff6 100644 --- a/proxy.worker.ts +++ b/proxy.worker.ts @@ -28,7 +28,10 @@ async function handleRequest(request: Request): Promise { actualUrl = ensureProtocol(actualUrl, url.protocol); actualUrl += url.search; - const newHeaders = filterHeaders(request.headers, (name) => !name.startsWith('cf-')); + const newHeaders = filterHeaders( + request.headers, + (name) => !name.startsWith('cf-'), + ); const modifiedRequest = new Request(actualUrl, { headers: newHeaders, method: request.method, @@ -44,7 +47,12 @@ async function handleRequest(request: Request): Promise { } if (response.headers.get('Content-Type')?.includes('text/html')) { - body = await handleHtmlContent(response, url.protocol, url.host, actualUrl); + body = await handleHtmlContent( + response, + url.protocol, + url.host, + actualUrl, + ); } const modifiedResponse = new Response(body, { @@ -64,12 +72,14 @@ async function handleRequest(request: Request): Promise { } function ensureProtocol(url: string, defaultProtocol: string): string { - return url.startsWith('http://') || url.startsWith('https://') ? url : `${defaultProtocol}//${url}`; + return url.startsWith('http://') || url.startsWith('https://') + ? url + : `${defaultProtocol}//${url}`; } function handleRedirect( response: Response, - body: BodyInit | ReadableStream | null + body: BodyInit | ReadableStream | null, ): Response { const locationHeader = response.headers.get('location'); if (!locationHeader) { @@ -96,13 +106,23 @@ async function handleHtmlContent( response: Response, protocol: string, host: string, - actualUrl: string + actualUrl: string, ): Promise { const originalText = await response.text(); - return replaceRelativePaths(originalText, protocol, host, new URL(actualUrl).origin); + return replaceRelativePaths( + originalText, + protocol, + host, + new URL(actualUrl).origin, + ); } -function replaceRelativePaths(text: string, protocol: string, host: string, origin: string): string { +function replaceRelativePaths( + text: string, + protocol: string, + host: string, + origin: string, +): string { const regex = new RegExp('((href|src|action)=["\'])/(?!/)', 'g'); return text.replace(regex, `$1${protocol}//${host}/${origin}/`); } @@ -116,7 +136,10 @@ function jsonResponse(data: Record, status: number): Response { }); } -function filterHeaders(headers: Headers, filterFunc: (name: string) => boolean): Headers { +function filterHeaders( + headers: Headers, + filterFunc: (name: string) => boolean, +): Headers { return new Headers([...headers].filter(([name]) => filterFunc(name))); } @@ -215,4 +238,4 @@ function getRootHtml(): string { `; -} \ No newline at end of file +} diff --git a/scripts/convert-changelog.ts b/scripts/convert-changelog.ts index 8890d7fe8..dcda8e643 100644 --- a/scripts/convert-changelog.ts +++ b/scripts/convert-changelog.ts @@ -29,7 +29,9 @@ function parseChangelog(content: string): { versions: OutputVersion[] } { for (const line of lines) { const trimmedLine = line.trim(); - const versionMatch = trimmedLine.match(/^## \[([\d.]+)\] - (\d{4}-\d{2}-\d{2})$/); + const versionMatch = trimmedLine.match( + /^## \[([\d.]+)\] - (\d{4}-\d{2}-\d{2})$/, + ); if (versionMatch) { if (currentVersion) { @@ -73,7 +75,11 @@ function parseChangelog(content: string): { versions: OutputVersion[] } { continue; } - if (trimmedLine && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('###')) { + if ( + trimmedLine && + !trimmedLine.startsWith('#') && + !trimmedLine.startsWith('###') + ) { currentVersion.content.push(trimmedLine); } } @@ -83,7 +89,10 @@ function parseChangelog(content: string): { versions: OutputVersion[] } { } const normalizedVersions = versions.map((version) => { - const hasCategories = version.added.length > 0 || version.changed.length > 0 || version.fixed.length > 0; + const hasCategories = + version.added.length > 0 || + version.changed.length > 0 || + version.fixed.length > 0; return { version: version.version, date: version.date, @@ -96,12 +105,20 @@ function parseChangelog(content: string): { versions: OutputVersion[] } { return { versions: normalizedVersions }; } -function generateTypeScript(changelogData: { versions: OutputVersion[] }): string { +function generateTypeScript(changelogData: { + versions: OutputVersion[]; +}): string { const entries = changelogData.versions .map((version) => { - const addedEntries = version.added.map((entry) => ` ${JSON.stringify(entry)}`).join(',\n'); - const changedEntries = version.changed.map((entry) => ` ${JSON.stringify(entry)}`).join(',\n'); - const fixedEntries = version.fixed.map((entry) => ` ${JSON.stringify(entry)}`).join(',\n'); + const addedEntries = version.added + .map((entry) => ` ${JSON.stringify(entry)}`) + .join(',\n'); + const changedEntries = version.changed + .map((entry) => ` ${JSON.stringify(entry)}`) + .join(',\n'); + const fixedEntries = version.fixed + .map((entry) => ` ${JSON.stringify(entry)}`) + .join(',\n'); return ` { version: ${JSON.stringify(version.version)}, @@ -156,7 +173,7 @@ function updateVersionTs(version: string): void { const content = fs.readFileSync(versionTsPath, 'utf8'); const updatedContent = content.replace( /const CURRENT_VERSION = ['"`][^'"`]+['"`];/, - `const CURRENT_VERSION = '${version}';` + `const CURRENT_VERSION = '${version}';`, ); fs.writeFileSync(versionTsPath, updatedContent, 'utf8'); @@ -210,7 +227,7 @@ function main(): void { console.log('📊 版本统计:'); changelogData.versions.forEach((version) => { console.log( - ` ${version.version} (${version.date}): +${version.added.length} ~${version.changed.length} !${version.fixed.length}` + ` ${version.version} (${version.date}): +${version.added.length} ~${version.changed.length} !${version.fixed.length}`, ); }); @@ -221,4 +238,4 @@ function main(): void { } } -main(); \ No newline at end of file +main(); diff --git a/scripts/generate-manifest.ts b/scripts/generate-manifest.ts index 515086763..d26c64f4b 100644 --- a/scripts/generate-manifest.ts +++ b/scripts/generate-manifest.ts @@ -24,7 +24,10 @@ interface WebManifest { } export function generateManifest(): void { - const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + const projectRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', + ); const publicDir = path.join(projectRoot, 'public'); const manifestPath = path.join(publicDir, 'manifest.json'); const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTVPlus'; @@ -60,6 +63,9 @@ export function generateManifest(): void { } } -if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { generateManifest(); -} \ No newline at end of file +} diff --git a/scripts/init-postgres.ts b/scripts/init-postgres.ts index 93f424fcb..055334113 100644 --- a/scripts/init-postgres.ts +++ b/scripts/init-postgres.ts @@ -26,7 +26,10 @@ if (migrationFiles.length === 0) { process.exit(1); } -console.log(`📄 Found ${migrationFiles.length} migration file(s):`, migrationFiles.join(', ')); +console.log( + `📄 Found ${migrationFiles.length} migration file(s):`, + migrationFiles.join(', '), +); async function main(): Promise { try { @@ -76,4 +79,4 @@ async function main(): Promise { } } -void main(); \ No newline at end of file +void main(); diff --git a/scripts/init-sqlite.ts b/scripts/init-sqlite.ts index e2d55ccde..684633eed 100644 --- a/scripts/init-sqlite.ts +++ b/scripts/init-sqlite.ts @@ -19,7 +19,10 @@ const db = new Database(dbPath); console.log('📦 Initializing SQLite database for development...'); console.log('📍 Database location:', dbPath); -const migrationPath = path.join(process.cwd(), 'migrations/001_initial_schema.sql'); +const migrationPath = path.join( + process.cwd(), + 'migrations/001_initial_schema.sql', +); if (!fs.existsSync(migrationPath)) { console.error('❌ Migration file not found:', migrationPath); process.exit(1); @@ -54,4 +57,4 @@ console.log('🎉 SQLite database initialized successfully!'); console.log(''); console.log('Next steps:'); console.log('1. Set NEXT_PUBLIC_STORAGE_TYPE=d1 in .env'); -console.log('2. Run: npm run dev'); \ No newline at end of file +console.log('2. Run: npm run dev'); diff --git a/scripts/reset-sqlite.ts b/scripts/reset-sqlite.ts index f11798908..af9ef26dc 100644 --- a/scripts/reset-sqlite.ts +++ b/scripts/reset-sqlite.ts @@ -12,4 +12,4 @@ for (const fileName of filesToDelete) { } } -await import('./init-sqlite'); \ No newline at end of file +await import('./init-sqlite'); diff --git a/server.ts b/server.ts index ed199e30d..daaae26ec 100644 --- a/server.ts +++ b/server.ts @@ -25,7 +25,8 @@ const app = next({ dev, hostname, port }); const handle = app.getRequestHandler(); async function getWatchRoomConfig(): Promise { - const serverType = process.env.WATCH_ROOM_SERVER_TYPE === 'external' ? 'external' : 'internal'; + const serverType = + process.env.WATCH_ROOM_SERVER_TYPE === 'external' ? 'external' : 'internal'; const config: WatchRoomConfig = { enabled: process.env.WATCH_ROOM_ENABLED === 'true', serverType, @@ -33,7 +34,9 @@ async function getWatchRoomConfig(): Promise { externalServerAuth: process.env.WATCH_ROOM_EXTERNAL_SERVER_AUTH, }; - console.log(`[WatchRoom] Watch room ${config.enabled ? 'enabled' : 'disabled'} via environment variable.`); + console.log( + `[WatchRoom] Watch room ${config.enabled ? 'enabled' : 'disabled'} via environment variable.`, + ); return config; } @@ -92,7 +95,9 @@ class WatchRoomServer { socket.join(roomId); - console.log(`[WatchRoom] Room created: ${roomId} by ${data.userName}`); + console.log( + `[WatchRoom] Room created: ${roomId} by ${data.userName}`, + ); callback({ success: true, room }); } catch (error) { console.error('[WatchRoom] Error creating room:', error); @@ -121,12 +126,16 @@ class WatchRoomServer { room.ownerId = userId; room.lastOwnerHeartbeat = Date.now(); this.rooms.set(data.roomId, room); - console.log(`[WatchRoom] Owner ${data.userName} reconnected to room ${data.roomId}`); + console.log( + `[WatchRoom] Owner ${data.userName} reconnected to room ${data.roomId}`, + ); } const deletionTimer = this.roomDeletionTimers.get(data.roomId); if (deletionTimer) { - console.log(`[WatchRoom] Cancelling deletion timer for room ${data.roomId}`); + console.log( + `[WatchRoom] Cancelling deletion timer for room ${data.roomId}`, + ); clearTimeout(deletionTimer); this.roomDeletionTimers.delete(data.roomId); } @@ -155,7 +164,9 @@ class WatchRoomServer { socket.join(data.roomId); socket.to(data.roomId).emit('room:member-joined', member); - console.log(`[WatchRoom] User ${data.userName} joined room ${data.roomId}${isOwner ? ' (as owner)' : ''}`); + console.log( + `[WatchRoom] User ${data.userName} joined room ${data.roomId}${isOwner ? ' (as owner)' : ''}`, + ); const members = Array.from(roomMembers?.values() || []); callback({ success: true, room, members }); @@ -170,15 +181,22 @@ class WatchRoomServer { }); socket.on('room:list', (callback) => { - const publicRooms = Array.from(this.rooms.values()).filter((room) => room.isPublic); + const publicRooms = Array.from(this.rooms.values()).filter( + (room) => room.isPublic, + ); callback(publicRooms); }); socket.on('play:update', (state) => { - console.log(`[WatchRoom] Received play:update from ${socket.id}:`, state); + console.log( + `[WatchRoom] Received play:update from ${socket.id}:`, + state, + ); const roomInfo = this.socketToRoom.get(socket.id); if (!roomInfo) { - console.log('[WatchRoom] No room info for socket, ignoring play:update'); + console.log( + '[WatchRoom] No room info for socket, ignoring play:update', + ); return; } @@ -190,19 +208,28 @@ class WatchRoomServer { room.currentState = state; this.rooms.set(roomInfo.roomId, room); - console.log(`[WatchRoom] Broadcasting play:update to room ${roomInfo.roomId} from ${roomInfo.userName}`); + console.log( + `[WatchRoom] Broadcasting play:update to room ${roomInfo.roomId} from ${roomInfo.userName}`, + ); socket.to(roomInfo.roomId).emit('play:update', state); }); socket.on('play:seek', (currentTime) => { - console.log(`[WatchRoom] Received play:seek from ${socket.id}:`, currentTime); + console.log( + `[WatchRoom] Received play:seek from ${socket.id}:`, + currentTime, + ); const roomInfo = this.socketToRoom.get(socket.id); if (!roomInfo) { - console.log('[WatchRoom] No room info for socket, ignoring play:seek'); + console.log( + '[WatchRoom] No room info for socket, ignoring play:seek', + ); return; } - console.log(`[WatchRoom] Broadcasting play:seek to room ${roomInfo.roomId}`); + console.log( + `[WatchRoom] Broadcasting play:seek to room ${roomInfo.roomId}`, + ); socket.to(roomInfo.roomId).emit('play:seek', currentTime); }); @@ -210,11 +237,15 @@ class WatchRoomServer { console.log(`[WatchRoom] Received play:play from ${socket.id}`); const roomInfo = this.socketToRoom.get(socket.id); if (!roomInfo) { - console.log('[WatchRoom] No room info for socket, ignoring play:play'); + console.log( + '[WatchRoom] No room info for socket, ignoring play:play', + ); return; } - console.log(`[WatchRoom] Broadcasting play:play to room ${roomInfo.roomId}`); + console.log( + `[WatchRoom] Broadcasting play:play to room ${roomInfo.roomId}`, + ); socket.to(roomInfo.roomId).emit('play:play'); }); @@ -222,19 +253,28 @@ class WatchRoomServer { console.log(`[WatchRoom] Received play:pause from ${socket.id}`); const roomInfo = this.socketToRoom.get(socket.id); if (!roomInfo) { - console.log('[WatchRoom] No room info for socket, ignoring play:pause'); + console.log( + '[WatchRoom] No room info for socket, ignoring play:pause', + ); return; } - console.log(`[WatchRoom] Broadcasting play:pause to room ${roomInfo.roomId}`); + console.log( + `[WatchRoom] Broadcasting play:pause to room ${roomInfo.roomId}`, + ); socket.to(roomInfo.roomId).emit('play:pause'); }); socket.on('play:change', (state) => { - console.log(`[WatchRoom] Received play:change from ${socket.id}:`, state); + console.log( + `[WatchRoom] Received play:change from ${socket.id}:`, + state, + ); const roomInfo = this.socketToRoom.get(socket.id); if (!roomInfo) { - console.log('[WatchRoom] No room info for socket, ignoring play:change'); + console.log( + '[WatchRoom] No room info for socket, ignoring play:change', + ); return; } @@ -251,7 +291,9 @@ class WatchRoomServer { room.currentState = state; this.rooms.set(roomInfo.roomId, room); - console.log(`[WatchRoom] Broadcasting play:change to room ${roomInfo.roomId}`); + console.log( + `[WatchRoom] Broadcasting play:change to room ${roomInfo.roomId}`, + ); socket.to(roomInfo.roomId).emit('play:change', state); }); @@ -389,7 +431,9 @@ class WatchRoomServer { socket.to(roomId).emit('room:member-left', userId); if (isOwner) { - console.log(`[WatchRoom] Owner actively left room ${roomId}, disbanding room`); + console.log( + `[WatchRoom] Owner actively left room ${roomId}, disbanding room`, + ); socket.to(roomId).emit('room:deleted', { reason: 'owner_left' }); for (const memberId of roomMembers.keys()) { @@ -404,12 +448,16 @@ class WatchRoomServer { this.roomDeletionTimers.delete(roomId); } } else if (roomMembers.size === 0) { - console.log(`[WatchRoom] Room ${roomId} is now empty, will delete in 30 seconds if no one rejoins`); + console.log( + `[WatchRoom] Room ${roomId} is now empty, will delete in 30 seconds if no one rejoins`, + ); const deletionTimer = setTimeout(() => { const currentRoomMembers = this.members.get(roomId); if (currentRoomMembers && currentRoomMembers.size === 0) { - console.log(`[WatchRoom] Room ${roomId} deletion timer expired, deleting room`); + console.log( + `[WatchRoom] Room ${roomId} deletion timer expired, deleting room`, + ); this.deleteRoom(roomId); this.roomDeletionTimers.delete(roomId); } @@ -443,8 +491,13 @@ class WatchRoomServer { for (const [roomId, room] of this.rooms.entries()) { const timeSinceHeartbeat = now - room.lastOwnerHeartbeat; - if (timeSinceHeartbeat > clearStateTimeout && room.currentState !== null) { - console.log(`[WatchRoom] Room ${roomId} owner inactive for 30s, clearing play state`); + if ( + timeSinceHeartbeat > clearStateTimeout && + room.currentState !== null + ) { + console.log( + `[WatchRoom] Room ${roomId} owner inactive for 30s, clearing play state`, + ); room.currentState = null; this.rooms.set(roomId, room); this.io.to(roomId).emit('state:cleared'); @@ -500,13 +553,16 @@ void app if (watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal') { console.log('[WatchRoom] Initializing Socket.IO server...'); - const io = new Server(httpServer, { - path: '/socket.io', - cors: { - origin: '*', - methods: ['GET', 'POST'], + const io = new Server( + httpServer, + { + path: '/socket.io', + cors: { + origin: '*', + methods: ['GET', 'POST'], + }, }, - }); + ); watchRoomServer = new WatchRoomServer(io); console.log('[WatchRoom] Socket.IO server initialized'); @@ -523,7 +579,10 @@ void app }) .listen(port, () => { console.log(`> Ready on http://${hostname}:${port}`); - if (watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal') { + if ( + watchRoomConfig.enabled && + watchRoomConfig.serverType === 'internal' + ) { console.log(`> Socket.IO ready on ws://${hostname}:${port}`); } }); @@ -543,4 +602,4 @@ void app .catch((error) => { console.error('[Server] Failed to start:', error); process.exit(1); - }); \ No newline at end of file + }); diff --git a/server/watch-room-standalone-server.ts b/server/watch-room-standalone-server.ts index d48feda1d..17c1b4c17 100644 --- a/server/watch-room-standalone-server.ts +++ b/server/watch-room-standalone-server.ts @@ -5,17 +5,25 @@ import { createServer } from 'node:http'; import { Server } from 'socket.io'; import { WatchRoomServer } from '../src/lib/watch-room-server'; -import type { ClientToServerEvents, ServerToClientEvents } from '../src/types/watch-room'; +import type { + ClientToServerEvents, + ServerToClientEvents, +} from '../src/types/watch-room'; const args = process.argv.slice(2); const portIndex = args.indexOf('--port'); const authIndex = args.indexOf('--auth'); -const port = Number.parseInt(portIndex >= 0 ? args[portIndex + 1] || '3001' : '3001', 10); +const port = Number.parseInt( + portIndex >= 0 ? args[portIndex + 1] || '3001' : '3001', + 10, +); const authKey = authIndex >= 0 ? args[authIndex + 1] || '' : ''; if (!authKey) { console.error('Error: --auth parameter is required'); - console.log('Usage: tsx server/watch-room-standalone-server.ts --port 3001 --auth YOUR_SECRET_KEY'); + console.log( + 'Usage: tsx server/watch-room-standalone-server.ts --port 3001 --auth YOUR_SECRET_KEY', + ); process.exit(1); } @@ -56,4 +64,4 @@ const shutdown = (): void => { }; process.on('SIGINT', shutdown); -process.on('SIGTERM', shutdown); \ No newline at end of file +process.on('SIGTERM', shutdown); diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index d6745d060..861c88148 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -45,7 +45,14 @@ import { Video, } from 'lucide-react'; import { GripVertical } from 'lucide-react'; -import { memo, Suspense, useCallback, useEffect, useMemo, useState } from 'react'; +import { + memo, + Suspense, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { createPortal } from 'react-dom'; import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; @@ -228,10 +235,7 @@ const AlertModal = ({ ) : ( // 普通提示:只显示确定按钮 - ) @@ -239,7 +243,7 @@ const AlertModal = ({ , - document.body + document.body, ); }; @@ -303,7 +307,7 @@ const useLoadingState = () => { const withLoading = async ( key: string, - operation: () => Promise + operation: () => Promise, ): Promise => { setLoading(key, true); try { @@ -410,11 +414,13 @@ const CollapsibleTab = ({ isParent = false, }: CollapsibleTabProps) => { return ( -
+
- {isExpanded &&
{children}
} + {isExpanded && ( +
+ {children} +
+ )}
); }; @@ -464,7 +482,17 @@ interface UserConfigProps { userListLoading: boolean; } -const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalPages, userTotal, fetchUsersV2, userListLoading }: UserConfigProps) => { +const UserConfig = ({ + config, + role, + refreshConfig, + usersV2, + userPage, + userTotalPages, + userTotal, + fetchUsersV2, + userListLoading, +}: UserConfigProps) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); const [showAddUserForm, setShowAddUserForm] = useState(false); @@ -523,7 +551,9 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP const currentUsername = getAuthInfoFromBrowserCookie()?.username || null; // 判断是否有旧版用户数据需要迁移 - const hasOldUserData = config?.UserConfig?.Users?.filter((u: any) => u.role !== 'owner').length ?? 0 > 0; + const hasOldUserData = + config?.UserConfig?.Users?.filter((u: any) => u.role !== 'owner').length ?? + 0 > 0; // 使用新版本用户列表(如果可用且没有旧数据),否则使用配置中的用户列表 const displayUsers: Array<{ @@ -534,7 +564,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP tags?: string[]; created_at?: number; oidcSub?: string; - }> = !hasOldUserData && usersV2 ? usersV2 : (config?.UserConfig?.Users || []); + }> = !hasOldUserData && usersV2 ? usersV2 : config?.UserConfig?.Users || []; // 使用 useMemo 计算全选状态,避免每次渲染都重新计算 const selectAllUsers = useMemo(() => { @@ -543,7 +573,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP (user) => role === 'owner' || (role === 'admin' && - (user.role === 'user' || user.username === currentUsername)) + (user.role === 'user' || user.username === currentUsername)), ).length || 0; return selectedUsers.size === selectableUserCount && selectedUsers.size > 0; }, [selectedUsers.size, displayUsers, role, currentUsername]); @@ -555,7 +585,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP const handleUserGroupAction = async ( action: 'add' | 'edit' | 'delete', groupName: string, - enabledApis?: string[] + enabledApis?: string[], ) => { return withLoading(`userGroup_${action}_${groupName}`, async () => { try { @@ -589,9 +619,9 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP action === 'add' ? '用户组添加成功' : action === 'edit' - ? '用户组更新成功' - : '用户组删除成功', - showAlert + ? '用户组更新成功' + : '用户组删除成功', + showAlert, ); } catch (err) { showError(err instanceof Error ? err.message : '操作失败', showAlert); @@ -610,7 +640,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP handleUserGroupAction( 'edit', editingUserGroup.name, - editingUserGroup.enabledApis + editingUserGroup.enabledApis, ); }; @@ -618,7 +648,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP // 计算会受影响的用户数量 const affectedUsers = config?.UserConfig?.Users?.filter( - (user) => user.tags && user.tags.includes(groupName) + (user) => user.tags && user.tags.includes(groupName), ) || []; setDeletingUserGroup({ @@ -655,7 +685,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP // 为用户分配用户组 const handleAssignUserGroup = async ( username: string, - userGroups: string[] + userGroups: string[], ) => { return withLoading(`assignUserGroup_${username}`, async () => { try { @@ -689,19 +719,19 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP const handleUnbanUser = async (uname: string) => { await withLoading(`unbanUser_${uname}`, () => - handleUserAction('unban', uname) + handleUserAction('unban', uname), ); }; const handleSetAdmin = async (uname: string) => { await withLoading(`setAdmin_${uname}`, () => - handleUserAction('setAdmin', uname) + handleUserAction('setAdmin', uname), ); }; const handleRemoveAdmin = async (uname: string) => { await withLoading(`removeAdmin_${uname}`, () => - handleUserAction('cancelAdmin', uname) + handleUserAction('cancelAdmin', uname), ); }; @@ -712,7 +742,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP 'add', newUser.username, newUser.password, - newUser.userGroup + newUser.userGroup, ); setNewUser({ username: '', password: '', userGroup: '' }); setShowAddUserForm(false); @@ -727,11 +757,11 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP await handleUserAction( 'changePassword', changePasswordUser.username, - changePasswordUser.password + changePasswordUser.password, ); setChangePasswordUser({ username: '', password: '' }); setShowChangePasswordForm(false); - } + }, ); }; @@ -775,7 +805,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP try { await handleAssignUserGroup( selectedUserForGroup.username, - selectedUserGroups + selectedUserGroups, ); setShowConfigureUserGroupModal(false); setSelectedUserForGroup(null); @@ -783,7 +813,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP } catch (err) { // 错误处理已在 handleAssignUserGroup 中处理 } - } + }, ); }; @@ -809,14 +839,14 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP (user) => role === 'owner' || (role === 'admin' && - (user.role === 'user' || user.username === currentUsername)) + (user.role === 'user' || user.username === currentUsername)), ).map((u) => u.username) || []; setSelectedUsers(new Set(selectableUsernames)); } else { setSelectedUsers(new Set()); } }, - [config?.UserConfig?.Users, role, currentUsername] + [config?.UserConfig?.Users, role, currentUsername], ); // 批量设置用户组 @@ -846,7 +876,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP setSelectedUserGroup(''); showSuccess( `已为 ${userCount} 个用户设置用户组: ${userGroup}`, - showAlert + showAlert, ); // 刷新配置 @@ -913,7 +943,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP | 'deleteUser', targetUsername: string, targetPassword?: string, - userGroup?: string + userGroup?: string, ) => { try { const res = await fetch('/api/admin/user', { @@ -979,69 +1009,77 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP {/* 数据迁移提示 */} {config.UserConfig.Users && - config.UserConfig.Users.filter(u => u.role !== 'owner').length > 0 && ( -
-
-
-
- 检测到旧版用户数据 -
-

- 建议迁移到新的用户存储结构,以获得更好的性能和安全性。迁移后用户密码将使用SHA256加密。 -

-
- + showAlert({ + type: 'success', + title: '用户数据迁移成功', + message: '所有用户已迁移到新的存储结构', + timer: 2000, + }); + await refreshConfig(); + } catch (error: any) { + console.error('迁移用户数据失败:', error); + showAlert({ + type: 'error', + title: '迁移失败', + message: + error.message || '迁移用户数据时发生错误', + }); + } + }); + }, + }); + }} + disabled={isLoading('migrateUsers')} + className={`ml-4 ${buttonStyles.warning} ${ + isLoading('migrateUsers') + ? 'opacity-50 cursor-not-allowed' + : '' + }`} + > + {isLoading('migrateUsers') ? '迁移中...' : '立即迁移'} + +
-
- )} + )} {/* 用户组管理 */} @@ -1318,404 +1356,414 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
{/* 迁移遮罩层 */} {config.UserConfig.Users && - config.UserConfig.Users.filter(u => u.role !== 'owner').length > 0 && ( -
-
-
- -

- 需要迁移数据 -

+ config.UserConfig.Users.filter((u) => u.role !== 'owner').length > + 0 && ( +
+
+
+ +

+ 需要迁移数据 +

+
+

+ 检测到旧版用户数据,请先迁移到新的存储结构后再进行用户管理操作。 +

+

+ 请在上方的"用户统计"区域点击"立即迁移"按钮完成数据迁移。 +

-

- 检测到旧版用户数据,请先迁移到新的存储结构后再进行用户管理操作。 -

-

- 请在上方的"用户统计"区域点击"立即迁移"按钮完成数据迁移。 -

-
- )} + )}
- - - - + ); + })} + + ); + })()} +
- - {(() => { - // 检查是否有权限操作任何用户 - const hasAnyPermission = config?.UserConfig?.Users?.some( - (user) => - role === 'owner' || - (role === 'admin' && - (user.role === 'user' || - user.username === currentUsername)) - ); + + + + - - - - - - - - - {/* 按规则排序用户:自己 -> 站长(若非自己) -> 管理员 -> 其他 */} - {(() => { - // 如果正在加载,显示加载状态 - if (userListLoading) { - return ( - - - - - - ); - } + return hasAnyPermission ? ( + + handleSelectAllUsers(e.target.checked) + } + className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' + /> + ) : ( +
+ ); + })()} + +
+ + + + + + + + {/* 按规则排序用户:自己 -> 站长(若非自己) -> 管理员 -> 其他 */} + {(() => { + // 如果正在加载,显示加载状态 + if (userListLoading) { + return ( + + + + + + ); + } - const sortedUsers = [...displayUsers].sort((a, b) => { - type UserInfo = (typeof displayUsers)[number]; - const priority = (u: UserInfo) => { - if (u.username === currentUsername) return 0; - if (u.role === 'owner') return 1; - if (u.role === 'admin') return 2; - return 3; - }; - return priority(a) - priority(b); - }); - return ( - - {sortedUsers.map((user) => { - // 修改密码权限:站长可修改管理员和普通用户密码,管理员可修改普通用户和自己的密码,但任何人都不能修改站长密码 - const canChangePassword = - user.role !== 'owner' && // 不能修改站长密码 - (role === 'owner' || // 站长可以修改管理员和普通用户密码 - (role === 'admin' && - (user.role === 'user' || - user.username === currentUsername))); // 管理员可以修改普通用户和自己的密码 - - // 删除用户权限:站长可删除除自己外的所有用户,管理员仅可删除普通用户 - const canDeleteUser = - user.username !== currentUsername && - (role === 'owner' || // 站长可以删除除自己外的所有用户 - (role === 'admin' && user.role === 'user')); // 管理员仅可删除普通用户 - - // 其他操作权限:不能操作自己,站长可操作所有用户,管理员可操作普通用户 - const canOperate = - user.username !== currentUsername && - (role === 'owner' || - (role === 'admin' && user.role === 'user')); - return ( - - + {sortedUsers.map((user) => { + // 修改密码权限:站长可修改管理员和普通用户密码,管理员可修改普通用户和自己的密码,但任何人都不能修改站长密码 + const canChangePassword = + user.role !== 'owner' && // 不能修改站长密码 + (role === 'owner' || // 站长可以修改管理员和普通用户密码 (role === 'admin' && (user.role === 'user' || - user.username === currentUsername)) ? ( - - handleSelectUser( - user.username, - e.target.checked - ) - } - className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' - /> - ) : ( -
- )} - -
+ - + - - - - + + + + - - ); - })} - - ); - })()} -
+ + {(() => { + // 检查是否有权限操作任何用户 + const hasAnyPermission = config?.UserConfig?.Users?.some( + (user) => + role === 'owner' || + (role === 'admin' && + (user.role === 'user' || + user.username === currentUsername)), + ); - return hasAnyPermission ? ( - handleSelectAllUsers(e.target.checked)} - className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' - /> - ) : ( -
- ); - })()} -
- 用户名 - - 角色 - - 状态 - - 用户组 - - 采集源权限 - - 操作 -
- 加载中... -
+ 用户名 + + 角色 + + 状态 + + 用户组 + + 采集源权限 + + 操作 +
+ 加载中... +
- - {role === 'owner' || + const sortedUsers = [...displayUsers].sort((a, b) => { + type UserInfo = (typeof displayUsers)[number]; + const priority = (u: UserInfo) => { + if (u.username === currentUsername) return 0; + if (u.role === 'owner') return 1; + if (u.role === 'admin') return 2; + return 3; + }; + return priority(a) - priority(b); + }); + return ( +
-
- {user.username} - {user.oidcSub && ( - - OIDC - + user.username === currentUsername))); // 管理员可以修改普通用户和自己的密码 + + // 删除用户权限:站长可删除除自己外的所有用户,管理员仅可删除普通用户 + const canDeleteUser = + user.username !== currentUsername && + (role === 'owner' || // 站长可以删除除自己外的所有用户 + (role === 'admin' && user.role === 'user')); // 管理员仅可删除普通用户 + + // 其他操作权限:不能操作自己,站长可操作所有用户,管理员可操作普通用户 + const canOperate = + user.username !== currentUsername && + (role === 'owner' || + (role === 'admin' && user.role === 'user')); + return ( +
+ + {role === 'owner' || + (role === 'admin' && + (user.role === 'user' || + user.username === currentUsername)) ? ( + + handleSelectUser( + user.username, + e.target.checked, + ) + } + className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' + /> + ) : ( +
)} -
-
- + +
+ {user.username} + {user.oidcSub && ( + + OIDC + + )} +
+
+ + {user.role === 'owner' + ? '站长' : user.role === 'admin' - ? 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300' - : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300' - }`} - > - {user.role === 'owner' - ? '站长' - : user.role === 'admin' - ? '管理员' - : '普通用户'} - - - - {!user.banned ? '正常' : '已封禁'} - - -
- - {user.tags && user.tags.length > 0 - ? user.tags.join(', ') - : '无用户组'} + ? '管理员' + : '普通用户'} - {/* 配置用户组按钮 */} - {(role === 'owner' || - (role === 'admin' && - (user.role === 'user' || - user.username === currentUsername))) && ( - - )} -
-
-
- - {user.enabledApis && user.enabledApis.length > 0 - ? `${user.enabledApis.length} 个源` - : '无限制'} - - {/* 配置采集源权限按钮 */} - {(role === 'owner' || - (role === 'admin' && - (user.role === 'user' || - user.username === currentUsername))) && ( - - )} -
-
- {/* 修改密码按钮 */} - {canChangePassword && ( - + - 修改密码 - - )} - {canOperate && ( - <> - {/* 其他操作按钮 */} - {user.role === 'user' && ( + {!user.banned ? '正常' : '已封禁'} + + +
+ + {user.tags && user.tags.length > 0 + ? user.tags.join(', ') + : '无用户组'} + + {/* 配置用户组按钮 */} + {(role === 'owner' || + (role === 'admin' && + (user.role === 'user' || + user.username === currentUsername))) && ( )} - {user.role === 'admin' && ( +
+
+
+ + {user.enabledApis && user.enabledApis.length > 0 + ? `${user.enabledApis.length} 个源` + : '无限制'} + + {/* 配置采集源权限按钮 */} + {(role === 'owner' || + (role === 'admin' && + (user.role === 'user' || + user.username === currentUsername))) && ( )} - {user.role !== 'owner' && - (!user.banned ? ( +
+
+ {/* 修改密码按钮 */} + {canChangePassword && ( + + )} + {canOperate && ( + <> + {/* 其他操作按钮 */} + {user.role === 'user' && ( - ) : ( + )} + {user.role === 'admin' && ( - ))} - - )} - {/* 删除用户按钮 - 放在最后,使用更明显的红色样式 */} - {canDeleteUser && ( - - )} -
- + )} + {user.role !== 'owner' && + (!user.banned ? ( + + ) : ( + + ))} + + )} + {/* 删除用户按钮 - 放在最后,使用更明显的红色样式 */} + {canDeleteUser && ( + + )} + +
+
- {/* 用户列表分页 */} - {!hasOldUserData && usersV2 && userTotalPages > 1 && ( -
-
- 共 {userTotal} 个用户,第 {userPage} / {userTotalPages} 页 -
-
- - - - + {/* 用户列表分页 */} + {!hasOldUserData && usersV2 && userTotalPages > 1 && ( +
+
+ 共 {userTotal} 个用户,第 {userPage} / {userTotalPages} 页 +
+
+ + + + +
-
- )} + )}
@@ -1809,7 +1857,9 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP setSelectedApis([...selectedApis, source.key]); } else { setSelectedApis( - selectedApis.filter((api) => api !== source.key) + selectedApis.filter( + (api) => api !== source.key, + ), ); } }} @@ -1843,7 +1893,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP onClick={() => { const allApis = config?.SourceConfig?.filter( - (source) => !source.disabled + (source) => !source.disabled, ).map((s) => s.key) || []; setSelectedApis(allApis); }} @@ -1877,7 +1927,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
, - document.body + document.body, )} {/* 添加用户组弹窗 */} @@ -1972,7 +2022,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP { if (e.target.checked) { @@ -1987,7 +2037,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP setNewUserGroup((prev) => ({ ...prev, enabledApis: prev.enabledApis.filter( - (api) => api !== source.key + (api) => api !== source.key, ), })); } @@ -2025,7 +2075,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP onClick={() => { const allApis = config?.SourceConfig?.filter( - (source) => !source.disabled + (source) => !source.disabled, ).map((s) => s.key) || []; setNewUserGroup((prev) => ({ ...prev, @@ -2072,7 +2122,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP , - document.body + document.body, )} {/* 编辑用户组弹窗 */} @@ -2133,7 +2183,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP { if (e.target.checked) { @@ -2146,7 +2196,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP source.key, ], } - : null + : null, ); } else { setEditingUserGroup((prev) => @@ -2154,10 +2204,10 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP ? { ...prev, enabledApis: prev.enabledApis.filter( - (api) => api !== source.key + (api) => api !== source.key, ), } - : null + : null, ); } }} @@ -2182,7 +2232,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP @@ -4281,18 +4476,28 @@ const EmbyConfigComponent = ({ 拼接MediaSourceId参数

- 启用后将调用 PlaybackInfo API 获取 MediaSourceId 并添加到播放链接 + 启用后将调用 PlaybackInfo API 获取 MediaSourceId + 并添加到播放链接

@@ -4309,9 +4514,16 @@ const EmbyConfigComponent = ({

- + - {/* 自定义User-Agent */} -
- - setFormData({ ...formData, customUserAgent: e.target.value })} - placeholder='留空使用默认浏览器UA' - className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white text-sm' - /> -

- 用于登录、获取影片和代理视频时的User-Agent,留空则使用默认浏览器UA -

-
+ {/* 自定义User-Agent */} +
+ + + setFormData({ + ...formData, + customUserAgent: e.target.value, + }) + } + placeholder='留空使用默认浏览器UA' + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white text-sm' + /> +

+ 用于登录、获取影片和代理视频时的User-Agent,留空则使用默认浏览器UA +

+
{/* 操作按钮 */} @@ -4373,10 +4594,7 @@ const EmbyConfigComponent = ({ > {isLoading('saveEmbySource') ? '保存中...' : '保存'} - @@ -4436,7 +4654,7 @@ const VideoSourceConfig = ({ // 批量操作相关状态 const [selectedSources, setSelectedSources] = useState>( - new Set() + new Set(), ); // 使用 useMemo 计算全选状态,避免每次渲染都重新计算 @@ -4485,7 +4703,7 @@ const VideoSourceConfig = ({ delay: 150, // 长按 150ms 后触发,避免与滚动冲突 tolerance: 5, }, - }) + }), ); // 初始化 @@ -4532,7 +4750,7 @@ const VideoSourceConfig = ({ if (!target) return; const action = target.disabled ? 'enable' : 'disable'; withLoading(`toggleSource_${key}`, () => - callSourceApi({ action, key }) + callSourceApi({ action, key }), ).catch(() => { console.error('操作失败', action, key); }); @@ -4540,7 +4758,7 @@ const VideoSourceConfig = ({ const handleDelete = (key: string) => { withLoading(`deleteSource_${key}`, () => - callSourceApi({ action: 'delete', key }) + callSourceApi({ action: 'delete', key }), ).catch(() => { console.error('操作失败', 'delete', key); }); @@ -4552,9 +4770,7 @@ const VideoSourceConfig = ({ // 更新本地状态 setSources((prev) => - prev.map((s) => - s.key === key ? { ...s, proxyMode: !s.proxyMode } : s - ) + prev.map((s) => (s.key === key ? { ...s, proxyMode: !s.proxyMode } : s)), ); // 调用API更新 @@ -4579,12 +4795,12 @@ const VideoSourceConfig = ({ // 失败时回滚本地状态 setSources((prev) => prev.map((s) => - s.key === key ? { ...s, proxyMode: !s.proxyMode } : s - ) + s.key === key ? { ...s, proxyMode: !s.proxyMode } : s, + ), ); showError( error instanceof Error ? error.message : '切换代理模式失败', - showAlert + showAlert, ); throw error; } @@ -4596,9 +4812,7 @@ const VideoSourceConfig = ({ const handleUpdateWeight = (key: string, weight: number) => { // 先乐观更新本地状态 setSources((prev) => - prev.map((s) => - s.key === key ? { ...s, weight } : s - ) + prev.map((s) => (s.key === key ? { ...s, weight } : s)), ); // 调用API更新 @@ -4622,15 +4836,16 @@ const VideoSourceConfig = ({ await refreshConfig(); } catch (error) { // 失败时回滚本地状态到配置中的值 - const originalWeight = config?.SourceConfig?.find(s => s.key === key)?.weight ?? 0; + const originalWeight = + config?.SourceConfig?.find((s) => s.key === key)?.weight ?? 0; setSources((prev) => prev.map((s) => - s.key === key ? { ...s, weight: originalWeight } : s - ) + s.key === key ? { ...s, weight: originalWeight } : s, + ), ); showError( error instanceof Error ? error.message : '更新权重失败', - showAlert + showAlert, ); throw error; } @@ -4675,7 +4890,7 @@ const VideoSourceConfig = ({ const handleSaveOrder = () => { const order = sources.map((s) => s.key); withLoading('saveSourceOrder', () => - callSourceApi({ action: 'sort', order }) + callSourceApi({ action: 'sort', order }), ) .then(() => { setOrderChanged(false); @@ -4715,8 +4930,8 @@ const VideoSourceConfig = ({ // 使用EventSource接收流式数据 const eventSource = new EventSource( `/api/admin/source/validate?q=${encodeURIComponent( - searchKeyword.trim() - )}` + searchKeyword.trim(), + )}`, ); eventSource.onmessage = (event) => { @@ -4746,11 +4961,11 @@ const VideoSourceConfig = ({ data.status === 'valid' ? '搜索正常' : data.status === 'no_results' - ? '无法搜索到结果' - : '连接失败', + ? '无法搜索到结果' + : '连接失败', resultCount: data.status === 'valid' ? 1 : 0, } - : r + : r, ); } else { return [ @@ -4765,8 +4980,8 @@ const VideoSourceConfig = ({ data.status === 'valid' ? '搜索正常' : data.status === 'no_results' - ? '无法搜索到结果' - : '连接失败', + ? '无法搜索到结果' + : '连接失败', resultCount: data.status === 'valid' ? 1 : 0, }, ]; @@ -4776,7 +4991,7 @@ const VideoSourceConfig = ({ case 'complete': console.log( - `检测完成,共检测 ${data.completedSources} 个视频源` + `检测完成,共检测 ${data.completedSources} 个视频源`, ); eventSource.close(); setIsValidating(false); @@ -4866,44 +5081,54 @@ const VideoSourceConfig = ({ }; // 权重输入组件 - 使用本地状态避免输入时失焦 - const WeightInput = memo(({ sourceKey, initialWeight }: { sourceKey: string; initialWeight: number }) => { - const [localWeight, setLocalWeight] = useState(initialWeight); - - // 当外部权重变化时同步 - useEffect(() => { - setLocalWeight(initialWeight); - }, [initialWeight]); - - return ( - { - const value = parseInt(e.target.value) || 0; - const clampedValue = Math.min(100, Math.max(0, value)); - setLocalWeight(clampedValue); - }} - onBlur={(e) => { - const newValue = parseInt(e.target.value) || 0; - const clampedValue = Math.min(100, Math.max(0, newValue)); - const originalWeight = config?.SourceConfig?.find(s => s.key === sourceKey)?.weight ?? 0; - - // 只有在值发生变化时才调用API - if (clampedValue !== originalWeight) { - handleUpdateWeight(sourceKey, clampedValue); - } - }} - onPointerDown={(e) => e.stopPropagation()} - onTouchStart={(e) => e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - className='w-16 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent' - title='权重范围:0-100,用于排序和优选评分' - /> - ); - }); + const WeightInput = memo( + ({ + sourceKey, + initialWeight, + }: { + sourceKey: string; + initialWeight: number; + }) => { + const [localWeight, setLocalWeight] = useState(initialWeight); + + // 当外部权重变化时同步 + useEffect(() => { + setLocalWeight(initialWeight); + }, [initialWeight]); + + return ( + { + const value = parseInt(e.target.value) || 0; + const clampedValue = Math.min(100, Math.max(0, value)); + setLocalWeight(clampedValue); + }} + onBlur={(e) => { + const newValue = parseInt(e.target.value) || 0; + const clampedValue = Math.min(100, Math.max(0, newValue)); + const originalWeight = + config?.SourceConfig?.find((s) => s.key === sourceKey)?.weight ?? + 0; + + // 只有在值发生变化时才调用API + if (clampedValue !== originalWeight) { + handleUpdateWeight(sourceKey, clampedValue); + } + }} + onPointerDown={(e) => e.stopPropagation()} + onTouchStart={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + className='w-16 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent' + title='权重范围:0-100,用于排序和优选评分' + /> + ); + }, + ); // 可拖拽行封装 (dnd-kit) const DraggableRow = memo(({ source }: { source: DataSource }) => { @@ -4991,8 +5216,14 @@ const VideoSourceConfig = ({ /> - - + + {(() => { @@ -5058,7 +5289,7 @@ const VideoSourceConfig = ({ setSelectedSources(new Set()); } }, - [sources] + [sources], ); // 单个选择 @@ -5076,7 +5307,7 @@ const VideoSourceConfig = ({ // 批量操作 const handleBatchOperation = async ( - action: 'batch_enable' | 'batch_disable' | 'batch_delete' + action: 'batch_enable' | 'batch_disable' | 'batch_delete', ) => { if (selectedSources.size === 0) { showAlert({ @@ -5114,11 +5345,15 @@ const VideoSourceConfig = ({ onConfirm: async () => { try { const result = await withLoading(`batchSource_${action}`, () => - callSourceApi({ action, keys }) + callSourceApi({ action, keys }), ); // 根据操作类型和结果显示不同的消息 - if (action === 'batch_delete' && result?.deleted !== undefined && result?.skipped !== undefined) { + if ( + action === 'batch_delete' && + result?.deleted !== undefined && + result?.skipped !== undefined + ) { const { deleted, skipped } = result; if (skipped > 0) { showAlert({ @@ -5476,7 +5711,7 @@ const VideoSourceConfig = ({ , - document.body + document.body, )} {/* 通用弹窗组件 */} @@ -5565,7 +5800,7 @@ const VideoSourceConfig = ({ , - document.body + document.body, )} ); @@ -5604,7 +5839,7 @@ const CategoryConfig = ({ delay: 150, // 长按 150ms 后触发,避免与滚动冲突 tolerance: 5, }, - }) + }), ); // 初始化 @@ -5643,7 +5878,7 @@ const CategoryConfig = ({ if (!target) return; const action = target.disabled ? 'enable' : 'disable'; withLoading(`toggleCategory_${query}_${type}`, () => - callCategoryApi({ action, query, type }) + callCategoryApi({ action, query, type }), ).catch(() => { console.error('操作失败', action, query, type); }); @@ -5651,7 +5886,7 @@ const CategoryConfig = ({ const handleDelete = (query: string, type: 'movie' | 'tv') => { withLoading(`deleteCategory_${query}_${type}`, () => - callCategoryApi({ action: 'delete', query, type }) + callCategoryApi({ action: 'delete', query, type }), ).catch(() => { console.error('操作失败', 'delete', query, type); }); @@ -5683,10 +5918,10 @@ const CategoryConfig = ({ const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = categories.findIndex( - (c) => `${c.query}:${c.type}` === active.id + (c) => `${c.query}:${c.type}` === active.id, ); const newIndex = categories.findIndex( - (c) => `${c.query}:${c.type}` === over.id + (c) => `${c.query}:${c.type}` === over.id, ); setCategories((prev) => arrayMove(prev, oldIndex, newIndex)); setOrderChanged(true); @@ -5695,7 +5930,7 @@ const CategoryConfig = ({ const handleSaveOrder = () => { const order = categories.map((c) => `${c.query}:${c.type}`); withLoading('saveCategoryOrder', () => - callCategoryApi({ action: 'sort', order }) + callCategoryApi({ action: 'sort', order }), ) .then(() => { setOrderChanged(false); @@ -5763,7 +5998,7 @@ const CategoryConfig = ({ )} @@ -6745,11 +6994,23 @@ const ThemeConfigComponent = ({ ))} @@ -6780,14 +7041,26 @@ const ThemeConfigComponent = ({ type='button' onClick={() => { setRegisterBackgroundImages( - registerBackgroundImages.filter((_, i) => i !== index) + registerBackgroundImages.filter( + (_, i) => i !== index, + ), ); }} className='px-3 py-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors' title='删除' > - - + + )} @@ -6795,11 +7068,23 @@ const ThemeConfigComponent = ({ ))} 添加URL @@ -6835,9 +7120,7 @@ const ThemeConfigComponent = ({ } className='w-4 h-4 text-blue-600' /> - - 默认圆点 - + 默认圆点 @@ -6881,9 +7160,24 @@ const ThemeConfigComponent = ({
{[ - { id: 'renako', name: '玲奈子', url: '/icons/q/renako.png', color: '#ec4899' }, - { id: 'irena', name: '伊蕾娜', url: '/icons/q/irena.png', color: '#f8fafc' }, - { id: 'emilia', name: '爱蜜莉雅', url: '/icons/q/emilia.png', color: '#f8fafc' }, + { + id: 'renako', + name: '玲奈子', + url: '/icons/q/renako.png', + color: '#ec4899', + }, + { + id: 'irena', + name: '伊蕾娜', + url: '/icons/q/irena.png', + color: '#f8fafc', + }, + { + id: 'emilia', + name: '爱蜜莉雅', + url: '/icons/q/emilia.png', + color: '#f8fafc', + }, ].map((thumb) => (
@@ -7192,7 +7507,8 @@ const MusicConfigComponent = ({

- 开启后将音乐解析结果(播放链接、歌词、元信息)和音频文件缓存到 OpenList,减少 API 调用次数并支持离线播放 + 开启后将音乐解析结果(播放链接、歌词、元信息)和音频文件缓存到 + OpenList,减少 API 调用次数并支持离线播放

@@ -7313,7 +7629,8 @@ const MusicConfigComponent = ({

- 开启后,如果 OpenList 有缓存,将通过代理方式返回给前端,并设置永久缓存头,提升加载速度 + 开启后,如果 OpenList + 有缓存,将通过代理方式返回给前端,并设置永久缓存头,提升加载速度

@@ -7424,8 +7741,14 @@ const SiteConfigComponent = ({ { value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss(阿里云)' }, { value: 'baidu', label: '百度图片代理' }, { value: 'custom', label: '自定义代理' }, - { value: 'direct', label: '直连(浏览器直接请求豆瓣,可能需要浏览器插件才能正常显示)' }, - { value: 'img3', label: '豆瓣官方精品 CDN(阿里云,可能需要浏览器插件才能正常显示)' }, + { + value: 'direct', + label: '直连(浏览器直接请求豆瓣,可能需要浏览器插件才能正常显示)', + }, + { + value: 'img3', + label: '豆瓣官方精品 CDN(阿里云,可能需要浏览器插件才能正常显示)', + }, ]; // 获取感谢信息 @@ -7465,7 +7788,8 @@ const SiteConfigComponent = ({ TMDBApiKey: config.SiteConfig.TMDBApiKey || '', TMDBProxy: config.SiteConfig.TMDBProxy || '', BannerDataSource: config.SiteConfig.BannerDataSource || 'Douban', - RecommendationDataSource: config.SiteConfig.RecommendationDataSource || 'Mixed', + RecommendationDataSource: + config.SiteConfig.RecommendationDataSource || 'Mixed', PansouApiUrl: config.SiteConfig.PansouApiUrl || '', PansouUsername: config.SiteConfig.PansouUsername || '', PansouPassword: config.SiteConfig.PansouPassword || '', @@ -7631,7 +7955,7 @@ const SiteConfigComponent = ({ > { doubanDataSourceOptions.find( - (option) => option.value === siteSettings.DoubanProxyType + (option) => option.value === siteSettings.DoubanProxyType, )?.label } @@ -7683,7 +8007,7 @@ const SiteConfigComponent = ({ onClick={() => window.open( getThanksInfo(siteSettings.DoubanProxyType)!.url, - '_blank' + '_blank', ) } className='flex items-center justify-center gap-1.5 w-full px-3 text-xs text-gray-500 dark:text-gray-400 cursor-pointer' @@ -7734,14 +8058,15 @@ const SiteConfigComponent = ({ type='button' onClick={() => setIsDoubanImageProxyDropdownOpen( - !isDoubanImageProxyDropdownOpen + !isDoubanImageProxyDropdownOpen, ) } className='w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left' > { doubanImageProxyTypeOptions.find( - (option) => option.value === siteSettings.DoubanImageProxyType + (option) => + option.value === siteSettings.DoubanImageProxyType, )?.label } @@ -7793,7 +8118,7 @@ const SiteConfigComponent = ({ onClick={() => window.open( getThanksInfo(siteSettings.DoubanImageProxyType)!.url, - '_blank' + '_blank', ) } className='flex items-center justify-center gap-1.5 w-full px-3 text-xs text-gray-500 dark:text-gray-400 cursor-pointer' @@ -8073,7 +8398,9 @@ const SiteConfigComponent = ({ className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' />

- 配置后首页将显示 TMDB 即将上映电影。支持配置多个 API Key(用英文逗号分隔)以实现轮询,避免单个 Key 请求限制。获取 API Key 请访问{' '} + 配置后首页将显示 TMDB 即将上映电影。支持配置多个 API + Key(用英文逗号分隔)以实现轮询,避免单个 Key 请求限制。获取 API Key + 请访问{' '} , - document.body + document.body, )} ); @@ -8381,7 +8708,8 @@ const RegistrationConfigComponent = ({ }) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); - const [showEnableRegistrationModal, setShowEnableRegistrationModal] = useState(false); + const [showEnableRegistrationModal, setShowEnableRegistrationModal] = + useState(false); const [registrationSettings, setRegistrationSettings] = useState<{ EnableRegistration: boolean; RegistrationRequireTurnstile: boolean; @@ -8422,15 +8750,18 @@ const RegistrationConfigComponent = ({ if (config?.SiteConfig) { setRegistrationSettings({ EnableRegistration: config.SiteConfig.EnableRegistration || false, - RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false, + RegistrationRequireTurnstile: + config.SiteConfig.RegistrationRequireTurnstile || false, LoginRequireTurnstile: config.SiteConfig.LoginRequireTurnstile || false, TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '', TurnstileSecretKey: config.SiteConfig.TurnstileSecretKey || '', DefaultUserTags: config.SiteConfig.DefaultUserTags || [], EnableOIDCLogin: config.SiteConfig.EnableOIDCLogin || false, - EnableOIDCRegistration: config.SiteConfig.EnableOIDCRegistration || false, + EnableOIDCRegistration: + config.SiteConfig.EnableOIDCRegistration || false, OIDCIssuer: config.SiteConfig.OIDCIssuer || '', - OIDCAuthorizationEndpoint: config.SiteConfig.OIDCAuthorizationEndpoint || '', + OIDCAuthorizationEndpoint: + config.SiteConfig.OIDCAuthorizationEndpoint || '', OIDCTokenEndpoint: config.SiteConfig.OIDCTokenEndpoint || '', OIDCUserInfoEndpoint: config.SiteConfig.OIDCUserInfoEndpoint || '', OIDCClientId: config.SiteConfig.OIDCClientId || '', @@ -8520,7 +8851,11 @@ const RegistrationConfigComponent = ({ - - - - {loading ? ( -

-
-
- ) : requests.length === 0 ? ( -
- 暂无求片 + +
- ) : ( -
- {requests.map((req) => ( -
-
- {req.poster && ( - {req.title} - )} -
-

- {req.title} {req.year && `(${req.year})`} -

-

- 求片人数: {req.requestCount} 人 -

-

- {new Date(req.createdAt).toLocaleString('zh-CN')} -

- {req.requestedBy && ( + + {loading ? ( +
+
+
+ ) : requests.length === 0 ? ( +
+ 暂无求片 +
+ ) : ( +
+ {requests.map((req) => ( +
+
+ {req.poster && ( + {req.title} + )} +
+

+ {req.title} {req.year && `(${req.year})`} +

+

+ 求片人数: {req.requestCount} 人 +

- 求片用户: {req.requestedBy.join(', ')} + {new Date(req.createdAt).toLocaleString('zh-CN')}

- )} -
-
- {filter === 'pending' && ( + {req.requestedBy && ( +

+ 求片用户: {req.requestedBy.join(', ')} +

+ )} +
+
+ {filter === 'pending' && ( + + )} - )} - +
-
- ))} -
- )} + ))} +
+ )}
('tavily'); + const [webSearchProvider, setWebSearchProvider] = useState< + 'tavily' | 'serper' | 'serpapi' + >('tavily'); const [tavilyApiKey, setTavilyApiKey] = useState(''); const [serperApiKey, setSerperApiKey] = useState(''); const [serpApiKey, setSerpApiKey] = useState(''); @@ -10408,7 +10850,10 @@ const AIConfigComponent = ({ showSuccess('AI配置保存成功', showAlert); await refreshConfig(); } catch (error) { - showError(error instanceof Error ? error.message : '保存失败', showAlert); + showError( + error instanceof Error ? error.message : '保存失败', + showAlert, + ); throw error; } }); @@ -10548,7 +10993,9 @@ const AIConfigComponent = ({

- 💡 提示: 决策模型用于智能判断是否需要调用各个数据源,建议使用成本较低的小模型(如 gpt-4o-mini)。会复用主模型的API Key和Base URL配置。 + 💡 提示:{' '} + 决策模型用于智能判断是否需要调用各个数据源,建议使用成本较低的小模型(如 + gpt-4o-mini)。会复用主模型的API Key和Base URL配置。

@@ -10605,7 +11052,15 @@ const AIConfigComponent = ({ className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100' />

- 在 tavily.com 注册获取 + 在{' '} + + tavily.com + {' '} + 注册获取

)} @@ -10623,7 +11078,15 @@ const AIConfigComponent = ({ className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100' />

- 在 serper.dev 注册获取 + 在{' '} + + serper.dev + {' '} + 注册获取

)} @@ -10641,7 +11104,15 @@ const AIConfigComponent = ({ className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100' />

- 在 serpapi.com 注册获取 + 在{' '} + + serpapi.com + {' '} + 注册获取

)} @@ -10656,11 +11127,32 @@ const AIConfigComponent = ({ {[ - { key: 'homepage', label: '首页入口', desc: '在首页显示AI问片入口', state: enableHomepageEntry, setState: setEnableHomepageEntry }, - { key: 'videocard', label: '视频卡片入口', desc: '在视频卡片菜单中显示AI问片选项', state: enableVideoCardEntry, setState: setEnableVideoCardEntry }, - { key: 'playpage', label: '播放页入口', desc: '在视频播放页显示AI问片功能', state: enablePlayPageEntry, setState: setEnablePlayPageEntry }, + { + key: 'homepage', + label: '首页入口', + desc: '在首页显示AI问片入口', + state: enableHomepageEntry, + setState: setEnableHomepageEntry, + }, + { + key: 'videocard', + label: '视频卡片入口', + desc: '在视频卡片菜单中显示AI问片选项', + state: enableVideoCardEntry, + setState: setEnableVideoCardEntry, + }, + { + key: 'playpage', + label: '播放页入口', + desc: '在视频播放页显示AI问片功能', + state: enablePlayPageEntry, + setState: setEnablePlayPageEntry, + }, ].map((item) => ( -
+
{item.label} @@ -10747,10 +11239,10 @@ const AIConfigComponent = ({