diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d4b3b5..9064c4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -342,6 +342,8 @@ jobs: if: github.event_name == 'push' runs-on: ${{ matrix.runner }} timeout-minutes: 45 + env: + DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/metapi strategy: fail-fast: false matrix: @@ -384,7 +386,7 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: 1467078763/metapi + images: ${{ env.DOCKERHUB_IMAGE }} tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=branch @@ -424,6 +426,8 @@ jobs: runs-on: ubuntu-latest needs: publish-docker-arch timeout-minutes: 15 + env: + DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/metapi steps: - name: Checkout @@ -449,7 +453,7 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: 1467078763/metapi + images: ${{ env.DOCKERHUB_IMAGE }} tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=branch diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7d63bc..e9bcc9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -283,14 +283,16 @@ jobs: generate_release_notes: true files: release-assets/* - publish-docker-arch: - name: Publish Docker Image (${{ matrix.arch }}) - needs: build-packages - runs-on: ${{ matrix.runner }} - timeout-minutes: 45 - if: startsWith(github.ref, 'refs/tags/') - strategy: - fail-fast: false + publish-docker-arch: + name: Publish Docker Image (${{ matrix.arch }}) + needs: build-packages + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + if: startsWith(github.ref, 'refs/tags/') + env: + DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/metapi + strategy: + fail-fast: false matrix: include: - arch: amd64 @@ -334,16 +336,16 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Docker metadata - id: meta - uses: docker/metadata-action@v6 - with: - images: | - 1467078763/metapi - ghcr.io/${{ github.repository }} - tags: | - type=ref,event=tag - type=raw,value=latest + - name: Docker metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: | + ${{ env.DOCKERHUB_IMAGE }} + ghcr.io/${{ github.repository }} + tags: | + type=ref,event=tag + type=raw,value=latest - name: Add architecture suffix to tags id: arch-tags @@ -373,14 +375,16 @@ jobs: cache-from: type=gha,scope=release-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=release-${{ matrix.arch }} - publish-docker: - name: Publish Docker Manifests - needs: publish-docker-arch - runs-on: ubuntu-latest - timeout-minutes: 15 - if: startsWith(github.ref, 'refs/tags/') - - steps: + publish-docker: + name: Publish Docker Manifests + needs: publish-docker-arch + runs-on: ubuntu-latest + timeout-minutes: 15 + if: startsWith(github.ref, 'refs/tags/') + env: + DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/metapi + + steps: - name: Checkout uses: actions/checkout@v6 @@ -407,16 +411,16 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Docker metadata - id: meta - uses: docker/metadata-action@v6 - with: - images: | - 1467078763/metapi - ghcr.io/${{ github.repository }} - tags: | - type=ref,event=tag - type=raw,value=latest + - name: Docker metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: | + ${{ env.DOCKERHUB_IMAGE }} + ghcr.io/${{ github.repository }} + tags: | + type=ref,event=tag + type=raw,value=latest - name: Create multi-arch manifests shell: bash diff --git a/.gitignore b/.gitignore index d789c71..54ca230 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ docker/.env # Local planning docs docs/plan/ docs/plans/ +.ace-tool/ diff --git a/package-lock.json b/package-lock.json index f9afbc6..4129059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,16 +14,16 @@ "@dnd-kit/utilities": "^3.2.2", "@fastify/cors": "^11.2.0", "@fastify/static": "^9.0.0", - "@visactor/react-vchart": "^2.0.20", + "@visactor/react-vchart": "^2.0.21", "better-sqlite3": "^12.8.0", - "dotenv": "^17.3.1", + "dotenv": "^17.4.1", "drizzle-orm": "^0.45.2", "electron-log": "^5.2.4", "electron-updater": "^6.6.2", "fastify": "^5.8.4", "get-port": "^7.2.0", "marked": "^17.0.5", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "minimist": "^1.2.8", "mysql2": "^3.20.0", "node-cron": "^4.2.1", @@ -41,7 +41,7 @@ "@types/better-sqlite3": "^7.6.11", "@types/node": "^22.10.1", "@types/node-cron": "^3.0.11", - "@types/nodemailer": "^7.0.11", + "@types/nodemailer": "^8.0.0", "@types/pg": "^8.20.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -50,13 +50,13 @@ "concurrently": "^9.1.0", "cross-env": "^7.0.3", "drizzle-kit": "^0.31.10", - "electron": "^41.1.0", + "electron": "^41.1.1", "electron-builder": "^26.0.12", - "jsdom": "^29.0.1", - "mermaid": "^11.13.0", + "jsdom": "^29.0.2", + "mermaid": "^11.14.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^7.13.2", + "react-router-dom": "^7.14.0", "react-test-renderer": "^18.3.1", "sharp": "^0.34.5", "tailwindcss": "^4.0.0", @@ -355,59 +355,37 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.6.tgz", + "integrity": "sha512-BXWCh8dHs9GOfpo/fWGDJtDmleta2VePN9rn6WQt3GjEbxzutVF4t0x2pmH+7dbMCLtuv3MlwqRsAuxlzFXqFg==", "dev": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", - "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.7.tgz", + "integrity": "sha512-d2BgqDUOS1Hfp4IzKUZqCNz+Kg3Y88AkaBvJK/ZVSQPU1f7OpPNi7nQTH6/oI47Dkdg+Z3e8Yp6ynOu4UMINAQ==", "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.7" + "is-potential-custom-element-name": "^1.0.1" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@asamuzakjp/nwsapi": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", @@ -3440,9 +3418,9 @@ "optional": true }, "node_modules/@mermaid-js/parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.1.tgz", - "integrity": "sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", + "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", "dev": true, "license": "MIT", "dependencies": { @@ -3626,9 +3604,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3645,9 +3620,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3664,9 +3636,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3683,9 +3652,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5115,9 +5081,9 @@ "license": "MIT" }, "node_modules/@types/nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", "dev": true, "license": "MIT", "dependencies": { @@ -5256,15 +5222,15 @@ } }, "node_modules/@visactor/react-vchart": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/@visactor/react-vchart/-/react-vchart-2.0.20.tgz", - "integrity": "sha512-SlOmCWQ/dd8QA4ez2pcE8dRSzKjt5/0/M6PQ0X8AkawFBcVwHgDPweZnEExsmDN6fvyfB2HqmKH0tefL/J6tUA==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/@visactor/react-vchart/-/react-vchart-2.0.21.tgz", + "integrity": "sha512-1qgPlW820ZQEbemKiddLRx+JIoEdVUAF4pEyIEAIYoh4W49RPpa8kncMTgKw7030gs3lGCQneqIeLoZMWfZJtA==", "license": "MIT", "dependencies": { - "@visactor/vchart": "2.0.20", - "@visactor/vchart-extension": "2.0.20", - "@visactor/vrender-core": "1.0.44", - "@visactor/vrender-kits": "1.0.44", + "@visactor/vchart": "2.0.21", + "@visactor/vchart-extension": "2.0.21", + "@visactor/vrender-core": "1.0.45", + "@visactor/vrender-kits": "1.0.45", "@visactor/vutils": "~1.0.23", "react-is": "^18.2.0" }, @@ -5274,35 +5240,35 @@ } }, "node_modules/@visactor/vchart": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/@visactor/vchart/-/vchart-2.0.20.tgz", - "integrity": "sha512-nolgr7b+kSa1GQ8q99UjKCoMEYib3brmnRtDC+/iYr4Idbr9hSHS+60wteZgX8GOIUxivOEKYcpTPNXVRNmBAg==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/@visactor/vchart/-/vchart-2.0.21.tgz", + "integrity": "sha512-jYrSwTS8EkV2qB3c+/iW9YBta4dCJSsbuxQzacHnhVuH2XAn7b+33wLdFHbG1h84L1EbVPMle6LrUoXe+CZjYw==", "license": "MIT", "dependencies": { "@visactor/vdataset": "~1.0.23", "@visactor/vlayouts": "~1.0.23", - "@visactor/vrender-animate": "1.0.44", - "@visactor/vrender-components": "1.0.44", - "@visactor/vrender-core": "1.0.44", - "@visactor/vrender-kits": "1.0.44", + "@visactor/vrender-animate": "1.0.45", + "@visactor/vrender-components": "1.0.45", + "@visactor/vrender-core": "1.0.45", + "@visactor/vrender-kits": "1.0.45", "@visactor/vscale": "~1.0.23", "@visactor/vutils": "~1.0.23", - "@visactor/vutils-extension": "2.0.20" + "@visactor/vutils-extension": "2.0.21" } }, "node_modules/@visactor/vchart-extension": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/@visactor/vchart-extension/-/vchart-extension-2.0.20.tgz", - "integrity": "sha512-+D1eVABTjQ/tla/nQUaWyECQ8GTcQU7oNU9ovPheOfYnPleSjPgDOJnEH2w2mEdiXNh8P+oIaVqm0ItwSPvprA==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/@visactor/vchart-extension/-/vchart-extension-2.0.21.tgz", + "integrity": "sha512-c930SfptDFmRUVrc2tf5sX3uw6EOg7eUFHENYlMmN80ufjoSUqX6MqOXgK/fsZh3lnQi3vzBQI6+9TtY81T4wg==", "license": "MIT", "dependencies": { - "@visactor/vchart": "2.0.20", + "@visactor/vchart": "2.0.21", "@visactor/vdataset": "~1.0.23", "@visactor/vlayouts": "~1.0.23", - "@visactor/vrender-animate": "1.0.44", - "@visactor/vrender-components": "1.0.44", - "@visactor/vrender-core": "1.0.44", - "@visactor/vrender-kits": "1.0.44", + "@visactor/vrender-animate": "1.0.45", + "@visactor/vrender-components": "1.0.45", + "@visactor/vrender-core": "1.0.45", + "@visactor/vrender-kits": "1.0.45", "@visactor/vutils": "~1.0.23" } }, @@ -5345,32 +5311,32 @@ } }, "node_modules/@visactor/vrender-animate": { - "version": "1.0.44", - "resolved": "https://registry.npmjs.org/@visactor/vrender-animate/-/vrender-animate-1.0.44.tgz", - "integrity": "sha512-MKtoaucmbQNJnuff5F6ws+c8+mRBz1gXVY/bHw38nfvSTZcsJcXYug2aDPf4V9XaUSValDnkx9TyLLbKJ0o0TA==", + "version": "1.0.45", + "resolved": "https://registry.npmjs.org/@visactor/vrender-animate/-/vrender-animate-1.0.45.tgz", + "integrity": "sha512-6v7LRpr+zugxR8JH3RCnodYxrrzHkSp4GaBTNwXuJ7bwThi/YzXTvmBz2pfQNmUG/rRze8Bm7vqneHVdXuFhng==", "license": "MIT", "dependencies": { - "@visactor/vrender-core": "1.0.44", + "@visactor/vrender-core": "1.0.45", "@visactor/vutils": "~1.0.12" } }, "node_modules/@visactor/vrender-components": { - "version": "1.0.44", - "resolved": "https://registry.npmjs.org/@visactor/vrender-components/-/vrender-components-1.0.44.tgz", - "integrity": "sha512-vbCa/eME1UJL2iy0l4JHYJh1m6SG3rR5jt5umblLVRQjjBEWPAnhnPwQAA2TNiyO5eHG6qnyvcjgV5aeCeuJYw==", + "version": "1.0.45", + "resolved": "https://registry.npmjs.org/@visactor/vrender-components/-/vrender-components-1.0.45.tgz", + "integrity": "sha512-rYsG/rncT5FgYYWqv09f6LkZEAk/IS45IEYWBD0SCgKguYnpEebYDmDba1muJGn+rRv/vGlqg1pYqXLw8mEU5w==", "license": "MIT", "dependencies": { - "@visactor/vrender-animate": "1.0.44", - "@visactor/vrender-core": "1.0.44", - "@visactor/vrender-kits": "1.0.44", + "@visactor/vrender-animate": "1.0.45", + "@visactor/vrender-core": "1.0.45", + "@visactor/vrender-kits": "1.0.45", "@visactor/vscale": "~1.0.12", "@visactor/vutils": "~1.0.12" } }, "node_modules/@visactor/vrender-core": { - "version": "1.0.44", - "resolved": "https://registry.npmjs.org/@visactor/vrender-core/-/vrender-core-1.0.44.tgz", - "integrity": "sha512-DrB+cg9//RkCZjchWConeXGOC99xwvEDdPfY7eEqajjPxUooJpSQ+pt51uD0VIwQCYtpsc2jVspefqdBh/e2Cg==", + "version": "1.0.45", + "resolved": "https://registry.npmjs.org/@visactor/vrender-core/-/vrender-core-1.0.45.tgz", + "integrity": "sha512-kvYAsKGZ+dXbhOQzjjbMkRzI815KgaHoWOW7iyVorXTldQBTsxLtFIi/J3VfYqGdM3apUgsFoy8uvjS5DepPUA==", "license": "MIT", "dependencies": { "@visactor/vutils": "~1.0.12", @@ -5378,13 +5344,13 @@ } }, "node_modules/@visactor/vrender-kits": { - "version": "1.0.44", - "resolved": "https://registry.npmjs.org/@visactor/vrender-kits/-/vrender-kits-1.0.44.tgz", - "integrity": "sha512-B28zr+aTWlFIziH3b/ufUlpKQMv1Ie9ck8kTkHIGdBM/ES+CO6//GHMa/5OxuId19a9UhJxzj8cpeE/9mZLIoQ==", + "version": "1.0.45", + "resolved": "https://registry.npmjs.org/@visactor/vrender-kits/-/vrender-kits-1.0.45.tgz", + "integrity": "sha512-iaeRitht8IqNvJwdKmcPKAYL0a4+f8xhK2bWUumSQjNbZJQx9UzGuDp73cIdNXzCNoCiJZElpsOiIGu3kymqiw==", "license": "MIT", "dependencies": { "@resvg/resvg-js": "2.4.1", - "@visactor/vrender-core": "1.0.44", + "@visactor/vrender-core": "1.0.45", "@visactor/vutils": "~1.0.12", "gifuct-js": "2.1.2", "lottie-web": "^5.12.2", @@ -5412,9 +5378,9 @@ } }, "node_modules/@visactor/vutils-extension": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/@visactor/vutils-extension/-/vutils-extension-2.0.20.tgz", - "integrity": "sha512-now0AqIHimJr1/+7U9cJBkZ022VAsVlVANTN8mKLVIG1C5wnEYfKYG1Ew6ghjcSmkXi1c+pdrsAsj6i+ifRlwA==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/@visactor/vutils-extension/-/vutils-extension-2.0.21.tgz", + "integrity": "sha512-E9owMC/COyfz481BgDrVPPOgK3O98NZtZldko2dCZBFWuByiRx+xcn4gvSO6vlt2SrBB0LzzyhLYkrNs9xb/0A==", "license": "MIT", "dependencies": { "@visactor/vdataset": "~1.0.23", @@ -6454,9 +6420,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -8321,9 +8287,9 @@ } }, "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -8541,9 +8507,9 @@ } }, "node_modules/electron": { - "version": "41.1.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-41.1.0.tgz", - "integrity": "sha512-0XRFyxRqetmqtkkBvV++wGbHYJ7bD++f6EgJW8y9kX4pPRagwlmKDtzqXZhKiu0DIQppm3sXxzHWK9GYP91OKQ==", + "version": "41.1.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.1.1.tgz", + "integrity": "sha512-8bgvDhBjli+3Z2YCKgzzoBPh6391pr7Xv2h/tTJG4ETgvPvUxZomObbZLs31mUzYb6VrlcDDd9cyWyNKtPm3tA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -10398,14 +10364,14 @@ } }, "node_modules/jsdom": { - "version": "29.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", - "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.0.1", - "@asamuzakjp/dom-selector": "^7.0.3", + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", @@ -11122,15 +11088,15 @@ "license": "CC0-1.0" }, "node_modules/mermaid": { - "version": "11.13.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.13.0.tgz", - "integrity": "sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", + "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", "dev": true, "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.0.1", + "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", @@ -11330,12 +11296,12 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -12550,9 +12516,9 @@ } }, "node_modules/protocol-buffers-schema": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", "license": "MIT" }, "node_modules/proxy-from-env": { @@ -12664,9 +12630,9 @@ } }, "node_modules/react-router": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", - "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12687,13 +12653,13 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", - "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", "dev": true, "license": "MIT", "dependencies": { - "react-router": "7.13.2" + "react-router": "7.14.0" }, "engines": { "node": ">=20.0.0" diff --git a/package.json b/package.json index 8569401..e16df67 100644 --- a/package.json +++ b/package.json @@ -72,16 +72,16 @@ "@dnd-kit/utilities": "^3.2.2", "@fastify/cors": "^11.2.0", "@fastify/static": "^9.0.0", - "@visactor/react-vchart": "^2.0.20", + "@visactor/react-vchart": "^2.0.21", "better-sqlite3": "^12.8.0", - "dotenv": "^17.3.1", + "dotenv": "^17.4.1", "drizzle-orm": "^0.45.2", "electron-log": "^5.2.4", "electron-updater": "^6.6.2", "fastify": "^5.8.4", "get-port": "^7.2.0", "marked": "^17.0.5", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "minimist": "^1.2.8", "mysql2": "^3.20.0", "node-cron": "^4.2.1", @@ -99,7 +99,7 @@ "@types/better-sqlite3": "^7.6.11", "@types/node": "^22.10.1", "@types/node-cron": "^3.0.11", - "@types/nodemailer": "^7.0.11", + "@types/nodemailer": "^8.0.0", "@types/pg": "^8.20.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -108,13 +108,13 @@ "concurrently": "^9.1.0", "cross-env": "^7.0.3", "drizzle-kit": "^0.31.10", - "electron": "^41.1.0", + "electron": "^41.1.1", "electron-builder": "^26.0.12", - "jsdom": "^29.0.1", - "mermaid": "^11.13.0", + "jsdom": "^29.0.2", + "mermaid": "^11.14.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^7.13.2", + "react-router-dom": "^7.14.0", "react-test-renderer": "^18.3.1", "sharp": "^0.34.5", "tailwindcss": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4bb18e..58441f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,14 +24,14 @@ importers: specifier: ^9.0.0 version: 9.0.0 '@visactor/react-vchart': - specifier: ^2.0.20 - version: 2.0.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.0.21 + version: 2.0.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1) better-sqlite3: specifier: ^12.8.0 version: 12.8.0 dotenv: - specifier: ^17.3.1 - version: 17.3.1 + specifier: ^17.4.1 + version: 17.4.1 drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.8.0)(mysql2@3.20.0(@types/node@22.19.15))(pg@8.20.0) @@ -51,8 +51,8 @@ importers: specifier: ^17.0.5 version: 17.0.5 minimatch: - specifier: ^10.2.4 - version: 10.2.4 + specifier: ^10.2.5 + version: 10.2.5 minimist: specifier: ^1.2.8 version: 1.2.8 @@ -68,6 +68,9 @@ importers: pg: specifier: ^8.16.3 version: 8.20.0 + rate-limiter-flexible: + specifier: ^11.0.0 + version: 11.0.0 socks: specifier: ^2.8.7 version: 2.8.7 @@ -77,6 +80,9 @@ importers: ws: specifier: ^8.20.0 version: 8.20.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@electron/notarize': specifier: ^3.1.0 @@ -94,8 +100,8 @@ importers: specifier: ^3.0.11 version: 3.0.11 '@types/nodemailer': - specifier: ^7.0.11 - version: 7.0.11 + specifier: ^8.0.0 + version: 8.0.0 '@types/pg': specifier: ^8.20.0 version: 8.20.0 @@ -121,17 +127,17 @@ importers: specifier: ^0.31.10 version: 0.31.10 electron: - specifier: ^41.1.0 - version: 41.1.0 + specifier: ^41.1.1 + version: 41.1.1 electron-builder: specifier: ^26.0.12 version: 26.8.1(electron-builder-squirrel-windows@26.8.1) jsdom: - specifier: ^29.0.1 - version: 29.0.1 + specifier: ^29.0.2 + version: 29.0.2 mermaid: - specifier: ^11.13.0 - version: 11.13.0 + specifier: ^11.14.0 + version: 11.14.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -139,8 +145,8 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-router-dom: - specifier: ^7.13.2 - version: 7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.14.0 + version: 7.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-test-renderer: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) @@ -164,10 +170,10 @@ importers: version: 1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.2) vitepress-plugin-mermaid: specifier: ^2.0.17 - version: 2.0.17(mermaid@11.13.0)(vitepress@1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.2)) + version: 2.0.17(mermaid@11.14.0)(vitepress@1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.2)) vitest: specifier: ^2.1.8 - version: 2.1.9(@types/node@22.19.15)(jsdom@29.0.1)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.15)(jsdom@29.0.2)(lightningcss@1.32.0) wait-on: specifier: ^9.0.4 version: 9.0.4 @@ -256,12 +262,12 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@asamuzakjp/css-color@5.0.1': - resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} + '@asamuzakjp/css-color@5.1.6': + resolution: {integrity: sha512-BXWCh8dHs9GOfpo/fWGDJtDmleta2VePN9rn6WQt3GjEbxzutVF4t0x2pmH+7dbMCLtuv3MlwqRsAuxlzFXqFg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@asamuzakjp/dom-selector@7.0.3': - resolution: {integrity: sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==} + '@asamuzakjp/dom-selector@7.0.7': + resolution: {integrity: sha512-d2BgqDUOS1Hfp4IzKUZqCNz+Kg3Y88AkaBvJK/ZVSQPU1f7OpPNi7nQTH6/oI47Dkdg+Z3e8Yp6ynOu4UMINAQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} '@asamuzakjp/nwsapi@2.3.9': @@ -399,8 +405,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.1': - resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==} + '@csstools/css-syntax-patches-for-csstree@1.1.2': + resolution: {integrity: sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -1198,105 +1204,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==} @@ -1360,8 +1350,8 @@ packages: '@mermaid-js/mermaid-mindmap@9.3.0': resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} - '@mermaid-js/parser@1.0.1': - resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==} + '@mermaid-js/parser@1.1.0': + resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} '@npmcli/agent@3.0.0': resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==} @@ -1413,28 +1403,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@resvg/resvg-js-linux-arm64-musl@2.4.1': resolution: {integrity: sha512-6mT0+JBCsermKMdi/O2mMk3m7SqOjwi9TKAwSngRZ/nQoL3Z0Z5zV+572ztgbWr0GODB422uD8e9R9zzz38dRQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@resvg/resvg-js-linux-x64-gnu@2.4.1': resolution: {integrity: sha512-60KnrscLj6VGhkYOJEmmzPlqqfcw1keDh6U+vMcNDjPhV3B5vRSkpP/D/a8sfokyeh4VEacPSYkWGezvzS2/mg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@resvg/resvg-js-linux-x64-musl@2.4.1': resolution: {integrity: sha512-0AMyZSICC1D7ge115cOZQW8Pcad6PjWuZkBFF3FJuSxC6Dgok0MQnLTs2MfMdKBlAcwO9dXsf3bv9tJZj8pATA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@resvg/resvg-js-win32-arm64-msvc@2.4.1': resolution: {integrity: sha512-76XDFOFSa3d0QotmcNyChh2xHwk+JTFiEQBVxMlHpHMeq7hNrQJ1IpE1zcHSQvrckvkdfLboKRrlGB86B10Qjw==} @@ -1495,79 +1481,66 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1672,28 +1645,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -1905,11 +1874,11 @@ packages: '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} - '@types/node@24.12.0': - resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} - '@types/nodemailer@7.0.11': - resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==} + '@types/nodemailer@8.0.0': + resolution: {integrity: sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==} '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -1955,17 +1924,17 @@ packages: '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} - '@visactor/react-vchart@2.0.20': - resolution: {integrity: sha512-SlOmCWQ/dd8QA4ez2pcE8dRSzKjt5/0/M6PQ0X8AkawFBcVwHgDPweZnEExsmDN6fvyfB2HqmKH0tefL/J6tUA==} + '@visactor/react-vchart@2.0.21': + resolution: {integrity: sha512-1qgPlW820ZQEbemKiddLRx+JIoEdVUAF4pEyIEAIYoh4W49RPpa8kncMTgKw7030gs3lGCQneqIeLoZMWfZJtA==} peerDependencies: react: '>=16.0.0' react-dom: '>=16.0.0' - '@visactor/vchart-extension@2.0.20': - resolution: {integrity: sha512-+D1eVABTjQ/tla/nQUaWyECQ8GTcQU7oNU9ovPheOfYnPleSjPgDOJnEH2w2mEdiXNh8P+oIaVqm0ItwSPvprA==} + '@visactor/vchart-extension@2.0.21': + resolution: {integrity: sha512-c930SfptDFmRUVrc2tf5sX3uw6EOg7eUFHENYlMmN80ufjoSUqX6MqOXgK/fsZh3lnQi3vzBQI6+9TtY81T4wg==} - '@visactor/vchart@2.0.20': - resolution: {integrity: sha512-nolgr7b+kSa1GQ8q99UjKCoMEYib3brmnRtDC+/iYr4Idbr9hSHS+60wteZgX8GOIUxivOEKYcpTPNXVRNmBAg==} + '@visactor/vchart@2.0.21': + resolution: {integrity: sha512-jYrSwTS8EkV2qB3c+/iW9YBta4dCJSsbuxQzacHnhVuH2XAn7b+33wLdFHbG1h84L1EbVPMle6LrUoXe+CZjYw==} '@visactor/vdataset@1.0.23': resolution: {integrity: sha512-zrLk9FBUWJoW6b30XnPKzXwAXl8USdLDfed6QZLsmdkylRU8V7yZeXE2aKwU8Lg1U4HmQngqmqOx7/QlbX44Tg==} @@ -1973,23 +1942,23 @@ packages: '@visactor/vlayouts@1.0.23': resolution: {integrity: sha512-fK1f5LmuumhYanLArk5yrT4BZxu4IAmdc8WMwfB/KAvV+2dTPFuBUMWbWnDl0siQoU9SX9l/bLozUnI9n7BwBQ==} - '@visactor/vrender-animate@1.0.44': - resolution: {integrity: sha512-MKtoaucmbQNJnuff5F6ws+c8+mRBz1gXVY/bHw38nfvSTZcsJcXYug2aDPf4V9XaUSValDnkx9TyLLbKJ0o0TA==} + '@visactor/vrender-animate@1.0.45': + resolution: {integrity: sha512-6v7LRpr+zugxR8JH3RCnodYxrrzHkSp4GaBTNwXuJ7bwThi/YzXTvmBz2pfQNmUG/rRze8Bm7vqneHVdXuFhng==} - '@visactor/vrender-components@1.0.44': - resolution: {integrity: sha512-vbCa/eME1UJL2iy0l4JHYJh1m6SG3rR5jt5umblLVRQjjBEWPAnhnPwQAA2TNiyO5eHG6qnyvcjgV5aeCeuJYw==} + '@visactor/vrender-components@1.0.45': + resolution: {integrity: sha512-rYsG/rncT5FgYYWqv09f6LkZEAk/IS45IEYWBD0SCgKguYnpEebYDmDba1muJGn+rRv/vGlqg1pYqXLw8mEU5w==} - '@visactor/vrender-core@1.0.44': - resolution: {integrity: sha512-DrB+cg9//RkCZjchWConeXGOC99xwvEDdPfY7eEqajjPxUooJpSQ+pt51uD0VIwQCYtpsc2jVspefqdBh/e2Cg==} + '@visactor/vrender-core@1.0.45': + resolution: {integrity: sha512-kvYAsKGZ+dXbhOQzjjbMkRzI815KgaHoWOW7iyVorXTldQBTsxLtFIi/J3VfYqGdM3apUgsFoy8uvjS5DepPUA==} - '@visactor/vrender-kits@1.0.44': - resolution: {integrity: sha512-B28zr+aTWlFIziH3b/ufUlpKQMv1Ie9ck8kTkHIGdBM/ES+CO6//GHMa/5OxuId19a9UhJxzj8cpeE/9mZLIoQ==} + '@visactor/vrender-kits@1.0.45': + resolution: {integrity: sha512-iaeRitht8IqNvJwdKmcPKAYL0a4+f8xhK2bWUumSQjNbZJQx9UzGuDp73cIdNXzCNoCiJZElpsOiIGu3kymqiw==} '@visactor/vscale@1.0.23': resolution: {integrity: sha512-XePhYuRoNAp+8MeSMuEOOvhVAlOwvM1sDT2yFxE6zdwVB2GjZk8mH+5N2xQGQWk75YmGJjlJASFtgwjlb1yWxw==} - '@visactor/vutils-extension@2.0.20': - resolution: {integrity: sha512-now0AqIHimJr1/+7U9cJBkZ022VAsVlVANTN8mKLVIG1C5wnEYfKYG1Ew6ghjcSmkXi1c+pdrsAsj6i+ifRlwA==} + '@visactor/vutils-extension@2.0.21': + resolution: {integrity: sha512-E9owMC/COyfz481BgDrVPPOgK3O98NZtZldko2dCZBFWuByiRx+xcn4gvSO6vlt2SrBB0LzzyhLYkrNs9xb/0A==} '@visactor/vutils@1.0.23': resolution: {integrity: sha512-M8SLqgdHhKN8QmQKTWD1gzEaHptpIV9pvMYvC6+VeOsqYvZZ6UdhSCAAczTYVo+m/uwcEC2JHSUspbrs8rzlRQ==} @@ -2278,14 +2247,14 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} browserslist@4.28.1: @@ -2517,8 +2486,8 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.33.1: - resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + cytoscape@3.33.2: + resolution: {integrity: sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==} engines: {node: '>=0.10'} d3-array@1.2.4: @@ -2722,8 +2691,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - delaunator@5.0.1: - resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} @@ -2774,8 +2743,8 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dotenv@17.3.1: - resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} engines: {node: '>=12'} drizzle-kit@0.31.10: @@ -2911,8 +2880,8 @@ packages: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} - electron@41.1.0: - resolution: {integrity: sha512-0XRFyxRqetmqtkkBvV++wGbHYJ7bD++f6EgJW8y9kX4pPRagwlmKDtzqXZhKiu0DIQppm3sXxzHWK9GYP91OKQ==} + electron@41.1.1: + resolution: {integrity: sha512-8bgvDhBjli+3Z2YCKgzzoBPh6391pr7Xv2h/tTJG4ETgvPvUxZomObbZLs31mUzYb6VrlcDDd9cyWyNKtPm3tA==} engines: {node: '>= 12.20.55'} hasBin: true @@ -3404,8 +3373,8 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@29.0.1: - resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==} + jsdom@29.0.2: + resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 @@ -3444,8 +3413,8 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - katex@0.16.39: - resolution: {integrity: sha512-FR2f6y85+81ZLO0GPhyQ+EJl/E5ILNWltJhpAeOTzRny952Z13x2867lTFDmvMZix//Ux3CuMQ2VkLXRbUwOFg==} + katex@0.16.45: + resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true keyv@4.5.4: @@ -3505,28 +3474,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -3547,6 +3512,9 @@ packages: lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} @@ -3557,6 +3525,9 @@ packages: lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -3581,8 +3552,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.7: - resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + lru-cache@11.3.2: + resolution: {integrity: sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -3630,8 +3601,8 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - mermaid@11.13.0: - resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==} + mermaid@11.14.0: + resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -3678,8 +3649,8 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -4041,8 +4012,8 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - protocol-buffers-schema@3.6.0: - resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} + protocol-buffers-schema@3.6.1: + resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==} proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -4061,6 +4032,9 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + rate-limiter-flexible@11.0.0: + resolution: {integrity: sha512-UhN3xVeU6Az3y6hAuxMUwFsKcKD1HffhMGK0MknbSxH9vkwslS/p19ovCvJqIVT97pE778nKu2sUgYAcxj4dmQ==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -4077,15 +4051,15 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} - react-router-dom@7.13.2: - resolution: {integrity: sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==} + react-router-dom@7.14.0: + resolution: {integrity: sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.13.2: - resolution: {integrity: sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==} + react-router@7.14.0: + resolution: {integrity: sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -4184,8 +4158,8 @@ packages: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} - robust-predicates@3.0.2: - resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} @@ -4507,11 +4481,11 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - tldts-core@7.0.26: - resolution: {integrity: sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==} + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} - tldts@7.0.26: - resolution: {integrity: sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==} + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} hasBin: true tmp-promise@3.0.3: @@ -4598,8 +4572,8 @@ packages: resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==} engines: {node: '>=18.17'} - undici@7.24.5: - resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==} + undici@7.24.7: + resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} engines: {node: '>=20.18.1'} unique-filename@4.0.0: @@ -4915,6 +4889,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -5039,21 +5016,19 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.4 - '@asamuzakjp/css-color@5.0.1': + '@asamuzakjp/css-color@5.1.6': dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.2.7 - '@asamuzakjp/dom-selector@7.0.3': + '@asamuzakjp/dom-selector@7.0.7': dependencies: '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.7 '@asamuzakjp/nwsapi@2.3.9': {} @@ -5213,7 +5188,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.2(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 @@ -5914,15 +5889,15 @@ snapshots: '@mermaid-js/mermaid-mindmap@9.3.0': dependencies: '@braintree/sanitize-url': 6.0.4 - cytoscape: 3.33.1 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) - cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + cytoscape: 3.33.2 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) + cytoscape-fcose: 2.2.0(cytoscape@3.33.2) d3: 7.9.0 khroma: 2.1.0 non-layered-tidy-tree-layout: 2.0.2 optional: true - '@mermaid-js/parser@1.0.1': + '@mermaid-js/parser@1.1.0': dependencies: langium: 4.2.1 @@ -6415,11 +6390,11 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.12.0': + '@types/node@24.12.2': dependencies: undici-types: 7.16.0 - '@types/nodemailer@7.0.11': + '@types/nodemailer@8.0.0': dependencies: '@types/node': 22.19.15 @@ -6476,39 +6451,39 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@visactor/react-vchart@2.0.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@visactor/react-vchart@2.0.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@visactor/vchart': 2.0.20 - '@visactor/vchart-extension': 2.0.20 - '@visactor/vrender-core': 1.0.44 - '@visactor/vrender-kits': 1.0.44 + '@visactor/vchart': 2.0.21 + '@visactor/vchart-extension': 2.0.21 + '@visactor/vrender-core': 1.0.45 + '@visactor/vrender-kits': 1.0.45 '@visactor/vutils': 1.0.23 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-is: 18.3.1 - '@visactor/vchart-extension@2.0.20': + '@visactor/vchart-extension@2.0.21': dependencies: - '@visactor/vchart': 2.0.20 + '@visactor/vchart': 2.0.21 '@visactor/vdataset': 1.0.23 '@visactor/vlayouts': 1.0.23 - '@visactor/vrender-animate': 1.0.44 - '@visactor/vrender-components': 1.0.44 - '@visactor/vrender-core': 1.0.44 - '@visactor/vrender-kits': 1.0.44 + '@visactor/vrender-animate': 1.0.45 + '@visactor/vrender-components': 1.0.45 + '@visactor/vrender-core': 1.0.45 + '@visactor/vrender-kits': 1.0.45 '@visactor/vutils': 1.0.23 - '@visactor/vchart@2.0.20': + '@visactor/vchart@2.0.21': dependencies: '@visactor/vdataset': 1.0.23 '@visactor/vlayouts': 1.0.23 - '@visactor/vrender-animate': 1.0.44 - '@visactor/vrender-components': 1.0.44 - '@visactor/vrender-core': 1.0.44 - '@visactor/vrender-kits': 1.0.44 + '@visactor/vrender-animate': 1.0.45 + '@visactor/vrender-components': 1.0.45 + '@visactor/vrender-core': 1.0.45 + '@visactor/vrender-kits': 1.0.45 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 - '@visactor/vutils-extension': 2.0.20 + '@visactor/vutils-extension': 2.0.21 '@visactor/vdataset@1.0.23': dependencies: @@ -6538,28 +6513,28 @@ snapshots: '@visactor/vutils': 1.0.23 eventemitter3: 4.0.7 - '@visactor/vrender-animate@1.0.44': + '@visactor/vrender-animate@1.0.45': dependencies: - '@visactor/vrender-core': 1.0.44 + '@visactor/vrender-core': 1.0.45 '@visactor/vutils': 1.0.23 - '@visactor/vrender-components@1.0.44': + '@visactor/vrender-components@1.0.45': dependencies: - '@visactor/vrender-animate': 1.0.44 - '@visactor/vrender-core': 1.0.44 - '@visactor/vrender-kits': 1.0.44 + '@visactor/vrender-animate': 1.0.45 + '@visactor/vrender-core': 1.0.45 + '@visactor/vrender-kits': 1.0.45 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 - '@visactor/vrender-core@1.0.44': + '@visactor/vrender-core@1.0.45': dependencies: '@visactor/vutils': 1.0.23 color-convert: 2.0.1 - '@visactor/vrender-kits@1.0.44': + '@visactor/vrender-kits@1.0.45': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.0.44 + '@visactor/vrender-core': 1.0.45 '@visactor/vutils': 1.0.23 gifuct-js: 2.1.2 lottie-web: 5.13.0 @@ -6569,7 +6544,7 @@ snapshots: dependencies: '@visactor/vutils': 1.0.23 - '@visactor/vutils-extension@2.0.20': + '@visactor/vutils-extension@2.0.21': dependencies: '@visactor/vdataset': 1.0.23 '@visactor/vutils': 1.0.23 @@ -6831,7 +6806,7 @@ snapshots: js-yaml: 4.1.1 json5: 2.2.3 lazy-val: 1.0.5 - minimatch: 10.2.4 + minimatch: 10.2.5 plist: 3.1.0 proper-lockfile: 4.1.2 resedit: 1.7.2 @@ -6912,16 +6887,16 @@ snapshots: boolean@3.2.0: optional: true - brace-expansion@1.1.12: + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.2: + brace-expansion@2.0.3: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -7030,7 +7005,7 @@ snapshots: chevrotain-allstar@0.3.1(chevrotain@11.1.2): dependencies: chevrotain: 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.1 chevrotain@11.1.2: dependencies: @@ -7174,17 +7149,17 @@ snapshots: csstype@3.2.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.2): dependencies: cose-base: 1.0.3 - cytoscape: 3.33.1 + cytoscape: 3.33.2 - cytoscape-fcose@2.2.0(cytoscape@3.33.1): + cytoscape-fcose@2.2.0(cytoscape@3.33.2): dependencies: cose-base: 2.2.0 - cytoscape: 3.33.1 + cytoscape: 3.33.2 - cytoscape@3.33.1: {} + cytoscape@3.33.2: {} d3-array@1.2.4: {} @@ -7218,7 +7193,7 @@ snapshots: d3-delaunay@6.0.4: dependencies: - delaunator: 5.0.1 + delaunator: 5.1.0 d3-dispatch@3.0.1: {} @@ -7370,7 +7345,7 @@ snapshots: dagre-d3-es@7.0.14: dependencies: d3: 7.9.0 - lodash-es: 4.17.23 + lodash-es: 4.18.1 data-urls@7.0.0: dependencies: @@ -7415,9 +7390,9 @@ snapshots: object-keys: 1.1.1 optional: true - delaunator@5.0.1: + delaunator@5.1.0: dependencies: - robust-predicates: 3.0.2 + robust-predicates: 3.0.3 delayed-stream@1.0.0: {} @@ -7476,7 +7451,7 @@ snapshots: dotenv@16.6.1: {} - dotenv@17.3.1: {} + dotenv@17.4.1: {} drizzle-kit@0.31.10: dependencies: @@ -7565,17 +7540,17 @@ snapshots: '@electron/asar': 3.4.1 debug: 4.4.3 fs-extra: 7.0.1 - lodash: 4.17.23 + lodash: 4.18.1 temp: 0.9.4 optionalDependencies: '@electron/windows-sign': 1.2.2 transitivePeerDependencies: - supports-color - electron@41.1.0: + electron@41.1.1: dependencies: '@electron/get': 2.0.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 extract-zip: 2.0.1 transitivePeerDependencies: - supports-color @@ -7979,7 +7954,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.4 + minimatch: 10.2.5 minipass: 7.1.3 path-scurry: 2.0.2 @@ -8199,24 +8174,24 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@29.0.1: + jsdom@29.0.2: dependencies: - '@asamuzakjp/css-color': 5.0.1 - '@asamuzakjp/dom-selector': 7.0.3 + '@asamuzakjp/css-color': 5.1.6 + '@asamuzakjp/dom-selector': 7.0.7 '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1) '@exodus/bytes': 1.15.0 css-tree: 3.2.1 data-urls: 7.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 6.0.0 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.7 + lru-cache: 11.3.2 parse5: 8.0.0 saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 6.0.1 - undici: 7.24.5 + undici: 7.24.7 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 @@ -8252,7 +8227,7 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - katex@0.16.39: + katex@0.16.45: dependencies: commander: 8.3.0 @@ -8333,12 +8308,16 @@ snapshots: lodash-es@4.17.23: {} + lodash-es@4.18.1: {} + lodash.escaperegexp@4.1.2: {} lodash.isequal@4.5.0: {} lodash@4.17.23: {} + lodash@4.18.1: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -8358,7 +8337,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.7: {} + lru-cache@11.3.2: {} lru-cache@5.1.1: dependencies: @@ -8417,24 +8396,24 @@ snapshots: mdn-data@2.27.1: {} - mermaid@11.13.0: + mermaid@11.14.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.1 + '@mermaid-js/parser': 1.1.0 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 - cytoscape: 3.33.1 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) - cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + cytoscape: 3.33.2 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) + cytoscape-fcose: 2.2.0(cytoscape@3.33.2) d3: 7.9.0 d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.20 dompurify: 3.3.3 - katex: 0.16.39 + katex: 0.16.45 khroma: 2.1.0 - lodash-es: 4.17.23 + lodash-es: 4.18.1 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 @@ -8474,21 +8453,21 @@ snapshots: mimic-response@3.1.0: {} - minimatch@10.2.4: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.13 minimatch@5.1.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.0.3 minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.0.3 minimist@1.2.0: {} @@ -8682,7 +8661,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.2.7 + lru-cache: 11.3.2 minipass: 7.1.3 path-source@0.1.3: @@ -8850,7 +8829,7 @@ snapshots: property-information@7.1.0: {} - protocol-buffers-schema@3.6.0: {} + protocol-buffers-schema@3.6.1: {} proxy-from-env@1.1.0: {} @@ -8865,6 +8844,8 @@ snapshots: quick-lru@5.1.1: {} + rate-limiter-flexible@11.0.0: {} + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -8882,13 +8863,13 @@ snapshots: react-refresh@0.17.0: {} - react-router-dom@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router-dom@7.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-router: 7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: cookie: 1.1.1 react: 18.3.1 @@ -8958,7 +8939,7 @@ snapshots: resolve-protobuf-schema@2.1.0: dependencies: - protocol-buffers-schema: 3.6.0 + protocol-buffers-schema: 3.6.1 responselike@2.0.1: dependencies: @@ -8991,7 +8972,7 @@ snapshots: sprintf-js: 1.1.3 optional: true - robust-predicates@3.0.2: {} + robust-predicates@3.0.3: {} rollup@4.59.0: dependencies: @@ -9357,11 +9338,11 @@ snapshots: tinyspy@3.0.2: {} - tldts-core@7.0.26: {} + tldts-core@7.0.28: {} - tldts@7.0.26: + tldts@7.0.28: dependencies: - tldts-core: 7.0.26 + tldts-core: 7.0.28 tmp-promise@3.0.3: dependencies: @@ -9383,7 +9364,7 @@ snapshots: tough-cookie@6.0.1: dependencies: - tldts: 7.0.26 + tldts: 7.0.28 tr46@6.0.0: dependencies: @@ -9429,7 +9410,7 @@ snapshots: undici@6.24.1: {} - undici@7.24.5: {} + undici@7.24.7: {} unique-filename@4.0.0: dependencies: @@ -9542,9 +9523,9 @@ snapshots: lightningcss: 1.32.0 tsx: 4.21.0 - vitepress-plugin-mermaid@2.0.17(mermaid@11.13.0)(vitepress@1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.2)): + vitepress-plugin-mermaid@2.0.17(mermaid@11.14.0)(vitepress@1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.2)): dependencies: - mermaid: 11.13.0 + mermaid: 11.14.0 vitepress: 1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.2) optionalDependencies: '@mermaid-js/mermaid-mindmap': 9.3.0 @@ -9598,7 +9579,7 @@ snapshots: - typescript - universal-cookie - vitest@2.1.9(@types/node@22.19.15)(jsdom@29.0.1)(lightningcss@1.32.0): + vitest@2.1.9(@types/node@22.19.15)(jsdom@29.0.2)(lightningcss@1.32.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.32.0)) @@ -9622,7 +9603,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.15 - jsdom: 29.0.1 + jsdom: 29.0.2 transitivePeerDependencies: - less - lightningcss @@ -9755,4 +9736,6 @@ snapshots: yocto-queue@0.1.0: {} + zod@4.3.6: {} + zwitch@2.0.4: {} diff --git a/scripts/dev/docker.workflow.test.ts b/scripts/dev/docker.workflow.test.ts index 2242fb5..8805c8d 100644 --- a/scripts/dev/docker.workflow.test.ts +++ b/scripts/dev/docker.workflow.test.ts @@ -16,6 +16,17 @@ describe('docker workflows', () => { expect(releaseWorkflow).toContain('"${tag}-armv7"'); }); + it('derives Docker Hub image names from the configured username secret', () => { + const ciWorkflow = readFileSync(resolve(process.cwd(), '.github/workflows/ci.yml'), 'utf8'); + const releaseWorkflow = readFileSync(resolve(process.cwd(), '.github/workflows/release.yml'), 'utf8'); + + expect(ciWorkflow).toContain('DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/metapi'); + expect(ciWorkflow).not.toContain('images: 1467078763/metapi'); + + expect(releaseWorkflow).toContain('DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/metapi'); + expect(releaseWorkflow).not.toContain('1467078763/metapi'); + }); + it('uses an armv7-capable node base image in the Dockerfile', () => { const dockerfile = readFileSync(resolve(process.cwd(), 'docker/Dockerfile'), 'utf8'); diff --git a/src/server/contracts/accountsRoutePayloads.ts b/src/server/contracts/accountsRoutePayloads.ts index 76132f6..6d76961 100644 --- a/src/server/contracts/accountsRoutePayloads.ts +++ b/src/server/contracts/accountsRoutePayloads.ts @@ -5,7 +5,8 @@ const accountCredentialModeSchema = z.enum(['auto', 'session', 'apikey']); const accountCreatePayloadSchema = z.object({ siteId: z.number().int().positive(), username: z.string().optional(), - accessToken: z.string(), + accessToken: z.string().optional(), + accessTokens: z.array(z.string()).optional(), apiToken: z.string().optional(), platformUserId: z.number().int().positive().optional(), checkinEnabled: z.boolean().optional(), @@ -95,6 +96,9 @@ function formatAccountsPayloadError(error: z.ZodError): string { if (firstPath === 'apiToken') { return 'Invalid apiToken. Expected string or null.'; } + if (firstPath === 'accessTokens') { + return 'Invalid accessTokens. Expected string[].'; + } if (firstPath === 'checkinEnabled') { return 'Invalid checkinEnabled. Expected boolean.'; } diff --git a/src/server/routes/api/accounts.apiSite.test.ts b/src/server/routes/api/accounts.apiSite.test.ts index de5775f..0da81f6 100644 --- a/src/server/routes/api/accounts.apiSite.test.ts +++ b/src/server/routes/api/accounts.apiSite.test.ts @@ -268,4 +268,97 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { expect(getModelsMock).toHaveBeenNthCalledWith(1, 'https://api-create-a.example.com', 'sk-nihao-create-rotate', undefined); expect(getModelsMock).toHaveBeenNthCalledWith(2, 'https://api-create-b.example.com', 'sk-nihao-create-rotate', undefined); }); + + it('supports batch creating multiple API key connections for one site', async () => { + getModelsMock + .mockResolvedValueOnce(['gpt-4o-mini']) + .mockResolvedValueOnce(['gpt-4.1-mini']); + + const site = await db.insert(schema.sites).values({ + name: 'Nihao Batch Pool', + url: 'https://console.example.com', + platform: 'new-api', + status: 'active', + }).returning().get(); + + await db.insert(schema.siteApiEndpoints).values({ + siteId: site.id, + url: 'https://api.example.com', + enabled: true, + sortOrder: 0, + }).run(); + + const response = await app.inject({ + method: 'POST', + url: '/api/accounts', + payload: { + siteId: site.id, + username: 'batch-key', + accessToken: 'sk-batch-a\nsk-batch-b', + credentialMode: 'apikey', + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + success: true, + batch: true, + totalCount: 2, + createdCount: 2, + failedCount: 0, + }); + expect(getModelsMock).toHaveBeenNthCalledWith(1, 'https://api.example.com', 'sk-batch-a', undefined); + expect(getModelsMock).toHaveBeenNthCalledWith(2, 'https://api.example.com', 'sk-batch-b', undefined); + + const accounts = await db.select().from(schema.accounts).all(); + expect(accounts).toHaveLength(2); + expect(accounts.map((item) => item.apiToken)).toEqual(['sk-batch-a', 'sk-batch-b']); + expect(accounts.map((item) => item.username)).toEqual(['batch-key #1', 'batch-key #2']); + }); + + it('treats accessTokens payloads as batch API key creation even without credentialMode', async () => { + getModelsMock + .mockResolvedValueOnce(['gpt-4o-mini']) + .mockResolvedValueOnce(['gpt-4.1-mini']); + + const site = await db.insert(schema.sites).values({ + name: 'Nihao Batch Array', + url: 'https://console.example.com', + platform: 'new-api', + status: 'active', + }).returning().get(); + + await db.insert(schema.siteApiEndpoints).values({ + siteId: site.id, + url: 'https://api.example.com', + enabled: true, + sortOrder: 0, + }).run(); + + const response = await app.inject({ + method: 'POST', + url: '/api/accounts', + payload: { + siteId: site.id, + username: 'array-key', + accessTokens: ['sk-array-a', 'sk-array-b'], + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + success: true, + batch: true, + totalCount: 2, + createdCount: 2, + failedCount: 0, + }); + expect(getModelsMock).toHaveBeenNthCalledWith(1, 'https://api.example.com', 'sk-array-a', undefined); + expect(getModelsMock).toHaveBeenNthCalledWith(2, 'https://api.example.com', 'sk-array-b', undefined); + + const accounts = await db.select().from(schema.accounts).all(); + expect(accounts).toHaveLength(2); + expect(accounts.map((item) => item.apiToken)).toEqual(['sk-array-a', 'sk-array-b']); + expect(accounts.map((item) => item.username)).toEqual(['array-key #1', 'array-key #2']); + }); }); diff --git a/src/server/routes/api/accounts.ts b/src/server/routes/api/accounts.ts index eafc1ce..eeda591 100644 --- a/src/server/routes/api/accounts.ts +++ b/src/server/routes/api/accounts.ts @@ -1,6 +1,6 @@ import { FastifyInstance } from 'fastify'; import { db, schema, runtimeDbDialect } from '../../db/index.js'; -import { getInsertedRowId, insertAndGetById } from '../../db/insertHelpers.js'; +import { insertAndGetById } from '../../db/insertHelpers.js'; import { and, eq, gte, lt, sql } from 'drizzle-orm'; import { refreshBalance } from '../../services/balanceService.js'; import { getAdapter } from '../../services/platforms/index.js'; @@ -34,6 +34,7 @@ import { appendSessionTokenRebindHint } from '../../services/alertRules.js'; import { parseSiteProxyUrlInput, withAccountProxyOverride, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; import { createRateLimitGuard } from '../../middleware/requestRateLimit.js'; import { + type AccountCreatePayload, parseAccountBatchPayload, parseAccountCreatePayload, parseAccountHealthRefreshPayload, @@ -44,6 +45,8 @@ import { parseAccountVerifyTokenPayload, } from '../../contracts/accountsRoutePayloads.js'; import { requireSiteApiBaseUrl, runWithSiteApiEndpointPool } from '../../services/siteApiEndpointService.js'; +import { buildBatchApiKeyConnectionName, parseBatchApiKeys } from '../../services/apiKeyBatch.js'; +import { createManualAccount } from '../../services/manualAccountCreationService.js'; type AccountWithSiteRow = { accounts: typeof schema.accounts.$inferSelect; @@ -65,17 +68,6 @@ type AccountCapabilities = { proxyOnly: boolean; }; -type AccountInitializationParams = { - accountId: number; - site: typeof schema.sites.$inferSelect; - adapter: NonNullable>; - tokenType: 'session' | 'apikey' | 'unknown'; - accessToken: string; - apiToken: string; - platformUserId?: number; - skipModelFetch?: boolean; -}; - type VerifyFailureReason = 'needs-user-id' | 'invalid-user-id' | 'shield-blocked' | null; const limitAccountLogin = createRateLimitGuard({ @@ -150,65 +142,18 @@ function normalizePinnedFlag(input: unknown): boolean | null { return null; } -async function initializeAccountInBackground({ - accountId, - site, - adapter, - tokenType, - accessToken, - apiToken, - platformUserId, - skipModelFetch, -}: AccountInitializationParams) { - const summary = { - accountId, - syncedTokenCount: 0, - refreshedBalance: false, - refreshedModels: false, - rebuiltRoutes: false, - }; - - let fetchedUpstreamTokens: Array<{ name?: string | null; key?: string | null; enabled?: boolean | null; tokenGroup?: string | null }> = []; - if (tokenType === 'session' && accessToken) { - try { - const syncedTokens = await adapter.getApiTokens(site.url, accessToken, platformUserId); - summary.syncedTokenCount = Array.isArray(syncedTokens) ? syncedTokens.length : 0; - fetchedUpstreamTokens = Array.isArray(syncedTokens) ? syncedTokens : []; - } catch {} +function resolveRequestedCreateTokens( + body: AccountCreatePayload, + credentialMode: AccountCredentialMode, +): string[] { + if (credentialMode !== 'apikey') { + const single = String(body.accessToken || '').trim(); + return single ? [single] : []; } - const convergence = await convergeAccountMutation({ - accountId, - preferredApiToken: tokenType === 'session' ? apiToken : null, - defaultTokenSource: 'manual', - ensurePreferredTokenBeforeSync: tokenType === 'session', - upstreamTokens: fetchedUpstreamTokens, - refreshBalance: tokenType === 'session', - refreshModels: skipModelFetch !== true, - rebuildRoutes: skipModelFetch !== true, - continueOnError: true, - }); - summary.refreshedBalance = convergence.refreshedBalance; - summary.refreshedModels = convergence.refreshedModels; - summary.rebuiltRoutes = convergence.rebuiltRoutes; - - return summary; -} - -function buildQueuedAccountInitializationMessage( - tokenType: 'session' | 'apikey' | 'unknown', - skipModelFetch?: boolean, -) { - if (tokenType === 'session' && skipModelFetch === true) { - return '账号已添加,后台正在同步令牌和余额信息。'; - } - if (tokenType === 'session') { - return '账号已添加,后台正在同步令牌、余额和模型信息。'; - } - if (skipModelFetch === true) { - return '已添加为 API Key 账号(可用于代理转发)。'; - } - return '已添加为 API Key 账号,后台正在同步模型和路由信息。'; + const batchTokens = parseBatchApiKeys(body.accessTokens); + if (batchTokens.length > 0) return batchTokens; + return parseBatchApiKeys(body.accessToken); } function normalizeSortOrder(input: unknown): number | null { @@ -1147,174 +1092,103 @@ export async function accountsRoutes(app: FastifyInstance) { return reply.code(400).send({ success: false, message: `platform not supported: ${site.platform}` }); } - const credentialMode = resolveRequestedCredentialMode(body.credentialMode); - const rawAccessToken = (body.accessToken || '').trim(); - if (!rawAccessToken) { + const explicitBatchTokens = parseBatchApiKeys(body.accessTokens); + const credentialMode = explicitBatchTokens.length > 0 + ? 'apikey' + : resolveRequestedCredentialMode(body.credentialMode); + const requestedTokens = explicitBatchTokens.length > 0 + ? explicitBatchTokens + : resolveRequestedCreateTokens(body, credentialMode); + if (requestedTokens.length === 0) { return reply.code(400).send({ success: false, message: '请填写 Token' }); } - let username = (body.username || '').trim(); - let accessToken = rawAccessToken; - let apiToken = (body.apiToken || '').trim(); - let tokenType: 'session' | 'apikey' | 'unknown' = 'unknown'; - let verifiedModels: string[] = []; + if (credentialMode === 'apikey' && requestedTokens.length > 1) { + const items: Array> = []; + let createdCount = 0; - if (credentialMode === 'apikey') { - if (body.skipModelFetch === true) { - tokenType = 'apikey'; - accessToken = ''; - if (!apiToken) apiToken = rawAccessToken; - } else { + for (const [index, token] of requestedTokens.entries()) { try { - const models = await getModelsWithSiteApiEndpointPool( + const created = await createManualAccount({ + body, site, adapter, - rawAccessToken, - body.platformUserId, - ); - verifiedModels = Array.isArray(models) - ? models.filter((item) => typeof item === 'string' && item.trim().length > 0) - : []; - } catch (err: any) { - return reply.code(400).send({ - success: false, - message: err?.message || 'API Key 验证失败', + credentialMode, + rawAccessToken: token, + usernameOverride: buildBatchApiKeyConnectionName(body.username, index, requestedTokens.length) || undefined, + }); + createdCount += 1; + items.push({ + index, + status: 'created', + id: created.account.id, + username: created.account.username || null, + queued: created.queued === true, + message: created.message || null, + modelCount: created.modelCount || 0, + }); + } catch (error: any) { + items.push({ + index, + status: 'failed', + message: error?.message || '创建失败', + requiresVerification: error?.requiresVerification === true, }); } - - if (verifiedModels.length === 0) { - return reply.code(400).send({ - success: false, - requiresVerification: true, - message: 'API Key 验证失败:未获取到可用模型', - }); - } - - tokenType = 'apikey'; - accessToken = ''; - if (!apiToken) apiToken = rawAccessToken; } - } else { - let verifyResult: any; - try { - verifyResult = await adapter.verifyToken(site.url, rawAccessToken, body.platformUserId); - } catch (err: any) { + + if (createdCount === 0) { return reply.code(400).send({ success: false, - message: appendSessionTokenRebindHint(err?.message || 'Token 验证失败'), + batch: true, + totalCount: requestedTokens.length, + createdCount: 0, + failedCount: requestedTokens.length, + message: `批量添加失败(0/${requestedTokens.length})`, + items, }); } - tokenType = verifyResult.tokenType; - if (tokenType === 'unknown') { - return reply.code(400).send({ - success: false, - requiresVerification: true, - message: 'Token 验证失败,请先点击“验证 Token”,验证成功后再绑定账号', - }); - } - - if (credentialMode === 'session' && tokenType !== 'session') { - return reply.code(400).send({ - success: false, - message: '当前凭证是 API Key,请切换到 API Key 模式,或改用 Session Token', - }); - } - - if (tokenType === 'session') { - if (!username && verifyResult.userInfo?.username) username = String(verifyResult.userInfo.username).trim(); - if (!apiToken && verifyResult.apiToken) apiToken = String(verifyResult.apiToken).trim(); - } else if (tokenType === 'apikey') { - accessToken = ''; - if (!apiToken) apiToken = rawAccessToken; - verifiedModels = Array.isArray(verifyResult.models) - ? verifyResult.models.filter((item: unknown) => typeof item === 'string' && item.trim().length > 0) - : []; - } + return { + success: true, + batch: true, + totalCount: requestedTokens.length, + createdCount, + failedCount: requestedTokens.length - createdCount, + message: `批量添加完成:成功 ${createdCount},失败 ${requestedTokens.length - createdCount}`, + items, + }; } - // Store platformUserId and credential mode in extraConfig. - const resolvedPlatformUserId = - body.platformUserId || guessPlatformUserIdFromUsername(username) || undefined; - const resolvedCredentialMode: AccountCredentialMode = tokenType === 'apikey' ? 'apikey' : 'session'; - const extraConfigPatch: Record = { credentialMode: resolvedCredentialMode }; - if (resolvedPlatformUserId) { - extraConfigPatch.platformUserId = resolvedPlatformUserId; + try { + const created = await createManualAccount({ + body, + site, + adapter, + credentialMode, + rawAccessToken: requestedTokens[0]!, + }); + return { + ...created.account, + tokenType: created.tokenType, + credentialMode: resolveStoredCredentialMode(created.account), + capabilities: buildCapabilitiesForAccount(created.account), + modelCount: created.modelCount, + apiTokenFound: created.apiTokenFound, + usernameDetected: created.usernameDetected, + queued: created.queued, + jobId: created.jobId, + message: created.message, + }; + } catch (err: any) { + return reply.code(400).send({ + success: false, + requiresVerification: err?.requiresVerification === true, + message: credentialMode !== 'apikey' + ? appendSessionTokenRebindHint(err?.message || 'Token 验证失败') + : (err?.message || 'API Key 验证失败'), + }); } - if ((site.platform || '').toLowerCase() === 'sub2api') { - const managedRefreshToken = normalizeManagedRefreshToken(body.refreshToken); - const managedTokenExpiresAt = normalizeManagedTokenExpiresAt(body.tokenExpiresAt); - if (managedRefreshToken) { - extraConfigPatch.sub2apiAuth = managedTokenExpiresAt - ? { refreshToken: managedRefreshToken, tokenExpiresAt: managedTokenExpiresAt } - : { refreshToken: managedRefreshToken }; - } - } - const extraConfig = mergeAccountExtraConfig(undefined, extraConfigPatch); - - const result = await insertAndGetById({ - table: schema.accounts, - idColumn: schema.accounts.id, - values: { - siteId: body.siteId, - username: username || undefined, - accessToken, - apiToken: apiToken || undefined, - checkinEnabled: tokenType === 'session' ? (body.checkinEnabled ?? true) : false, - extraConfig, - isPinned: false, - sortOrder: await getNextAccountSortOrder(), - }, - insertErrorMessage: '创建账号失败', - loadErrorMessage: '创建账号失败', - }); - - const shouldQueueInitialization = tokenType === 'session' || body.skipModelFetch !== true; - let queuedTaskId: string | undefined; - let queuedMessage: string | undefined; - if (shouldQueueInitialization) { - const taskTitle = `初始化连接 #${result.id}`; - const { task } = startBackgroundTask( - { - type: 'account-init', - title: taskTitle, - dedupeKey: `account-init-${result.id}`, - notifyOnFailure: true, - successMessage: () => `${taskTitle}已完成`, - failureMessage: (currentTask) => `${taskTitle}失败:${currentTask.error || 'unknown error'}`, - }, - async () => initializeAccountInBackground({ - accountId: result.id, - site, - adapter, - tokenType, - accessToken, - apiToken, - platformUserId: resolvedPlatformUserId, - skipModelFetch: body.skipModelFetch, - }), - ); - queuedTaskId = task.id; - queuedMessage = buildQueuedAccountInitializationMessage(tokenType, body.skipModelFetch); - } - - const account = await db.select().from(schema.accounts).where(eq(schema.accounts.id, result.id)).get(); - const finalCredentialMode = account ? resolveStoredCredentialMode(account) : resolvedCredentialMode; - const capabilities = account - ? buildCapabilitiesForAccount(account) - : buildCapabilitiesFromCredentialMode(finalCredentialMode, tokenType === 'session', null); - return { - ...account, - tokenType, - credentialMode: finalCredentialMode, - capabilities, - modelCount: verifiedModels.length, - apiTokenFound: !!apiToken, - usernameDetected: !!(!body.username && username), - queued: !!queuedTaskId, - jobId: queuedTaskId, - message: queuedMessage, - }; }); // Update an account diff --git a/src/server/routes/api/oauth.test.ts b/src/server/routes/api/oauth.test.ts index 990632a..d9d1de3 100644 --- a/src/server/routes/api/oauth.test.ts +++ b/src/server/routes/api/oauth.test.ts @@ -3246,6 +3246,97 @@ describe('oauth routes', { timeout: 15_000 }, () => { } }); + it('restores oauth route unit route channels when delete rebuild keeps failing', async () => { + const site = await db.insert(schema.sites).values({ + name: 'ChatGPT Codex OAuth', + url: 'https://chatgpt.com/backend-api/codex', + platform: 'codex', + status: 'active', + }).returning().get(); + + const first = await db.insert(schema.accounts).values({ + siteId: site.id, + username: 'rollback-delete-double-fail-a@example.com', + accessToken: 'rollback-delete-double-fail-access-a', + status: 'active', + oauthProvider: 'codex', + oauthAccountKey: 'rollback-delete-double-fail-a', + extraConfig: JSON.stringify({ + oauth: { + provider: 'codex', + accountId: 'rollback-delete-double-fail-a', + accountKey: 'rollback-delete-double-fail-a', + email: 'rollback-delete-double-fail-a@example.com', + refreshToken: 'rollback-delete-double-fail-refresh-a', + }, + }), + }).returning().get(); + const second = await db.insert(schema.accounts).values({ + siteId: site.id, + username: 'rollback-delete-double-fail-b@example.com', + accessToken: 'rollback-delete-double-fail-access-b', + status: 'active', + oauthProvider: 'codex', + oauthAccountKey: 'rollback-delete-double-fail-b', + extraConfig: JSON.stringify({ + oauth: { + provider: 'codex', + accountId: 'rollback-delete-double-fail-b', + accountKey: 'rollback-delete-double-fail-b', + email: 'rollback-delete-double-fail-b@example.com', + refreshToken: 'rollback-delete-double-fail-refresh-b', + }, + }), + }).returning().get(); + + await db.insert(schema.modelAvailability).values([ + { accountId: first.id, modelName: 'gpt-5.4', available: true }, + { accountId: second.id, modelName: 'gpt-5.4', available: true }, + ]).run(); + + const routeRefreshWorkflow = await import('../../services/routeRefreshWorkflow.js'); + await routeRefreshWorkflow.rebuildRoutesOnly(); + + const createResponse = await app.inject({ + method: 'POST', + url: '/api/oauth/route-units', + payload: { + accountIds: [first.id, second.id], + name: 'Rollback Delete Double Fail Pool', + strategy: 'round_robin', + }, + }); + expect(createResponse.statusCode).toBe(200); + const routeUnitId = createResponse.json().routeUnit?.id as number; + + const rebuildSpy = vi.spyOn(routeRefreshWorkflow, 'rebuildRoutesOnly'); + rebuildSpy.mockImplementation(async () => { + throw new Error('route rebuild failed'); + }); + + try { + const response = await app.inject({ + method: 'DELETE', + url: `/api/oauth/route-units/${routeUnitId}`, + }); + + expect(response.statusCode).toBe(500); + expect(response.json()).toMatchObject({ + message: 'route rebuild failed', + }); + + const routeUnits = await db.select().from(schema.oauthRouteUnits).all(); + expect(routeUnits).toHaveLength(1); + const members = await db.select().from(schema.oauthRouteUnitMembers).all(); + expect(members).toHaveLength(2); + const routeChannels = await db.select().from(schema.routeChannels).all(); + expect(routeChannels).toHaveLength(1); + expect(routeChannels[0]?.oauthRouteUnitId).toBe(routeUnitId); + } finally { + rebuildSpy.mockRestore(); + } + }); + it('accepts normalized route unit strategy values from the HTTP payload', async () => { const site = await db.insert(schema.sites).values({ name: 'ChatGPT Codex OAuth', diff --git a/src/server/routes/api/tokens.route-update-rebuild.test.ts b/src/server/routes/api/tokens.route-update-rebuild.test.ts index 66d2025..3a0d3a9 100644 --- a/src/server/routes/api/tokens.route-update-rebuild.test.ts +++ b/src/server/routes/api/tokens.route-update-rebuild.test.ts @@ -658,7 +658,7 @@ describe('PUT /api/routes/:id route rebuild', () => { expect(updated?.tokenId).toBe(seeded.token.id); }); - it('prefers an exact route over a colliding explicit-group display name', async () => { + it('prefers an explicit-group display name over a colliding exact route', async () => { const exactCandidate = await seedAccountWithToken('claude-opus-4-6'); const groupedCandidate = await seedAccountWithToken('claude-opus-4-5'); @@ -716,9 +716,8 @@ describe('PUT /api/routes/:id route rebuild', () => { success: true, decision: { matched: true, - routeId: exactRoute.id, - modelPattern: 'claude-opus-4-6', - actualModel: 'claude-opus-4-6', + routeId: groupResponse.json().id, + actualModel: 'claude-opus-4-5', }, }); }); diff --git a/src/server/services/apiKeyBatch.ts b/src/server/services/apiKeyBatch.ts new file mode 100644 index 0000000..8cfa30e --- /dev/null +++ b/src/server/services/apiKeyBatch.ts @@ -0,0 +1,4 @@ +export { + buildBatchApiKeyConnectionName, + parseBatchApiKeys, +} from './shared/apiKeyBatchCore.js'; diff --git a/src/server/services/manualAccountCreationService.ts b/src/server/services/manualAccountCreationService.ts new file mode 100644 index 0000000..f1b3f82 --- /dev/null +++ b/src/server/services/manualAccountCreationService.ts @@ -0,0 +1,305 @@ +import { eq } from 'drizzle-orm'; +import { db, schema } from '../db/index.js'; +import { insertAndGetById } from '../db/insertHelpers.js'; +import { startBackgroundTask } from './backgroundTaskService.js'; +import { getAdapter } from './platforms/index.js'; +import { + guessPlatformUserIdFromUsername, + mergeAccountExtraConfig, + type AccountCredentialMode, +} from './accountExtraConfig.js'; +import { runWithSiteApiEndpointPool } from './siteApiEndpointService.js'; +import { type AccountCreatePayload } from '../contracts/accountsRoutePayloads.js'; +import { convergeAccountMutation } from './accountMutationWorkflow.js'; + +const ACCOUNT_VERIFY_TIMEOUT_MS = 10_000; + +type AccountInitializationParams = { + accountId: number; + site: typeof schema.sites.$inferSelect; + adapter: NonNullable>; + tokenType: 'session' | 'apikey' | 'unknown'; + accessToken: string; + apiToken: string; + platformUserId?: number; + skipModelFetch?: boolean; +}; + +export type CreateManualAccountParams = { + body: AccountCreatePayload; + site: typeof schema.sites.$inferSelect; + adapter: NonNullable>; + credentialMode: AccountCredentialMode; + rawAccessToken: string; + usernameOverride?: string; +}; + +export type CreateManualAccountResult = { + account: typeof schema.accounts.$inferSelect; + tokenType: 'session' | 'apikey' | 'unknown'; + modelCount: number; + apiTokenFound: boolean; + usernameDetected: boolean; + queued: boolean; + jobId?: string; + message?: string; +}; + +async function withTimeout(fn: () => Promise, timeoutMs: number, timeoutMessage: string): Promise { + let timer: ReturnType | null = null; + try { + return await Promise.race([ + fn(), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +function buildAccountVerifyTimeoutMessage(): string { + return `Token verification timed out (${Math.max(1, Math.round(ACCOUNT_VERIFY_TIMEOUT_MS / 1000))}s)`; +} + +async function getNextAccountSortOrder(): Promise { + const rows = await db.select({ sortOrder: schema.accounts.sortOrder }).from(schema.accounts).all(); + const max = rows.reduce((currentMax, row) => Math.max(currentMax, row.sortOrder || 0), -1); + return max + 1; +} + +async function getModelsWithSiteApiEndpointPool( + site: typeof schema.sites.$inferSelect, + adapter: NonNullable>, + accessToken: string, + platformUserId?: number, +): Promise { + const timeoutMessage = buildAccountVerifyTimeoutMessage(); + const deadline = Date.now() + ACCOUNT_VERIFY_TIMEOUT_MS; + return runWithSiteApiEndpointPool(site, (target) => { + const remainingMs = deadline - Date.now(); + if (remainingMs <= 0) { + throw new Error(timeoutMessage); + } + return withTimeout( + () => adapter.getModels(target.baseUrl, accessToken, platformUserId), + remainingMs, + timeoutMessage, + ); + }); +} + +async function initializeAccountInBackground({ + accountId, + site, + adapter, + tokenType, + accessToken, + apiToken, + platformUserId, + skipModelFetch, +}: AccountInitializationParams) { + const summary = { + accountId, + syncedTokenCount: 0, + refreshedBalance: false, + refreshedModels: false, + rebuiltRoutes: false, + }; + + let fetchedUpstreamTokens: Array<{ name?: string | null; key?: string | null; enabled?: boolean | null; tokenGroup?: string | null }> = []; + if (tokenType === 'session' && accessToken) { + try { + const syncedTokens = await adapter.getApiTokens(site.url, accessToken, platformUserId); + summary.syncedTokenCount = Array.isArray(syncedTokens) ? syncedTokens.length : 0; + fetchedUpstreamTokens = Array.isArray(syncedTokens) ? syncedTokens : []; + } catch {} + } + + const convergence = await convergeAccountMutation({ + accountId, + preferredApiToken: tokenType === 'session' ? apiToken : null, + defaultTokenSource: 'manual', + ensurePreferredTokenBeforeSync: tokenType === 'session', + upstreamTokens: fetchedUpstreamTokens, + refreshBalance: tokenType === 'session', + refreshModels: skipModelFetch !== true, + rebuildRoutes: skipModelFetch !== true, + continueOnError: true, + }); + summary.refreshedBalance = convergence.refreshedBalance; + summary.refreshedModels = convergence.refreshedModels; + summary.rebuiltRoutes = convergence.rebuiltRoutes; + + return summary; +} + +function buildQueuedAccountInitializationMessage( + tokenType: 'session' | 'apikey' | 'unknown', + skipModelFetch?: boolean, +) { + if (tokenType === 'session' && skipModelFetch === true) { + return '账号已添加,后台正在同步令牌和余额信息。'; + } + if (tokenType === 'session') { + return '账号已添加,后台正在同步令牌、余额和模型信息。'; + } + if (skipModelFetch === true) { + return '已添加为 API Key 账号(可用于代理转发)。'; + } + return '已添加为 API Key 账号,后台正在同步模型和路由信息。'; +} + +export async function createManualAccount({ + body, + site, + adapter, + credentialMode, + rawAccessToken, + usernameOverride, +}: CreateManualAccountParams): Promise { + let username = typeof usernameOverride === 'string' + ? usernameOverride.trim() + : (body.username || '').trim(); + let accessToken = rawAccessToken; + let apiToken = (body.apiToken || '').trim(); + let tokenType: 'session' | 'apikey' | 'unknown' = 'unknown'; + let verifiedModels: string[] = []; + + if (credentialMode === 'apikey') { + if (body.skipModelFetch === true) { + tokenType = 'apikey'; + accessToken = ''; + if (!apiToken) apiToken = rawAccessToken; + } else { + const models = await getModelsWithSiteApiEndpointPool( + site, + adapter, + rawAccessToken, + body.platformUserId, + ); + verifiedModels = Array.isArray(models) + ? models.filter((item) => typeof item === 'string' && item.trim().length > 0) + : []; + if (verifiedModels.length === 0) { + const error = new Error('API Key 验证失败:未获取到可用模型'); + (error as Error & { requiresVerification?: boolean }).requiresVerification = true; + throw error; + } + + tokenType = 'apikey'; + accessToken = ''; + if (!apiToken) apiToken = rawAccessToken; + } + } else { + const verifyResult = await withTimeout( + () => adapter.verifyToken(site.url, rawAccessToken, body.platformUserId), + ACCOUNT_VERIFY_TIMEOUT_MS, + buildAccountVerifyTimeoutMessage(), + ); + tokenType = verifyResult.tokenType; + if (tokenType === 'unknown') { + const error = new Error('Token 验证失败,请先点击“验证 Token”,验证成功后再绑定账号'); + (error as Error & { requiresVerification?: boolean }).requiresVerification = true; + throw error; + } + + if (credentialMode === 'session' && tokenType !== 'session') { + throw new Error('当前凭证是 API Key,请切换到 API Key 模式,或改用 Session Token'); + } + + if (tokenType === 'session') { + if (!username && verifyResult.userInfo?.username) username = String(verifyResult.userInfo.username).trim(); + if (!apiToken && verifyResult.apiToken) apiToken = String(verifyResult.apiToken).trim(); + } else if (tokenType === 'apikey') { + accessToken = ''; + if (!apiToken) apiToken = rawAccessToken; + verifiedModels = Array.isArray(verifyResult.models) + ? verifyResult.models.filter((item: unknown) => typeof item === 'string' && item.trim().length > 0) + : []; + } + } + + const resolvedPlatformUserId = + body.platformUserId || guessPlatformUserIdFromUsername(username) || undefined; + const resolvedCredentialMode: AccountCredentialMode = tokenType === 'apikey' ? 'apikey' : 'session'; + const extraConfigPatch: Record = { credentialMode: resolvedCredentialMode }; + if (resolvedPlatformUserId) { + extraConfigPatch.platformUserId = resolvedPlatformUserId; + } + if ((site.platform || '').toLowerCase() === 'sub2api') { + const managedRefreshToken = typeof body.refreshToken === 'string' ? body.refreshToken.trim() : ''; + const managedTokenExpiresAt = typeof body.tokenExpiresAt === 'number' + ? Math.trunc(body.tokenExpiresAt) + : (typeof body.tokenExpiresAt === 'string' ? Number.parseInt(body.tokenExpiresAt.trim(), 10) : undefined); + if (managedRefreshToken) { + extraConfigPatch.sub2apiAuth = managedTokenExpiresAt && Number.isFinite(managedTokenExpiresAt) && managedTokenExpiresAt > 0 + ? { refreshToken: managedRefreshToken, tokenExpiresAt: managedTokenExpiresAt } + : { refreshToken: managedRefreshToken }; + } + } + const extraConfig = mergeAccountExtraConfig(undefined, extraConfigPatch); + + const result = await insertAndGetById({ + table: schema.accounts, + idColumn: schema.accounts.id, + values: { + siteId: body.siteId, + username: username || undefined, + accessToken, + apiToken: apiToken || undefined, + checkinEnabled: tokenType === 'session' ? (body.checkinEnabled ?? true) : false, + extraConfig, + isPinned: false, + sortOrder: await getNextAccountSortOrder(), + }, + insertErrorMessage: '创建账号失败', + loadErrorMessage: '创建账号失败', + }); + + const shouldQueueInitialization = tokenType === 'session' || body.skipModelFetch !== true; + let queuedTaskId: string | undefined; + let queuedMessage: string | undefined; + if (shouldQueueInitialization) { + const taskTitle = `初始化连接 #${result.id}`; + const { task } = startBackgroundTask( + { + type: 'account-init', + title: taskTitle, + dedupeKey: `account-init-${result.id}`, + notifyOnFailure: true, + successMessage: () => `${taskTitle}已完成`, + failureMessage: (currentTask) => `${taskTitle}失败:${currentTask.error || 'unknown error'}`, + }, + async () => initializeAccountInBackground({ + accountId: result.id, + site, + adapter, + tokenType, + accessToken, + apiToken, + platformUserId: resolvedPlatformUserId, + skipModelFetch: body.skipModelFetch, + }), + ); + queuedTaskId = task.id; + queuedMessage = buildQueuedAccountInitializationMessage(tokenType, body.skipModelFetch); + } + + const account = await db.select().from(schema.accounts).where(eq(schema.accounts.id, result.id)).get(); + if (!account) { + throw new Error('创建账号失败'); + } + + return { + account, + tokenType, + modelCount: verifiedModels.length, + apiTokenFound: !!apiToken, + usernameDetected: !!(!body.username && username), + queued: !!queuedTaskId, + jobId: queuedTaskId, + message: queuedMessage, + }; +} diff --git a/src/server/services/oauth/routeUnitService.ts b/src/server/services/oauth/routeUnitService.ts index ec5101a..6f10289 100644 --- a/src/server/services/oauth/routeUnitService.ts +++ b/src/server/services/oauth/routeUnitService.ts @@ -153,6 +153,7 @@ async function rollbackCreatedOauthRouteUnit(routeUnitId: number): Promise async function restoreDeletedOauthRouteUnit(snapshot: { unit: typeof schema.oauthRouteUnits.$inferSelect; members: Array; + channels: Array; }): Promise { await db.transaction(async (tx) => { await tx.insert(schema.oauthRouteUnits).values({ @@ -176,6 +177,31 @@ async function restoreDeletedOauthRouteUnit(snapshot: { updatedAt: member.updatedAt, }))).run(); } + + if (snapshot.channels.length > 0) { + await tx.insert(schema.routeChannels).values(snapshot.channels.map((channel) => ({ + id: channel.id, + routeId: channel.routeId, + accountId: channel.accountId, + tokenId: channel.tokenId, + oauthRouteUnitId: channel.oauthRouteUnitId, + sourceModel: channel.sourceModel, + priority: channel.priority, + weight: channel.weight, + enabled: channel.enabled, + manualOverride: channel.manualOverride, + successCount: channel.successCount, + failCount: channel.failCount, + totalLatencyMs: channel.totalLatencyMs, + totalCost: channel.totalCost, + lastUsedAt: channel.lastUsedAt, + lastSelectedAt: channel.lastSelectedAt, + lastFailAt: channel.lastFailAt, + consecutiveFailCount: channel.consecutiveFailCount, + cooldownLevel: channel.cooldownLevel, + cooldownUntil: channel.cooldownUntil, + }))).run(); + } }); } @@ -373,6 +399,9 @@ export async function deleteOauthRouteUnit(routeUnitId: number) { const existingMembers = await db.select().from(schema.oauthRouteUnitMembers) .where(eq(schema.oauthRouteUnitMembers.unitId, routeUnitId)) .all(); + const existingChannels = await db.select().from(schema.routeChannels) + .where(eq(schema.routeChannels.oauthRouteUnitId, routeUnitId)) + .all(); await db.delete(schema.routeChannels) .where(eq(schema.routeChannels.oauthRouteUnitId, routeUnitId)) @@ -390,6 +419,7 @@ export async function deleteOauthRouteUnit(routeUnitId: number) { await restoreDeletedOauthRouteUnit({ unit: existing, members: existingMembers, + channels: existingChannels, }); try { await routeRefreshWorkflow.rebuildRoutesOnly(); diff --git a/src/server/services/shared/apiKeyBatchCore.ts b/src/server/services/shared/apiKeyBatchCore.ts new file mode 100644 index 0000000..9ca236c --- /dev/null +++ b/src/server/services/shared/apiKeyBatchCore.ts @@ -0,0 +1,26 @@ +export function parseBatchApiKeys(input: unknown): string[] { + if (Array.isArray(input)) { + return Array.from(new Set( + input + .map((item) => String(item || '').trim()) + .filter((item) => item.length > 0), + )); + } + + const raw = String(input || '').trim(); + if (!raw) return []; + + return Array.from(new Set( + raw + .split(/[\r\n,,;;\s]+/) + .map((item) => item.trim()) + .filter((item) => item.length > 0), + )); +} + +export function buildBatchApiKeyConnectionName(baseName: string | null | undefined, index: number, total: number): string { + const normalized = String(baseName || '').trim(); + if (!normalized) return ''; + if (total <= 1) return normalized; + return `${normalized} #${index + 1}`; +} diff --git a/src/server/services/tokenRouter.patterns.test.ts b/src/server/services/tokenRouter.patterns.test.ts index f764b24..23ec683 100644 --- a/src/server/services/tokenRouter.patterns.test.ts +++ b/src/server/services/tokenRouter.patterns.test.ts @@ -189,12 +189,11 @@ describe('TokenRouter patterns and model mapping', () => { expect(exposedModels).toContain('claude-opus-4-6'); }); - it('prefers an exact route over a colliding group display-name alias', async () => { - await createRouteWithSingleChannel( - 're:^claude-(opus|sonnet)-4-5$', + it('prefers a group display-name alias over a colliding exact route', async () => { + const source = await createRouteWithSingleChannel( + 'claude-opus-4-5', undefined, { - displayName: 'claude-opus-4-6', sourceModel: 'claude-opus-4-5', }, ); @@ -205,15 +204,18 @@ describe('TokenRouter patterns and model mapping', () => { sourceModel: 'claude-opus-4-6', }, ); + const grouped = await createExplicitGroupRoute('claude-opus-4-6', [source.route.id]); const router = new TokenRouter(); const selected = await router.selectChannel('claude-opus-4-6'); const decision = await router.explainSelection('claude-opus-4-6'); expect(selected).toBeTruthy(); - expect(selected?.channel.id).toBe(exact.channel.id); - expect(selected?.actualModel).toBe('claude-opus-4-6'); - expect(decision.actualModel).toBe('claude-opus-4-6'); + expect(selected?.channel.routeId).toBe(source.route.id); + expect(selected?.channel.id).not.toBe(exact.channel.id); + expect(selected?.actualModel).toBe('claude-opus-4-5'); + expect(decision.routeId).toBe(grouped.id); + expect(decision.actualModel).toBe('claude-opus-4-5'); }); it('falls back to the source exact-route model when explicit-group channels omit sourceModel', async () => { diff --git a/src/server/services/tokenRouter.ts b/src/server/services/tokenRouter.ts index 24301c0..a9bbd27 100644 --- a/src/server/services/tokenRouter.ts +++ b/src/server/services/tokenRouter.ts @@ -3041,12 +3041,12 @@ export class TokenRouter { routes = routes.filter((route) => allowSet.has(route.id)); } - const matchedRoute = routes.find((route) => ( - !isExplicitGroupRoute(route) - && isExactRouteModelPattern(route.modelPattern) - && (route.modelPattern || '').trim() === model - )) - || routes.find((route) => isExplicitGroupRoute(route) && isRouteDisplayNameMatch(model, route.displayName)) + const matchedRoute = routes.find((route) => isExplicitGroupRoute(route) && isRouteDisplayNameMatch(model, route.displayName)) + || routes.find((route) => ( + !isExplicitGroupRoute(route) + && isExactRouteModelPattern(route.modelPattern) + && (route.modelPattern || '').trim() === model + )) || routes.find((route) => !isExplicitGroupRoute(route) && isRouteDisplayNameMatch(model, route.displayName)) || routes.find((route) => !isExplicitGroupRoute(route) && matchesModelPattern(model, route.modelPattern)); diff --git a/src/shared/apiKeyBatch.ts b/src/shared/apiKeyBatch.ts new file mode 100644 index 0000000..12b7a6f --- /dev/null +++ b/src/shared/apiKeyBatch.ts @@ -0,0 +1,4 @@ +export { + buildBatchApiKeyConnectionName, + parseBatchApiKeys, +} from '../server/services/shared/apiKeyBatchCore.js'; diff --git a/src/web/pages/Accounts.tsx b/src/web/pages/Accounts.tsx index 2341226..119dd6d 100644 --- a/src/web/pages/Accounts.tsx +++ b/src/web/pages/Accounts.tsx @@ -29,6 +29,7 @@ import { buildCustomReorderUpdates, sortItemsForDisplay, type SortMode } from '. import { shouldIgnoreRowSelectionClick } from './helpers/rowSelection.js'; import { SITE_DOCS_URL } from '../docsLink.js'; import { getSiteInitializationPreset } from '../../shared/siteInitializationPresets.js'; +import { parseBatchApiKeys } from '../../shared/apiKeyBatch.js'; type ConnectionsSegment = 'session' | 'apikey' | 'tokens'; @@ -177,6 +178,11 @@ export default function Accounts() { () => sites.find((item) => item.id === tokenForm.siteId) || null, [sites, tokenForm.siteId], ); + const parsedApiKeys = useMemo( + () => activeSegment === 'apikey' ? parseBatchApiKeys(tokenForm.accessToken) : [], + [activeSegment, tokenForm.accessToken], + ); + const isBatchApiKeyInput = activeSegment === 'apikey' && parsedApiKeys.length > 1; const siteSelectOptions = useMemo( () => [ { value: '0', label: '选择站点' }, @@ -322,6 +328,10 @@ export default function Accounts() { const handleVerifyToken = async () => { if (!tokenForm.siteId || !tokenForm.accessToken) return; + if (isBatchApiKeyInput) { + toast.info(`检测到 ${parsedApiKeys.length} 个 API Key,批量模式会在添加时逐条校验`); + return; + } const credentialMode = activeSegment === 'apikey' ? 'apikey' : 'session'; setVerifying(true); setVerifyResult(null); @@ -352,7 +362,7 @@ export default function Accounts() { const handleTokenAdd = async () => { if (!tokenForm.siteId || !tokenForm.accessToken) return; - if (!verifyResult?.success && !tokenForm.skipModelFetch) { + if (!isBatchApiKeyInput && !verifyResult?.success && !tokenForm.skipModelFetch) { toast.error('请先验证 Token 成功后再添加账号'); return; } @@ -364,6 +374,7 @@ export default function Accounts() { siteId: tokenForm.siteId, username: tokenForm.username.trim() || undefined, accessToken: tokenForm.accessToken, + accessTokens: isBatchApiKeyInput ? parsedApiKeys : undefined, platformUserId: tokenForm.platformUserId ? parseInt(tokenForm.platformUserId) : undefined, refreshToken: isSub2ApiSelected && tokenForm.refreshToken.trim() ? tokenForm.refreshToken.trim() @@ -374,6 +385,23 @@ export default function Accounts() { credentialMode, skipModelFetch: tokenForm.skipModelFetch, }); + if (result?.batch) { + closeAddPanel(); + const createdCount = Number(result.createdCount) || 0; + const failedCount = Number(result.failedCount) || 0; + if (createdCount > 0) { + toast.success(`批量添加完成:成功 ${createdCount},失败 ${failedCount}`); + } + const failedItems = Array.isArray(result.items) + ? result.items.filter((item: any) => item?.status === 'failed') + : []; + if (failedItems.length > 0) { + const firstMessage = failedItems[0]?.message || '创建失败'; + toast.error(`失败 ${failedItems.length} 条:${firstMessage}`); + } + load(); + return; + } let seededRecommendedModels = false; const recommendedModels = initializationPreset?.recommendedModels || []; const createdAccountId = Number(result?.id) || 0; @@ -951,6 +979,9 @@ export default function Accounts() { || (activeSegment === 'session' && verifyResult.tokenType === 'session') ), ); + const canSubmitApiKeyConnection = activeSegment === 'apikey' + ? (isBatchApiKeyInput || canAddVerifiedConnection || !!tokenForm.skipModelFetch) + : canAddVerifiedConnection; return (
@@ -1421,9 +1452,18 @@ export default function Accounts() {