From 4393bf68cd21fab03549df458f345f10aadb0386 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Wed, 5 Mar 2025 17:45:09 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=20vue=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 17 + frontend/README.md | 5 + frontend/index.html | 14 + frontend/package-lock.json | 1256 +++++++++++++++++ frontend/package.json | 28 + frontend/public/vite.svg | 1 + frontend/src/App.vue | 41 + frontend/src/assets/vue.svg | 1 + .../src/components/AnnouncementBanner.vue | 128 ++ frontend/src/components/ApiConfigPanel.vue | 339 +++++ frontend/src/components/MarketTimeDisplay.vue | 133 ++ frontend/src/components/StockAnalysisApp.vue | 489 +++++++ frontend/src/components/StockCard.vue | 207 +++ frontend/src/components/StockSearch.vue | 220 +++ frontend/src/main.ts | 6 + frontend/src/services/api.ts | 59 + frontend/src/style.css | 79 ++ frontend/src/types/index.ts | 80 ++ frontend/src/utils/index.ts | 201 +++ frontend/src/vite-env.d.ts | 1 + frontend/tsconfig.app.json | 18 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 25 + frontend/vite.config.ts | 43 + frontend/yarn.lock | 731 ++++++++++ stock_analyzer.py | 2 +- web_server.py | 48 +- 27 files changed, 4164 insertions(+), 15 deletions(-) create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/assets/vue.svg create mode 100644 frontend/src/components/AnnouncementBanner.vue create mode 100644 frontend/src/components/ApiConfigPanel.vue create mode 100644 frontend/src/components/MarketTimeDisplay.vue create mode 100644 frontend/src/components/StockAnalysisApp.vue create mode 100644 frontend/src/components/StockCard.vue create mode 100644 frontend/src/components/StockSearch.vue create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/style.css create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/utils/index.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 frontend/yarn.lock diff --git a/.gitignore b/.gitignore index db3d017..9759f13 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,23 @@ build_upload.log *.spec *.zip +# frontend +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +dist/ +dist-ssr +*.local +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..96ad038 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1256 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@types/node": "^22.13.9", + "@vicons/ionicons5": "^0.13.0", + "@vueuse/core": "^12.7.0", + "axios": "^1.8.1", + "marked": "^15.0.7", + "naive-ui": "^2.41.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@types/marked": "^5.0.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.7.2", + "vite": "^6.2.0", + "vue-tsc": "^2.2.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@css-render/plugin-bem": { + "version": "0.15.14", + "resolved": "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz", + "integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==", + "license": "MIT", + "peerDependencies": { + "css-render": "~0.15.14" + } + }, + "node_modules/@css-render/vue3-ssr": { + "version": "0.15.14", + "resolved": "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz", + "integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "license": "Apache-2.0" + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/marked": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/@types/marked/-/marked-5.0.2.tgz", + "integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.9", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vicons/ionicons5": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz", + "integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", + "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.11", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.11.tgz", + "integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.11" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.11", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.11.tgz", + "integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.11", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.11.tgz", + "integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.11", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.8", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.8.tgz", + "integrity": "sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~2.4.11", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "12.7.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.7.0.tgz", + "integrity": "sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.7.0", + "@vueuse/shared": "12.7.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.7.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.7.0.tgz", + "integrity": "sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.7.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.7.0.tgz", + "integrity": "sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/alien-signals": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.4.tgz", + "integrity": "sha512-DJqqQD3XcsaQcQ1s+iE2jDUZmmQpXwHiR6fCAim/w87luaW+vmLY8fMlrdkmRwzaFXhkxf3rqPCR59tKVv1MDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/css-render": { + "version": "0.15.14", + "resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz", + "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "~0.8.0", + "csstype": "~3.0.5" + } + }, + "node_modules/css-render/node_modules/csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/marked": { + "version": "15.0.7", + "resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.7.tgz", + "integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/naive-ui": { + "version": "2.41.0", + "resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.41.0.tgz", + "integrity": "sha512-KnmLg+xPLwXV8QVR7ZZ69eCjvel7R5vru8+eFe4VoAJHEgqAJgVph6Zno9K2IVQRpSF3GBGea3tjavslOR4FAA==", + "license": "MIT", + "dependencies": { + "@css-render/plugin-bem": "^0.15.14", + "@css-render/vue3-ssr": "^0.15.14", + "@types/katex": "^0.16.2", + "@types/lodash": "^4.14.198", + "@types/lodash-es": "^4.17.9", + "async-validator": "^4.2.5", + "css-render": "^0.15.14", + "csstype": "^3.1.3", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", + "evtd": "^0.2.4", + "highlight.js": "^11.8.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "seemly": "^0.3.8", + "treemate": "^0.3.11", + "vdirs": "^0.1.8", + "vooks": "^0.2.12", + "vueuc": "^0.4.63" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.34.9", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.34.9.tgz", + "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.9", + "@rollup/rollup-android-arm64": "4.34.9", + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-freebsd-arm64": "4.34.9", + "@rollup/rollup-freebsd-x64": "4.34.9", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", + "@rollup/rollup-linux-arm-musleabihf": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", + "@rollup/rollup-linux-riscv64-gnu": "4.34.9", + "@rollup/rollup-linux-s390x-gnu": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-ia32-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9", + "fsevents": "~2.3.2" + } + }, + "node_modules/seemly": { + "version": "0.3.10", + "resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz", + "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/treemate": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz", + "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/vdirs": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz", + "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==", + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/vite": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vooks": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz", + "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==", + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "2.2.8", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.8.tgz", + "integrity": "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.2.8" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vueuc": { + "version": "0.4.64", + "resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.64.tgz", + "integrity": "sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA==", + "license": "MIT", + "dependencies": { + "@css-render/vue3-ssr": "^0.15.10", + "@juggle/resize-observer": "^3.3.1", + "css-render": "^0.15.10", + "evtd": "^0.2.4", + "seemly": "^0.3.6", + "vdirs": "^0.1.4", + "vooks": "^0.2.4" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..cbe1418 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@types/node": "^22.13.9", + "@vicons/ionicons5": "^0.13.0", + "@vueuse/core": "^12.7.0", + "axios": "^1.8.1", + "marked": "^15.0.7", + "naive-ui": "^2.41.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@types/marked": "^5.0.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.7.2", + "vite": "^6.2.0", + "vue-tsc": "^2.2.4" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..84095a0 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/AnnouncementBanner.vue b/frontend/src/components/AnnouncementBanner.vue new file mode 100644 index 0000000..ab872cb --- /dev/null +++ b/frontend/src/components/AnnouncementBanner.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/frontend/src/components/ApiConfigPanel.vue b/frontend/src/components/ApiConfigPanel.vue new file mode 100644 index 0000000..f840b70 --- /dev/null +++ b/frontend/src/components/ApiConfigPanel.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/frontend/src/components/MarketTimeDisplay.vue b/frontend/src/components/MarketTimeDisplay.vue new file mode 100644 index 0000000..2b9e48a --- /dev/null +++ b/frontend/src/components/MarketTimeDisplay.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/frontend/src/components/StockAnalysisApp.vue b/frontend/src/components/StockAnalysisApp.vue new file mode 100644 index 0000000..a6ac6bb --- /dev/null +++ b/frontend/src/components/StockAnalysisApp.vue @@ -0,0 +1,489 @@ + + + + + diff --git a/frontend/src/components/StockCard.vue b/frontend/src/components/StockCard.vue new file mode 100644 index 0000000..211a512 --- /dev/null +++ b/frontend/src/components/StockCard.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/frontend/src/components/StockSearch.vue b/frontend/src/components/StockSearch.vue new file mode 100644 index 0000000..ed7eed7 --- /dev/null +++ b/frontend/src/components/StockSearch.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..e03fc18 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +const app = createApp(App) +app.mount('#app') diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..6c38f9a --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,59 @@ +import axios from 'axios'; +import type { AnalyzeRequest, TestApiRequest, TestApiResponse, SearchResult } from '@/types'; + +// 在开发环境中前缀为空,因为已经在vite.config.ts中配置了代理 +const API_PREFIX = ''; + +export const apiService = { + // 分析股票 + analyzeStocks: async (request: AnalyzeRequest) => { + return axios.post(`${API_PREFIX}/analyze`, request, { + responseType: 'stream' + }); + }, + + // 测试API连接 + testApiConnection: async (request: TestApiRequest): Promise => { + try { + const response = await axios.post(`${API_PREFIX}/test_api_connection`, request); + return response.data; + } catch (error: any) { + if (error.response) { + return error.response.data; + } + return { + success: false, + message: error.message || '连接失败' + }; + } + }, + + // 搜索美股 + searchUsStocks: async (keyword: string): Promise => { + try { + const response = await axios.get(`${API_PREFIX}/search_us_stocks`, { + params: { keyword } + }); + return response.data.results || []; + } catch (error) { + console.error('搜索美股时出错:', error); + return []; + } + }, + + // 获取配置 + getConfig: async () => { + try { + const response = await axios.get(`${API_PREFIX}/config`); + return response.data; + } catch (error) { + console.error('获取配置时出错:', error); + return { + announcement: '', + default_api_url: '', + default_api_model: 'gpt-3.5-turbo', + default_api_timeout: '60' + }; + } + } +}; diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..f691315 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,79 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..10d1e83 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,80 @@ +// API接口相关类型 +export interface ApiConfig { + apiUrl: string; + apiKey: string; + apiModel: string; + apiTimeout: string; + saveApiConfig: boolean; +} + +export interface StockInfo { + code: string; + name: string; + marketType: string; + price?: number; + changePercent?: number; + marketValue?: number; + analysis?: string; + analysisStatus: 'waiting' | 'analyzing' | 'completed' | 'error'; + error?: string; +} + +export interface SearchResult { + symbol: string; + name: string; + market: string; + marketValue?: number; +} + +export interface MarketStatus { + isOpen: boolean; + nextTime: string; +} + +export interface MarketTimeInfo { + currentTime: string; + cnMarket: MarketStatus; + hkMarket: MarketStatus; + usMarket: MarketStatus; +} + +// 分析请求和响应 +export interface AnalyzeRequest { + stock_codes: string[]; + market_type: string; + api_url?: string; + api_key?: string; + api_model?: string; + api_timeout?: string; +} + +export interface TestApiRequest { + api_url: string; + api_key: string; + api_model: string; + api_timeout: string; +} + +export interface TestApiResponse { + success: boolean; + message: string; + status_code?: number; +} + +// 流式响应类型 +export interface StreamInitMessage { + stream_type: 'single' | 'batch'; + stock_code?: string; + stock_codes?: string[]; +} + +export interface StreamAnalysisUpdate { + stock_code: string; + analysis?: string; + status: 'analyzing' | 'completed' | 'error'; + error?: string; + name?: string; + price?: number; + change_percent?: number; + market_value?: number; +} diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..9877d89 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,201 @@ +import type { MarketTimeInfo } from '@/types'; +import { marked } from 'marked'; + +// 防抖函数 +export function debounce any>( + func: T, + wait: number +): (...args: Parameters) => void { + let timeout: number | null = null; + + return function(...args: Parameters): void { + const later = () => { + timeout = null; + func(...args); + }; + + if (timeout !== null) { + clearTimeout(timeout); + } + timeout = window.setTimeout(later, wait); + }; +} + +// 格式化市值 +export function formatMarketValue(value: number): string { + if (!value) return '未知'; + + if (value >= 1000000000000) { + return (value / 1000000000000).toFixed(2) + '万亿'; + } else if (value >= 100000000) { + return (value / 100000000).toFixed(2) + '亿'; + } else if (value >= 10000) { + return (value / 10000).toFixed(2) + '万'; + } else { + return value.toFixed(2); + } +} + +// 解析Markdown +export function parseMarkdown(text: string): string { + try { + const result = marked(text); + if (typeof result === 'string') { + return result; + } + return ''; + } catch (e) { + console.error('解析Markdown出错:', e); + return text; + } +} + +// 更新市场时间信息 +export function updateMarketTimeInfo(): MarketTimeInfo { + const now = new Date(); + + // 当前时间 + const currentTime = now.toLocaleTimeString('zh-CN', { hour12: false }); + + // 中国时间 + const cnOptions = { timeZone: 'Asia/Shanghai', hour12: false } as Intl.DateTimeFormatOptions; + const cnTime = now.toLocaleString('zh-CN', cnOptions); + const cnHour = new Date(cnTime).getHours(); + const cnMinute = new Date(cnTime).getMinutes(); + + // A股市场状态 + const cnMarketOpen = (cnHour === 9 && cnMinute >= 30) || (cnHour === 10) || + (cnHour === 11 && cnMinute <= 30) || + (cnHour >= 13 && cnHour < 15); + + const cnNextTime = getNextTimeText(cnMarketOpen, cnHour, cnMinute, 9, 30, 15, 0); + + // 港股市场状态(与A股相同时区) + const hkMarketOpen = (cnHour === 9 && cnMinute >= 30) || + (cnHour === 10) || (cnHour === 11) || + (cnHour >= 13 && cnHour < 16); + + const hkNextTime = getNextTimeText(hkMarketOpen, cnHour, cnMinute, 9, 30, 16, 0); + + // 获取美国东部时间 + const usOptions = { timeZone: 'America/New_York', hour12: false } as Intl.DateTimeFormatOptions; + const usTime = now.toLocaleString('zh-CN', usOptions); + const usHour = new Date(usTime).getHours(); + const usMinute = new Date(usTime).getMinutes(); + + // 美股市场状态 + const usMarketOpen = (usHour >= 9 && usHour < 16) || + (usHour === 16 && usMinute === 0); + + const usNextTime = getNextTimeText(usMarketOpen, usHour, usMinute, 9, 30, 16, 0); + + return { + currentTime, + cnMarket: { isOpen: cnMarketOpen, nextTime: cnNextTime }, + hkMarket: { isOpen: hkMarketOpen, nextTime: hkNextTime }, + usMarket: { isOpen: usMarketOpen, nextTime: usNextTime } + }; +} + +// 辅助函数:获取距离下一次开/闭市的时间文本 +function getNextTimeText( + isOpen: boolean, + currentHour: number, + currentMinute: number, + openHour: number, + openMinute: number, + closeHour: number, + closeMinute: number +): string { + if (isOpen) { + // 计算距离收盘时间 + let timeToCloseMinutes = (closeHour - currentHour) * 60 + (closeMinute - currentMinute); + + if (timeToCloseMinutes <= 0) { + return '即将收盘'; + } + + const hours = Math.floor(timeToCloseMinutes / 60); + const minutes = timeToCloseMinutes % 60; + + return `距离收盘还有 ${hours}小时${minutes}分钟`; + } else { + // 计算距离开盘时间 + let nextOpenHour = openHour; + let nextOpenMinute = openMinute; + let isNextDay = false; + + if (currentHour >= closeHour) { + // 已经过了今天的收盘时间,下一个开盘是明天 + isNextDay = true; + } else if (currentHour < openHour || (currentHour === openHour && currentMinute < openMinute)) { + // 还没到今天的开盘时间 + isNextDay = false; + } else { + // 当前处于盘中休息时间,下一个开盘时间是当天下午 + nextOpenHour = 13; + nextOpenMinute = 0; + } + + let timeToOpenMinutes; + + if (isNextDay) { + timeToOpenMinutes = (24 - currentHour + nextOpenHour) * 60 + (nextOpenMinute - currentMinute); + } else { + timeToOpenMinutes = (nextOpenHour - currentHour) * 60 + (nextOpenMinute - currentMinute); + } + + if (timeToOpenMinutes <= 0) { + return '即将开盘'; + } + + const hours = Math.floor(timeToOpenMinutes / 60); + const minutes = timeToOpenMinutes % 60; + + return `距离开盘还有 ${hours}小时${minutes}分钟`; + } +} + +// 保存API配置到localStorage +export function saveApiConfigToLocalStorage(config: Partial>): void { + if (window.localStorage) { + localStorage.setItem('apiConfig', JSON.stringify(config)); + } +} + +// 从localStorage加载API配置 +export function loadApiConfig(): Partial<{ + apiUrl: string, + apiKey: string, + apiModel: string, + apiTimeout: string, + saveApiConfig: boolean +}> { + if (window.localStorage) { + const saved = localStorage.getItem('apiConfig'); + if (saved) { + try { + return JSON.parse(saved); + } catch (e) { + console.error('解析保存的API配置出错:', e); + } + } + } + return { + apiUrl: '', + apiKey: '', + apiModel: '', + apiTimeout: '', + saveApiConfig: false + }; +} + +// 清除API配置 +export function clearApiConfig(): void { + if (window.localStorage) { + localStorage.removeItem('apiConfig'); + } +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..c86af31 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,18 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "incremental": true, // 启用增量编译 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..69fc25c --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "incremental": true, // 启用增量编译 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..181d624 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath } from 'url' +import { dirname, resolve } from 'path' + +// 获取当前文件的目录路径(在ESM中替代__dirname) +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + server: { + proxy: { + '/api': { + target: 'http://127.0.0.1:8888', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + }, + '/analyze': { + target: 'http://127.0.0.1:8888', + changeOrigin: true, + }, + '/test_api_connection': { + target: 'http://127.0.0.1:8888', + changeOrigin: true, + }, + '/search_us_stocks': { + target: 'http://127.0.0.1:8888', + changeOrigin: true, + }, + '/config': { + target: 'http://127.0.0.1:8888', + changeOrigin: true, + }, + }, + }, +}) diff --git a/frontend/yarn.lock b/frontend/yarn.lock new file mode 100644 index 0000000..81a0230 --- /dev/null +++ b/frontend/yarn.lock @@ -0,0 +1,731 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/parser@^7.25.3": + version "7.26.9" + resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.9.tgz" + integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A== + dependencies: + "@babel/types" "^7.26.9" + +"@babel/types@^7.26.9": + version "7.26.9" + resolved "https://registry.npmmirror.com/@babel/types/-/types-7.26.9.tgz" + integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@css-render/plugin-bem@^0.15.14": + version "0.15.14" + resolved "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz" + integrity sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg== + +"@css-render/vue3-ssr@^0.15.10", "@css-render/vue3-ssr@^0.15.14": + version "0.15.14" + resolved "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz" + integrity sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g== + +"@emotion/hash@~0.8.0": + version "0.8.0" + resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@esbuild/win32-x64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz" + integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== + +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@juggle/resize-observer@^3.3.1": + version "3.4.0" + resolved "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz" + integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== + +"@rollup/rollup-win32-x64-msvc@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz" + integrity sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw== + +"@types/estree@1.0.6": + version "1.0.6" + resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/katex@^0.16.2": + version "0.16.7" + resolved "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz" + integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ== + +"@types/lodash-es@^4.17.9": + version "4.17.12" + resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz" + integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*", "@types/lodash@^4.14.198": + version "4.17.16" + resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz" + integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== + +"@types/marked@^5.0.2": + version "5.0.2" + resolved "https://registry.npmmirror.com/@types/marked/-/marked-5.0.2.tgz" + integrity sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg== + +"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.13.9": + version "22.13.9" + resolved "https://registry.npmmirror.com/@types/node/-/node-22.13.9.tgz" + integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw== + dependencies: + undici-types "~6.20.0" + +"@types/web-bluetooth@^0.0.20": + version "0.0.20" + resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz" + integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== + +"@vicons/ionicons5@^0.13.0": + version "0.13.0" + resolved "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz" + integrity sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ== + +"@vitejs/plugin-vue@^5.2.1": + version "5.2.1" + resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz" + integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ== + +"@volar/language-core@~2.4.11", "@volar/language-core@2.4.11": + version "2.4.11" + resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.11.tgz" + integrity sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg== + dependencies: + "@volar/source-map" "2.4.11" + +"@volar/source-map@2.4.11": + version "2.4.11" + resolved "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.11.tgz" + integrity sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ== + +"@volar/typescript@~2.4.11": + version "2.4.11" + resolved "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.11.tgz" + integrity sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw== + dependencies: + "@volar/language-core" "2.4.11" + path-browserify "^1.0.1" + vscode-uri "^3.0.8" + +"@vue/compiler-core@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz" + integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/shared" "3.5.13" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@^3.5.0", "@vue/compiler-dom@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz" + integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA== + dependencies: + "@vue/compiler-core" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/compiler-sfc@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz" + integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/compiler-core" "3.5.13" + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + estree-walker "^2.0.2" + magic-string "^0.30.11" + postcss "^8.4.48" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz" + integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/compiler-vue2@^2.7.16": + version "2.7.16" + resolved "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz" + integrity sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A== + dependencies: + de-indent "^1.0.2" + he "^1.2.0" + +"@vue/language-core@2.2.8": + version "2.2.8" + resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.8.tgz" + integrity sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ== + dependencies: + "@volar/language-core" "~2.4.11" + "@vue/compiler-dom" "^3.5.0" + "@vue/compiler-vue2" "^2.7.16" + "@vue/shared" "^3.5.0" + alien-signals "^1.0.3" + minimatch "^9.0.3" + muggle-string "^0.4.1" + path-browserify "^1.0.1" + +"@vue/reactivity@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz" + integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg== + dependencies: + "@vue/shared" "3.5.13" + +"@vue/runtime-core@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz" + integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw== + dependencies: + "@vue/reactivity" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/runtime-dom@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz" + integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog== + dependencies: + "@vue/reactivity" "3.5.13" + "@vue/runtime-core" "3.5.13" + "@vue/shared" "3.5.13" + csstype "^3.1.3" + +"@vue/server-renderer@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz" + integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA== + dependencies: + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/shared@^3.5.0", "@vue/shared@3.5.13": + version "3.5.13" + resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz" + integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ== + +"@vue/tsconfig@^0.7.0": + version "0.7.0" + resolved "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.7.0.tgz" + integrity sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg== + +"@vueuse/core@^12.7.0": + version "12.7.0" + resolved "https://registry.npmmirror.com/@vueuse/core/-/core-12.7.0.tgz" + integrity sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA== + dependencies: + "@types/web-bluetooth" "^0.0.20" + "@vueuse/metadata" "12.7.0" + "@vueuse/shared" "12.7.0" + vue "^3.5.13" + +"@vueuse/metadata@12.7.0": + version "12.7.0" + resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.7.0.tgz" + integrity sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g== + +"@vueuse/shared@12.7.0": + version "12.7.0" + resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.7.0.tgz" + integrity sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw== + dependencies: + vue "^3.5.13" + +alien-signals@^1.0.3: + version "1.0.4" + resolved "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.4.tgz" + integrity sha512-DJqqQD3XcsaQcQ1s+iE2jDUZmmQpXwHiR6fCAim/w87luaW+vmLY8fMlrdkmRwzaFXhkxf3rqPCR59tKVv1MDw== + +async-validator@^4.2.5: + version "4.2.5" + resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz" + integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.8.1: + version "1.8.1" + resolved "https://registry.npmmirror.com/axios/-/axios-1.8.1.tgz" + integrity sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +css-render@^0.15.10, css-render@^0.15.14, css-render@~0.15.14: + version "0.15.14" + resolved "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz" + integrity sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg== + dependencies: + "@emotion/hash" "~0.8.0" + csstype "~3.0.5" + +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +csstype@~3.0.5: + version "3.0.11" + resolved "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz" + integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== + +date-fns-tz@^3.1.3: + version "3.2.0" + resolved "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz" + integrity sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ== + +"date-fns@^3.0.0 || ^4.0.0", date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz" + integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +esbuild@^0.25.0: + version "0.25.0" + resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.0.tgz" + integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.0" + "@esbuild/android-arm" "0.25.0" + "@esbuild/android-arm64" "0.25.0" + "@esbuild/android-x64" "0.25.0" + "@esbuild/darwin-arm64" "0.25.0" + "@esbuild/darwin-x64" "0.25.0" + "@esbuild/freebsd-arm64" "0.25.0" + "@esbuild/freebsd-x64" "0.25.0" + "@esbuild/linux-arm" "0.25.0" + "@esbuild/linux-arm64" "0.25.0" + "@esbuild/linux-ia32" "0.25.0" + "@esbuild/linux-loong64" "0.25.0" + "@esbuild/linux-mips64el" "0.25.0" + "@esbuild/linux-ppc64" "0.25.0" + "@esbuild/linux-riscv64" "0.25.0" + "@esbuild/linux-s390x" "0.25.0" + "@esbuild/linux-x64" "0.25.0" + "@esbuild/netbsd-arm64" "0.25.0" + "@esbuild/netbsd-x64" "0.25.0" + "@esbuild/openbsd-arm64" "0.25.0" + "@esbuild/openbsd-x64" "0.25.0" + "@esbuild/sunos-x64" "0.25.0" + "@esbuild/win32-arm64" "0.25.0" + "@esbuild/win32-ia32" "0.25.0" + "@esbuild/win32-x64" "0.25.0" + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +evtd@^0.2.2, evtd@^0.2.4: + version "0.2.4" + resolved "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz" + integrity sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw== + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +form-data@^4.0.0: + version "4.0.2" + resolved "https://registry.npmmirror.com/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.12" + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +highlight.js@^11.8.0: + version "11.11.1" + resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz" + integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w== + +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +magic-string@^0.30.11: + version "0.30.17" + resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz" + integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + +marked@^15.0.7: + version "15.0.7" + resolved "https://registry.npmmirror.com/marked/-/marked-15.0.7.tgz" + integrity sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +muggle-string@^0.4.1: + version "0.4.1" + resolved "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz" + integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ== + +naive-ui@^2.41.0: + version "2.41.0" + resolved "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.41.0.tgz" + integrity sha512-KnmLg+xPLwXV8QVR7ZZ69eCjvel7R5vru8+eFe4VoAJHEgqAJgVph6Zno9K2IVQRpSF3GBGea3tjavslOR4FAA== + dependencies: + "@css-render/plugin-bem" "^0.15.14" + "@css-render/vue3-ssr" "^0.15.14" + "@types/katex" "^0.16.2" + "@types/lodash" "^4.14.198" + "@types/lodash-es" "^4.17.9" + async-validator "^4.2.5" + css-render "^0.15.14" + csstype "^3.1.3" + date-fns "^3.6.0" + date-fns-tz "^3.1.3" + evtd "^0.2.4" + highlight.js "^11.8.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + seemly "^0.3.8" + treemate "^0.3.11" + vdirs "^0.1.8" + vooks "^0.2.12" + vueuc "^0.4.63" + +nanoid@^3.3.8: + version "3.3.8" + resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.8.tgz" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +postcss@^8.4.48, postcss@^8.5.3: + version "8.5.3" + resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +rollup@^4.30.1: + version "4.34.9" + resolved "https://registry.npmmirror.com/rollup/-/rollup-4.34.9.tgz" + integrity sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.34.9" + "@rollup/rollup-android-arm64" "4.34.9" + "@rollup/rollup-darwin-arm64" "4.34.9" + "@rollup/rollup-darwin-x64" "4.34.9" + "@rollup/rollup-freebsd-arm64" "4.34.9" + "@rollup/rollup-freebsd-x64" "4.34.9" + "@rollup/rollup-linux-arm-gnueabihf" "4.34.9" + "@rollup/rollup-linux-arm-musleabihf" "4.34.9" + "@rollup/rollup-linux-arm64-gnu" "4.34.9" + "@rollup/rollup-linux-arm64-musl" "4.34.9" + "@rollup/rollup-linux-loongarch64-gnu" "4.34.9" + "@rollup/rollup-linux-powerpc64le-gnu" "4.34.9" + "@rollup/rollup-linux-riscv64-gnu" "4.34.9" + "@rollup/rollup-linux-s390x-gnu" "4.34.9" + "@rollup/rollup-linux-x64-gnu" "4.34.9" + "@rollup/rollup-linux-x64-musl" "4.34.9" + "@rollup/rollup-win32-arm64-msvc" "4.34.9" + "@rollup/rollup-win32-ia32-msvc" "4.34.9" + "@rollup/rollup-win32-x64-msvc" "4.34.9" + fsevents "~2.3.2" + +seemly@^0.3.6, seemly@^0.3.8: + version "0.3.10" + resolved "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz" + integrity sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q== + +source-map-js@^1.2.0, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +treemate@^0.3.11: + version "0.3.11" + resolved "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz" + integrity sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg== + +typescript@*, typescript@>=5.0.0, typescript@~5.7.2, typescript@5.x: + version "5.7.3" + resolved "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz" + integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.20.0.tgz" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +vdirs@^0.1.4, vdirs@^0.1.8: + version "0.1.8" + resolved "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz" + integrity sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw== + dependencies: + evtd "^0.2.2" + +"vite@^5.0.0 || ^6.0.0", vite@^6.2.0: + version "6.2.0" + resolved "https://registry.npmmirror.com/vite/-/vite-6.2.0.tgz" + integrity sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ== + dependencies: + esbuild "^0.25.0" + postcss "^8.5.3" + rollup "^4.30.1" + optionalDependencies: + fsevents "~2.3.3" + +vooks@^0.2.12, vooks@^0.2.4: + version "0.2.12" + resolved "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz" + integrity sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q== + dependencies: + evtd "^0.2.2" + +vscode-uri@^3.0.8: + version "3.1.0" + resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz" + integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ== + +vue-tsc@^2.2.4: + version "2.2.8" + resolved "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.8.tgz" + integrity sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ== + dependencies: + "@volar/typescript" "~2.4.11" + "@vue/language-core" "2.2.8" + +vue@^3.0.0, vue@^3.0.11, vue@^3.2.25, vue@^3.4.0, vue@^3.5.13, vue@3.5.13: + version "3.5.13" + resolved "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz" + integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-sfc" "3.5.13" + "@vue/runtime-dom" "3.5.13" + "@vue/server-renderer" "3.5.13" + "@vue/shared" "3.5.13" + +vueuc@^0.4.63: + version "0.4.64" + resolved "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.64.tgz" + integrity sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA== + dependencies: + "@css-render/vue3-ssr" "^0.15.10" + "@juggle/resize-observer" "^3.3.1" + css-render "^0.15.10" + evtd "^0.2.4" + seemly "^0.3.6" + vdirs "^0.1.4" + vooks "^0.2.4" diff --git a/stock_analyzer.py b/stock_analyzer.py index 1d92aaa..57d0a71 100644 --- a/stock_analyzer.py +++ b/stock_analyzer.py @@ -476,7 +476,7 @@ class StockAnalyzer: def _process_ai_stream(self, response, stock_code) -> Generator[str, None, None]: """处理AI流式响应""" - logger.info(f"开始处理 {stock_code} 的AI流式响应") + logger.info(f"开始处理股票 {stock_code} 的AI流式响应\n") buffer = "" chunk_count = 0 diff --git a/web_server.py b/web_server.py index 2e13be7..a14ef1d 100644 --- a/web_server.py +++ b/web_server.py @@ -1,4 +1,4 @@ -from flask import Flask, render_template, request, jsonify, Response, stream_with_context +from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory from stock_analyzer import StockAnalyzer from us_stock_service import USStockService from fund_service import FundService # 新增导入 @@ -16,24 +16,44 @@ load_dotenv() # 获取日志器 logger = get_logger() -app = Flask(__name__) +app = Flask(__name__, + static_folder='frontend/dist', + static_url_path='/') + analyzer = StockAnalyzer() us_stock_service = USStockService() fund_service = FundService() # 新增服务实例 @app.route('/') def index(): - announcement = os.getenv('ANNOUNCEMENT_TEXT') or None - # 获取默认API配置信息 - default_api_url = os.getenv('API_URL', '') - default_api_model = os.getenv('API_MODEL', 'gpt-3.5-turbo') - default_api_timeout = os.getenv('API_TIMEOUT', '60') - # 不传递API_KEY到前端,出于安全考虑 - return render_template('index.html', - announcement=announcement, - default_api_url=default_api_url, - default_api_model=default_api_model, - default_api_timeout=default_api_timeout) + # 检查是否使用前端构建版本 + frontend_dist = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frontend', 'dist') + if os.path.exists(frontend_dist): + return send_from_directory(frontend_dist, 'index.html') + else: + # 传统模板渲染,用于兼容旧版本 + announcement = os.getenv('ANNOUNCEMENT_TEXT') or None + # 获取默认API配置信息 + default_api_url = os.getenv('API_URL', '') + default_api_model = os.getenv('API_MODEL', 'gpt-3.5-turbo') + default_api_timeout = os.getenv('API_TIMEOUT', '60') + # 不传递API_KEY到前端,出于安全考虑 + return render_template('index.html', + announcement=announcement, + default_api_url=default_api_url, + default_api_model=default_api_model, + default_api_timeout=default_api_timeout) + +@app.route('/config') +def get_config(): + """返回系统配置信息""" + config = { + 'announcement': os.getenv('ANNOUNCEMENT_TEXT') or '', + 'default_api_url': os.getenv('API_URL', ''), + 'default_api_model': os.getenv('API_MODEL', 'gpt-3.5-turbo'), + 'default_api_timeout': os.getenv('API_TIMEOUT', '60') + } + return jsonify(config) @app.route('/analyze', methods=['POST']) def analyze(): @@ -200,4 +220,4 @@ def test_api_connection(): if __name__ == '__main__': logger.info("股票分析系统启动") - app.run(host='0.0.0.0', port=8888, debug=True) \ No newline at end of file + app.run(host='127.0.0.1', port=8888, debug=True) \ No newline at end of file From 8781eebdfa27e5662e59f578cdda4a8320493cc9 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Thu, 6 Mar 2025 14:46:59 +0800 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E5=86=85=E5=AE=B9=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/StockAnalysisApp.vue | 141 ++++- frontend/src/components/StockCard.vue | 598 +++++++++++++++++-- frontend/src/types/index.ts | 17 + frontend/yarn.lock | 233 +++++++- stock_analyzer.py | 3 +- 5 files changed, 939 insertions(+), 53 deletions(-) diff --git a/frontend/src/components/StockAnalysisApp.vue b/frontend/src/components/StockAnalysisApp.vue index a6ac6bb..385523b 100644 --- a/frontend/src/components/StockAnalysisApp.vue +++ b/frontend/src/components/StockAnalysisApp.vue @@ -227,22 +227,37 @@ function handleStreamInit(data: StreamInitMessage) { // 处理流式更新消息 function handleStreamUpdate(data: StreamAnalysisUpdate) { - const stockIndex = analyzedStocks.value.findIndex(s => s.code === data.stock_code); + const stockIndex = analyzedStocks.value.findIndex((s: StockInfo) => s.code === data.stock_code); if (stockIndex >= 0) { const stock = { ...analyzedStocks.value[stockIndex] }; // 更新分析状态 - stock.analysisStatus = data.status; + if (data.status) { + stock.analysisStatus = data.status; + } // 如果有分析结果,则更新 if (data.analysis !== undefined) { stock.analysis = data.analysis; } + // 处理AI分析片段 + if (data.ai_analysis_chunk !== undefined) { + // 如果之前没有分析内容,则初始化 + if (!stock.analysis) { + stock.analysis = ''; + } + // 追加新的分析片段 + stock.analysis += data.ai_analysis_chunk; + // 确保分析状态为正在分析 + stock.analysisStatus = 'analyzing'; + } + // 如果有错误,则更新 if (data.error !== undefined) { stock.error = data.error; + stock.analysisStatus = 'error'; } // 更新股票名称、价格等信息 @@ -262,6 +277,41 @@ function handleStreamUpdate(data: StreamAnalysisUpdate) { stock.marketValue = data.market_value; } + // 添加新字段的处理 + if (data.score !== undefined) { + stock.score = data.score; + } + + if (data.recommendation !== undefined) { + stock.recommendation = data.recommendation; + } + + if (data.price_change !== undefined) { + stock.price_change = data.price_change; + } + + if (data.rsi !== undefined) { + stock.rsi = data.rsi; + } + + // 添加技术指标字段的处理 + if (data.ma_trend !== undefined) { + stock.ma_trend = data.ma_trend; + } + + if (data.macd_signal !== undefined) { + stock.macd_signal = data.macd_signal; + } + + if (data.volume_status !== undefined) { + stock.volume_status = data.volume_status; + } + + // 添加分析日期字段的处理 + if (data.analysis_date !== undefined) { + stock.analysis_date = data.analysis_date; + } + // 更新数组中的股票信息 analyzedStocks.value[stockIndex] = stock; } @@ -280,8 +330,8 @@ async function analyzeStocks() { // 解析股票代码 const codes = stockCodes.value .split(/[,\s\n]+/) - .map(code => code.trim()) - .filter(code => code); + .map((code: string) => code.trim()) + .filter((code: string) => code); if (codes.length === 0) { message.warning('未找到有效的股票代码'); @@ -362,6 +412,14 @@ async function analyzeStocks() { processStreamData(buffer); } + // 将所有分析中的股票状态更新为已完成 + analyzedStocks.value.forEach((stock, index) => { + if (stock.analysisStatus === 'analyzing') { + const updatedStock = { ...stock, analysisStatus: 'completed' }; + analyzedStocks.value[index] = updatedStock; + } + }); + message.success('分析完成'); } catch (error: any) { message.error(`分析出错: ${error.message || '未知错误'}`); @@ -381,9 +439,78 @@ async function copyAnalysisResults() { try { // 格式化分析结果 const formattedResults = analyzedStocks.value - .filter(stock => stock.analysisStatus === 'completed') - .map(stock => { - return `【${stock.code} ${stock.name || ''}】\n${stock.analysis || '无分析结果'}\n`; + .filter((stock: StockInfo) => stock.analysisStatus === 'completed') + .map((stock: StockInfo) => { + let result = `【${stock.code} ${stock.name || ''}】\n`; + + // 添加分析日期 + if (stock.analysis_date) { + try { + const date = new Date(stock.analysis_date); + if (!isNaN(date.getTime())) { + result += `分析日期: ${date.toISOString().split('T')[0]}\n`; + } else { + result += `分析日期: ${stock.analysis_date}\n`; + } + } catch (e) { + result += `分析日期: ${stock.analysis_date}\n`; + } + } + + // 添加评分和推荐信息 + if (stock.score !== undefined) { + result += `评分: ${stock.score}\n`; + } + + if (stock.recommendation) { + result += `推荐: ${stock.recommendation}\n`; + } + + // 添加技术指标信息 + if (stock.rsi !== undefined) { + result += `RSI: ${stock.rsi.toFixed(2)}\n`; + } + + if (stock.price_change !== undefined) { + const sign = stock.price_change > 0 ? '+' : ''; + result += `价格变动: ${sign}${stock.price_change.toFixed(2)}\n`; + } + + if (stock.ma_trend) { + const trendMap: Record = { + 'UP': '上升', + 'DOWN': '下降', + 'NEUTRAL': '平稳' + }; + const trend = trendMap[stock.ma_trend] || stock.ma_trend; + result += `均线趋势: ${trend}\n`; + } + + if (stock.macd_signal) { + const signalMap: Record = { + 'BUY': '买入', + 'SELL': '卖出', + 'HOLD': '持有', + 'NEUTRAL': '中性' + }; + const signal = signalMap[stock.macd_signal] || stock.macd_signal; + result += `MACD信号: ${signal}\n`; + } + + if (stock.volume_status) { + const statusMap: Record = { + 'HIGH': '放量', + 'LOW': '缩量', + 'NORMAL': '正常' + }; + const status = statusMap[stock.volume_status] || stock.volume_status; + result += `成交量: ${status}\n`; + } + + // 添加分析结果 + result += `\n${stock.analysis || '无分析结果'}\n`; + + return result; }) .join('\n'); diff --git a/frontend/src/components/StockCard.vue b/frontend/src/components/StockCard.vue index 211a512..52673b7 100644 --- a/frontend/src/components/StockCard.vue +++ b/frontend/src/components/StockCard.vue @@ -3,15 +3,74 @@
{{ stock.code }}
-
{{ stock.name || '加载中...' }}
-
{{ stock.price.toFixed(2) }}
+
当前价格: {{ stock.price.toFixed(2) }}
- {{ formatChangePercent(stock.changePercent) }} + 涨跌幅: {{ formatChangePercent(calculatedChangePercent) }} +
+
+
+ +
+
+
{{ stock.score }}
+
评分
+
+
+
{{ stock.recommendation }}
+
推荐
+
+
+ +
+ + + 分析日期: {{ formatDate(stock.analysis_date) }} + +
+ +
+ 技术指标 + +
+
+
{{ stock.rsi.toFixed(2) }}
+
RSI
+
+ +
+
{{ formatPriceChange(stock.price_change) }}
+
价格变动
+
+ +
+
+ {{ getChineseTrend(stock.ma_trend) }} +
+
均线趋势
+
+ +
+
+ {{ getChineseSignal(stock.macd_signal) }} +
+
MACD信号
+
+ +
+
+ {{ getChineseVolumeStatus(stock.volume_status) }} +
+
成交量
@@ -31,6 +90,7 @@ 正在分析... +
- diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 10d1e83..d73a9f9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -17,6 +17,14 @@ export interface StockInfo { analysis?: string; analysisStatus: 'waiting' | 'analyzing' | 'completed' | 'error'; error?: string; + score?: number; + recommendation?: string; + price_change?: number; + rsi?: number; + ma_trend?: string; + macd_signal?: string; + volume_status?: string; + analysis_date?: string; } export interface SearchResult { @@ -77,4 +85,13 @@ export interface StreamAnalysisUpdate { price?: number; change_percent?: number; market_value?: number; + score?: number; + recommendation?: string; + price_change?: number; + rsi?: number; + ma_trend?: string; + macd_signal?: string; + volume_status?: string; + analysis_date?: string; + ai_analysis_chunk?: string; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 81a0230..5584045 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -42,6 +42,126 @@ resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== +"@esbuild/aix-ppc64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" + integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== + +"@esbuild/android-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" + integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== + +"@esbuild/android-arm@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" + integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== + +"@esbuild/android-x64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" + integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== + +"@esbuild/darwin-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" + integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== + +"@esbuild/darwin-x64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" + integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== + +"@esbuild/freebsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" + integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== + +"@esbuild/freebsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" + integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== + +"@esbuild/linux-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" + integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== + +"@esbuild/linux-arm@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" + integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== + +"@esbuild/linux-ia32@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" + integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== + +"@esbuild/linux-loong64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" + integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== + +"@esbuild/linux-mips64el@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" + integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== + +"@esbuild/linux-ppc64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" + integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== + +"@esbuild/linux-riscv64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" + integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== + +"@esbuild/linux-s390x@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" + integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== + +"@esbuild/linux-x64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" + integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== + +"@esbuild/netbsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" + integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== + +"@esbuild/netbsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" + integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== + +"@esbuild/openbsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" + integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== + +"@esbuild/openbsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" + integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== + +"@esbuild/sunos-x64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" + integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== + +"@esbuild/win32-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" + integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== + +"@esbuild/win32-ia32@0.25.0": + version "0.25.0" + resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" + integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== + "@esbuild/win32-x64@0.25.0": version "0.25.0" resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz" @@ -57,6 +177,96 @@ resolved "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz" integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== +"@rollup/rollup-android-arm-eabi@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz#661a45a4709c70e59e596ec78daa9cb8b8d27604" + integrity sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA== + +"@rollup/rollup-android-arm64@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz#128fe8dd510d880cf98b4cb6c7add326815a0c4b" + integrity sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg== + +"@rollup/rollup-darwin-arm64@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz#363467bc49fd0b1e17075798ac8e9ad1e1e29535" + integrity sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ== + +"@rollup/rollup-darwin-x64@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz#c2fe3d85fffe47f0ed0f076b3563ada22c8af19c" + integrity sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q== + +"@rollup/rollup-freebsd-arm64@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz#d95bd8f6eaaf829781144fc8bd2d5d71d9f6a9f5" + integrity sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw== + +"@rollup/rollup-freebsd-x64@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz#c3576c6011656e4966ded29f051edec636b44564" + integrity sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g== + +"@rollup/rollup-linux-arm-gnueabihf@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz#48c87d0dee4f8dc9591a416717f91b4a89d77e3d" + integrity sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg== + +"@rollup/rollup-linux-arm-musleabihf@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz#f4c4e7c03a7767f2e5aa9d0c5cfbf5c0f59f2d41" + integrity sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA== + +"@rollup/rollup-linux-arm64-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz#1015c9d07a99005025d13b8622b7600029d0b52f" + integrity sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw== + +"@rollup/rollup-linux-arm64-musl@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz#8f895eb5577748fc75af21beae32439626e0a14c" + integrity sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A== + +"@rollup/rollup-linux-loongarch64-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz#c9cd5dbbdc6b3ca4dbeeb0337498cf31949004a0" + integrity sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg== + +"@rollup/rollup-linux-powerpc64le-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz#7ebb5b4441faa17843a210f7d0583a20c93b40e4" + integrity sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA== + +"@rollup/rollup-linux-riscv64-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz#10f5d7349fbd2fe78f9e36ecc90aab3154435c8d" + integrity sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg== + +"@rollup/rollup-linux-s390x-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz#196347d2fa20593ab09d0b7e2589fb69bdf742c6" + integrity sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ== + +"@rollup/rollup-linux-x64-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz#7193cbd8d128212b8acda37e01b39d9e96259ef8" + integrity sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A== + +"@rollup/rollup-linux-x64-musl@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz#29a6867278ca0420b891574cfab98ecad70c59d1" + integrity sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA== + +"@rollup/rollup-win32-arm64-msvc@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz#89427dcac0c8e3a6d32b13a03a296a275d0de9a9" + integrity sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q== + +"@rollup/rollup-win32-ia32-msvc@4.34.9": + version "4.34.9" + resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz#ecb9711ba2b6d2bf6ee51265abe057ab90913deb" + integrity sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w== + "@rollup/rollup-win32-x64-msvc@4.34.9": version "4.34.9" resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz" @@ -89,7 +299,7 @@ resolved "https://registry.npmmirror.com/@types/marked/-/marked-5.0.2.tgz" integrity sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg== -"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.13.9": +"@types/node@^22.13.9": version "22.13.9" resolved "https://registry.npmmirror.com/@types/node/-/node-22.13.9.tgz" integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw== @@ -111,7 +321,7 @@ resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz" integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ== -"@volar/language-core@~2.4.11", "@volar/language-core@2.4.11": +"@volar/language-core@2.4.11", "@volar/language-core@~2.4.11": version "2.4.11" resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.11.tgz" integrity sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg== @@ -143,7 +353,7 @@ estree-walker "^2.0.2" source-map-js "^1.2.0" -"@vue/compiler-dom@^3.5.0", "@vue/compiler-dom@3.5.13": +"@vue/compiler-dom@3.5.13", "@vue/compiler-dom@^3.5.0": version "3.5.13" resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz" integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA== @@ -229,7 +439,7 @@ "@vue/compiler-ssr" "3.5.13" "@vue/shared" "3.5.13" -"@vue/shared@^3.5.0", "@vue/shared@3.5.13": +"@vue/shared@3.5.13", "@vue/shared@^3.5.0": version "3.5.13" resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz" integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ== @@ -312,7 +522,7 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -css-render@^0.15.10, css-render@^0.15.14, css-render@~0.15.14: +css-render@^0.15.10, css-render@^0.15.14: version "0.15.14" resolved "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz" integrity sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg== @@ -335,7 +545,7 @@ date-fns-tz@^3.1.3: resolved "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz" integrity sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ== -"date-fns@^3.0.0 || ^4.0.0", date-fns@^3.6.0: +date-fns@^3.6.0: version "3.6.0" resolved "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz" integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== @@ -447,6 +657,11 @@ form-data@^4.0.0: es-set-tostringtag "^2.1.0" mime-types "^2.1.12" +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz" @@ -658,7 +873,7 @@ treemate@^0.3.11: resolved "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz" integrity sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg== -typescript@*, typescript@>=5.0.0, typescript@~5.7.2, typescript@5.x: +typescript@~5.7.2: version "5.7.3" resolved "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz" integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== @@ -675,7 +890,7 @@ vdirs@^0.1.4, vdirs@^0.1.8: dependencies: evtd "^0.2.2" -"vite@^5.0.0 || ^6.0.0", vite@^6.2.0: +vite@^6.2.0: version "6.2.0" resolved "https://registry.npmmirror.com/vite/-/vite-6.2.0.tgz" integrity sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ== @@ -706,7 +921,7 @@ vue-tsc@^2.2.4: "@volar/typescript" "~2.4.11" "@vue/language-core" "2.2.8" -vue@^3.0.0, vue@^3.0.11, vue@^3.2.25, vue@^3.4.0, vue@^3.5.13, vue@3.5.13: +vue@^3.5.13: version "3.5.13" resolved "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz" integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ== diff --git a/stock_analyzer.py b/stock_analyzer.py index 57d0a71..c28d4ac 100644 --- a/stock_analyzer.py +++ b/stock_analyzer.py @@ -502,7 +502,8 @@ class StockAnalyzer: try: json_data = json.loads(data_content) - if 'choices' in json_data: + # 检查 choices 列表是否为空 + if 'choices' in json_data and json_data['choices']: delta = json_data['choices'][0].get('delta', {}) content = delta.get('content', '') From bcf64f00413e2713bf016feb223c54aa16143a01 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Thu, 6 Mar 2025 15:40:26 +0800 Subject: [PATCH 03/17] =?UTF-8?q?refactor:=20flask=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=BC=82=E6=AD=A5fastapi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 9 +- templates/index.html | 1287 ------------------------------------------ utils/api_control.py | 43 ++ web_server.py | 251 ++++---- 4 files changed, 202 insertions(+), 1388 deletions(-) delete mode 100644 templates/index.html create mode 100644 utils/api_control.py diff --git a/requirements.txt b/requirements.txt index c77a82b..f758152 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,11 +10,14 @@ scipy==1.15.1 akshare==1.16.22 tqdm==4.67.1 +# Web框架与异步处理 +fastapi==0.115.11 +uvicorn[standard]==0.34.0 +pydantic==2.10.6 +httpx==0.28.1 -# 网络和API请求 -requests==2.32.3 +# 环境配置 python-dotenv==1.0.1 -flask==3.1.0 # 日志和系统工具 loguru==0.7.2 diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 0f159c7..0000000 --- a/templates/index.html +++ /dev/null @@ -1,1287 +0,0 @@ - - - - - - 股票分析系统 - - - - - - {% if announcement %} -
-
-
-
- - - -
-
-

-

-
- -
-
-
- - - {% endif %} - -
-

股票分析系统

- - -
-
-
- -
-

当前时间

-

-
- - -
-

A股市场

-

-

-
- - -
-

港股市场

-

-

-
- - -
-

美股市场

-

-

-
-
-
-
- - - -
- -
-

股票批量分析

- - -
-
-

API配置

- -
- - -
- - -
- - -
- - - -
- - - -
- - -
- - - - -
- - -
-
-

分析结果

- -
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/utils/api_control.py b/utils/api_control.py new file mode 100644 index 0000000..ba10342 --- /dev/null +++ b/utils/api_control.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from typing import Optional, Any +from fastapi import Response +from pydantic import BaseModel + + +class ResponseModel(BaseModel): + """ + 统一返回模型 + """ + code: int = 200 + msg: str = "Success" + data: Optional[Any] = None + + +class ApiResponse: + @staticmethod + def __response(code: int, msg: str, data: Optional[Any] = None) -> ResponseModel: + return ResponseModel(code=code, msg=msg, data=data) + + @classmethod + def success(cls, *, code: int = 200, msg: str = 'Success', data: Optional[Any] = None) -> Response: + response_model = cls.__response(code=code, msg=msg, data=data) + return cls(content=response_model.model_dump()) + + @classmethod + def fail(cls, *, code: int = 400, msg: str = 'Bad Request', data: Optional[Any] = None) -> Response: + response_model = cls.__response(code=code, msg=msg, data=data) + return cls(content=response_model.model_dump()) + +response_api = ApiResponse() + +""" 示例 +@app.get("/example-success") +async def example_success(): + return response_api.success(data={"key": "value"}) + +@app.get("/example-fail") +async def example_fail(): + return response_api.fail(msg="Something went wrong", data={"error": "details"}) +""" diff --git a/web_server.py b/web_server.py index a14ef1d..c761334 100644 --- a/web_server.py +++ b/web_server.py @@ -1,51 +1,80 @@ -from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory +from fastapi import FastAPI, Request, Response, Depends, HTTPException, BackgroundTasks +from fastapi.responses import JSONResponse, StreamingResponse, FileResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +from typing import List, Optional, Generator from stock_analyzer import StockAnalyzer from us_stock_service import USStockService -from fund_service import FundService # 新增导入 +from fund_service import FundService +import asyncio import threading import os import traceback -import requests +import httpx from logger import get_logger from utils.api_utils import APIUtils # 加载环境变量 from dotenv import load_dotenv +import uvicorn load_dotenv() # 获取日志器 logger = get_logger() -app = Flask(__name__, - static_folder='frontend/dist', - static_url_path='/') +app = FastAPI( + title="Stock Scanner API", + description="股票分析API", + version="1.0.0" +) + +# 添加CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 开发环境允许所有来源,生产环境应该限制 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 设置静态文件 +frontend_dist = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frontend', 'dist') +if os.path.exists(frontend_dist): + app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="frontend") analyzer = StockAnalyzer() us_stock_service = USStockService() -fund_service = FundService() # 新增服务实例 +fund_service = FundService() -@app.route('/') -def index(): +# 定义请求和响应模型 +class AnalyzeRequest(BaseModel): + stock_codes: List[str] + market_type: str = "A" + api_url: Optional[str] = None + api_key: Optional[str] = None + api_model: Optional[str] = None + api_timeout: Optional[str] = None + +class TestAPIRequest(BaseModel): + api_url: str + api_key: str + api_model: Optional[str] = None + api_timeout: Optional[int] = 10 + +@app.get("/") +async def index(request: Request): # 检查是否使用前端构建版本 - frontend_dist = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frontend', 'dist') if os.path.exists(frontend_dist): - return send_from_directory(frontend_dist, 'index.html') + index_file = os.path.join(frontend_dist, 'index.html') + return FileResponse(index_file) else: - # 传统模板渲染,用于兼容旧版本 - announcement = os.getenv('ANNOUNCEMENT_TEXT') or None - # 获取默认API配置信息 - default_api_url = os.getenv('API_URL', '') - default_api_model = os.getenv('API_MODEL', 'gpt-3.5-turbo') - default_api_timeout = os.getenv('API_TIMEOUT', '60') - # 不传递API_KEY到前端,出于安全考虑 - return render_template('index.html', - announcement=announcement, - default_api_url=default_api_url, - default_api_model=default_api_model, - default_api_timeout=default_api_timeout) + # 不再使用模板渲染,而是重定向到API文档页面 + logger.warning("前端构建目录不存在,重定向到API文档页面") + return RedirectResponse(url="/docs") -@app.route('/config') -def get_config(): +@app.get("/config") +async def get_config(): """返回系统配置信息""" config = { 'announcement': os.getenv('ANNOUNCEMENT_TEXT') or '', @@ -53,23 +82,22 @@ def get_config(): 'default_api_model': os.getenv('API_MODEL', 'gpt-3.5-turbo'), 'default_api_timeout': os.getenv('API_TIMEOUT', '60') } - return jsonify(config) + return config -@app.route('/analyze', methods=['POST']) -def analyze(): +@app.post("/analyze") +async def analyze(request: AnalyzeRequest): try: logger.info("开始处理分析请求") - data = request.json - stock_codes = data.get('stock_codes', []) - market_type = data.get('market_type', 'A') + stock_codes = request.stock_codes + market_type = request.market_type logger.debug(f"接收到分析请求: stock_codes={stock_codes}, market_type={market_type}") # 获取自定义API配置 - custom_api_url = data.get('api_url') - custom_api_key = data.get('api_key') - custom_api_model = data.get('api_model') - custom_api_timeout = data.get('api_timeout') + custom_api_url = request.api_url + custom_api_key = request.api_key + custom_api_model = request.api_model + custom_api_timeout = request.api_timeout logger.debug(f"自定义API配置: URL={custom_api_url}, 模型={custom_api_model}, API Key={'已提供' if custom_api_key else '未提供'}, Timeout={custom_api_timeout}") @@ -83,10 +111,10 @@ def analyze(): if not stock_codes: logger.warning("未提供股票代码") - return jsonify({'error': '请输入代码'}), 400 + raise HTTPException(status_code=400, detail="请输入代码") - # 使用流式响应 - def generate(): + # 定义流式生成器 + async def generate_stream(): if len(stock_codes) == 1: # 单个股票分析流式处理 stock_code = stock_codes[0].strip() @@ -97,7 +125,16 @@ def analyze(): logger.debug(f"开始处理股票 {stock_code} 的流式响应") chunk_count = 0 - for chunk in custom_analyzer.analyze_stock(stock_code, market_type, stream=True): + + # 使用线程池执行同步分析 + def run_analysis(): + return list(custom_analyzer.analyze_stock(stock_code, market_type, stream=True)) + + # 在线程中执行同步操作 + loop = asyncio.get_event_loop() + chunks = await loop.run_in_executor(None, run_analysis) + + for chunk in chunks: chunk_count += 1 yield chunk + '\n' logger.info(f"股票 {stock_code} 流式分析完成,共发送 {chunk_count} 个块") @@ -110,114 +147,132 @@ def analyze(): logger.debug(f"开始处理批量股票的流式响应") chunk_count = 0 - for chunk in custom_analyzer.scan_stocks( - [code.strip() for code in stock_codes], - min_score=0, - market_type=market_type, - stream=True - ): + + # 使用线程池执行同步分析 + def run_batch_analysis(): + return list(custom_analyzer.scan_stocks( + [code.strip() for code in stock_codes], + min_score=0, + market_type=market_type, + stream=True + )) + + # 在线程中执行同步操作 + loop = asyncio.get_event_loop() + chunks = await loop.run_in_executor(None, run_batch_analysis) + + for chunk in chunks: chunk_count += 1 yield chunk + '\n' logger.info(f"批量流式分析完成,共发送 {chunk_count} 个块") logger.info("成功创建流式响应生成器") - return Response(stream_with_context(generate()), mimetype='application/json') + return StreamingResponse(generate_stream(), media_type='application/json') except Exception as e: error_msg = f"分析时出错: {str(e)}" logger.error(error_msg) logger.exception(e) - return jsonify({'error': error_msg}), 500 + raise HTTPException(status_code=500, detail=error_msg) -@app.route('/search_us_stocks', methods=['GET']) -def search_us_stocks(): +@app.get("/search_us_stocks") +async def search_us_stocks(keyword: str = ""): try: - keyword = request.args.get('keyword', '') if not keyword: - return jsonify({'error': '请输入搜索关键词'}), 400 - - results = us_stock_service.search_us_stocks(keyword) - return jsonify({'results': results}) + raise HTTPException(status_code=400, detail="请输入搜索关键词") + + # 在异步上下文中运行同步的搜索函数 + loop = asyncio.get_event_loop() + results = await loop.run_in_executor(None, us_stock_service.search_us_stocks, keyword) + return {"results": results} except Exception as e: - print(f"搜索美股代码时出错: {str(e)}") - return jsonify({'error': str(e)}), 500 + logger.error(f"搜索美股代码时出错: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) -# 添加基金搜索路由 -@app.route('/search_funds', methods=['GET']) -def search_funds(): +@app.get("/search_funds") +async def search_funds(keyword: str = "", market_type: str = ""): try: - keyword = request.args.get('keyword', '') - market_type = request.args.get('market_type', '') if not keyword: - return jsonify({'error': '请输入搜索关键词'}), 400 - - results = fund_service.search_funds(keyword, market_type) - return jsonify({'results': results}) + raise HTTPException(status_code=400, detail="请输入搜索关键词") + + # 在异步上下文中运行同步的搜索函数 + loop = asyncio.get_event_loop() + results = await loop.run_in_executor(None, lambda: fund_service.search_funds(keyword, market_type)) + return {"results": results} except Exception as e: logger.error(f"搜索基金代码时出错: {str(e)}") - return jsonify({'error': str(e)}), 500 + raise HTTPException(status_code=500, detail=str(e)) -@app.route('/test_api_connection', methods=['POST']) -def test_api_connection(): +@app.post("/test_api_connection") +async def test_api_connection(request: TestAPIRequest): """测试API连接""" try: logger.info("开始测试API连接") - data = request.json - api_url = data.get('api_url') - api_key = data.get('api_key') - api_model = data.get('api_model') - api_timeout = data.get('api_timeout', 10) # 默认测试连接超时为10秒 + api_url = request.api_url + api_key = request.api_key + api_model = request.api_model + api_timeout = request.api_timeout logger.debug(f"测试API连接: URL={api_url}, 模型={api_model}, API Key={'已提供' if api_key else '未提供'}, Timeout={api_timeout}") if not api_url: logger.warning("未提供API URL") - return jsonify({'error': '请提供API URL'}), 400 + raise HTTPException(status_code=400, detail="请提供API URL") if not api_key: logger.warning("未提供API Key") - return jsonify({'error': '请提供API Key'}), 400 + raise HTTPException(status_code=400, detail="请提供API Key") # 构建API URL test_url = APIUtils.format_api_url(api_url) logger.debug(f"完整API测试URL: {test_url}") - # 发送测试请求 - response = requests.post( - test_url, - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json" - }, - json={ - "model": api_model or "gpt-3.5-turbo", - "messages": [ - {"role": "user", "content": "Hello, this is a test message. Please respond with 'API connection successful'."} - ], - "max_tokens": 20 - }, - timeout=int(api_timeout) - ) + # 使用异步HTTP客户端发送测试请求 + async with httpx.AsyncClient(timeout=float(api_timeout)) as client: + response = await client.post( + test_url, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + }, + json={ + "model": api_model or "gpt-3.5-turbo", + "messages": [ + {"role": "user", "content": "Hello, this is a test message. Please respond with 'API connection successful'."} + ], + "max_tokens": 20 + } + ) # 检查响应 if response.status_code == 200: logger.info(f"API 连接测试成功: {response.status_code}") - return jsonify({'success': True, 'message': 'API 连接测试成功'}) + return {"success": True, "message": "API 连接测试成功"} else: - error_message = response.json().get('error', {}).get('message', '未知错误') + error_data = response.json() + error_message = error_data.get('error', {}).get('message', '未知错误') logger.warning(f"API连接测试失败: {response.status_code} - {error_message}") - return jsonify({'success': False, 'message': f'API 连接测试失败: {error_message}', 'status_code': response.status_code}), 400 + return JSONResponse( + status_code=400, + content={"success": False, "message": f"API 连接测试失败: {error_message}", "status_code": response.status_code} + ) - except requests.exceptions.RequestException as e: + except httpx.RequestError as e: logger.error(f"API 连接请求错误: {str(e)}") - return jsonify({'success': False, 'message': f'请求错误: {str(e)}'}), 400 + return JSONResponse( + status_code=400, + content={"success": False, "message": f"请求错误: {str(e)}"} + ) except Exception as e: logger.error(f"测试 API 连接时出错: {str(e)}") logger.exception(e) - return jsonify({'success': False, 'message': f'API 测试连接时出错: {str(e)}'}), 500 + return JSONResponse( + status_code=500, + content={"success": False, "message": f"API 测试连接时出错: {str(e)}"} + ) if __name__ == '__main__': logger.info("股票分析系统启动") - app.run(host='127.0.0.1', port=8888, debug=True) \ No newline at end of file + uvicorn.run("web_server:app", host="127.0.0.1", port=8888, reload=True) \ No newline at end of file From 1e53d16b3aa655f778e62324d3cfba1bf0252c3f Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Thu, 6 Mar 2025 17:11:15 +0800 Subject: [PATCH 04/17] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/__init__.py | 2 + services/ai_analyzer.py | 269 +++++++++++++++++++++++++++++ services/fund_service_async.py | 225 ++++++++++++++++++++++++ services/stock_analyzer_service.py | 178 +++++++++++++++++++ services/stock_data_provider.py | 184 ++++++++++++++++++++ services/stock_scorer.py | 128 ++++++++++++++ services/technical_indicator.py | 187 ++++++++++++++++++++ services/us_stock_service_async.py | 151 ++++++++++++++++ web_server.py | 94 ++++++---- 9 files changed, 1380 insertions(+), 38 deletions(-) create mode 100644 services/__init__.py create mode 100644 services/ai_analyzer.py create mode 100644 services/fund_service_async.py create mode 100644 services/stock_analyzer_service.py create mode 100644 services/stock_data_provider.py create mode 100644 services/stock_scorer.py create mode 100644 services/technical_indicator.py create mode 100644 services/us_stock_service_async.py diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..7267b70 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1,2 @@ +# services包初始化文件 +# 用于组织股票分析服务的各个模块 \ No newline at end of file diff --git a/services/ai_analyzer.py b/services/ai_analyzer.py new file mode 100644 index 0000000..93c063f --- /dev/null +++ b/services/ai_analyzer.py @@ -0,0 +1,269 @@ +import pandas as pd +import numpy as np +import os +import json +import asyncio +import httpx +from typing import Dict, List, Optional, Any, Generator, AsyncGenerator +from dotenv import load_dotenv +from logger import get_logger +from utils.api_utils import APIUtils + +# 获取日志器 +logger = get_logger() + +class AIAnalyzer: + """ + 异步AI分析服务 + 负责调用AI API对股票数据进行分析 + """ + + def __init__(self, custom_api_url=None, custom_api_key=None, custom_api_model=None, custom_api_timeout=None): + """ + 初始化AI分析服务 + + Args: + custom_api_url: 自定义API URL + custom_api_key: 自定义API密钥 + custom_api_model: 自定义API模型 + custom_api_timeout: 自定义API超时时间 + """ + # 加载环境变量 + load_dotenv() + + # 设置API配置 + self.API_URL = custom_api_url or os.getenv('API_URL') + self.API_KEY = custom_api_key or os.getenv('API_KEY') + self.API_MODEL = custom_api_model or os.getenv('API_MODEL', 'gpt-3.5-turbo') + self.API_TIMEOUT = int(custom_api_timeout or os.getenv('API_TIMEOUT', 60)) + + logger.debug(f"初始化AIAnalyzer: API_URL={self.API_URL}, API_MODEL={self.API_MODEL}, API_KEY={'已提供' if self.API_KEY else '未提供'}, API_TIMEOUT={self.API_TIMEOUT}") + + async def get_ai_analysis(self, df: pd.DataFrame, stock_code: str, market_type: str = 'A', stream: bool = False) -> AsyncGenerator[str, None]: + """ + 对股票数据进行AI分析 + + Args: + df: 包含技术指标的DataFrame + stock_code: 股票代码 + market_type: 市场类型,默认为'A'股 + stream: 是否使用流式响应 + + Returns: + 异步生成器,生成分析结果字符串 + """ + try: + logger.info(f"开始AI分析 {stock_code}, 流式模式: {stream}") + recent_data = df.tail(14).to_dict('records') + + technical_summary = { + 'trend': 'upward' if df.iloc[-1]['MA5'] > df.iloc[-1]['MA20'] else 'downward', + 'volatility': f"{df.iloc[-1]['Volatility']:.2f}%", + 'volume_trend': 'increasing' if df.iloc[-1]['Volume_Ratio'] > 1 else 'decreasing', + 'rsi_level': df.iloc[-1]['RSI'] + } + + # 根据市场类型调整分析提示 + if market_type in ['ETF', 'LOF']: + prompt = f""" + 分析基金 {stock_code}: + + 技术指标概要: + {technical_summary} + + 近14日交易数据: + {recent_data} + + 请分析该基金的技术面状况,包括: + 1. 趋势分析:判断基金当前的趋势方向 + 2. 动量分析:基于RSI和交易量评估基金动量 + 3. 支撑与阻力位:确定关键价格位 + 4. 技术面总结 + 5. 投资建议 + + 将分析结果格式化为JSON,像这样: + {{ + "trend_analysis": "趋势分析结果...", + "momentum_analysis": "动量分析结果...", + "support_resistance": "支撑阻力位分析...", + "technical_summary": "技术面总结...", + "investment_advice": "投资建议..." + }} + """ + else: + prompt = f""" + 分析股票 {stock_code}: + + 技术指标概要: + {technical_summary} + + 近14日交易数据: + {recent_data} + + 请分析该股票的技术面状况,包括: + 1. 趋势分析:当前趋势方向及强度 + 2. 动量分析:基于MACD、RSI等指标 + 3. 支撑与阻力位:关键价格位分析 + 4. 成交量分析:交易量的变化及意义 + 5. 波动性评估:ATR和波动率分析 + 6. 技术面总结 + 7. 投资建议:根据技术分析给出操作建议 + + 将分析结果格式化为JSON,像这样: + {{ + "trend_analysis": "趋势分析结果...", + "momentum_analysis": "动量分析结果...", + "support_resistance": "支撑阻力位分析...", + "volume_analysis": "成交量分析...", + "volatility_assessment": "波动性评估...", + "technical_summary": "技术面总结...", + "investment_advice": "投资建议..." + }} + """ + + # 格式化API URL + api_url = APIUtils.format_api_url(self.API_URL) + + # 准备请求数据 + request_data = { + "model": self.API_MODEL, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.7, + "stream": stream + } + + # 准备请求头 + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.API_KEY}" + } + + # 异步请求API + async with httpx.AsyncClient(timeout=self.API_TIMEOUT) as client: + # 记录请求 + logger.debug(f"发送AI请求: URL={api_url}, MODEL={self.API_MODEL}, STREAM={stream}") + + if stream: + # 流式响应处理 + async with client.stream("POST", api_url, json=request_data, headers=headers) as response: + if response.status_code != 200: + error_text = await response.aread() + error_data = json.loads(error_text) + error_message = error_data.get('error', {}).get('message', '未知错误') + logger.error(f"AI API请求失败: {response.status_code} - {error_message}") + yield json.dumps({"error": f"API请求失败: {error_message}"}) + return + + # 处理流式响应 + buffer = "" + collected_messages = [] + + async for chunk in response.aiter_text(): + if chunk: + chunk_str = chunk.strip() + if chunk_str.startswith("data: "): + chunk_str = chunk_str[6:] # 去除"data: "前缀 + + if chunk_str == "[DONE]": + continue + + try: + # 解析数据块 + chunk_data = json.loads(chunk_str) + delta = chunk_data.get("choices", [{}])[0].get("delta", {}) + content = delta.get("content", "") + + if content: + buffer += content + # 尝试提取完整的JSON + if buffer.strip().startswith("{") and buffer.strip().endswith("}"): + try: + result_json = json.loads(buffer) + yield json.dumps({ + "stock_code": stock_code, + "analysis": result_json + }) + buffer = "" # 重置缓冲区 + except json.JSONDecodeError: + # JSON不完整,继续收集 + pass + + # 达到一定长度就输出 + if len(buffer) > 100: + yield json.dumps({ + "stock_code": stock_code, + "partial_content": buffer + }) + collected_messages.append(buffer) + buffer = "" + except json.JSONDecodeError: + # 忽略无法解析的块 + continue + + # 处理最后的缓冲区 + if buffer: + yield json.dumps({ + "stock_code": stock_code, + "partial_content": buffer + }) + collected_messages.append(buffer) + + # 尝试从整个内容中提取JSON + full_content = "".join(collected_messages) + + # 如果没有成功解析JSON,返回原始内容 + if not full_content.strip().startswith("{"): + yield json.dumps({ + "stock_code": stock_code, + "raw_analysis": full_content + }) + else: + # 非流式响应处理 + response = await client.post(api_url, json=request_data, headers=headers) + + if response.status_code != 200: + error_data = response.json() + error_message = error_data.get('error', {}).get('message', '未知错误') + logger.error(f"AI API请求失败: {response.status_code} - {error_message}") + yield json.dumps({"error": f"API请求失败: {error_message}"}) + return + + response_data = response.json() + analysis_text = response_data.get("choices", [{}])[0].get("message", {}).get("content", "") + + try: + # 尝试解析JSON + analysis_json = json.loads(analysis_text) + yield json.dumps({ + "stock_code": stock_code, + "analysis": analysis_json + }) + except json.JSONDecodeError: + # 返回原始文本 + yield json.dumps({ + "stock_code": stock_code, + "raw_analysis": analysis_text + }) + + logger.info(f"完成对 {stock_code} 的AI分析") + + except Exception as e: + logger.error(f"AI分析 {stock_code} 时出错: {str(e)}") + logger.exception(e) + yield json.dumps({"error": f"分析出错: {str(e)}"}) + + def _truncate_json_for_logging(self, json_obj, max_length=500): + """ + 截断JSON对象以便记录日志 + + Args: + json_obj: JSON对象 + max_length: 最大长度 + + Returns: + 截断后的字符串 + """ + json_str = json.dumps(json_obj, ensure_ascii=False) + if len(json_str) <= max_length: + return json_str + return json_str[:max_length] + "..." \ No newline at end of file diff --git a/services/fund_service_async.py b/services/fund_service_async.py new file mode 100644 index 0000000..8bafada --- /dev/null +++ b/services/fund_service_async.py @@ -0,0 +1,225 @@ +import asyncio +import pandas as pd +from typing import List, Dict, Any, Optional +from logger import get_logger +from datetime import datetime, timedelta + +# 获取日志器 +logger = get_logger() + +class FundServiceAsync: + """ + 异步基金服务 + 提供基金数据的异步搜索和获取功能 + """ + + def __init__(self): + """初始化异步基金服务""" + logger.debug("初始化FundServiceAsync") + + # 添加缓存 + self._etf_cache = None + self._lof_cache = None + self._cache_timestamp = None + self._cache_duration = timedelta(minutes=30) # 缓存30分钟 + + async def search_funds(self, keyword: str, market_type: str = 'ETF') -> List[Dict[str, Any]]: + """ + 异步搜索基金代码 + + Args: + keyword: 搜索关键词 + market_type: 市场类型,'ETF'或'LOF' + + Returns: + 匹配的基金列表 + """ + try: + logger.info(f"异步搜索基金: {keyword}, 类型: {market_type}") + + # 获取基金数据 + df = await self._get_funds_data(market_type) + + # 模糊匹配搜索(同时匹配代码和名称) + mask = (df['name'].str.contains(keyword, case=False, na=False) | + df['symbol'].str.contains(keyword, case=False, na=False)) + results = df[mask] + + # 格式化返回结果并处理 NaN 值 + formatted_results = [] + for _, row in results.iterrows(): + formatted_results.append({ + 'name': row['name'] if pd.notna(row['name']) else '', + 'symbol': str(row['symbol']) if pd.notna(row['symbol']) else '', + 'price': float(row['price']) if pd.notna(row['price']) else 0.0, + 'volume': float(row['volume']) if pd.notna(row['volume']) else 0.0, + 'market_value': float(row['market_value']) if pd.notna(row['market_value']) else 0.0, + 'total_value': float(row['total_value']) if pd.notna(row['total_value']) else 0.0, + }) + + logger.info(f"基金搜索完成,找到 {len(formatted_results)} 个匹配项") + return formatted_results + + except Exception as e: + error_msg = f"搜索基金代码失败: {str(e)}" + logger.error(error_msg) + logger.exception(e) + raise Exception(error_msg) + + async def _get_funds_data(self, market_type: str = 'ETF') -> pd.DataFrame: + """ + 异步获取基金数据,支持缓存 + + Args: + market_type: 市场类型,'ETF'或'LOF' + + Returns: + 包含基金数据的DataFrame + """ + # 检查缓存是否有效 + now = datetime.now() + cache_valid = ( + self._cache_timestamp is not None and + (now - self._cache_timestamp) < self._cache_duration + ) + + if market_type == 'ETF' and cache_valid and self._etf_cache is not None: + logger.debug("使用ETF缓存数据") + return self._etf_cache + elif market_type == 'LOF' and cache_valid and self._lof_cache is not None: + logger.debug("使用LOF缓存数据") + return self._lof_cache + + # 缓存无效,重新获取数据 + try: + logger.debug(f"从API获取{market_type}数据") + + # 使用线程池执行同步的akshare调用 + if market_type == 'ETF': + df = await asyncio.to_thread(self._get_etf_data) + self._etf_cache = df + else: + df = await asyncio.to_thread(self._get_lof_data) + self._lof_cache = df + + self._cache_timestamp = now + return df + + except Exception as e: + logger.error(f"获取{market_type}数据失败: {str(e)}") + logger.exception(e) + raise + + def _get_etf_data(self) -> pd.DataFrame: + """ + 获取ETF数据(同步方法,将被异步方法调用) + + Returns: + 包含ETF数据的DataFrame + """ + import akshare as ak + + try: + # 获取ETF基金数据 + df = ak.fund_etf_spot_em() + + # 转换列名 + df = df.rename(columns={ + "代码": "symbol", + "名称": "name", + "最新价": "price", + "涨跌额": "price_change", + "涨跌幅": "price_change_percent", + "成交量": "volume", + "流通市值": "market_value", + "总市值": "total_value", + "基金折价率": "discount_rate", + }) + + return df + + except Exception as e: + logger.error(f"获取ETF数据失败: {str(e)}") + logger.exception(e) + raise Exception(f"获取ETF数据失败: {str(e)}") + + def _get_lof_data(self) -> pd.DataFrame: + """ + 获取LOF数据(同步方法,将被异步方法调用) + + Returns: + 包含LOF数据的DataFrame + """ + import akshare as ak + + try: + # 获取LOF基金数据 + df = ak.fund_lof_spot_em() + + # 转换列名 + df = df.rename(columns={ + "代码": "symbol", + "名称": "name", + "最新价": "price", + "涨跌额": "price_change", + "涨跌幅": "price_change_percent", + "成交量": "volume", + "流通市值": "market_value", + "总市值": "total_value", + "基金折价率": "discount_rate", + }) + + return df + + except Exception as e: + logger.error(f"获取LOF数据失败: {str(e)}") + logger.exception(e) + raise Exception(f"获取LOF数据失败: {str(e)}") + + async def get_fund_detail(self, symbol: str, market_type: str = 'ETF') -> Dict[str, Any]: + """ + 异步获取单个基金详细信息 + + Args: + symbol: 基金代码 + market_type: 市场类型,'ETF'或'LOF' + + Returns: + 基金详细信息 + """ + try: + logger.info(f"获取{market_type}基金详情: {symbol}") + + # 获取基金数据 + df = await self._get_funds_data(market_type) + + # 精确匹配基金代码 + result = df[df['symbol'] == symbol] + + if len(result) == 0: + raise Exception(f"未找到基金代码: {symbol}") + + # 获取第一行数据 + row = result.iloc[0] + + # 格式化为字典 + fund_detail = { + 'name': row['name'] if pd.notna(row['name']) else '', + 'symbol': str(row['symbol']) if pd.notna(row['symbol']) else '', + 'price': float(row['price']) if pd.notna(row['price']) else 0.0, + 'price_change': float(row['price_change']) if pd.notna(row['price_change']) else 0.0, + 'price_change_percent': float(row['price_change_percent'].strip('%'))/100 if pd.notna(row['price_change_percent']) else 0.0, + 'volume': float(row['volume']) if pd.notna(row['volume']) else 0.0, + 'market_value': float(row['market_value']) if pd.notna(row['market_value']) else 0.0, + 'total_value': float(row['total_value']) if pd.notna(row['total_value']) else 0.0, + 'discount_rate': float(row['discount_rate'].strip('%'))/100 if pd.notna(row['discount_rate']) else 0.0 + } + + logger.info(f"获取基金详情成功: {symbol}") + return fund_detail + + except Exception as e: + error_msg = f"获取基金详情失败: {str(e)}" + logger.error(error_msg) + logger.exception(e) + raise Exception(error_msg) \ No newline at end of file diff --git a/services/stock_analyzer_service.py b/services/stock_analyzer_service.py new file mode 100644 index 0000000..91df74a --- /dev/null +++ b/services/stock_analyzer_service.py @@ -0,0 +1,178 @@ +import pandas as pd +import numpy as np +import asyncio +import json +from typing import Dict, List, Optional, Tuple, Any, AsyncGenerator +from logger import get_logger +from services.stock_data_provider import StockDataProvider +from services.technical_indicator import TechnicalIndicator +from services.stock_scorer import StockScorer +from services.ai_analyzer import AIAnalyzer + +# 获取日志器 +logger = get_logger() + +class StockAnalyzerService: + """ + 股票分析服务 + 作为门面类协调数据提供、指标计算、评分和AI分析等组件 + """ + + def __init__(self, custom_api_url=None, custom_api_key=None, custom_api_model=None, custom_api_timeout=None): + """ + 初始化股票分析服务 + + Args: + custom_api_url: 自定义API URL + custom_api_key: 自定义API密钥 + custom_api_model: 自定义API模型 + custom_api_timeout: 自定义API超时时间 + """ + # 初始化各个组件 + self.data_provider = StockDataProvider() + self.indicator = TechnicalIndicator() + self.scorer = StockScorer() + self.ai_analyzer = AIAnalyzer( + custom_api_url=custom_api_url, + custom_api_key=custom_api_key, + custom_api_model=custom_api_model, + custom_api_timeout=custom_api_timeout + ) + + logger.info("初始化StockAnalyzerService完成") + + async def analyze_stock(self, stock_code: str, market_type: str = 'A', stream: bool = False) -> AsyncGenerator[str, None]: + """ + 分析单只股票 + + Args: + stock_code: 股票代码 + market_type: 市场类型,默认为'A'股 + stream: 是否使用流式响应 + + Returns: + 异步生成器,生成分析结果的JSON字符串 + """ + try: + logger.info(f"开始分析股票: {stock_code}, 市场: {market_type}") + + # 获取股票数据 + df = await self.data_provider.get_stock_data(stock_code, market_type) + + # 计算技术指标 + df_with_indicators = self.indicator.calculate_indicators(df) + + # 计算评分 + score = self.scorer.calculate_score(df_with_indicators) + recommendation = self.scorer.get_recommendation(score) + + # 生成基本分析结果 + basic_result = { + "stock_code": stock_code, + "score": score, + "recommendation": recommendation, + "data_point_count": len(df), + "market_type": market_type + } + + # 输出基本分析结果 + logger.info(f"基本分析结果: {json.dumps(basic_result)}") + yield json.dumps(basic_result) + + # 使用AI进行深入分析 + async for analysis_chunk in self.ai_analyzer.get_ai_analysis(df_with_indicators, stock_code, market_type, stream): + yield analysis_chunk + + logger.info(f"完成股票分析: {stock_code}") + + except Exception as e: + error_msg = f"分析股票 {stock_code} 时出错: {str(e)}" + logger.error(error_msg) + logger.exception(e) + yield json.dumps({"error": error_msg}) + + async def scan_stocks(self, stock_codes: List[str], market_type: str = 'A', min_score: int = 0, stream: bool = False) -> AsyncGenerator[str, None]: + """ + 批量扫描股票 + + Args: + stock_codes: 股票代码列表 + market_type: 市场类型 + min_score: 最低评分阈值 + stream: 是否使用流式响应 + + Returns: + 异步生成器,生成扫描结果的JSON字符串 + """ + try: + logger.info(f"开始批量扫描 {len(stock_codes)} 只股票, 市场: {market_type}") + + # 输出初始状态 + yield json.dumps({ + "status": "scanning", + "total_stocks": len(stock_codes), + "market_type": market_type, + "min_score": min_score + }) + + # 批量获取股票数据 + stock_data_dict = await self.data_provider.get_multiple_stocks_data(stock_codes, market_type) + + # 计算技术指标 + stock_with_indicators = {} + for code, df in stock_data_dict.items(): + try: + stock_with_indicators[code] = self.indicator.calculate_indicators(df) + except Exception as e: + logger.error(f"计算 {code} 技术指标时出错: {str(e)}") + + # 评分股票 + results = self.scorer.batch_score_stocks(stock_with_indicators) + + # 过滤低于最低评分的股票 + filtered_results = [r for r in results if r[1] >= min_score] + + # 输出评分结果 + yield json.dumps({ + "scan_results": [ + { + "stock_code": code, + "score": score, + "recommendation": rec + } for code, score, rec in filtered_results + ], + "total_matched": len(filtered_results), + "total_scanned": len(results) + }) + + # 如果需要进一步分析,对评分较高的股票进行AI分析 + if stream and filtered_results: + top_stocks = filtered_results[:3] # 只分析前3只评分最高的股票 + + for stock_code, score, _ in top_stocks: + df = stock_with_indicators.get(stock_code) + if df is not None: + # 输出正在分析的股票信息 + yield json.dumps({ + "analyzing": stock_code, + "score": score + }) + + # AI分析 + async for analysis_chunk in self.ai_analyzer.get_ai_analysis(df, stock_code, market_type, stream): + yield analysis_chunk + + # 输出扫描完成信息 + yield json.dumps({ + "status": "completed", + "total_scanned": len(results), + "total_matched": len(filtered_results) + }) + + logger.info(f"完成批量扫描 {len(stock_codes)} 只股票, 符合条件: {len(filtered_results)}") + + except Exception as e: + error_msg = f"批量扫描股票时出错: {str(e)}" + logger.error(error_msg) + logger.exception(e) + yield json.dumps({"error": error_msg}) \ No newline at end of file diff --git a/services/stock_data_provider.py b/services/stock_data_provider.py new file mode 100644 index 0000000..48cebb4 --- /dev/null +++ b/services/stock_data_provider.py @@ -0,0 +1,184 @@ +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +import asyncio +import os +from typing import Dict, List, Optional, Tuple, Any +from logger import get_logger + +# 获取日志器 +logger = get_logger() + +class StockDataProvider: + """ + 异步股票数据提供服务 + 负责获取股票、基金等金融产品的历史数据 + """ + + def __init__(self): + """初始化数据提供者服务""" + logger.debug("初始化StockDataProvider") + + async def get_stock_data(self, stock_code: str, market_type: str = 'A', + start_date: Optional[str] = None, + end_date: Optional[str] = None) -> pd.DataFrame: + """ + 异步获取股票或基金数据 + + Args: + stock_code: 股票代码 + market_type: 市场类型,默认为'A'股 + start_date: 开始日期,格式YYYYMMDD,默认为一年前 + end_date: 结束日期,格式YYYYMMDD,默认为今天 + + Returns: + 包含历史数据的DataFrame + """ + # 使用线程池执行同步的akshare调用 + return await asyncio.to_thread( + self._get_stock_data_sync, + stock_code, + market_type, + start_date, + end_date + ) + + def _get_stock_data_sync(self, stock_code: str, market_type: str = 'A', + start_date: Optional[str] = None, + end_date: Optional[str] = None) -> pd.DataFrame: + """ + 同步获取股票数据的实现 + 将被异步方法调用 + """ + import akshare as ak + + if start_date is None: + start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d') + if end_date is None: + end_date = datetime.now().strftime('%Y%m%d') + + try: + # 验证股票代码格式 + if market_type == 'A': + # 上海证券交易所股票代码以6开头 + # 深圳证券交易所股票代码以0或3开头 + # 科创板股票代码以688开头 + # 北京证券交易所股票代码以8开头 + valid_prefixes = ['0', '3', '6', '688', '8'] + valid_format = False + + for prefix in valid_prefixes: + if stock_code.startswith(prefix): + valid_format = True + break + + if not valid_format: + error_msg = f"无效的A股股票代码格式: {stock_code}。A股代码应以0、3、6、688或8开头" + logger.error(f"[股票代码格式错误] {error_msg}") + raise ValueError(error_msg) + + logger.debug(f"获取A股数据: {stock_code}") + df = ak.stock_zh_a_hist( + symbol=stock_code, + start_date=start_date, + end_date=end_date, + adjust="qfq" + ) + + elif market_type in ['HK']: + logger.debug(f"获取港股数据: {stock_code}") + df = ak.stock_hk_daily( + symbol=stock_code, + start_date=start_date, + end_date=end_date, + adjust="qfq" + ) + + elif market_type in ['US']: + logger.debug(f"获取美股数据: {stock_code}") + df = ak.stock_us_daily( + symbol=stock_code, + adjust="qfq" + ) + # 过滤日期 + df = df[(df.index >= start_date) & (df.index <= end_date)] + + elif market_type in ['ETF', 'LOF']: + logger.debug(f"获取{market_type}基金数据: {stock_code}") + df = ak.fund_etf_hist_sina( + symbol=stock_code, + start_date=start_date.replace('-', ''), + end_date=end_date.replace('-', '') + ) + + else: + error_msg = f"不支持的市场类型: {market_type}" + logger.error(f"[市场类型错误] {error_msg}") + raise ValueError(error_msg) + + # 标准化列名 + if market_type == 'A': + # 根据实际数据结构调整列名映射 + # 实际数据列:['日期', '股票代码', '开盘', '收盘', '最高', '最低', '成交量', '成交额', '振幅', '涨跌幅', '涨跌额', '换手率'] + df.columns = ['Date', 'Code', 'Open', 'Close', 'High', 'Low', 'Volume', 'Amount', 'Amplitude', 'Change_pct', 'Change', 'Turnover'] + elif market_type in ['HK', 'US']: + # 根据实际情况调整 + df.columns = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume', 'Amount'] + elif market_type in ['ETF', 'LOF']: + # 基金数据可能有不同的列 + df.columns = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume', 'Amount'] + + # 确保日期列是日期类型 + if 'Date' in df.columns: + df['Date'] = pd.to_datetime(df['Date']) + df.set_index('Date', inplace=True) + + # 确保按日期升序排序 + df.sort_index(inplace=True) + + logger.info(f"成功获取{market_type}数据 {stock_code}, 数据点数: {len(df)}") + return df + + except Exception as e: + error_msg = f"获取{market_type}数据失败 {stock_code}: {str(e)}" + logger.error(error_msg) + logger.exception(e) + raise Exception(error_msg) + + async def get_multiple_stocks_data(self, stock_codes: List[str], + market_type: str = 'A', + start_date: Optional[str] = None, + end_date: Optional[str] = None, + max_concurrency: int = 5) -> Dict[str, pd.DataFrame]: + """ + 异步批量获取多只股票数据 + + Args: + stock_codes: 股票代码列表 + market_type: 市场类型,默认为'A'股 + start_date: 开始日期,格式YYYYMMDD + end_date: 结束日期,格式YYYYMMDD + max_concurrency: 最大并发数,默认为5 + + Returns: + 字典,键为股票代码,值为对应的DataFrame + """ + # 使用信号量控制并发数 + semaphore = asyncio.Semaphore(max_concurrency) + + async def get_with_semaphore(code): + async with semaphore: + try: + return code, await self.get_stock_data(code, market_type, start_date, end_date) + except Exception as e: + logger.error(f"获取股票 {code} 数据时出错: {str(e)}") + return code, None + + # 创建异步任务 + tasks = [get_with_semaphore(code) for code in stock_codes] + + # 等待所有任务完成 + results = await asyncio.gather(*tasks) + + # 构建结果字典,过滤掉失败的请求 + return {code: df for code, df in results if df is not None} \ No newline at end of file diff --git a/services/stock_scorer.py b/services/stock_scorer.py new file mode 100644 index 0000000..c631f14 --- /dev/null +++ b/services/stock_scorer.py @@ -0,0 +1,128 @@ +import pandas as pd +import numpy as np +from typing import Dict, Optional, Any, List, Tuple +from logger import get_logger + +# 获取日志器 +logger = get_logger() + +class StockScorer: + """ + 股票评分服务 + 负责根据技术指标计算股票的综合评分 + """ + + def __init__(self): + """初始化股票评分服务""" + logger.debug("初始化StockScorer") + + def calculate_score(self, df: pd.DataFrame) -> int: + """ + 计算股票评分(满分100分) + + Args: + df: 包含技术指标的DataFrame + + Returns: + 股票评分(0-100的整数) + """ + try: + # 使用最新的数据点进行评分 + latest = df.iloc[-1] + + # 初始得分为0 + score = 0 + + # 移动平均线评分(25分) + if latest['MA5'] > latest['MA20'] > latest['MA60']: + # 短期、中期和长期均线呈多头排列 + score += 25 + elif latest['MA5'] > latest['MA20']: + # 短期均线在中期均线之上 + score += 15 + elif latest['Close'] > latest['MA20']: + # 股价在中期均线之上 + score += 10 + + # RSI评分(25分) + rsi = latest['RSI'] + if 45 <= rsi <= 55: + # RSI在中间区域,可能即将爆发 + score += 15 + elif 55 < rsi < 70: + # RSI在强势区域但未超买 + score += 25 + elif 30 < rsi < 45: + # RSI在弱势区域但未超卖 + score += 10 + elif rsi >= 70: + # RSI超买 + score += 5 + elif rsi <= 30: + # RSI超卖 + score += 15 + + # MACD得分(20分) + if latest['MACD'] > latest['Signal']: + score += 20 + + # 成交量得分(30分) + if latest['Volume_Ratio'] > 1.5: + score += 30 + elif latest['Volume_Ratio'] > 1: + score += 15 + + return score + + except Exception as e: + logger.error(f"计算评分时出错: {str(e)}") + logger.exception(e) + raise + + def get_recommendation(self, score: int) -> str: + """ + 根据评分获取投资建议 + + Args: + score: 股票评分(0-100) + + Returns: + 投资建议文本 + """ + if score >= 80: + return "强烈推荐" + elif score >= 70: + return "推荐" + elif score >= 60: + return "谨慎推荐" + elif score >= 40: + return "观望" + elif score >= 20: + return "不推荐" + else: + return "强烈不推荐" + + def batch_score_stocks(self, stock_dfs: Dict[str, pd.DataFrame]) -> List[Tuple[str, int, str]]: + """ + 批量评分多只股票 + + Args: + stock_dfs: 字典,键为股票代码,值为DataFrame + + Returns: + 评分结果列表,每项为(股票代码, 评分, 推荐)的三元组 + """ + results = [] + + for stock_code, df in stock_dfs.items(): + try: + score = self.calculate_score(df) + recommendation = self.get_recommendation(score) + results.append((stock_code, score, recommendation)) + except Exception as e: + logger.error(f"评分股票 {stock_code} 时出错: {str(e)}") + + # 按评分降序排序 + results.sort(key=lambda x: x[1], reverse=True) + + return results \ No newline at end of file diff --git a/services/technical_indicator.py b/services/technical_indicator.py new file mode 100644 index 0000000..0f8aacb --- /dev/null +++ b/services/technical_indicator.py @@ -0,0 +1,187 @@ +import pandas as pd +import numpy as np +from typing import Dict, Optional, Any +from logger import get_logger + +# 获取日志器 +logger = get_logger() + +class TechnicalIndicator: + """ + 技术指标计算服务 + 负责计算常见的股票技术指标 + """ + + def __init__(self, params: Optional[Dict[str, Any]] = None): + """ + 初始化技术指标计算服务 + + Args: + params: 技术指标参数配置 + """ + # 默认参数设置 + self.params = params or { + 'ma_periods': {'short': 5, 'medium': 20, 'long': 60}, + 'rsi_period': 14, + 'bollinger_period': 20, + 'bollinger_std': 2, + 'volume_ma_period': 20, + 'atr_period': 14 + } + + logger.debug(f"初始化TechnicalIndicator,参数: {self.params}") + + def calculate_ema(self, series: pd.Series, period: int) -> pd.Series: + """ + 计算指数移动平均线 + + Args: + series: 价格序列 + period: 周期 + + Returns: + EMA序列 + """ + return series.ewm(span=period, adjust=False).mean() + + def calculate_rsi(self, series: pd.Series, period: int) -> pd.Series: + """ + 计算相对强弱指标(RSI) + + Args: + series: 价格序列 + period: 周期 + + Returns: + RSI序列 + """ + delta = series.diff() + gain = delta.where(delta > 0, 0) + loss = -delta.where(delta < 0, 0) + + avg_gain = gain.rolling(window=period).mean() + avg_loss = loss.rolling(window=period).mean() + + rs = avg_gain / avg_loss + rsi = 100 - (100 / (1 + rs)) + + return rsi + + def calculate_macd(self, series: pd.Series) -> tuple: + """ + 计算MACD指标 + + Args: + series: 价格序列 + + Returns: + (MACD线, 信号线, 柱状图)的元组 + """ + ema12 = self.calculate_ema(series, 12) + ema26 = self.calculate_ema(series, 26) + + macd = ema12 - ema26 + signal = self.calculate_ema(macd, 9) + histogram = macd - signal + + return macd, signal, histogram + + def calculate_bollinger_bands(self, series: pd.Series, period: int, std_dev: float) -> tuple: + """ + 计算布林带 + + Args: + series: 价格序列 + period: 周期 + std_dev: 标准差倍数 + + Returns: + (中轨, 上轨, 下轨)的元组 + """ + middle = series.rolling(window=period).mean() + std = series.rolling(window=period).std() + + upper = middle + std_dev * std + lower = middle - std_dev * std + + return middle, upper, lower + + def calculate_atr(self, df: pd.DataFrame, period: int) -> pd.Series: + """ + 计算平均真实波幅(ATR) + + Args: + df: 包含High, Low, Close列的DataFrame + period: 周期 + + Returns: + ATR序列 + """ + high = df['High'] + low = df['Low'] + close = df['Close'] + + tr1 = high - low + tr2 = abs(high - close.shift()) + tr3 = abs(low - close.shift()) + + tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) + atr = tr.rolling(window=period).mean() + + return atr + + def calculate_indicators(self, df: pd.DataFrame) -> pd.DataFrame: + """ + 计算所有技术指标 + + Args: + df: 原始价格数据,包含Open, High, Low, Close, Volume列 + + Returns: + 添加了技术指标的DataFrame + """ + try: + # 复制数据框 + result_df = df.copy() + + # 移动平均线 + for name, period in self.params['ma_periods'].items(): + result_df[f'MA{period}'] = result_df['Close'].rolling(window=period).mean() + + # RSI + result_df['RSI'] = self.calculate_rsi(result_df['Close'], self.params['rsi_period']) + + # MACD + macd, signal, histogram = self.calculate_macd(result_df['Close']) + result_df['MACD'] = macd + result_df['Signal'] = signal + result_df['Histogram'] = histogram + + # 布林带 + middle, upper, lower = self.calculate_bollinger_bands( + result_df['Close'], + self.params['bollinger_period'], + self.params['bollinger_std'] + ) + result_df['BB_Middle'] = middle + result_df['BB_Upper'] = upper + result_df['BB_Lower'] = lower + + # 成交量移动平均 + result_df['Volume_MA'] = result_df['Volume'].rolling(window=self.params['volume_ma_period']).mean() + + # 成交量比率 + result_df['Volume_Ratio'] = result_df['Volume'] / result_df['Volume_MA'] + + # ATR + result_df['ATR'] = self.calculate_atr(result_df, self.params['atr_period']) + + # 波动率 (过去20天收盘价的标准差/均值) + result_df['Volatility'] = result_df['Close'].rolling(window=20).std() / result_df['Close'].rolling(window=20).mean() * 100 + + return result_df + + except Exception as e: + logger.error(f"计算技术指标时出错: {str(e)}") + logger.exception(e) + raise \ No newline at end of file diff --git a/services/us_stock_service_async.py b/services/us_stock_service_async.py new file mode 100644 index 0000000..d0a5b0b --- /dev/null +++ b/services/us_stock_service_async.py @@ -0,0 +1,151 @@ +import asyncio +import pandas as pd +from typing import List, Dict, Any, Optional +from logger import get_logger + +# 获取日志器 +logger = get_logger() + +class USStockServiceAsync: + """ + 异步美股服务 + 提供美股数据的异步搜索和获取功能 + """ + + def __init__(self): + """初始化异步美股服务""" + logger.debug("初始化USStockServiceAsync") + + # 可选:添加缓存以减少频繁请求 + self._cache = None + self._cache_timestamp = None + + async def search_us_stocks(self, keyword: str) -> List[Dict[str, Any]]: + """ + 异步搜索美股代码 + + Args: + keyword: 搜索关键词 + + Returns: + 匹配的股票列表 + """ + try: + logger.info(f"异步搜索美股: {keyword}") + + # 使用线程池执行同步的akshare调用 + df = await asyncio.to_thread(self._get_us_stocks_data) + + # 模糊匹配搜索 + mask = df['name'].str.contains(keyword, case=False, na=False) + results = df[mask] + + # 格式化返回结果并处理 NaN 值 + formatted_results = [] + for _, row in results.iterrows(): + formatted_results.append({ + 'name': row['name'] if pd.notna(row['name']) else '', + 'symbol': str(row['symbol']) if pd.notna(row['symbol']) else '', + 'price': float(row['price']) if pd.notna(row['price']) else 0.0, + 'market_value': float(row['market_value']) if pd.notna(row['market_value']) else 0.0 + }) + + logger.info(f"美股搜索完成,找到 {len(formatted_results)} 个匹配项") + return formatted_results + + except Exception as e: + error_msg = f"搜索美股代码失败: {str(e)}" + logger.error(error_msg) + logger.exception(e) + raise Exception(error_msg) + + def _get_us_stocks_data(self) -> pd.DataFrame: + """ + 获取美股数据(同步方法,将被异步方法调用) + + Returns: + 包含美股数据的DataFrame + """ + import akshare as ak + + try: + # 获取美股数据 + df = ak.stock_us_spot_em() + + # 转换列名 + df = df.rename(columns={ + "序号": "index", + "名称": "name", + "最新价": "price", + "涨跌额": "price_change", + "涨跌幅": "price_change_percent", + "开盘价": "open", + "最高价": "high", + "最低价": "low", + "昨收价": "pre_close", + "总市值": "market_value", + "市盈率": "pe_ratio", + "成交量": "volume", + "成交额": "turnover", + "振幅": "amplitude", + "换手率": "turnover_rate", + "代码": "symbol" + }) + + return df + + except Exception as e: + logger.error(f"获取美股数据失败: {str(e)}") + logger.exception(e) + raise Exception(f"获取美股数据失败: {str(e)}") + + async def get_us_stock_detail(self, symbol: str) -> Dict[str, Any]: + """ + 异步获取单个美股详细信息 + + Args: + symbol: 股票代码 + + Returns: + 股票详细信息 + """ + try: + logger.info(f"获取美股详情: {symbol}") + + # 使用线程池执行同步的akshare调用 + df = await asyncio.to_thread(self._get_us_stocks_data) + + # 精确匹配股票代码 + result = df[df['symbol'] == symbol] + + if len(result) == 0: + raise Exception(f"未找到股票代码: {symbol}") + + # 获取第一行数据 + row = result.iloc[0] + + # 格式化为字典 + stock_detail = { + 'name': row['name'] if pd.notna(row['name']) else '', + 'symbol': str(row['symbol']) if pd.notna(row['symbol']) else '', + 'price': float(row['price']) if pd.notna(row['price']) else 0.0, + 'price_change': float(row['price_change']) if pd.notna(row['price_change']) else 0.0, + 'price_change_percent': float(row['price_change_percent'].strip('%'))/100 if pd.notna(row['price_change_percent']) else 0.0, + 'open': float(row['open']) if pd.notna(row['open']) else 0.0, + 'high': float(row['high']) if pd.notna(row['high']) else 0.0, + 'low': float(row['low']) if pd.notna(row['low']) else 0.0, + 'pre_close': float(row['pre_close']) if pd.notna(row['pre_close']) else 0.0, + 'market_value': float(row['market_value']) if pd.notna(row['market_value']) else 0.0, + 'pe_ratio': float(row['pe_ratio']) if pd.notna(row['pe_ratio']) else 0.0, + 'volume': float(row['volume']) if pd.notna(row['volume']) else 0.0, + 'turnover': float(row['turnover']) if pd.notna(row['turnover']) else 0.0 + } + + logger.info(f"获取美股详情成功: {symbol}") + return stock_detail + + except Exception as e: + error_msg = f"获取美股详情失败: {str(e)}" + logger.error(error_msg) + logger.exception(e) + raise Exception(error_msg) \ No newline at end of file diff --git a/web_server.py b/web_server.py index c761334..5550a49 100644 --- a/web_server.py +++ b/web_server.py @@ -3,10 +3,11 @@ from fastapi.responses import JSONResponse, StreamingResponse, FileResponse, Red from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field -from typing import List, Optional, Generator -from stock_analyzer import StockAnalyzer -from us_stock_service import USStockService -from fund_service import FundService +from typing import List, Optional, Dict, Any, Generator +from services.stock_analyzer_service import StockAnalyzerService +# 导入新的异步服务 +from services.us_stock_service_async import USStockServiceAsync +from services.fund_service_async import FundServiceAsync import asyncio import threading import os @@ -25,7 +26,7 @@ logger = get_logger() app = FastAPI( title="Stock Scanner API", - description="股票分析API", + description="异步股票分析API", version="1.0.0" ) @@ -43,9 +44,10 @@ frontend_dist = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fronte if os.path.exists(frontend_dist): app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="frontend") -analyzer = StockAnalyzer() -us_stock_service = USStockService() -fund_service = FundService() +# 初始化异步服务 +# StockAnalyzerService 不需要全局初始化,在 /analyze 接口中按需创建 +us_stock_service = USStockServiceAsync() +fund_service = FundServiceAsync() # 定义请求和响应模型 class AnalyzeRequest(BaseModel): @@ -102,7 +104,7 @@ async def analyze(request: AnalyzeRequest): logger.debug(f"自定义API配置: URL={custom_api_url}, 模型={custom_api_model}, API Key={'已提供' if custom_api_key else '未提供'}, Timeout={custom_api_timeout}") # 创建新的分析器实例,使用自定义配置 - custom_analyzer = StockAnalyzer( + custom_analyzer = StockAnalyzerService( custom_api_url=custom_api_url, custom_api_key=custom_api_key, custom_api_model=custom_api_model, @@ -126,17 +128,11 @@ async def analyze(request: AnalyzeRequest): logger.debug(f"开始处理股票 {stock_code} 的流式响应") chunk_count = 0 - # 使用线程池执行同步分析 - def run_analysis(): - return list(custom_analyzer.analyze_stock(stock_code, market_type, stream=True)) - - # 在线程中执行同步操作 - loop = asyncio.get_event_loop() - chunks = await loop.run_in_executor(None, run_analysis) - - for chunk in chunks: + # 使用异步生成器 + async for chunk in custom_analyzer.analyze_stock(stock_code, market_type, stream=True): chunk_count += 1 yield chunk + '\n' + logger.info(f"股票 {stock_code} 流式分析完成,共发送 {chunk_count} 个块") else: # 批量分析流式处理 @@ -148,22 +144,16 @@ async def analyze(request: AnalyzeRequest): logger.debug(f"开始处理批量股票的流式响应") chunk_count = 0 - # 使用线程池执行同步分析 - def run_batch_analysis(): - return list(custom_analyzer.scan_stocks( - [code.strip() for code in stock_codes], - min_score=0, - market_type=market_type, - stream=True - )) - - # 在线程中执行同步操作 - loop = asyncio.get_event_loop() - chunks = await loop.run_in_executor(None, run_batch_analysis) - - for chunk in chunks: + # 使用异步生成器 + async for chunk in custom_analyzer.scan_stocks( + [code.strip() for code in stock_codes], + min_score=0, + market_type=market_type, + stream=True + ): chunk_count += 1 yield chunk + '\n' + logger.info(f"批量流式分析完成,共发送 {chunk_count} 个块") logger.info("成功创建流式响应生成器") @@ -181,9 +171,8 @@ async def search_us_stocks(keyword: str = ""): if not keyword: raise HTTPException(status_code=400, detail="请输入搜索关键词") - # 在异步上下文中运行同步的搜索函数 - loop = asyncio.get_event_loop() - results = await loop.run_in_executor(None, us_stock_service.search_us_stocks, keyword) + # 直接使用异步服务的异步方法 + results = await us_stock_service.search_us_stocks(keyword) return {"results": results} except Exception as e: @@ -196,9 +185,8 @@ async def search_funds(keyword: str = "", market_type: str = ""): if not keyword: raise HTTPException(status_code=400, detail="请输入搜索关键词") - # 在异步上下文中运行同步的搜索函数 - loop = asyncio.get_event_loop() - results = await loop.run_in_executor(None, lambda: fund_service.search_funds(keyword, market_type)) + # 直接使用异步服务的异步方法 + results = await fund_service.search_funds(keyword, market_type) return {"results": results} except Exception as e: @@ -273,6 +261,36 @@ async def test_api_connection(request: TestAPIRequest): content={"success": False, "message": f"API 测试连接时出错: {str(e)}"} ) +# 新增 API 端点:获取美股详情 +@app.get("/us_stock_detail/{symbol}") +async def get_us_stock_detail(symbol: str): + try: + if not symbol: + raise HTTPException(status_code=400, detail="请提供股票代码") + + # 使用异步服务获取详情 + detail = await us_stock_service.get_us_stock_detail(symbol) + return detail + + except Exception as e: + logger.error(f"获取美股详情时出错: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +# 新增 API 端点:获取基金详情 +@app.get("/fund_detail/{symbol}") +async def get_fund_detail(symbol: str, market_type: str = "ETF"): + try: + if not symbol: + raise HTTPException(status_code=400, detail="请提供基金代码") + + # 使用异步服务获取详情 + detail = await fund_service.get_fund_detail(symbol, market_type) + return detail + + except Exception as e: + logger.error(f"获取基金详情时出错: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + if __name__ == '__main__': logger.info("股票分析系统启动") uvicorn.run("web_server:app", host="127.0.0.1", port=8888, reload=True) \ No newline at end of file From 5444edf680bdf39c98d9f2731227d8d25a6842c8 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Thu, 6 Mar 2025 18:23:44 +0800 Subject: [PATCH 05/17] Update stock_analyzer_service.py --- services/stock_analyzer_service.py | 58 ++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/services/stock_analyzer_service.py b/services/stock_analyzer_service.py index 91df74a..2e24a72 100644 --- a/services/stock_analyzer_service.py +++ b/services/stock_analyzer_service.py @@ -2,6 +2,7 @@ import pandas as pd import numpy as np import asyncio import json +from datetime import datetime from typing import Dict, List, Optional, Tuple, Any, AsyncGenerator from logger import get_logger from services.stock_data_provider import StockDataProvider @@ -66,13 +67,64 @@ class StockAnalyzerService: score = self.scorer.calculate_score(df_with_indicators) recommendation = self.scorer.get_recommendation(score) + # 获取最新数据 + latest_data = df_with_indicators.iloc[-1] + previous_data = df_with_indicators.iloc[-2] if len(df_with_indicators) > 1 else latest_data + + # 计算价格变化百分比 + price_change = ((latest_data['Close'] - previous_data['Close']) / previous_data['Close']) * 100 + + # 确定MA趋势 + ma_short = latest_data.get('MA5', 0) + ma_medium = latest_data.get('MA20', 0) + ma_long = latest_data.get('MA60', 0) + + if ma_short > ma_medium > ma_long: + ma_trend = "UP" + elif ma_short < ma_medium < ma_long: + ma_trend = "DOWN" + else: + ma_trend = "FLAT" + + # 确定MACD信号 + macd = latest_data.get('MACD', 0) + signal = latest_data.get('Signal', 0) + + if macd > signal: + macd_signal = "BUY" + elif macd < signal: + macd_signal = "SELL" + else: + macd_signal = "HOLD" + + # 确定成交量状态 + volume = latest_data.get('Volume', 0) + volume_ma = latest_data.get('Volume_MA', 0) + + if volume > volume_ma * 1.5: + volume_status = "HIGH" + elif volume < volume_ma * 0.5: + volume_status = "LOW" + else: + volume_status = "NORMAL" + + # 当前分析日期 + analysis_date = datetime.now().strftime('%Y-%m-%d') + # 生成基本分析结果 basic_result = { "stock_code": stock_code, + "market_type": market_type, + "analysis_date": analysis_date, "score": score, + "price": latest_data['Close'], + "price_change": price_change, + "ma_trend": ma_trend, + "rsi": latest_data.get('RSI', 0), + "macd_signal": macd_signal, + "volume_status": volume_status, "recommendation": recommendation, - "data_point_count": len(df), - "market_type": market_type + "ai_analysis": "" } # 输出基本分析结果 @@ -175,4 +227,4 @@ class StockAnalyzerService: error_msg = f"批量扫描股票时出错: {str(e)}" logger.error(error_msg) logger.exception(e) - yield json.dumps({"error": error_msg}) \ No newline at end of file + yield json.dumps({"error": error_msg}) From 9246e7f02b1b773c8828907383f8e528bbcd4c02 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Thu, 6 Mar 2025 19:04:23 +0800 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/StockAnalysisApp.vue | 4 +++- services/ai_analyzer.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/StockAnalysisApp.vue b/frontend/src/components/StockAnalysisApp.vue index 385523b..e852e01 100644 --- a/frontend/src/components/StockAnalysisApp.vue +++ b/frontend/src/components/StockAnalysisApp.vue @@ -162,7 +162,9 @@ const apiConfig = ref({ const marketOptions = [ { label: 'A股', value: 'A' }, { label: '港股', value: 'HK' }, - { label: '美股', value: 'US' } + { label: '美股', value: 'US' }, + { label: 'ETF', value: 'ETF' }, + { label: 'LOF', value: 'LOF' } ]; // 更新API配置 diff --git a/services/ai_analyzer.py b/services/ai_analyzer.py index 93c063f..f1d6049 100644 --- a/services/ai_analyzer.py +++ b/services/ai_analyzer.py @@ -54,14 +54,19 @@ class AIAnalyzer: """ try: logger.info(f"开始AI分析 {stock_code}, 流式模式: {stream}") + + # AI 分析内容 + # 最近14天的股票数据记录 recent_data = df.tail(14).to_dict('records') + # 包含trend, volatility, volume_trend, rsi_level的字典 technical_summary = { 'trend': 'upward' if df.iloc[-1]['MA5'] > df.iloc[-1]['MA20'] else 'downward', 'volatility': f"{df.iloc[-1]['Volatility']:.2f}%", 'volume_trend': 'increasing' if df.iloc[-1]['Volume_Ratio'] > 1 else 'decreasing', 'rsi_level': df.iloc[-1]['RSI'] } + print(recent_data, technical_summary) # 根据市场类型调整分析提示 if market_type in ['ETF', 'LOF']: @@ -210,7 +215,7 @@ class AIAnalyzer: # 尝试从整个内容中提取JSON full_content = "".join(collected_messages) - + print(f"尝试从整个内容中提取JSON: {full_content}") # 如果没有成功解析JSON,返回原始内容 if not full_content.strip().startswith("{"): yield json.dumps({ From 35cf295b29b1746f2c6415308017e5655536ccf8 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Thu, 6 Mar 2025 19:38:36 +0800 Subject: [PATCH 07/17] Update ai_analyzer.py --- services/ai_analyzer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/ai_analyzer.py b/services/ai_analyzer.py index f1d6049..73ba626 100644 --- a/services/ai_analyzer.py +++ b/services/ai_analyzer.py @@ -66,7 +66,6 @@ class AIAnalyzer: 'volume_trend': 'increasing' if df.iloc[-1]['Volume_Ratio'] > 1 else 'decreasing', 'rsi_level': df.iloc[-1]['RSI'] } - print(recent_data, technical_summary) # 根据市场类型调整分析提示 if market_type in ['ETF', 'LOF']: @@ -197,7 +196,7 @@ class AIAnalyzer: if len(buffer) > 100: yield json.dumps({ "stock_code": stock_code, - "partial_content": buffer + "ai_analysis_chunk": buffer }) collected_messages.append(buffer) buffer = "" @@ -209,13 +208,13 @@ class AIAnalyzer: if buffer: yield json.dumps({ "stock_code": stock_code, - "partial_content": buffer + "ai_analysis_chunk": buffer }) collected_messages.append(buffer) # 尝试从整个内容中提取JSON full_content = "".join(collected_messages) - print(f"尝试从整个内容中提取JSON: {full_content}") + # 如果没有成功解析JSON,返回原始内容 if not full_content.strip().startswith("{"): yield json.dumps({ From ff5b820a579b6259d36ebaeb806afb8897bcfb26 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Thu, 6 Mar 2025 20:30:54 +0800 Subject: [PATCH 08/17] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/StockAnalysisApp.vue | 335 ++++++++++++++++++- frontend/src/components/StockCard.vue | 30 ++ services/ai_analyzer.py | 311 ++++++++++++----- services/stock_analyzer_service.py | 49 ++- 4 files changed, 608 insertions(+), 117 deletions(-) diff --git a/frontend/src/components/StockAnalysisApp.vue b/frontend/src/components/StockAnalysisApp.vue index e852e01..4b580a0 100644 --- a/frontend/src/components/StockAnalysisApp.vue +++ b/frontend/src/components/StockAnalysisApp.vue @@ -76,6 +76,45 @@
+
+ + 分析结果 ({{ analyzedStocks.length }}) + + + + 复制结果 + + + + 导出 + + + + + +
+