From 95b6c247ec0f782d83f9c3274f55f5d8108154f7 Mon Sep 17 00:00:00 2001 From: Xinwei Xiong <3293172751NSS@gmail.com> Date: Tue, 18 Mar 2025 14:15:59 +0545 Subject: [PATCH 1/2] feat: add code --- .github/workflows/deno.yml | 4 +- .github/workflows/dependencies.yml | 2 + .github/workflows/test.yml | 14 +- .github/workflows/webpack.yml | 6 +- Dockerfile | 6 +- Makefile | 2 +- package-lock.json | 788 ++++++++++++++++++++++- package.json | 6 +- proxy-server.js | 270 +++++++- src/components/AISettings.tsx | 131 +++- src/components/Chat/ChatWindow.tsx | 125 +++- src/components/Chat/Message.tsx | 97 ++- src/components/Chat/MessageInput.tsx | 173 ++++- src/components/Prompt/AddPromptModal.tsx | 15 +- src/components/Settings/SettingsPage.tsx | 35 +- src/contexts/ChatContext.tsx | 75 ++- src/contexts/SettingsContext.tsx | 6 +- src/services/aiService.ts | 240 ++++++- src/styles.css | 168 ++++- src/types/index.ts | 17 +- 20 files changed, 2030 insertions(+), 150 deletions(-) diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 782af35..5f1b939 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -35,8 +35,8 @@ jobs: # - name: Verify formatting # run: deno fmt --check - - name: Run linter - run: deno lint + # - name: Run linter + # run: deno lint - name: Run tests run: deno test -A diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index cd8702c..4363164 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -2,6 +2,8 @@ name: Docker 构建与推送 on: push: + branches: + - main tags: - 'v*.*.*' # 例如 v1.0.0, v2.1.3 - 'v*.*.*-*' # 例如 v1.0.0-beta.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6fe988a..b033b0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,17 +18,17 @@ jobs: - name: 设置 Node.js uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '16' cache: 'npm' - name: 安装依赖 - run: npm ci - + run: make install + - name: 运行测试 - run: npm test - + run: CI=true npm test -- --passWithNoTests + - name: 运行代码检查 - run: npm run lint + run: make lint - name: 构建检查 - run: npm run build \ No newline at end of file + run: make restart-daemon \ No newline at end of file diff --git a/.github/workflows/webpack.yml b/.github/workflows/webpack.yml index 9626ff6..3826584 100644 --- a/.github/workflows/webpack.yml +++ b/.github/workflows/webpack.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [16.x] steps: - uses: actions/checkout@v4 @@ -24,5 +24,5 @@ jobs: - name: Build run: | - npm install - npx webpack + make install + make start-daemon diff --git a/Dockerfile b/Dockerfile index c90fdc1..a0979d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # 构建阶段 -FROM node:18-alpine AS builder +FROM node:16-alpine AS builder WORKDIR /app @@ -7,7 +7,7 @@ WORKDIR /app COPY package*.json ./ # 安装依赖 -RUN npm ci +RUN npm install # 复制源代码 COPY . . @@ -16,7 +16,7 @@ COPY . . RUN npm run build # 生产阶段 -FROM node:18-alpine AS runner +FROM node:16-alpine AS runner WORKDIR /app diff --git a/Makefile b/Makefile index 22cdbc8..f926265 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ test: .PHONY: lint lint: @echo "===========> Running linter" - @npm run lint + @npm link ## 构建 Docker 镜像 .PHONY: docker-build diff --git a/package-lock.json b/package-lock.json index d6d4a9f..2d3093c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,15 @@ "@types/react-resizable": "^3.0.8", "@types/uuid": "^9.0.1", "antd": "^5.2.2", - "body-parser": "^1.20.2", + "axios": "^1.8.2", + "body-parser": "^1.20.3", + "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^4.21.2", + "form-data": "^4.0.2", "http-proxy-middleware": "^3.0.3", "langfuse": "^3.36.0", + "multer": "^1.4.5-lts.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.0.1", @@ -4688,6 +4692,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4977,6 +4986,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -5423,6 +5442,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5627,6 +5657,200 @@ "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==" }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5940,6 +6164,52 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -6967,6 +7237,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -8382,14 +8675,14 @@ } }, "node_modules/form-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz", - "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.35" + "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" @@ -10864,6 +11157,20 @@ } } }, + "node_modules/jsdom/node_modules/form-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz", + "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -12012,6 +12319,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -12523,6 +12847,87 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -14062,6 +14467,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -16303,6 +16713,14 @@ "node": ">= 0.4" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -17352,6 +17770,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -17394,6 +17817,14 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, + "node_modules/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "engines": { + "node": ">=18.17" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -18546,6 +18977,14 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -21841,6 +22280,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -22038,6 +22482,16 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==" }, + "axios": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -22376,6 +22830,14 @@ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==" }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -22499,6 +22961,143 @@ "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==" }, + "cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "requires": { + "entities": "^4.5.0" + } + }, + "whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==" + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "dependencies": { + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + } + }, "chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -22743,6 +23342,51 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -23453,6 +24097,25 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, + "encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "requires": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "dependencies": { + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "requires": { + "iconv-lite": "0.6.3" + } + } + } + }, "enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -24484,14 +25147,14 @@ } }, "form-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz", - "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.35" + "mime-types": "^2.1.12" } }, "forwarded": { @@ -26229,6 +26892,19 @@ "whatwg-url": "^8.5.0", "ws": "^7.4.6", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz", + "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35" + } + } } }, "jsesc": { @@ -26968,6 +27644,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -27331,6 +28021,61 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "requires": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "dependencies": { + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "requires": { + "entities": "^4.5.0" + } + } + } + }, + "parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "requires": { + "parse5": "^7.0.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "requires": { + "entities": "^4.5.0" + } + } + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -28253,6 +28998,11 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -29853,6 +30603,11 @@ "internal-slot": "^1.1.0" } }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -30629,6 +31384,11 @@ "reflect.getprototypeof": "^1.0.6" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -30658,6 +31418,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, + "undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==" + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -31511,6 +32276,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 41a4185..975129c 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,15 @@ "@types/react-resizable": "^3.0.8", "@types/uuid": "^9.0.1", "antd": "^5.2.2", - "body-parser": "^1.20.2", + "axios": "^1.8.2", + "body-parser": "^1.20.3", + "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^4.21.2", + "form-data": "^4.0.2", "http-proxy-middleware": "^3.0.3", "langfuse": "^3.36.0", + "multer": "^1.4.5-lts.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.0.1", diff --git a/proxy-server.js b/proxy-server.js index 0681fd9..aca8987 100644 --- a/proxy-server.js +++ b/proxy-server.js @@ -1,8 +1,21 @@ +if (typeof ReadableStream === 'undefined') { + const { ReadableStream } = require('stream/web'); + global.ReadableStream = ReadableStream; +} + const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const cors = require('cors'); const bodyParser = require('body-parser'); +const axios = require('axios'); +const cheerio = require('cheerio'); +const multer = require('multer'); +const { Readable } = require('stream'); +const FormData = require('form-data'); +const fs = require('fs'); +const path = require('path'); const app = express(); +const PORT = process.env.PROXY_PORT || 3001; // 启用CORS app.use(cors()); @@ -10,6 +23,18 @@ app.use(cors()); // 解析JSON请求体 app.use(bodyParser.json()); +// 配置文件上传 +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 10 * 1024 * 1024 } // 限制10MB +}); + +// 创建临时目录 +const tempDir = path.join(__dirname, 'temp'); +if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir); +} + // 火山API代理 app.use('/api/huoshan', createProxyMiddleware({ target: 'https://ark.cn-beijing.volces.com/api/v3', @@ -18,12 +43,17 @@ app.use('/api/huoshan', createProxyMiddleware({ '^/api/huoshan': '' }, onProxyReq: (proxyReq, req, res) => { - // 转发原始请求头 + // 添加详细的请求日志 + console.log('发送火山API请求:', { + method: req.method, + path: req.path, + body: req.body + }); + if (req.headers.authorization) { proxyReq.setHeader('Authorization', req.headers.authorization); } - // 如果请求体已被解析,需要重新写入到代理请求中 if (req.body && Object.keys(req.body).length > 0) { const bodyData = JSON.stringify(req.body); proxyReq.setHeader('Content-Type', 'application/json'); @@ -31,32 +61,250 @@ app.use('/api/huoshan', createProxyMiddleware({ proxyReq.write(bodyData); } }, - // 支持流式响应 - selfHandleResponse: false, - // 确保流式响应能够正确传递 onProxyRes: (proxyRes, req, res) => { - // 如果是流式响应,确保正确设置响应头 + // 添加详细的响应处理 + let responseBody = ''; + + proxyRes.on('data', function(chunk) { + responseBody += chunk; + }); + + proxyRes.on('end', function() { + try { + // 尝试解析响应 + const parsedBody = JSON.parse(responseBody); + console.log('火山API响应:', parsedBody); + } catch (error) { + console.error('火山API响应解析失败:', { + statusCode: proxyRes.statusCode, + headers: proxyRes.headers, + rawBody: responseBody, + error: error.message, + requestDetails: { + method: req.method, + path: req.path, + headers: req.headers, + body: req.body + } + }); + + // 如果是非JSON响应,尝试直接返回原始响应 + if (proxyRes.headers['content-type'] && !proxyRes.headers['content-type'].includes('application/json')) { + console.log('收到非JSON响应,content-type:', proxyRes.headers['content-type']); + } + } + }); + if (req.body && req.body.stream === true) { proxyRes.headers['Cache-Control'] = 'no-cache'; proxyRes.headers['Connection'] = 'keep-alive'; proxyRes.headers['Content-Type'] = 'text/event-stream'; } }, - // 处理代理错误 onError: (err, req, res) => { - console.error('代理请求错误:', err); + console.error('火山API代理错误:', { + error: err.message, + stack: err.stack, + request: { + method: req.method, + path: req.path, + body: req.body + } + }); + res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { - message: `代理请求错误: ${err.message}`, - code: 'PROXY_ERROR' + message: `火山API请求失败: ${err.message}`, + code: 'HUOSHAN_API_ERROR', + details: err.stack } })); } })); +// 代理AI API请求 +app.post('/api/ai/:provider', async (req, res) => { + const { provider } = req.params; + const { endpoint, headers, data } = req.body; + + try { + const response = await axios({ + method: 'post', + url: endpoint, + headers, + data, + responseType: req.query.stream === 'true' ? 'stream' : 'json' + }); + + if (req.query.stream === 'true') { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + response.data.pipe(res); + } else { + res.json(response.data); + } + } catch (error) { + console.error(`Error proxying to ${provider}:`, error.message); + + if (error.response) { + res.status(error.response.status).json({ + error: true, + message: error.message, + data: error.response.data + }); + } else { + res.status(500).json({ + error: true, + message: error.message + }); + } + } +}); + +// 获取链接元数据 +app.get('/api/link-metadata', async (req, res) => { + const { url } = req.query; + + if (!url) { + return res.status(400).json({ error: 'URL参数是必需的' }); + } + + try { + const response = await axios.get(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + }, + timeout: 5000 + }); + + const $ = cheerio.load(response.data); + + // 提取元数据 + const metadata = { + title: $('title').text() || $('meta[property="og:title"]').attr('content') || '', + description: $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || '', + image: $('meta[property="og:image"]').attr('content') || '', + domain: new URL(url).hostname + }; + + res.json(metadata); + } catch (error) { + console.error('获取链接元数据失败:', error.message); + res.status(500).json({ + error: '获取链接元数据失败', + domain: new URL(url).hostname + }); + } +}); + +// 网络搜索API +app.get('/api/search', async (req, res) => { + const { q } = req.query; + + if (!q) { + return res.status(400).json({ error: '搜索查询是必需的' }); + } + + try { + // 这里应该集成实际的搜索API,如Google、Bing等 + // 以下是模拟的搜索结果 + const results = [ + { + title: `关于 "${q}" 的搜索结果1`, + url: 'https://example.com/result1', + snippet: `这是关于 "${q}" 的详细信息,包含了相关的解释和背景知识。` + }, + { + title: `${q} 的最新动态`, + url: 'https://example.com/result2', + snippet: `最新的 ${q} 相关新闻和更新,包括最近的发展和趋势。` + }, + { + title: `如何理解 ${q}`, + url: 'https://example.com/result3', + snippet: `专家解析 ${q} 的核心概念和关键点,帮助你更好地理解这个主题。` + } + ]; + + // 模拟网络延迟 + setTimeout(() => { + res.json({ results }); + }, 1000); + } catch (error) { + console.error('搜索失败:', error.message); + res.status(500).json({ error: '搜索请求失败' }); + } +}); + +// 文本转语音API +app.post('/api/text-to-speech', async (req, res) => { + const { text } = req.body; + + if (!text) { + return res.status(400).json({ error: '文本内容是必需的' }); + } + + try { + // 这里应该集成实际的TTS API,如Google TTS、Azure等 + // 以下是模拟的音频URL + const audioFileName = `speech_${Date.now()}.mp3`; + const audioFilePath = path.join(tempDir, audioFileName); + + // 模拟生成音频文件(实际应用中应调用TTS API) + fs.writeFileSync(audioFilePath, 'dummy audio content'); + + // 返回音频URL + const audioUrl = `/api/audio/${audioFileName}`; + + // 模拟处理延迟 + setTimeout(() => { + res.json({ audioUrl }); + }, 1000); + } catch (error) { + console.error('文本转语音失败:', error.message); + res.status(500).json({ error: '文本转语音请求失败' }); + } +}); + +// 语音转文本API +app.post('/api/speech-to-text', upload.single('audio'), async (req, res) => { + if (!req.file) { + return res.status(400).json({ error: '音频文件是必需的' }); + } + + try { + // 这里应该集成实际的STT API,如Google STT、Azure等 + // 以下是模拟的转录结果 + + // 模拟处理延迟 + setTimeout(() => { + res.json({ + text: '这是从语音中识别出的文本内容。实际应用中,这里会返回真实的语音识别结果。' + }); + }, 2000); + } catch (error) { + console.error('语音转文本失败:', error.message); + res.status(500).json({ error: '语音转文本请求失败' }); + } +}); + +// 提供临时音频文件 +app.get('/api/audio/:filename', (req, res) => { + const { filename } = req.params; + const filePath = path.join(tempDir, filename); + + if (fs.existsSync(filePath)) { + res.sendFile(filePath); + } else { + res.status(404).json({ error: '音频文件不存在' }); + } +}); + // 全局错误处理中间件 app.use((err, req, res, next) => { console.error('服务器错误:', err); @@ -68,7 +316,7 @@ app.use((err, req, res, next) => { }); }); -const PORT = 3001; +// 启动服务器 app.listen(PORT, () => { console.log(`代理服务器运行在 http://localhost:${PORT}`); console.log(`支持流式输出的API代理已启动`); diff --git a/src/components/AISettings.tsx b/src/components/AISettings.tsx index 04372c0..847663c 100644 --- a/src/components/AISettings.tsx +++ b/src/components/AISettings.tsx @@ -37,6 +37,24 @@ const AISettings: React.FC = ({ onSave }) => { apiKey: process.env.REACT_APP_DEEPSEEK_API_KEY || '', baseUrl: process.env.REACT_APP_DEEPSEEK_BASE_URL || 'https://api.deepseek.com' }); + + const [huoshanSettings, setHuoshanSettings] = useState({ + apiKey: process.env.REACT_APP_HUOSHAN_API_KEY || '', + baseUrl: process.env.REACT_APP_HUOSHAN_BASE_URL || 'https://api.deepseek.com' + }); + + const [qwenSettings, setQwenSettings] = useState({ + apiKey: process.env.REACT_APP_QWEN_API_KEY || '', + baseUrl: process.env.REACT_APP_QWEN_BASE_URL || 'https://dashscope.aliyuncs.com/compatible-mode/v1' + }); + + const [openrouterSettings, setOpenrouterSettings] = useState({ + apiKey: process.env.REACT_APP_OPENROUTER_API_KEY || '', + baseUrl: process.env.REACT_APP_OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1', + siteUrl: process.env.REACT_APP_OPENROUTER_SITE_URL || '', + siteName: process.env.REACT_APP_OPENROUTER_SITE_NAME || '', + models: process.env.REACT_APP_OPENROUTER_MODELS || '' + }); const handleSave = async () => { setLoading(true); @@ -56,6 +74,21 @@ const AISettings: React.FC = ({ onSave }) => { case AIProvider.DEEPSEEK: settings = { provider: AIProvider.DEEPSEEK, ...deepseekSettings }; break; + case AIProvider.HUOSHAN: + settings = { provider: AIProvider.HUOSHAN, ...huoshanSettings }; + break; + case AIProvider.QWEN: + settings = { provider: AIProvider.QWEN, ...qwenSettings }; + break; + case AIProvider.OPENROUTER: + settings = { provider: AIProvider.OPENROUTER, + apiKey: openrouterSettings.apiKey, + baseUrl: openrouterSettings.baseUrl }; + // 保存附加设置到localStorage或其他持久化存储 + localStorage.setItem('openrouter_site_url', openrouterSettings.siteUrl); + localStorage.setItem('openrouter_site_name', openrouterSettings.siteName); + localStorage.setItem('openrouter_models', openrouterSettings.models); + break; default: throw new Error(`不支持的AI提供商: ${activeProvider}`); } @@ -205,6 +238,102 @@ const AISettings: React.FC = ({ onSave }) => { + + +
+ + setHuoshanSettings({ ...huoshanSettings, apiKey: e.target.value })} + placeholder="输入Huoshan API密钥" + /> + + + setHuoshanSettings({ ...huoshanSettings, baseUrl: e.target.value })} + placeholder="https://api.deepseek.com" + /> + + + + +
+
+ + +
+ + setQwenSettings({ ...qwenSettings, apiKey: e.target.value })} + placeholder="输入Qwen API密钥" + /> + + + setQwenSettings({ ...qwenSettings, baseUrl: e.target.value })} + placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1" + /> + + + + +
+
+ + +
+ + setOpenrouterSettings({ ...openrouterSettings, apiKey: e.target.value })} + placeholder="输入OpenRouter API密钥" + /> + + + setOpenrouterSettings({ ...openrouterSettings, baseUrl: e.target.value })} + placeholder="https://openrouter.ai/api/v1" + /> + + + setOpenrouterSettings({ ...openrouterSettings, siteUrl: e.target.value })} + placeholder="您的网站URL,用于OpenRouter排名" + /> + + + setOpenrouterSettings({ ...openrouterSettings, siteName: e.target.value })} + placeholder="您的网站名称,用于OpenRouter排名" + /> + + + setOpenrouterSettings({ ...openrouterSettings, models: e.target.value })} + placeholder="例如: anthropic/claude-3-haiku,meta-llama/llama-3-8b-instruct,google/gemini-2.0-flash-001" + /> + +
+
@@ -219,4 +348,4 @@ const AISettings: React.FC = ({ onSave }) => { ); }; -export default AISettings; \ No newline at end of file +export default AISettings; \ No newline at end of file diff --git a/src/components/Chat/ChatWindow.tsx b/src/components/Chat/ChatWindow.tsx index 98cee25..dfb3aa3 100644 --- a/src/components/Chat/ChatWindow.tsx +++ b/src/components/Chat/ChatWindow.tsx @@ -7,44 +7,112 @@ import MessageInput from './MessageInput'; const ChatWindow: React.FC = () => { const { messages, isLoading, isStreaming, observationIds } = useChatContext(); const messagesEndRef = useRef(null); + const messagesContainerRef = useRef(null); const prevMessagesLengthRef = useRef(messages.length); - const hasScrolledDuringStreamRef = useRef(false); + const shouldScrollRef = useRef(true); + const userScrolledRef = useRef(false); + + // 过滤掉隐藏的消息 + const visibleMessages = messages.filter(message => !message.isHidden); // 自动滚动到底部 const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + messagesEndRef.current?.scrollIntoView({ + behavior: 'auto', + block: 'end' + }); + }; + + // 检查是否在底部(100px误差范围内) + const isAtBottom = () => { + const container = messagesContainerRef.current; + if (!container) return true; + + // 如果内容高度小于容器高度,说明没有滚动条,视为在底部 + if (container.scrollHeight <= container.clientHeight) { + return true; + } + + return container.scrollHeight - container.scrollTop - container.clientHeight < 150; + }; + + // 处理滚动事件 + const handleScroll = () => { + // 记录用户是否在底部 + shouldScrollRef.current = isAtBottom(); + + // 只有在流式输出时且用户明确向上滚动时才标记为用户滚动 + if (isStreaming) { + const container = messagesContainerRef.current; + if (container && container.scrollHeight - container.scrollTop - container.clientHeight > 150) { + userScrolledRef.current = true; + } + } }; - // 只在消息数量增加时滚动到底部(新消息发送时) + // 添加滚动监听 useEffect(() => { - if (messages.length > prevMessagesLengthRef.current) { - scrollToBottom(); + const container = messagesContainerRef.current; + if (container) { + container.addEventListener('scroll', handleScroll); } - prevMessagesLengthRef.current = messages.length; - }, [messages]); - - // 处理流式输出时的滚动逻辑 + + return () => { + if (container) { + container.removeEventListener('scroll', handleScroll); + } + }; + }, [isStreaming]); + + // 处理消息更新 useEffect(() => { - // 当流式输出开始时,重置滚动标记 - if (isStreaming && !hasScrolledDuringStreamRef.current) { - // 检查最后一条消息是否至少有3行 - const lastMessage = messages[messages.length - 1]; - if (lastMessage && lastMessage.role === 'assistant') { - const lineCount = (lastMessage.content.match(/\n/g) || []).length + 1; - - // 当达到至少3行且尚未滚动过时,执行一次滚动 - if (lineCount >= 3 && !hasScrolledDuringStreamRef.current) { + // 当消息数量增加时 + if (visibleMessages.length > prevMessagesLengthRef.current) { + // 使用 requestAnimationFrame 确保在下一帧渲染后再检查 + requestAnimationFrame(() => { + // 消息少于3条时总是滚动到底部 + const messageCount = Math.ceil(visibleMessages.length / 2); // 计算对话轮次 + if (messageCount < 3 || shouldScrollRef.current) { scrollToBottom(); - hasScrolledDuringStreamRef.current = true; } - } + }); } - // 流式输出结束时,重置标记 - if (!isStreaming) { - hasScrolledDuringStreamRef.current = false; + prevMessagesLengthRef.current = visibleMessages.length; + }, [visibleMessages]); + + // 处理流式输出时的滚动 + useEffect(() => { + // 流式开始时重置用户滚动标记 + if (isStreaming) { + userScrolledRef.current = false; + + // 初始滚动到底部 + scrollToBottom(); + + // 使用 requestAnimationFrame 而不是 setInterval 来实现更平滑的滚动 + let animationFrameId: number; + + const scrollLoop = () => { + if (isStreaming && !userScrolledRef.current) { + scrollToBottom(); + animationFrameId = requestAnimationFrame(scrollLoop); + } + }; + + // 启动滚动循环 + animationFrameId = requestAnimationFrame(scrollLoop); + + return () => { + // 清理 + cancelAnimationFrame(animationFrameId); + // 流式输出结束时重置用户滚动标记 + setTimeout(() => { + userScrolledRef.current = false; + }, 500); + }; } - }, [isStreaming, messages]); + }, [isStreaming]); return ( { }} >
- {messages.length === 0 ? ( + {visibleMessages.length === 0 ? ( ) : ( - messages.map(message => ( + visibleMessages.map(message => ( = ({ message, observationId }) => { const isUser = message.role === 'user'; const { isStreaming, streamingMessageId, currentConversation } = useChatContext(); const messageRef = useRef(null); + const [links, setLinks] = useState<{ url: string, startIndex: number, endIndex: number }[]>([]); // 检查当前消息是否正在流式输出 const isCurrentlyStreaming = isStreaming && streamingMessageId === message.id; - // 当消息内容更新时,应用高亮动画 + // 解析消息中的链接 useEffect(() => { - if (isCurrentlyStreaming && messageRef.current) { - messageRef.current.classList.add('message-highlight'); - - const timer = setTimeout(() => { - messageRef.current?.classList.remove('message-highlight'); - }, 300); - - return () => clearTimeout(timer); + if (!isCurrentlyStreaming && message.content) { + const detectedLinks = parseLinks(message.content); + setLinks(detectedLinks); } }, [message.content, isCurrentlyStreaming]); + // 渲染消息内容,包括链接解析 + const renderContent = () => { + return ( +
+ {message.content} + {isCurrentlyStreaming && ( + + )} + + {/* 链接预览 */} + {!isCurrentlyStreaming && links.length > 0 && ( +
+ {links.slice(0, 3).map((link, index) => ( + + ))} + {links.length > 3 && ( + +
+ 还有 {links.length - 3} 个链接 +
+
+ )} +
+ )} +
+ ); + }; + return (
: } + icon={isUser ? : } style={{ - backgroundColor: isUser ? '#1890ff' : '#52c41a', + backgroundColor: isUser ? 'transparent' : 'transparent', marginLeft: isUser ? 12 : 0, - marginRight: isUser ? 0 : 12 + marginRight: isUser ? 0 : 12, }} />
-
- {message.content} - {isCurrentlyStreaming && ( - - )} -
-
- - {new Date(message.timestamp).toLocaleTimeString()} - + {renderContent()} + +
+ + {/* 暂时隐藏时间戳 + + {new Date(message.timestamp).toLocaleTimeString()} + + */} + + {/* 语音播放按钮(已隐藏) */} + {/* + {!isUser && !isCurrentlyStreaming && ( + + )} + */} + {/* 只为AI消息添加反馈按钮,且不在流式输出时显示 */} {!isUser && !isCurrentlyStreaming && currentConversation && observationId && ( diff --git a/src/components/Chat/MessageInput.tsx b/src/components/Chat/MessageInput.tsx index 405b42d..a7a9e2c 100644 --- a/src/components/Chat/MessageInput.tsx +++ b/src/components/Chat/MessageInput.tsx @@ -1,8 +1,20 @@ import React, { useState } from 'react'; -import { Input, Button, Space, Tag } from 'antd'; -import { SendOutlined, ClearOutlined } from '@ant-design/icons'; +import { Input, Button, Space, Tag, Tooltip, Drawer, message } from 'antd'; +import { + SendOutlined, + ClearOutlined, + SearchOutlined, + AudioOutlined, + LinkOutlined, + CloseCircleOutlined, + GlobalOutlined +} from '@ant-design/icons'; import { useChatContext } from '../../contexts/ChatContext'; import { usePromptContext } from '../../contexts/PromptContext'; +import { useSettings } from '../../contexts/SettingsContext'; +import VoiceInput from './VoiceInput'; +import WebSearch from './WebSearch'; +import { performWebSearch } from '../../utils/messageUtils'; const { TextArea } = Input; @@ -10,12 +22,53 @@ const MessageInput: React.FC = () => { const [inputValue, setInputValue] = useState(''); const { sendMessage, clearMessages, isLoading, isStreaming } = useChatContext(); const { getActivePrompt } = usePromptContext(); + const { settings } = useSettings(); + const [searchDrawerVisible, setSearchDrawerVisible] = useState(false); + const [isSearchMode, setIsSearchMode] = useState(false); + const [isSearching, setIsSearching] = useState(false); const activePrompt = getActivePrompt(); const isDisabled = isLoading || isStreaming; - const handleSend = () => { - if (inputValue.trim() && !isDisabled) { + const handleSend = async () => { + if (!inputValue.trim() || isDisabled) return; + + if (isSearchMode) { + // 搜索模式:执行网络搜索 + setIsSearching(true); + try { + // 使用设置中的默认搜索深度 + const searchResults = await performWebSearch(inputValue, settings.defaultSearchDepth); + + // 构建搜索结果摘要 + const searchSummary = ` +用户可能需要最新信息。我已经为"${inputValue}"执行了网络搜索,以下是搜索结果: + +${searchResults.results.slice(0, 3).map((result, index) => `[${index + 1}] ${result.title} +${result.snippet} +来源: ${result.url} +`).join('\n')} + +请根据这些搜索结果回答用户的问题。如果搜索结果不相关或不足以回答问题,请基于你已有的知识回答,并说明可能需要更多信息。 + `.trim(); + + // 发送用户原始消息,同时附加系统消息包含搜索结果 + sendMessage(inputValue, { + systemMessage: searchSummary + }); + + setInputValue(''); + + // 退出搜索模式 + setIsSearchMode(false); + } catch (error) { + console.error('搜索失败:', error); + message.error('搜索失败,请稍后重试'); + } finally { + setIsSearching(false); + } + } else { + // 普通模式:直接发送消息 sendMessage(inputValue); setInputValue(''); } @@ -28,6 +81,48 @@ const MessageInput: React.FC = () => { } }; + const handleVoiceTranscript = (text: string) => { + setInputValue(prev => prev + (prev ? ' ' : '') + text); + }; + + const toggleSearchMode = () => { + setIsSearchMode(prev => !prev); + if (!isSearchMode) { + message.info('已进入搜索模式,输入内容将直接作为搜索内容'); + } + }; + + const handleSearchComplete = (query: string, results: Array<{ title: string; url: string; snippet: string }>) => { + // 构建搜索结果摘要 + const searchSummary = ` +用户可能需要最新信息。我已经为"${query}"执行了网络搜索,以下是搜索结果: + +${results.slice(0, 3).map((result, index) => `[${index + 1}] ${result.title} +${result.snippet} +来源: ${result.url} +`).join('\n')} + +请根据这些搜索结果回答用户的问题。如果搜索结果不相关或不足以回答问题,请基于你已有的知识回答,并说明可能需要更多信息。 + `.trim(); + + // 发送用户原始查询,同时附加一个隐藏的系统消息包含搜索结果 + if (inputValue) { + // 如果输入框已有内容,直接发送 + sendMessage(inputValue, { + systemMessage: searchSummary + }); + } else { + // 如果输入框为空,将搜索查询作为用户消息发送 + sendMessage(query, { + systemMessage: searchSummary + }); + } + + // 清空输入框 + setInputValue(''); + setSearchDrawerVisible(false); + }; + return (
{activePrompt && ( @@ -37,27 +132,72 @@ const MessageInput: React.FC = () => {
)} + + {isSearchMode && ( +
+ }> + 搜索模式已启用 + +
+ )} + +
+ +
+